[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\nindent_style = tab\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.mulaw binary\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "Contributing to Janus\n=====================\n\nIf you really want to help, then first of all, thank you! Janus is a\nproject we firmly believe in, and so any contribution is more than\nwelcome to make it better.\n\nIf you want to open an issue, please make sure that:\n\n1. you actually found a bug: for generic questions, use the\n[Discourse](https://janus.discourse.group/)\ngroup instead;\n2. nobody opened the same bug already (do a search);\n3. the issue wasn't already solved;\n4. you don't paste a huge amount of text in the issue or comments: use\na service like [pastebin](https://pastebin.com/) or similar;\n5. you provide a GDB stacktrace if Janus crashed on you, and/or output\nfrom AddressSanitizer or Valgrind (more details available\n[here](https://janus.conf.meetecho.com/docs/debug.html)).\n\nIf you want to contribute to the project by providing fixes, enhancements,\nnew features or other materials, then just clone the repo, make your changes, push to\nyour fork (good commit messages that explain what changed are welcome!)\nand submit a pull request. Please make sure that, especially if you're\nmodifying the C code, you follow the same coding style we're using and\nthat you properly comment the code as we usually do ourselves.\n\nThat said, please beware that we do require all contributors to\n<a href=\"https://cla.conf.meetecho.com/janus\">sign the Contributor License Agreement</a>\nbefore we can accept any code.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n- name: 💬 Janus Community (Discourse)\n  url: https://janus.discourse.group/\n  about: Please ask questions or discuss runtime problems on the group, not GitHub\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/janus-0-x-issue--legacy-.md",
    "content": "---\nname: Janus 0.x issue (legacy)\nabout: Open an issue related to Janus 0.x (legacy)\ntitle: \"[0.x]\"\nlabels: legacy\nassignees: ''\n\n---\n\n**What version of Janus is this happening on?**\nPut the version and the commit identifier (available in `version.c` or from an `info` request) here\n\n**Have you tested a more recent version of Janus too?**\nYes/no. If yes, which one. If not, why.\n\n**Was this working before?**\nIf you have information on a version/commit where the issue wasn't there (e.g., the result of a `git bisect`), please provide it here.\n\n**Is there a gdb or libasan trace of the issue?**\nIf you have a trace related to the issue (e.g., after a segfault), provide it as a gist/pastebin/other link here, or as a [collapsible section](https://gist.github.com/pierrejoubert73/902cc94d79424356a8d20be2b382e1ab). Do **NOT** paste logs inline. For more information on the kind of traces we may find useful, please refer to the [documentation](https://janus-legacy.conf.meetecho.com/docs/debug).\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/janus-1-x-issue--multistream-.md",
    "content": "---\nname: Janus 1.x issue (multistream)\nabout: Open an issue related to Janus 1.x (multistream)\ntitle: \"[1.x]\"\nlabels: multistream\nassignees: ''\n\n---\n\n**What version of Janus is this happening on?**\nPut the version and the commit identifier (available in `version.c` or from an `info` request) here\n\n**Have you tested a more recent version of Janus too?**\nYes/no. If yes, which one. If not, why.\n\n**Was this working before?**\nIf you have information on a version/commit where the issue wasn't there (e.g., the result of a `git bisect`), please provide it here.\n\n**Is there a gdb or libasan trace of the issue?**\nIf you have a trace related to the issue (e.g., after a segfault), provide it as a gist/pastebin/other link here, or as a [collapsible section](https://gist.github.com/pierrejoubert73/902cc94d79424356a8d20be2b382e1ab). Do **NOT** paste logs inline. For more information on the kind of traces we may find useful, please refer to the [documentation](https://janus.conf.meetecho.com/docs/debug).\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/workflows/janus-ci.yml",
    "content": "name: janus-ci\n\non:\n  push:\n    branches:\n      - master\n      - test-ci\n  pull_request:\n    branches:\n      - master\npermissions:\n  contents: read\njobs:\n  build:\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        compiler: [gcc, clang]\n        datachannels: [\"enable-datachannels\", \"disable-datachannels\"]\n        libcurl: [\"enable-libcurl\", \"disable-libcurl\"]\n        include:\n          - datachannels: \"enable-datachannels\"\n            libcurl: \"enable-libcurl\"\n            deps_from_src: \"yes\"\n            janus_config_opts: \"\"\n          - datachannels: \"enable-datachannels\"\n            libcurl: \"disable-libcurl\"\n            deps_from_src: \"no\"\n            janus_config_opts: \"--disable-aes-gcm -disable-mqtt --disable-mqtt-event-handler --disable-turn-rest-api --disable-sample-event-handler\"\n          - datachannels: \"disable-datachannels\"\n            libcurl: \"enable-libcurl\"\n            deps_from_src: \"no\"\n            janus_config_opts: \"--disable-aes-gcm -disable-mqtt --disable-mqtt-event-handler --disable-data-channels\"\n        exclude:\n          - datachannels: \"disable-datachannels\"\n            libcurl: \"disable-libcurl\"\n    env:\n      CC: ${{ matrix.compiler }}\n    steps:\n      - name: install janus apt dependencies\n        run: >\n          sudo apt-get update && sudo apt-get --no-install-recommends -y install\n          duktape-dev\n          libavcodec-dev\n          libavformat-dev\n          libavutil-dev\n          libconfig-dev\n          libglib2.0-dev\n          libgirepository1.0-dev\n          liblua5.3-dev\n          libjansson-dev\n          libmicrohttpd-dev\n          libnanomsg-dev\n          libogg-dev\n          libopus-dev\n          libpcap-dev\n          librabbitmq-dev\n          libsofia-sip-ua-dev\n          libssl-dev\n          libtool\n          meson\n          ninja-build\n      - name: setup additional dependencies from apt\n        if: ${{ matrix.deps_from_src == 'no' }}\n        run: >\n          sudo apt-get --no-install-recommends -y install\n          libnice-dev\n          libsrtp2-dev\n          libusrsctp-dev\n          libwebsockets-dev\n      # Workaround for https://github.com/actions/runner-images/issues/11926\n      - name: install cmake v3.31\n        uses: jwlawson/actions-setup-cmake@v2\n        with:\n          cmake-version: \"3.31.6\"\n      - name: install libcurl from apt\n        if: ${{ matrix.libcurl == 'enable-libcurl' }}\n        run: sudo apt-get --no-install-recommends -y install libcurl4-openssl-dev\n      - name: setup python\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.13\"\n          architecture: \"x64\"\n      - name: checkout libnice source\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        uses: actions/checkout@v4\n        with:\n          repository: libnice/libnice\n          ref: master\n      - name: setup libnice from sources\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        run: |\n          meson setup -Dprefix=/usr -Dlibdir=lib -Dc_args=\"-O0 -Wno-cast-align\" \\\n            -Dexamples=disabled \\\n            -Dgtk_doc=disabled \\\n            -Dgstreamer=disabled \\\n            -Dgupnp=disabled \\\n            -Dtests=disabled \\\n            build\n          ninja -C build\n          sudo ninja -C build install\n      - name: checkout libsrtp source\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        uses: actions/checkout@v4\n        with:\n          repository: cisco/libsrtp\n          ref: v2.7.0\n      - name: setup libsrtp from sources\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        run: |\n          ./configure --prefix=/usr CFLAGS=\"-O0\" \\\n            --disable-pcap \\\n            --enable-openssl\n          make -j$(nproc) shared_library\n          sudo make install\n      - name: checkout usrsctp source\n        if: ${{ matrix.datachannels == 'enable-datachannels' && matrix.deps_from_src == 'yes' }}\n        uses: actions/checkout@v4\n        with:\n          repository: sctplab/usrsctp\n          ref: master\n      - name: setup usrsctp from sources\n        if: ${{ matrix.datachannels == 'enable-datachannels' && matrix.deps_from_src == 'yes' }}\n        run: |\n          ./bootstrap\n          ./configure --prefix=/usr CFLAGS=\"-O0\" \\\n            --disable-debug \\\n            --disable-inet \\\n            --disable-inet6 \\\n            --disable-programs \\\n            --disable-static \\\n            --enable-shared\n          make -j$(nproc)\n          sudo make install\n      - name: checkout lws source\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        uses: actions/checkout@v4\n        with:\n          repository: warmcat/libwebsockets\n          ref: v4.3-stable\n      - name: setup lws from sources\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        run: |\n          mkdir build && cd build\n          cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=RELEASE -DCMAKE_C_FLAGS=\"-O0\" \\\n            -DLWS_ROLE_RAW_FILE=OFF \\\n            -DLWS_WITH_HTTP2=OFF \\\n            -DLWS_WITHOUT_EXTENSIONS=OFF \\\n            -DLWS_WITHOUT_TESTAPPS=ON \\\n            -DLWS_WITHOUT_TEST_CLIENT=ON \\\n            -DLWS_WITHOUT_TEST_PING=ON \\\n            -DLWS_WITHOUT_TEST_SERVER=ON \\\n            -DLWS_WITH_STATIC=OFF \\\n            ..\n          make -j$(nproc)\n          sudo make install\n      - name: checkout paho-mqtt source\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        uses: actions/checkout@v4\n        with:\n          repository: eclipse/paho.mqtt.c\n          ref: v1.3.14\n      - name: setup paho-mqtt from sources\n        if: ${{ matrix.deps_from_src == 'yes' }}\n        run: |\n          mkdir build.paho && cd build.paho\n          cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=RELEASE -DCMAKE_C_FLAGS=\"-O0\" \\\n            -DPAHO_HIGH_PERFORMANCE=TRUE \\\n            -DPAHO_BUILD_DOCUMENTATION=FALSE \\\n            -DPAHO_BUILD_SAMPLES=FALSE \\\n            -DPAHO_BUILD_SHARED=TRUE \\\n            -DPAHO_BUILD_STATIC=FALSE \\\n            -DPAHO_ENABLE_TESTING=FALSE \\\n            -DPAHO_WITH_SSL=TRUE \\\n            ..\n          make -j$(nproc)\n          sudo make install\n      - name: checkout janus source\n        uses: actions/checkout@v4\n      - name: build janus from sources\n        env:\n          JANUS_CONFIG_COMMON: \"--disable-docs --enable-post-processing --enable-plugin-lua --enable-plugin-duktape --enable-json-logger\"\n          JANUS_CONFIG_OPTS: ${{ matrix.janus_config_opts }}\n        run: |\n          ./autogen.sh\n          ./configure $JANUS_CONFIG_COMMON $JANUS_CONFIG_OPTS CFLAGS=\"-O0\"\n          make -j$(nproc)\n          make check-fuzzers\n  javascript-lint:\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install modules\n        run: |\n          cd npm\n          npm install\n      - name: Run ESLint\n        run: |\n          cd npm\n          npm run lint\n  javascript-dist:\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install modules\n        run: |\n          cd npm\n          npm install\n      - name: Make dist files\n        run: |\n          cd npm\n          npm run prerelease\n      - uses: actions/upload-artifact@v4\n        with:\n          name: janus.es.js\n          path: npm/dist/janus.es.js\n"
  },
  {
    "path": ".gitignore",
    "content": "cmdline.c\ncmdline.h\nversion.c\ndocs/html/\njanus-gateway.pc\n\njanus\njanus-cfgconv\njanus-pp-rec\nmjr2pcap\npcap2mjr\n*.so\n\nMakefile\nMakefile.in\n/build\n/configure\n/configure~\n/autom4te.cache\n/aclocal.m4\n/m4\n/missing\n/libtool\n/depcomp\n/compile\n/install-sh\n/install-sh~\n/ltmain.sh\n\n/config.log\n/config.guess\n/config.status\n/config.sub\n\n*.o\n*.a\n*.lo\n*.la\n.libs\npp-cmdline.c\npp-cmdline.h\np2m-cmdline.c\np2m-cmdline.h\n/fuzzers/out\n\nnpm/dist\nnpm/src\n\n/conf/janus.cfg.sample\n/conf/janus.plugin.duktape.cfg.sample\n/conf/janus.plugin.lua.cfg.sample\n/conf/janus.plugin.recordplay.cfg.sample\n/conf/janus.plugin.streaming.cfg.sample\n/conf/janus.transport.http.cfg.sample\n/conf/janus.transport.websockets.cfg.sample\n/conf/janus.jcfg.sample\n/conf/janus.plugin.duktape.jcfg.sample\n/conf/janus.plugin.lua.jcfg.sample\n/conf/janus.plugin.recordplay.jcfg.sample\n/conf/janus.plugin.streaming.jcfg.sample\n\n.deps\n.dirstamp\n\ncov-int\n\n# OS X\n.DS_Store\n\n# CLion\n.idea\n\n.vscode\nnode_modules\n.venv\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n\n## [v1.4.0] - 2026-02-06\n\n- Generate a pkg-config .pc file when installing Janus [[PR-3596](https://github.com/meetecho/janus-gateway/pull/3596)]\n- Implemented compound RTCP reports (thanks @addisonpolcyn!) [[PR-3585](https://github.com/meetecho/janus-gateway/pull/3585)]\n- Fixed when to negotiate the rtcp-fb transport-cc SDP attribute [[PR-3595](https://github.com/meetecho/janus-gateway/pull/3595)]\n- Strip some WebRTC-specific SDP attributes in SIP and NoSIP plugins\n- Added RTP forwarders support to SIP and NoSIP plugins (thanks @WebTrit!) [[PR-3583](https://github.com/meetecho/janus-gateway/pull/3583)]\n- Fixed subscription renewal possibly going to the wrong host in the SIP plugin (thanks @digiboridev!) [[PR-3602](https://github.com/meetecho/janus-gateway/pull/3602)]\n- Laid foundations for plugin-specific unique session ID in SIP and NoSIP plugins [[PR-3607](https://github.com/meetecho/janus-gateway/pull/3607)]\n- Fixed 'use-after-free' in SIP session helpers (thanks @oleksandr-mihal-zultys!) [[PR-3613](https://github.com/meetecho/janus-gateway/pull/3613)]\n- Fixed SIP plugin not offering SRTP in response to offerless INVITEs [[PR-3618](https://github.com/meetecho/janus-gateway/pull/3618)]\n- Added new janus-pp-rec option to ignore RTP headers for timing purposes\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.3.3] - 2025-10-21\n\n- Refactored keyframe buffering in Streaming plugin to store following deltas too (thanks [Byborg](https://www.byborgenterprises.com/)!) [[PR-3564](https://github.com/meetecho/janus-gateway/pull/3564)]\n- Added optional events for RTSP disconnections/reconnections in the Streaming plugin [[PR-3578](https://github.com/meetecho/janus-gateway/pull/3578)]\n- Fixed rare crash when reconnecting to RTSP servers in the Streaming plugin [[PR-3580](https://github.com/meetecho/janus-gateway/pull/3580)]\n- Fixed small leak when recording Streaming mountpoints\n- Fixed memory leak when stopping non-existing forwarders in the VideoRoom plugin\n- Fixed rare crash in AudioBridge when notifying participants [[PR-3589](https://github.com/meetecho/janus-gateway/pull/3589)]\n- Fixed payload type when RTP-forwarding AudioBridge rooms using G.711\n- Added option for managing ringing manually in the SIP plugin (thanks @adnanel!) [[PR-3556](https://github.com/meetecho/janus-gateway/pull/3556)]\n- Save custom headers and send them on SIP hangup event (thanks @kenangenjac!) [[PR-3558](https://github.com/meetecho/janus-gateway/pull/3558)]\n- Fix small memory leak in SIP plugin (thanks @oleksandr-mihal-zultys!) [[PR-3565](3565)]\n- Added support for Linux cooked capture v2 to pcap2mjr\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.3.2] - 2025-07-10\n\n- Added workaround to potential libnice issue\n- Deprecated nice_debug option (libnice debugging can be enabled via env variables) [[PR-3546](https://github.com/meetecho/janus-gateway/pull/3546)]\n- Fixed broken session count in case of timeouts [[Issue-3526](https://github.com/meetecho/janus-gateway/issues/3526)]\n- Fixed broken session count in case of API disconnections [[Issue-3532](https://github.com/meetecho/janus-gateway/issues/3532)]\n- Added experimental support for the video-layers-allocation extension [PR-3504](https://github.com/meetecho/janus-gateway/pull/3504)]\n- Added optional signal handler for log rotation [[PR-3550](https://github.com/meetecho/janus-gateway/pull/3550)]\n- Fixed memory leaks when using dummy publishers [[PR-3541](https://github.com/meetecho/janus-gateway/pull/3541)]\n- New options to advertise VideoRoom dummy publishers as e2ee [[PR-3553](https://github.com/meetecho/janus-gateway/pull/3553)]\n- Fixed rare crash when using remote publishers in VideoRoom [[PR-3557](https://github.com/meetecho/janus-gateway/pull/3557)]\n- Added request to stop all AudioBridge announcements in a room (thanks @keremcadirci!) [[PR-3403](https://github.com/meetecho/janus-gateway/pull/3403)]\n- Allow plain RTP participants in AudioBridge to use generate_offer too [[PR-3534](https://github.com/meetecho/janus-gateway/pull/3534)]\n- Fixed breaking condition in AudioBridge\n- Fixed broken canvas demo [[Issue-3527](https://github.com/meetecho/janus-gateway/issues/3527)]\n- Fixed broken multiopus demo\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.3.1] - 2025-03-05\n\n- Make ip-utils aware of 0.0.0.0 and :: [[Issue-3470](https://github.com/meetecho/janus-gateway/issues/3470)]\n- Optimize NACKs handling [[PR-3491](https://github.com/meetecho/janus-gateway/pull/3491)]\n- Skip medium lookup when relaying RTCP by plugins to fix rare deadlock [[PR-3515](https://github.com/meetecho/janus-gateway/pull/3515)]\n- Fixed FEC and DTX negotiated in VideoRoom also when not enabled in the room settings\n- Fixed memory leak in VideoRoom (thanks @m08pvv!) [[PR-3493](https://github.com/meetecho/janus-gateway/pull/3493)]\n- Fixed broken recordings when using remote publishers in the VideoRoom [[PR-3509](https://github.com/meetecho/janus-gateway/pull/3509)]\n- Check if IPv6 is disabled when loading the Streaming plugin [[PR-3519](https://github.com/meetecho/janus-gateway/pull/3519)]\n- Fixed SSRC not being set to a default value in AudioBridge forwarders when not explicitly set in the request\n- Add support for \"progress\" request on SIP Plugin (thanks @adnan-mujagic!) [[PR-3466](https://github.com/meetecho/janus-gateway/pull/3466)]\n- New programmatic API to ask for keyframes in SIP and NoSIP plugins [[PR-3517](https://github.com/meetecho/janus-gateway/pull/3517)]\n- Fixed missing libcurl linking in TextRoom plugin\n- Added support for private recordings in Record&Play [[PR-3518](https://github.com/meetecho/janus-gateway/pull/3518)]\n- Fixed leak in recorder when adding descriptions (thanks @m08pvv!) [[PR-3487](https://github.com/meetecho/janus-gateway/pull/3487)]\n- Fixed WebSocket transport binding to the wrong IP address in particular setups [[Issue-3500](https://github.com/meetecho/janus-gateway/issues/3550)]\n- Improve DTX detection in janus-pp-rec [[PR-3488](#3488)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.3.0] - 2024-11-25\n\n- Refactored logging internals [[PR-3428](https://github.com/meetecho/janus-gateway/pull/3428)]\n- Use strtok to parse SDPs [[PR-3424](https://github.com/meetecho/janus-gateway/pull/3424)]\n- Fixed rare condition that could lead to a deadlock in the VideoRoom [[PR-3446](https://github.com/meetecho/janus-gateway/pull/3446)]\n- Fixed broken switch when using remote publishers in VideoRoom [[PR-3447](https://github.com/meetecho/janus-gateway/pull/3447)]\n- Added SRTP support to VideoRoom remote publishers (thanks @spscream!) [[PR-3449](https://github.com/meetecho/janus-gateway/pull/3449)]\n- Added support for generic JSON metadata to VideoRoom publishers (thanks @spscream!) [[PR-3467](https://github.com/meetecho/janus-gateway/pull/3467)]\n- Fixed deadlock in VideoRoom when failing to open a socket for a new RTP forwarder (thanks @spscream!) [[PR-3468](https://github.com/meetecho/janus-gateway/pull/3468)]\n- Fixed deadlock in VideoRoom caused by reverse ordering of mutex locks [[PR-3474](https://github.com/meetecho/janus-gateway/pull/3474)]\n- Fixed memory leaks when using remote publishers in VideoRoom [[PR-3475](https://github.com/meetecho/janus-gateway/pull/3475)]\n- Diluted frequency of PLI in the VideoRoom (thanks @natikaltura!) [[PR-3423](https://github.com/meetecho/janus-gateway/pull/3423)]\n- Better cleanup after failed mountpoint creations in Streaming plugin [[PR-3465](https://github.com/meetecho/janus-gateway/pull/3465)]\n- Fixed compilation of AudioBridge in case libogg isn't available (thanks @tmatth!) [[PR-3438](https://github.com/meetecho/janus-gateway/pull/3438)]\n- Better management of call cleanup in SIP plugin [[Issue-3430](https://github.com/meetecho/janus-gateway/issues/3430)]\n- Change the way call-IDs are tracked in the SIP plugin (thanks WebTrit!) [[PR-3443](https://github.com/meetecho/janus-gateway/pull/3443)]\n- Increased maximum size of custom SIP headers [[Issue-3459](https://github.com/meetecho/janus-gateway/issues/3459)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.2.4] - 2024-09-10\n\n- Limit number of SDP lines when parsing (workaround for OSS-Fuzz issue) [[PR-3414](https://github.com/meetecho/janus-gateway/pull/3414)]\n- Normalized monotonic time to Janus start\n- Added documentation for remote publishers feature in VideoRoom (SFU cascading)\n- Added PLC (packet loss concealment) support to the AudioBridge (thanks @spscream!) [[PR-3349](https://github.com/meetecho/janus-gateway/pull/3349)]\n- Cleanup participant queues when muted in the AudioBridge [[PR-3368](https://github.com/meetecho/janus-gateway/pull/3368)]\n- Added \"listannouncements\" request to the AudioBridge (thanks @keremcadirci!) [[PR-3391](https://github.com/meetecho/janus-gateway/pull/3391)]\n- Use sequence numbers instead of timestamps for the jitter buffer in the AudioBridge [[PR-3406](https://github.com/meetecho/janus-gateway/pull/3406)]\n- Fixed event handers for SIP plugin when using Sofia SIP >= 1.13 (thanks @ ycherniavskyi!) [[PR-3386](https://github.com/meetecho/janus-gateway/pull/3386)]\n- Fixed management of data buffering in Streaming plugin [[PR-3412](https://github.com/meetecho/janus-gateway/pull/3412)]\n- Fixed small leak in Lua and Duktape plugins [[PR-3409](https://github.com/meetecho/janus-gateway/pull/3409)]\n- Fixed recvonly m-lines not being added to SDP in janus.js when offering\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.2.3] - 2024-06-20\n\n- Reduced size of RTP header struct in core\n- Added support for helper threads to VideoRoom [[PR-3067]((https://github.com/meetecho/janus-gateway/pull/3067)]\n- Fixed rare race condition in VideoRoom when destroying rooms [[PR-3361]((https://github.com/meetecho/janus-gateway/pull/3361)]\n- Fixed rare crash in VideoRoom when using SVC\n- Added optional RNNoise support to AudioBridge [[PR-3185]((https://github.com/meetecho/janus-gateway/pull/3185)]\n- Handle jitter buffer delay manually in AudioBridge [[PR-3353]((https://github.com/meetecho/janus-gateway/pull/3353)]\n- Fixed rare segfault when changing rooms in AudioBridge [[PR-3356]((https://github.com/meetecho/janus-gateway/pull/3356)]\n- Empty queues in AudioBridge when muting status changes [[PR-3368]((https://github.com/meetecho/janus-gateway/pull/3368)]\n- Fixed rare deadlock in AudioBridge plugin when closing connections [[PR-3387]((https://github.com/meetecho/janus-gateway/pull/3387)]\n- Fixed compilation errors on MacOS for HTTP transport plugin [[Issue-3366]((https://github.com/meetecho/janus-gateway/issues/3366)]\n- Fixed missing '--version' command line switch (thanks @fancycode!) [[PR-3384]((https://github.com/meetecho/janus-gateway/pull/3384)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.2.2] - 2024-04-02\n\n* Update demos and docs to Bootstrap 5.x [[PR-3300](https://github.com/meetecho/janus-gateway/pull/3300)]\n* Fixed rare race condition in VideoRoom [[PR-3331](https://github.com/meetecho/janus-gateway/pull/3331)]\n* Fixed broken end-to-end encryption for subscribers in VideoRoom\n* Fixed ports leak when using remote publishers in VideoRoom plugin [[Issue-3345](https://github.com/meetecho/janus-gateway/issues/3345)]\n* Perform audio-level detection in AudioBridge participant thread [[PR-3312](https://github.com/meetecho/janus-gateway/pull/3312)]\n* Fixed memory leak in AudioBridge in case of late packets\n* Ship speexdsp's jitter buffer as part of local AudioBridge dependencies [[PR-3348](https://github.com/meetecho/janus-gateway/pull/3348)]\n* Add support of abs-capture-time RTP extension to Streaming plugin (thanks @IbrayevRamil!) [[PR-3291](https://github.com/meetecho/janus-gateway/pull/3291)]\n* Don't call close_pc in SIP plugin if there was no SDP [[PR-3339](https://github.com/meetecho/janus-gateway/pull/3339)]\n* Fixed broken faststart when postprocessing AV1 recordings (thanks @corthiclem!) [[PR-3317](https://github.com/meetecho/janus-gateway/pull/3317)]\n* Added new connectionState callback to janus.js (thanks @RSATom!) [[PR-3343](https://github.com/meetecho/janus-gateway/pull/3343)]\n* Exposed Janus and Admin API ping request via GET [[Issue-3336](https://github.com/meetecho/janus-gateway/issues/3336)]\n* Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.2.1] - 2023-12-06\n\n* Added support for abs-capture-time RTP extension [[PR-3161](https://github.com/meetecho/janus-gateway/pull/3161)]\n* Fixed truncated label in datachannels (thanks @veeting!) [[PR-3265](https://github.com/meetecho/janus-gateway/pull/3265)]\n* Support larger values for SDP attributes (thanks @petarminchev!) [[PR-3282](https://github.com/meetecho/janus-gateway/pull/3282)]\n* Fixed rare crash in VideoRoom plugin (thanks @tmatth!) [[PR-3259](https://github.com/meetecho/janus-gateway/pull/3259)]\n* Don't create VideoRoom subscriptions to publisher streams with no associated codecs\n* Added suspend/resume participant API to AudioBridge [[PR-3301](https://github.com/meetecho/janus-gateway/pull/3301)]\n* Fixed rare crash in AudioBridge\n* Fixed rare crash in Streaming plugin when doing ICE restarts [[Issue-3288](https://github.com/meetecho/janus-gateway/issues/3288)]\n* Allow SIP and NoSIP plugins to bind media to a specific address (thanks @pawnnail!) [[PR-3263](https://github.com/meetecho/janus-gateway/pull/3263)]\n* Removed advertised support for SIP UPDATE in SIP plugin\n* Parse RFC2833 DTMF to custom events in SIP plugin (thanks @ywmoyue!) [[PR-3280](https://github.com/meetecho/janus-gateway/pull/3280)]\n* Fixed missing Contact header in some dialogs in SIP plugin (thanks @ycherniavskyi!) [[PR-3286](https://github.com/meetecho/janus-gateway/pull/3286)]\n* Properly set mid when notifying about ended tracks in janus.js\n* Fixed broken restamping in janus-pp-rec\n* Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.2.0] - 2023-08-09\n\n- Added support for VP9/AV1 simulcast, and fixed broken AV1/SVC support [[PR-3218](https://github.com/meetecho/janus-gateway/pull/3218)]\n- Fixed RTCP out quality stats [[PR-3228](https://github.com/meetecho/janus-gateway/pull/3228)]\n- Default link quality stats to 100\n- Added support for ICE consent freshness [[PR-3234](https://github.com/meetecho/janus-gateway/pull/3234)]\n- Fixed rare race condition in VideoRoom [[PR-3219](https://github.com/meetecho/janus-gateway/pull/3219)] [[PR-3247](https://github.com/meetecho/janus-gateway/pull/3247)]\n- Use speexdsp's jitter buffer in the AudioBridge [[PR-3233](https://github.com/meetecho/janus-gateway/pull/3233)]\n- Fixed crash in Streaming plugin on mountpoints with too many streams [[Issue-3225](https://github.com/meetecho/janus-gateway/issues/3225)]\n- Support for batched configure requests in Streaming plugin (thanks @petarminchev!) [[PR-3239](https://github.com/meetecho/janus-gateway/pull/3239)]\n- Fixed rare deadlock in Streamin plugin [[PR-3250](https://github.com/meetecho/janus-gateway/pull/3250)]\n- Fix simulated leave message for longer string ID rooms in TextRoom (thanks @adnanel!) [PR-3243](https://github.com/meetecho/janus-gateway/pull/3243)]\n- Notify about count of sessions, handles and PeerConnections on a regular basis, when event handlers are enabled [[PR-3221](https://github.com/meetecho/janus-gateway/pull/3221)]\n- Fixed broken Insertable Streams for recvonly streams when answering in janus.js\n- Added background selector and blur support to Virtual Background demo\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.1.4] - 2023-05-19\n\n- Moved discussions from Google Group to Discourse\n- Fixed typo in command line argument validation\n- Refactored RTP forwarder internals as a core feature [[PR-3155](https://github.com/meetecho/janus-gateway/pull/3155)]\n- Refactored SVC processing as a core feature, and removed deprecated VP9/SVC demo [[PR-3174](https://github.com/meetecho/janus-gateway/pull/3174)]\n- Don't create IPv6 sockets if IPv6 is completely disabled [[PR-3179](https://github.com/meetecho/janus-gateway/pull/3179)]\n- Fixed some VideoRoom race conditions [[PR-3167](https://github.com/meetecho/janus-gateway/pull/3167)]\n- Added simulcast/SVC params to switch in VideoRoom (thanks @brave44!) [[PR-3197](https://github.com/meetecho/janus-gateway/pull/3197)]\n- Add support for receiving offers in Streaming plugin (for WHEP) [[PR-3199](https://github.com/meetecho/janus-gateway/pull/3199)]\n- Add newline for SIP headers that are overflown in length (thanks @zayim!) [[PR-3184](https://github.com/meetecho/janus-gateway/pull/3184)]\n- Save SIP reason state on multiple callbacks (thanks @kenangenjac!) [[PR-3210](https://github.com/meetecho/janus-gateway/pull/3210)]\n- Avoid parsing whitespace as invalid JSON when receiving WebSocket messages (thanks @htrendev!) [[PR-3208](https://github.com/meetecho/janus-gateway/pull/3208)]\n- Remove old tracks before adding/replacing new ones in janus.js [[PR-3203](https://github.com/meetecho/janus-gateway/pull/3203)]\n- Tweaks to some janus.js internals (thanks @i8-pi!) [[PR-3211](https://github.com/meetecho/janus-gateway/pull/3211)]\n- Fixed some typos and added some tweaks to Admin API demo\n- Refactored npm version of janus.js\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.1.3] - 2023-03-06\n\n- Use getaddrinfo instead of gethostbyname [[PR-3159](https://github.com/meetecho/janus-gateway/pull/3159)]\n- Removed VoiceMail plugin and demo [[PR-3172](https://github.com/meetecho/janus-gateway/pull/3172)]\n- Added configurable cap to number of queued events when reconnecting WebSocket event handler [[PR-3148](https://github.com/meetecho/janus-gateway/pull/3148)]\n- Fixed broken support for text datachannels in Streaming plugin\n- Add option to manually insert SPS/PPS RTP packets for H.264 mountpoints [[PR-3168](https://github.com/meetecho/janus-gateway/pull/3168)]\n- Fixed From/To checks when getting a SIP INVITE [[Issue-3164](https://github.com/meetecho/janus-gateway/issues/3164)]\n- Allow changing mjrs dir also when stopping recordings in AudioBridge [[Issue-3171](https://github.com/meetecho/janus-gateway/issues/3171)]\n- Allow Lua and Duktape plugins to relay extensions when relaying RTP packets [[PR-3162](https://github.com/meetecho/janus-gateway/pull/3162)]\n- Optionally support X-Forwarded-For in both HTTP and WebSocket transports [[PR-3160](https://github.com/meetecho/janus-gateway/pull/3160)]\n- Add reason of track being added/removed in onremotetrack in janus.js (thanks @marekpiechut!) [[PR-3150](https://github.com/meetecho/janus-gateway/pull/3150)]\n- Fixed broken VP9-SVC demo room [[Issue-3169](https://github.com/meetecho/janus-gateway/issues/3169)]\n- Linted whole JS demo codebase [[PR-3170](https://github.com/meetecho/janus-gateway/pull/3170)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.1.2] - 2023-01-17\n\n- Always add mid to the SDP, even for disabled m-lines\n- Don't allow mid changes for m-line during renegotiations [[PR-3136](https://github.com/meetecho/janus-gateway/pull/3136)]\n- Consider RTCP feedback messages when evaluating receiver SSRC\n- Added partial support for L16 codec (uncompressed audio) [[PR-3116](https://github.com/meetecho/janus-gateway/pull/3116)]\n- Fixed overwriting of 7-bit PictureID when doing VP8 simulcast [[PR-3121](https://github.com/meetecho/janus-gateway/pull/3121)]\n- Send data stats when using event handlers [[PR-3126](https://github.com/meetecho/janus-gateway/pull/3126)]\n- Copy formats from datachannel m-lines also when rejecting them [[Issue-3134](https://github.com/meetecho/janus-gateway/issues/3134)]\n- Fixed compiler issue with recent versions of libcurl (thanks @bkmgit!) [[PR-3138](https://github.com/meetecho/janus-gateway/pull/3138)]\n- Close mountpoint sockets when leaving relay thread [[PR-3143](https://github.com/meetecho/janus-gateway/pull/3143)]\n- Fixed segfault in SIP plugin in case of broken SUBSCRIBE [[Issue-3133](https://github.com/meetecho/janus-gateway/issues/3133)]\n- Support multiple requests in a single websocket message (thanks @jwittner!) [[PR-3123](https://github.com/meetecho/janus-gateway/pull/3123)]\n- Fixed inability to add recvonly tracks in janus.js ([[Issue-3119](https://github.com/meetecho/janus-gateway/issues/3119)]\n- Updated janus.d.ts type definitions (thanks @jerry4718!) [[PR-3125](https://github.com/meetecho/janus-gateway/pull/3125)]\n- Fixed out of range error when passing large SSRC values to pcap2mjr\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.1.1] - 2022-12-07\n\n- Added timing info on ICE starting and gathering done to Admin API\n- Fixed rare crash when generating SDP to send [[Issue-3081](https://github.com/meetecho/janus-gateway/issues/3081)]\n- Fixed rare crash when checking payload types (thanks @zevarito!) [[PR-3086](3086)]\n- Fixed double a=ssrc attribute in SDP for inactive m-line\n- Replaced non-portable strcasestr() with strncasecmp() (thanks @iskraman!) [[PR-3076](https://github.com/meetecho/janus-gateway/pull/3076)]\n- Fixed parameters not being URL-encoded when using TURN REST API [[Issue-3112](https://github.com/meetecho/janus-gateway/issues/3112)]\n- Fixed renegotiation sent to VideoRoom subscribers when a room is destroyed [[Issue-3083](https://github.com/meetecho/janus-gateway/issues/3083)]\n- Added option to prevent automatic SDP offer updates to VideoRoom subscribers when a publisher leaves\n- Fixed \"send\" property not being automatically reset to \"true\" in the VideoRoom for new subscriptions\n- Fixed small memory leak in AudioBridge (thanks @RSATom!) [[PR-3088](https://github.com/meetecho/janus-gateway/pull/3088)]\n- Minor fixes to the Streaming plugin\n- Enforced media direction policies when SIP call is on hold [PR-3087](https://github.com/meetecho/janus-gateway/pull/3087)]\n- Added code to send PLI to SIP peer when recording [[PR-3093](https://github.com/meetecho/janus-gateway/pull/3093)]\n- Fixed renegotiations in VideoCall not updating session properties\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.1.0] - 2022-10-03\n\n- Added versioning to .so files [[PR-3075](https://github.com/meetecho/janus-gateway/pull/3075)]\n- Allow plugins to specify msid in SDPs [[PR-2998](https://github.com/meetecho/janus-gateway/pull/2998)]\n- Fixed broken RTCP timestamp on 32bit architectures [[Issue-3045](https://github.com/meetecho/janus-gateway/issues/3045)]\n- Fixed problems compiling against recent versions of libwebsockets [[Issue-3039](https://github.com/meetecho/janus-gateway/issues/3039)]\n- Updated deprecated DTLS functions to OpenSSL v3.0 [PR-3048](https://github.com/meetecho/janus-gateway/pull/3048)]\n- Switched to SHA256 for signing self signed DTLS certificates (thanks @tgabi333!) [[PR-3069](https://github.com/meetecho/janus-gateway/pull/3069)]\n- Started using strnlen to optimize performance of some strlen calls (thanks @tmatth!) [[PR-3059](https://github.com/meetecho/janus-gateway/pull/3059)]\n- Added checks to avoid RTX payload type collisions [[PR-3080](https://github.com/meetecho/janus-gateway/pull/3080)]\n- Added new APIs for cascading VideoRoom publishers [[PR-3014](https://github.com/meetecho/janus-gateway/pull/3014)]\n- Fixed deadlock when using legacy switch in VideoRoom [[Issue-3066](https://github.com/meetecho/janus-gateway/issues/3066)]\n- Fixed disabled property not being advertized to subscribers when VideoRoom publishers removed tracks\n- Fixed occasional deadlock when using G.711 in the AudioBridge [[Issue-3062](https://github.com/meetecho/janus-gateway/issues/3062)]\n- Added new way of capturing devices/tracks in janus.js [[PR-3003](https://github.com/meetecho/janus-gateway/pull/3003)]\n- Removed call to .stop() for remote tracks in demos [[PR-3056](https://github.com/meetecho/janus-gateway/pull/3056)]\n- Fixed missing message/info/transfer buttons in SIP demo page\n- Fixed postprocessing compilation issue on older FFmpeg versions [[PR-3064](https://github.com/meetecho/janus-gateway/pull/3064)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.0.4] - 2022-08-01\n\n- Fixed problem with duplicate ptypes when codecs are added in renegotiations\n- Added codec info to event handlers stats\n- Allow offers to include other roles besides 'actpass' for DTLS [[PR-3020](https://github.com/meetecho/janus-gateway/pull/3020)]\n- Fixed rare race conditions when attempting to relay packets sent by plugins [[PR-3010](https://github.com/meetecho/janus-gateway/pull/3010)]\n- Fixed unprotected access to medium instances in janus_plugin_handle_sdp\n- Set appropriate channel type when sending DATA_CHANNEL_OPEN_REQUEST message (thanks @ktyu!) [[PR-3018](https://github.com/meetecho/janus-gateway/pull/3018)]\n- Fixed rare race condition when handling incoming RTCP feedback in VideoRoom\n- Fixed memory leak in VideoRoom when using rid-based simulcast (thanks @OxleyS!) [[PR-2995](https://github.com/meetecho/janus-gateway/pull/2995)]\n- Fixed IPv6 always enabled for VideoRoom RTP forwarders [[Issue-3011](https://github.com/meetecho/janus-gateway/issues/3011)]\n- Start recording VideoRoom publisher on PeerConnection establishment, if needed (thanks @adnanel!) [[PR-3013](https://github.com/meetecho/janus-gateway/pull/3013)]\n- Added an optional ID in subscribe requests to match with subscription events (thanks @JanFellner!) [[PR-3027](https://github.com/meetecho/janus-gateway/pull/3027)]\n- Make Streaming plugin use SDP utils, and codecs instead of rtpmaps [[PR-2994](https://github.com/meetecho/janus-gateway/pull/2994)]\n- Check response codes of RTSP requests in Streaming plugin [[Issue-3015](https://github.com/meetecho/janus-gateway/issues/3015)]\n- Fixed small memory leak in SIP plugin [[Issue-3032](https://github.com/meetecho/janus-gateway/issues/3032)]\n- Fixed broken simulcast support in Lua and Duktape plugins\n- Don't use .clone() on tracks to render them in demos [[PR-3009](https://github.com/meetecho/janus-gateway/pull/3009)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.0.3] - 2022-06-20\n\n- Keep track of RTP extensions when storing packets for retransmission [[PR-2981](https://github.com/meetecho/janus-gateway/pull/2981)]\n- Fixed negotiation of RTP extensions when direction is involved\n- Fixed broken VP8 payload descriptor parsing when 7-bit PictureID are used\n- Support for batched configure requests in VideoRoom [[PR-2986](https://github.com/meetecho/janus-gateway/pull/2986)]\n- Added missing info to VideoRoom publisher's info own event [[Issue-2988](https://github.com/meetecho/janus-gateway/issues/2988)]\n- Fixed memory leaks in when upgrading old-style Videoroom requests (thanks @OxleyS!) [[PR-3002](https://github.com/meetecho/janus-gateway/pull/3002)]\n- Fixed memory leak in VideoRoom when updating subscriptions with no changes\n- Added 'kick_all' requests and possibility to remove PIN code to both Audiobridge and Streaming plugins (thanks @mikaelnousiainen!) [[PR-2978](https://github.com/meetecho/janus-gateway/pull/2978)]\n- Added support for notifications in the Streaming plugin when metadata for a mountpoint is changed (thanks @amoizard!) [[PR-3000](https://github.com/meetecho/janus-gateway/pull/3000)]\n- Fixed missing checks on auth challenges in SIP plugin\n- Fixed missing Contact header in SUBSCRIBE requests in SIP plugin [[PR-2973](https://github.com/meetecho/janus-gateway/pull/2973)]\n- Fixed segfault in SIP plugin when freeing a session with a subscription still active [[PR-2974](https://github.com/meetecho/janus-gateway/pull/2974)]\n- Add new shared JavaScript file for settings in demos [[PR-2991](https://github.com/meetecho/janus-gateway/pull/2991)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.0.2] - 2022-05-23\n\n- Abort DTLS handshake if DTLSv1_handle_timeout returns an error\n- Fixed rtx not being offered on Janus originated PeerConnections\n- Added configurable property to put a cap to task threads [[Issue-2964](https://github.com/meetecho/janus-gateway/issues/2964)]\n- Fixed build issue with libressl >= 3.5.0 (thanks @ffontaine!) [[PR-2980](https://github.com/meetecho/janus-gateway/pull/2980)]\n- Link to -lresolv explicitly when building websockets transport\n- Fixed RED parsing not returning blocks when only primary data is available\n- Fixed typo in stereo support in EchoTest plugin\n- Added support for dummy publishers in VideoRoom [[PR-2958](https://github.com/meetecho/janus-gateway/pull/2958)]\n- Added new VideoRoom request to combine subscribe and unsubscribe operations [[PR-2962](https://github.com/meetecho/janus-gateway/pull/2962)]\n- Fixed incorrect removal of owner/subscriptions mapping in VideoRoom plugin [[Issue-2965](https://github.com/meetecho/janus-gateway/issues/2965)]\n- Explicitly return list of IDs VideoRoom users are subscribed to for data [[Issue-2967](https://github.com/meetecho/janus-gateway/issues/2967)]\n- Fixed data port not being returned when creating Streaming mountpoints with the legacy API\n- Fix address size in Streaming plugin RTCP sendto call (thanks @sjkummer!) [[PR-2976](https://github.com/meetecho/janus-gateway/pull/2976)]\n- Added custom headers for SIP SUBSCRIBE requests (thanks @oriol-c!) [[PR-2971](https://github.com/meetecho/janus-gateway/pull/2971)]\n- Make SIP timer T1X64 configurable (thanks @oriol-c!) [[PR-2972](https://github.com/meetecho/janus-gateway/pull/2972)]\n- Disable IPv6 in WebSockets transport if binding to IPv4 address explicitly [[Issue-2969](https://github.com/meetecho/janus-gateway/issues/2969)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.0.1] - 2022-04-26\n\n- Removed gengetopt as a dependency, to use Glib's GOptionEntry instead [[PR-2898](https://github.com/meetecho/janus-gateway/pull/2898)]\n- Fixed occasional problem of duplicate mid attribute in Janus SDPs [[Issue-2917](https://github.com/meetecho/janus-gateway/issues/2917)]\n- Fixed receiving=false events not being sent right away for higher simulcast substreams [[Issue-2919](https://github.com/meetecho/janus-gateway/issues/2919)]\n- Fix highest sequence number not being properly initialized in the RTCP context [[Issues-2920](https://github.com/meetecho/janus-gateway/issues/2920)]\n- Reset rids when renegotiating SDPs [[PR-2931](https://github.com/meetecho/janus-gateway/pull/2931)]\n- Fixed missing PLI when restoring previously paused streams in VideoRoom (thanks @flaviogrossi!) [[PR-2922](https://github.com/meetecho/janus-gateway/pull/2922)]\n- Fixed deadlock when using the moderate API in the VideoRoom [[Issue-2956](https://github.com/meetecho/janus-gateway/issues/2956)]\n- Check if IPv6 is disabled to avoid failure when creating forwarder sockets in AudioBridge and VideoRoom [[PR-2916](https://github.com/meetecho/janus-gateway/pull/2916)]\n- Fixed invalid computation of Streaming mountpoint stream age (thanks @RouquinBlanc!) [[PR-2928](https://github.com/meetecho/janus-gateway/pull/2928)]\n- Also return reason header protocol and cause if present in BYE in the SIP plugin (thanks @ajsa-terko!) [[PR-2935](https://github.com/meetecho/janus-gateway/pull/2935)]\n- Fixed segfault in UNIX transport teardown caused by pathnames of different sizes\n- Added new demos on WebAudio and Virtual Backgrounds [[PR-2941](https://github.com/meetecho/janus-gateway/pull/2941)]\n- Fixed potential race conditions in multistream VideoRoom demo [[Issue-2929](https://github.com/meetecho/janus-gateway/issues/2929)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v1.0.0] - 2022-03-03\n\n- Refactored Janus to support multistream PeerConnections [[PR-2211](https://github.com/meetecho/janus-gateway/pull/2211)]\n- Moved all source files under new 'src' folder to unclutter the repo [[PR-2885](https://github.com/meetecho/janus-gateway/pull/2885)]\n- Fixed definition of trylock wrapper when using pthreads [[Issue-2894](https://github.com/meetecho/janus-gateway/issues/2894)]\n- Fixed broken RTP when no extensions are negotiated\n- Added checks when inserting RTP extensions to avoid buffer overflows\n- Added missing support for disabled rid simulcast substreams in SDP [[PR-2888](https://github.com/meetecho/janus-gateway/pull/2888)]\n- Fixed TWCC feedback when simulcast SSRCs are missing (thanks @OxleyS!) [[PR-2908](https://github.com/meetecho/janus-gateway/pull/2908)]\n- Added support for playout-delay RTP extension [[PR-2895](https://github.com/meetecho/janus-gateway/pull/2895)]\n- Fixed partially broken H.264 support when using Firefox in VideoRoom\n- Fixed new VideoRoom rtp_forward API ignoring some properties\n- Fixed deadlock and segfault when stopping Streaming mountpoint recordings [[Issue-2902](https://github.com/meetecho/janus-gateway/issues/2902)]\n- Fixed RTSP support in Streaming plugin for cameras that expect path-only DESCRIBE requests (thanks @jp-bennett!) [[PR-2909](https://github.com/meetecho/janus-gateway/pull/2909)]\n- Fixed RTP being relayed incorrectly in Lua and Duktape plugins\n- Added Duktape as optional dependency, instead of embedding the engine code [[PR-2886](https://github.com/meetecho/janus-gateway/pull/2886)]\n- Fixed crash at startup when not able to connect to RabbitMQ server\n- Improved fuzzing and checks on RTP extensions\n- Removed distinction between simulcast and simulcast2 in janus.js [[PR-2887](https://github.com/meetecho/janus-gateway/pull/2887)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.8] - 2022-02-11\n\n- Added initial (and limited) integration of RED audio [[PR-2685](https://github.com/meetecho/janus-gateway/pull/2685)]\n- Added support for Two-Byte header RTP extensions (RFC8285) and, partially, for the new Depencency Descriptor RTP extension (needed for AV1-SVC) [[PR-2741](https://github.com/meetecho/janus-gateway/pull/2741)]\n- Fixed rare race conditions between sending a packet and closing a connection [[PR-2869](https://github.com/meetecho/janus-gateway/pull/2869)]\n- Fix last stats before closing PeerConnection not being sent to handlers (thanks @zodiak83!) [[PR-2874](https://github.com/meetecho/janus-gateway/pull/2874)]\n- Changed automatic allocation on static loops from round robin to least used [[PR-2878](https://github.com/meetecho/janus-gateway/pull/2878)]\n- Added new API to bulk start/stop MJR-based recordings in AudioBridge [[PR-2862](https://github.com/meetecho/janus-gateway/pull/2862)]\n- Fixed broken duration in spatial AudioBridge recordings\n- Fixed broken G.711 RTP forwarding in AudioBridge (thanks @AlexYaremchuk!) [[PR-2875](https://github.com/meetecho/janus-gateway/pull/2875)]\n- Fixed broken recordings in NoSIP plugin\n- Fixed warnings when postprocessing Opus recordings with DTX packets\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.7] - 2022-01-24\n\n- Added faster strlcat variant that uses memccpy for writing SDPs [[PR-2835](https://github.com/meetecho/janus-gateway/pull/2835)]\n- Fixed occasional crash when updating WebRTC sessions [[Issue-2840](https://github.com/meetecho/janus-gateway/issues/2840)]\n- Changed SDP syntax for AV1 from \"AV1X\" to \"AV1\" [[Issue-2844](https://github.com/meetecho/janus-gateway/issues/2844)]\n- Fixed signed_tokens property not being saved to permanent rooms in VideoRoom (thanks @timsolov!) [[PR-2843](https://github.com/meetecho/janus-gateway/pull/2843)]\n- Made record directory changeable via \"edit\" in both AudioBridge and VideoRoom\n- Added configurable expected loss to AudioBridge to actually send FEC [[PR-2802](https://github.com/meetecho/janus-gateway/pull/2802)]\n- Fixed SIP plugin not working when using Sofia SIP >= 1.13 [[Issue-2683](https://github.com/meetecho/janus-gateway/issues/2683)]\n- Fixed occasional crashes in SIP plugin [[Issue-2853](https://github.com/meetecho/janus-gateway/issues/2853)]\n- Take note of video orientation extension when recording video in SIP plugin (thanks @adnanel!) [[PR-2836](https://github.com/meetecho/janus-gateway/pull/2836)]\n- Allow 180 besides 183 to have SDP as well (thanks @lejlasolak!) [[PR-2849](https://github.com/meetecho/janus-gateway/pull/2849)]\n- Fixed post-processor compilation issue with newer versions of FFmpeg [[Issue-2833](https://github.com/meetecho/janus-gateway/issues/2833)]\n- Added option to print extended info on MJR file as JSON in postprocessor (thanks @adnanel!) [[PR-2858](https://github.com/meetecho/janus-gateway/pull/2858)]\n- Allow pcap2mjr to autodetect SSRC\n- Fixed problems compiling post-processor with older versions of FFmpeg\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.6] - 2021-12-13\n\n- Added strlcat helper to detect and report truncations [[PR-2792](https://github.com/meetecho/janus-gateway/pull/2792)]\n- Grow buffer as needed when generating SDPs [[PR-2797](https://github.com/meetecho/janus-gateway/pull/2797)]\n- Added DTX support to some plugins [[PR-2789](https://github.com/meetecho/janus-gateway/pull/2789)]\n- Added option to forcibly quit Janus when getting dlopen errors (thanks @tmatth!) [[PR-2828](https://github.com/meetecho/janus-gateway/pull/2828)]\n- Fixed broken signed tokens in VideoRoom when using UUIDs (thanks @timsolov!) [[PR-2812](https://github.com/meetecho/janus-gateway/pull/2812)]\n- Added option to choose whether signed tokens should be used in the VideoRoom when enabled in the core [[PR-2825](https://github.com/meetecho/janus-gateway/pull/2825)]\n- Added MESSAGE authentication and out-of-dialog MESSAGE support to SIP plugin (thanks thetechpanda!) [[PR-2786](https://github.com/meetecho/janus-gateway/pull/2786)]\n- Fixed potential race conditions in SIP plugin [[PR-2823](https://github.com/meetecho/janus-gateway/pull/2823)]\n- Added basic history support to TextRoom plugin [[PR-2814](https://github.com/meetecho/janus-gateway/pull/2814)]\n- Fixed Cross-site Scripting (XSS) vulnerability in some of the demos (thanks @SoufElhabti!) [[PR-2817](https://github.com/meetecho/janus-gateway/pull/2817)]\n- Added support for custom datachannel options in janus.js (thanks @sqxieshuai!) [[PR-2806](https://github.com/meetecho/janus-gateway/pull/2806)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.5] - 2021-10-18\n\n- Add API to optionally force Janus to use TURN [[PR-2774](https://github.com/meetecho/janus-gateway/pull/2774)]\n- Fixed slow path on SDP parsing [[PR-2776](https://github.com/meetecho/janus-gateway/pull/2776)]\n- Added event handlers option to send stats for a PeerConnection in a single event, rather than per-media (thanks @JanFellner!) [[PR-2785](https://github.com/meetecho/janus-gateway/pull/2785)]\n- Fixed occasional deadlocks on malformed requests in VideoRoom [[Issue-2780](https://github.com/meetecho/janus-gateway/issues/2780)]\n- Fixed AudioBridge plain RTP thread sometimes exiting prematurely\n- Fixed broken upsampling when using G.711 in AudioBridge\n- Add pause/resume recording functionality to Record&Play and SIP plugins (thanks @isnumanagic!) [[PR-2724](https://github.com/meetecho/janus-gateway/pull/2724)]\n- Fixed broken support for Unix Sockets in WebSockets Admin API (thanks @thatsmydoing!) [[PR-2787](https://github.com/meetecho/janus-gateway/pull/2787)]\n- Added timing info for video rotation when post-processing recordings\n- Added linter checks to janus.js (thanks @davel!) [[PR-2272](https://github.com/meetecho/janus-gateway/pull/2272)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.4] - 2021-09-06\n\n- Fixed ICE restart issues with recent versions of libnice [[PR-2729](https://github.com/meetecho/janus-gateway/pull/2729)]\n- Changed randon number generators to use crypto-safe functions (thanks @jmfotokite!) [[PR-2738](https://github.com/meetecho/janus-gateway/pull/2738)]\n- Added support for abs-send-time RTP extension [[PR-2721](https://github.com/meetecho/janus-gateway/pull/2721)]\n- Added configurable mechanism for manually setting static event loop to use for new handles [[PR-2684](https://github.com/meetecho/janus-gateway/pull/2684)]\n- Fixed datachannel protocol not being sent to plugins for incoming messages [[Issue-2753](https://github.com/meetecho/janus-gateway/issues/2753)]\n- Added ability to specify recordings folder in AudioBridge [[PR-2707](https://github.com/meetecho/janus-gateway/pull/2707)]\n- Added support for forwarding groups in AudioBridge [[PR-2653](https://github.com/meetecho/janus-gateway/pull/2653)]\n- Fixed missing Contact header in SIP plugin when using Sofia >= 1.13 [[PR-2708](https://github.com/meetecho/janus-gateway/pull/2708)]\n- Better SDES-SRTP negotiation in SIP and NoSIP plugins [[PR-2727](https://github.com/meetecho/janus-gateway/pull/2727)]\n- Fixed WebSocket transport and event handler lagging 25/30s when shutting down or reconnecting (thanks @JanFellner!) [[Issue-2734](https://github.com/meetecho/janus-gateway/issues/2734)]\n- Fixed incoming_header_prefixes not working for helper sessions in SIP plugin\n- Fix partial/broken ACL support in TextRoom plugin [[PR-2763](https://github.com/meetecho/janus-gateway/pull/2763)]\n- Fixed potential race condition when reclaiming sessions in HTTP transport plugin\n- Fixed WebSocket event handler reconnect mechanism (thanks @JanFellner!) [[PR-2736](https://github.com/meetecho/janus-gateway/pull/2736)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.3] - 2021-06-15\n\n- Fixed rare crash when detaching handles [[Issue-2464](https://github.com/meetecho/janus-gateway/issues/2464)]\n- Added option to offer IPv6 link-local candidates as well [[PR-2689](https://github.com/meetecho/janus-gateway/pull/2689)]\n- Added spatial audio support to AudioBridge via stereo mixing [[PR-2446](https://github.com/meetecho/janus-gateway/pull/2446)]\n- Added support for plain RTP participants to AudioBridge [[PR-2464](https://github.com/meetecho/janus-gateway/pull/2464)]\n- Added API to start/stop AudioBridge recordings dynamically (thanks @rajneeshksoni!) [[PR-2674](https://github.com/meetecho/janus-gateway/pull/2674)]\n- Fixed broken mountpoint switching when using different payload types in Streaming plugin [[PR-2692](https://github.com/meetecho/janus-gateway/pull/2692)]\n- Fixed occasional deadlock on Streaming plugin mountpoint destroy during RTSP reconnects (thanks @lionelnicolas!) [[PR-2700](https://github.com/meetecho/janus-gateway/pull/2700)]\n- Added \"Expires\" support to SUBSCRIBE in SIP plugin (thanks @nicolasduteil!) [[PR-2661](https://github.com/meetecho/janus-gateway/pull/2661)]\n- Added option to specify Call-ID for SUBSCRIBE dialogs in SIP plugin (thanks @nicolasduteil!) [[PR-2664](https://github.com/meetecho/janus-gateway/pull/2664)]\n- Fixed broken simulcast support in VideoCall plugin (thanks @lucily-star!) [[PR-2671](https://github.com/meetecho/janus-gateway/pull/2671)]\n- Implemented RabbitMQ reconnection logic, in both transport and event handler (thanks @chriswiggins!) [[PR-2651](https://github.com/meetecho/janus-gateway/pull/2651)]\n- Added support for renegotiation of external streams in janus.js (thanks @kmeyerhofer!) [[PR-2604](https://github.com/meetecho/janus-gateway/pull/2604)]\n- Added support for HEVC/H.265 aggregation packets (AP) to janus-pp-rec (thanks @nu774!) [[PR-2662](https://github.com/meetecho/janus-gateway/pull/2662)]\n- Refactored janus-pp-rec to cleanup the code, and use libavformat for Opus as well (thanks @lu-zero!) [[PR-2665](https://github.com/meetecho/janus-gateway/pull/2665)]\n- Added additional target formats for some recorded codecs [[PR-2680](https://github.com/meetecho/janus-gateway/pull/2680)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.2] - 2021-05-03\n\n- Added support for relative paths in config files, currently only in MQTT event handler (thanks @RSATom!) [[PR-2623](https://github.com/meetecho/janus-gateway/pull/2623)]\n- Removed support for now deprecated frame-marking RTP extension [[PR-2640](https://github.com/meetecho/janus-gateway/pull/2640)]\n- Fixed rare race condition between VideoRoom publisher leaving and subscriber hanging up [[PR-2637](https://github.com/meetecho/janus-gateway/pull/2637)]\n- Fixed occasional crash when using announcements in AudioBridge\n- Fixed rare crash in Streaming plugin when reconnecting RTSP streams (thanks @lucylu-star!) [[PR-2542](https://github.com/meetecho/janus-gateway/pull/2542)]\n- Fixed broken switch in Streaming plugin when using helper threads\n- Fixed rare race conditions on socket close in SIP and NoSIP plugins [[PR-2599](https://github.com/meetecho/janus-gateway/pull/2599)]\n- Added support for out-of-dialog SIP MESSAGE requests (thanks @ihusejnovic!) [[PR-2616](https://github.com/meetecho/janus-gateway/pull/2616)]\n- Fixed memory leak when using helper threads in Streaming plugin\n- Added support for datachannel label/protocol to Lua and Duktape plugins [[PR-2641](https://github.com/meetecho/janus-gateway/pull/2641)]\n- Added ability to use WebSockets transport over Unix sockets (thanks @mdevaev!) [[PR-2620](https://github.com/meetecho/janus-gateway/pull/2620)]\n- Added janus-pp-rec mechanism to correct wrong RTP timestamps in MJR recordings (thanks @tbence94!) [[PR-2573](https://github.com/meetecho/janus-gateway/pull/2573)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.11.1] - 2021-04-06\n\n- Add new option to configure ICE nomination mode, if libnice is recent enough [[PR-2541](https://github.com/meetecho/janus-gateway/pull/2541)]\n- Added support for per-session timeout values (thanks @alg!) [[PR-2577](https://github.com/meetecho/janus-gateway/pull/2577)]\n- Added support for compilation on FreeBSD (thanks @jsm222!) [[PR-2508](https://github.com/meetecho/janus-gateway/pull/2508)]\n- Fixed occasional auth errors when using both API secret and stored tokens (thanks @deep9!) [[PR-2581](https://github.com/meetecho/janus-gateway/pull/2581)]\n- Added support for stdout logging to daemon-mode as well (thanks @mtorromeo!) [[PR-2591](https://github.com/meetecho/janus-gateway/pull/2591)]\n- Fixed odr-violation issue between Lua and Duktape plugins [[PR-2540](https://github.com/meetecho/janus-gateway/pull/2540)]\n- Fixed missing simulcast stats in Admin API and Event Handlers when using rid [[Issue-2610](https://github.com/meetecho/janus-gateway/issues/2610)]\n- Fixed VideoRoom recording not stopped for participants entering after global recording was started [[PR-2550](https://github.com/meetecho/janus-gateway/pull/2550)]\n- Fixed 'audiocodec'/'videocodec' being ignored when joining a VideoRoom via 'joinandconfigure'\n- Added content type support to MESSAGE in SIP plugin (thanks @tijmenNL!) [[PR-2567](https://github.com/meetecho/janus-gateway/pull/2567)]\n- Made RTSP timeouts configurable in Streaming plugin (thanks @pontscho!) [[PR-2598](https://github.com/meetecho/janus-gateway/pull/2598)]\n- Fixed incorrect parsing of backend URL in WebSockets event handler [[Issue-2603](https://github.com/meetecho/janus-gateway/issues/2603)]\n- Added support for secure connections and lws debugging to WebSockets event handler\n- Fixed occasionally broken AV1 recordings post-processing\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.10.10] - 2021-02-06\n\n- Reduced verbosity of a few LOG_WARN messages at startup\n- Close libnice agent resources asynchronously when hanging up PeerConnections (thanks @fbellet!) [[PR-2492](https://github.com/meetecho/janus-gateway/pull/2492)]\n- Fixed broken parsing of SDP when trying to match specific codec profiles [[PR-2549](https://github.com/meetecho/janus-gateway/pull/2549)]\n- Added muting/moderation API to the VideoRoom plugin [[PR-2513](https://github.com/meetecho/janus-gateway/pull/2513)]\n- Fixed a few race conditions in VideoRoom plugin that could lead to crashes [[PR-2539](https://github.com/meetecho/janus-gateway/pull/2539)]\n- Send 480 instead of BYE when hanging up calls in early dialog in the SIP plugin (thanks @zayim!) [[PR-2521](https://github.com/meetecho/janus-gateway/pull/2521)]\n- Added configurable media direction when putting calls on-hold in the SIP plugin [[PR-2525](https://github.com/meetecho/janus-gateway/pull/2525)]\n- Fixed rare race condition in AudioBridge when using \"changeroom\" (thanks @JeckLabs!) [[PR-2535](https://github.com/meetecho/janus-gateway/pull/2535)]\n- Fixed broken API secret management in HTTP long polls (thanks @remvst!) [[PR-2524](https://github.com/meetecho/janus-gateway/pull/2524)]\n- Report failure if binding to a socket fails in WebSockets transport plugin (thanks @Symbiatch!) [[PR-2534](https://github.com/meetecho/janus-gateway/pull/2534)]\n- Updated RabbitMQ logic in both transport and event handler (thanks @chriswiggins!) [[PR-2430](https://github.com/meetecho/janus-gateway/pull/2430)]\n- Fixed segfault in WebSocket event handler when backend was unreachable\n- Added TLS support to MQTT event handler (thanks @RSATom!) [[PR-2517](https://github.com/meetecho/janus-gateway/pull/2517)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.10.9] - 2020-12-23\n\n- Replaced Travis CI with GitHub Actions [[PR-2486](https://github.com/meetecho/janus-gateway/pull/2486)]\n- Fixed data channel messages potentially getting stuck in case of burst transfers (thanks @afshin2003!) [[PR-2427](https://github.com/meetecho/janus-gateway/pull/2427)]\n- Fixed simulcast issues when renegotiating PeerConnections [[Issue-2466](https://github.com/meetecho/janus-gateway/issues/2466)]\n- Added configurable TURN REST API timeout (thanks @evorw!) [[PR-2470](https://github.com/meetecho/janus-gateway/pull/2470)]\n- Added support for recording of binary data channels [[PR-2481](https://github.com/meetecho/janus-gateway/pull/2481)]\n- Fixed occasional SRTP errors when pausing and then resuming Streaming plugin handles after a long time\n- Fixed occasional SRTP errors when leaving and joining AudioBridge rooms without a new PeerConnection after a long time\n- Added support for playout of data channels in Record&Play plugin and demo (thanks @ricardo-salgado-tekever!) [[PR-2468](https://github.com/meetecho/janus-gateway/pull/2468)]\n- Added option to override connections limit in HTTP transport plugin [[PR-2489](https://github.com/meetecho/janus-gateway/pull/2489)]\n- Added options to enable libmicrohttpd debugging in HTTP transport plugin (thanks @evorw!) [[PR-2471](https://github.com/meetecho/janus-gateway/pull/2471)]\n- Fixed a few compile and runtime issues in WebSocket event handler\n- Refactored postprocessing management of timestamps to fix some key issues [[PR-2345](https://github.com/meetecho/janus-gateway/pull/2345)]\n- Fixed postprocessing of audio recordings containing RTP silence suppression packets [[PR-2467](https://github.com/meetecho/janus-gateway/pull/2467)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.10.8] - 2020-11-23\n\n- Added differentiation between IPv4 and IPv6 NAT-1-1 addresses [[PR-2423](https://github.com/meetecho/janus-gateway/pull/2423)]\n- Made NACK buffer cleanup on outgoing keyframe disabled by default but configurable [[PR-2402](https://github.com/meetecho/janus-gateway/pull/2402)]\n- Added support for simulcast and TWCC to Duktape and Lua plugins [[PR-2409](https://github.com/meetecho/janus-gateway/pull/2409)]\n- Fixed rare crash in AudioBridge plugin when leaving a room [[Issue-2432](https://github.com/meetecho/janus-gateway/issues/2432)]\n- Fixed codec names not being updated in the SIP plugin after renegotiations (thanks @ihusejnovic!) [[PR-2417](https://github.com/meetecho/janus-gateway/pull/2417)]\n- Fixed crash in SIP plugin when handling REGISTER challenges without WWW-Authenticate headers [[Issue-2419](https://github.com/meetecho/janus-gateway/issues/2419)]\n- Added option to SIP plugin to let users CANCEL pending transactions without waiting for a 1xx [[PR-2434](https://github.com/meetecho/janus-gateway/pull/2434)]\n- Added option to enforce CORS on the server side in both HTTP and WebSocket transport plugins [[PR-2410](https://github.com/meetecho/janus-gateway/pull/2410)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.10.7] - 2020-10-30\n\n- Fixed SDP negotiation when client uses max-bundle [[Issue-2390](https://github.com/meetecho/janus-gateway/issues/2390)]\n- Added optional JSEP flag to invert processing order of simulcast \"rid\" in SDP [[PR-2385](https://github.com/meetecho/janus-gateway/pull/2385)]\n- Fixed broken rid-based simulcast when using less than 3 substreams\n- Fixed occasional misleading \"private IP\" warning on startup (thanks @npikimasu!) [[PR-2386](https://github.com/meetecho/janus-gateway/pull/2386)]\n- Added \"plugin-offer mode\" to AudioBridge [[PR-2366](https://github.com/meetecho/janus-gateway/pull/2366)]\n- Fixed occasional deadlock when sending SUBSCRIBE messages via SIP plugin [[PR-2387](https://github.com/meetecho/janus-gateway/pull/2387)]\n- Fixed occasional SIGABRT in RabbitMQ transport (thanks @david-goncalves!) [[PR-2380](https://github.com/meetecho/janus-gateway/pull/2380)]\n- Fixed broken RTP parsing in janus-pp-rec when there were too many extensions (thanks @isnumanagic!) [[PR-2411](https://github.com/meetecho/janus-gateway/pull/2411)]\n- Fixed occasional segfault when post-processing G.722 mjr recordings\n- Added configurable simulcast encodings to janus.js (thanks @fippo!) [[PR-2393](https://github.com/meetecho/janus-gateway/pull/2392)]\n- Updated old Insertable Streams APIs in janus.js and e2etest.js\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.10.6] - 2020-10-05\n\n- New mechanism to tweak/query transport plugins via Admin API [[PR-2354](https://github.com/meetecho/janus-gateway/pull/2354)]\n- Fixed occasional segfault when using event handlers and VideoRoom [[Issue-2352](https://github.com/meetecho/janus-gateway/issues/2352)]\n- Fixed occasional \"Unsupported codec 'none'\" log errors (thanks @neilyoung!) [[PR-2357](https://github.com/meetecho/janus-gateway/pull/2357)]\n- Fixed broken AudioBridge RTP forwarding when using G711 [[Issue-2375](https://github.com/meetecho/janus-gateway/issues/2375)]\n- Added helper threads support to RTSP mountpoints as well [[PR-2361](https://github.com/meetecho/janus-gateway/pull/2361)]\n- Fixed data channels not working as expected in Streaming plugin when using helper threads\n- Fixed simulcast occasionally not working in Streaming plugin until manual PLI trigger\n- Added proper fragmentation in WebSockets transport plugin [[PR-2355](https://github.com/meetecho/janus-gateway/pull/2355)]\n- Fixed timing resolution issue in MQTT transport (thanks @feymartynov!)) [[PR-2358](https://github.com/meetecho/janus-gateway/pull/2358)]\n- Fixed MQTT transport issue when trying to shutdown gracefully (thanks @feymartynov!)) [[PR-2374](https://github.com/meetecho/janus-gateway/pull/2374)]\n- Fixed broken configuration of Nanomsg Admin API (thanks @sdamodharan!)) [[PR-2372](https://github.com/meetecho/janus-gateway/pull/2372)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.10.5] - 2020-09-08\n\n- Fixed occasional crash in event handlers [[Issue-2312](https://github.com/meetecho/janus-gateway/issues/2312)]\n- Fixed occasional crash in VideoRoom plugin [[Issue-2318](https://github.com/meetecho/janus-gateway/issues/2318)]\n- Fixed missing PLI when switching Streaming mountpoint [[Issue-2333](https://github.com/meetecho/janus-gateway/issues/2333)]\n- Fixed broken recordings in VideoCall plugin (thanks @SamyCookie!) [[PR-2325](https://github.com/meetecho/janus-gateway/pull/2325)]\n- Fixed \"kick\" not working in TextRoom plugin (thanks @backface!) [[PR-2332](https://github.com/meetecho/janus-gateway/pull/2332)]\n- Fixed occasional post-processing issues with incomplete mjr files (thanks @SamyCookie!) [[PR-2356](https://github.com/meetecho/janus-gateway/pull/2356)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n## [v0.10.4] - 2020-08-07\n\n- Fixed usrsctp vulnerability by using internal hashmap in SCTP code [[PR-2302](https://github.com/meetecho/janus-gateway/pull/2302)]\n- Fixed some issues when using BoringSSL for DTLS (thanks @fancycode!) [[PR-2278](https://github.com/meetecho/janus-gateway/pull/2278)]\n- Added support for multiple nat-1-1 addresses (thanks @fancycode!) [[PR-2279](https://github.com/meetecho/janus-gateway/pull/2279)]\n- Fixed negotiation issue on Firefox when Janus is built without datachannels [[PR-2281](https://github.com/meetecho/janus-gateway/pull/2281)]\n- Fixed small memory leaks when dealing with local candidates (thanks @fancycode!) [[PR-2288](https://github.com/meetecho/janus-gateway/pull/2288)]\n- Fixed occasional segfault in VideoRoom when failing to setup a new subscriber [[Issue-2277](https://github.com/meetecho/janus-gateway/issues/2277)]\n- Fixed potential deadlock in AudioBridge when switching rooms (thanks @JeckLabs!) [[PR-2280](https://github.com/meetecho/janus-gateway/pull/2280)]\n- Fixed small memory leak in AudioBridge (thanks @JeckLabs!) [[PR-2298](https://github.com/meetecho/janus-gateway/pull/2298)]\n- Fixed occasional segfault in VideoCall when hanging up calls [[Issue-2300](https://github.com/meetecho/janus-gateway/issues/2300)]\n- Fixed occasional curl hiccups with RTSP on some cameras\n- Added reconnect mechanism to RabbitMQ event handler (thanks @david-goncalves!) [[PR-2267](https://github.com/meetecho/janus-gateway/pull/2267)]\n- Extended MQTT support in transport and event handler to v5 (thanks @feymartynov!) [[PR-2273](https://github.com/meetecho/janus-gateway/pull/2273)]\n- Added settings to configure MQTT buffers in the transport plugin (thanks @feymartynov!) [[PR-2286](https://github.com/meetecho/janus-gateway/pull/2286)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.10.3] - 2020-07-09\n\n- Fixed occasional crashes in VideoRoom related to subscribers activity [[PR-2236](https://github.com/meetecho/janus-gateway/pull/2236)] [[PR-2253](https://github.com/meetecho/janus-gateway/pull/2253)]\n- Fixed AudioBridge compilation issues when libogg is missing (thanks @ffontaine!) [[PR-2238](https://github.com/meetecho/janus-gateway/pull/2238)]\n- Fixed broken SRTP forwarders in AudioBridge [[PR-2258](https://github.com/meetecho/janus-gateway/pull/2258)]\n- Fixed occasional segfaults due to race conditions in SIP plugin [[PR-2247](https://github.com/meetecho/janus-gateway/pull/2247)]\n- Fixed occasional recording issues in Janus and Duktape plugins\n- Added timeout (120s) on idle connections in HTTP transport\n- Fixed Opus recordings occasionally being way too large than the source file when processed via janus-pp-rec (thanks @neilkinnish!) [[PR-2250](https://github.com/meetecho/janus-gateway/pull/2250)]\n- Added a new web demo to use canvas elements as a media source for PeerConnections [[PR-2261](https://github.com/meetecho/janus-gateway/pull/2261)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.10.2] - 2020-06-17\n\n- Fixed sscanf-related security issues [[PR-2229](https://github.com/meetecho/janus-gateway/pull/2229)]\n- Fixed some RTP extensions not working after renegotiations [[Issue-2192](https://github.com/meetecho/janus-gateway/issues/2192)]\n- Fixed occasionally broken simulcast behaviour [[PR-2231](https://github.com/meetecho/janus-gateway/pull/2231)]\n- Fixed \"switch\" request not taking simulcast/SVC into account in VideoRoom and Streaming plugins [[Issue-2219](https://github.com/meetecho/janus-gateway/issues/2219)]\n- Fixed inability to ask for random ports when creating Streaming plugin mountpoints with simulcast support [[PR-2225](https://github.com/meetecho/janus-gateway/pull/2225)]\n- Fixed occasional crashes in SIP plugin when using helpers [[PR-2216](https://github.com/meetecho/janus-gateway/pull/2216)]\n- Updated Duktape dependencies to v2.5, and fixed Duktape plugin relaying text data as binary [[PR-2233](https://github.com/meetecho/janus-gateway/pull/2233)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.10.1] - 2020-06-11\n\n- Added initial support for AV1 and H.265 video codecs [[PR-2120](https://github.com/meetecho/janus-gateway/pull/2120)]\n- Added initial support for end-to-end encryption via Insertable Streams [[PR-2074](https://github.com/meetecho/janus-gateway/pull/2074)]\n- Fixed security issues when processing SDPs [[PR-2214](https://github.com/meetecho/janus-gateway/pull/2214)]\n- Fixed occasional codec profile negotiation issues (thanks @groupboard!) [[PR-2212](https://github.com/meetecho/janus-gateway/pull/2212)]\n- Fixed occasional segfaults when hanging up VideoRoom subscribers\n- Fixed RTSP issues when fmtp is missing (thanks @lionelnicolas!) [[PR-2190](https://github.com/meetecho/janus-gateway/pull/2190)]\n- Fixed RTSP not following redirects, when used (thanks @lionelnicolas!) [[PR-2195](https://github.com/meetecho/janus-gateway/pull/2195)]\n- Fixed SRTP-SDES and renegotiation issues in NoSIP plugin (thanks @ihusejnovic!) [[PR-2196](https://github.com/meetecho/janus-gateway/pull/2196)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.10.0] - 2020-06-01\n\n- Added support for negotiation of codec profiles (mainly VP9 and H.264) [[PR-2080](https://github.com/meetecho/janus-gateway/pull/2080)]\n- Added new callback to let plugins know when the datachannel first becomes available, and then any time it's writable (empty buffers) [[PR-2060](https://github.com/meetecho/janus-gateway/pull/2060)]\n- Added support for data channel subprotocols [[PR-2157](https://github.com/meetecho/janus-gateway/pull/2157)]\n- Added new event handler for GrayLog using GELF (thanks @mirkobrankovic!) [[PR-1788](https://github.com/meetecho/janus-gateway/pull/1788)]\n- Added per-user override of global room 'audio_active_packets' and 'audio_level_average' properties to AudioBridge and VideoRoom (thanks @mirkobrankovic!) [[PR-2158](https://github.com/meetecho/janus-gateway/pull/2158)]\n- Notify speaker that started/stopped talking too, when talking events are triggered in VideoRoom and AudioBridge (thanks @maxboehm!) [[PR-2172](https://github.com/meetecho/janus-gateway/pull/2172)]\n- Allow listing of private rooms/mountpoints if an admin_key is used (thanks @robby2016!) [[PR-2161](https://github.com/meetecho/janus-gateway/pull/2161)]\n- Fixed RTCP support not triggering PLIs for new simulcast mountpoint viewers [[Issue-2156](https://github.com/meetecho/janus-gateway/issues/2156)]\n- Fixed occasional issue binding multicast mountpoints (thanks @PaulKerr!) [[PR-2167](https://github.com/meetecho/janus-gateway/pull/2167)]\n- Fixed buffering of keyframes not working in Streaming plugin (thanks @TomFFF!) [[PR-2170](https://github.com/meetecho/janus-gateway/pull/2170)]\n- Added support for buffering of keyframes to RTSP mountpoints too (thanks @lionelnicolas!) [[PR-2180](https://github.com/meetecho/janus-gateway/pull/2180)]\n- Fixed renegotiation support in SIP plugin when audio/video is added (thanks @ihusejnovic!) [[PR-2164](https://github.com/meetecho/janus-gateway/pull/2164)] [[PR-2173](https://github.com/meetecho/janus-gateway/pull/2173)]\n- Fixed menus in html documentation when using Doxygen >= 1.8.14 (thanks @i8-pi!) [[PR-2155](https://github.com/meetecho/janus-gateway/pull/2155)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.9.5] - 2020-05-18\n\n- Fixed sessions not being cleaned up when disabling session timeouts and the transport disconnects (thanks @nicolasduteil!) [[PR-2143](https://github.com/meetecho/janus-gateway/pull/2143)]\n- Added option to keep candidates with private host addresses when using nat-1-1, and advertize them too instead of just replacing them\n- Added auth token, if available, to 'attached' event (handlers) and to Admin API (handle_info)\n- Added new API to start/stop recording a VideoRoom as a whole, and a new option to prevent participants from starting/stopping their own recording (thanks @wheresjames!) [[PR-2137](https://github.com/meetecho/janus-gateway/pull/2137)]\n- Fixed rare deadlock when wrapping up Streaming plugin mountpoints [[PR-2141](https://github.com/meetecho/janus-gateway/pull/2141)]\n- Fixed rare deadlock when destroying AudioBridge rooms\n- Added synchronous request to check if an announcement is playing in the AudioBridge\n- Fixed AudioBridge announcement not waking up sleeping forwarder\n- Added global room mute/unmute support to AudioBridge\n- Added configurable DSCP support for outgoing RTP packets to SIP and NoSIP plugins (thanks @GerardM22!) [[PR-2150](https://github.com/meetecho/janus-gateway/pull/2150)]\n- Added support for RTP extensions (audio-level, video-orientation) to NoSIP plugin [[Issue-2152](https://github.com/meetecho/janus-gateway/issues/2152)]\n- Added option to configure ciphers suite for secure WebSockets (thanks @agclark81!) [[PR-2135](https://github.com/meetecho/janus-gateway/pull/2135)]\n- Added timer to janus.js to avoid spamming onmute/onunmute events and flashing videos [[PR-2147](https://github.com/meetecho/janus-gateway/pull/2147)]\n- Added a new tool to convert .pcap captures to .mjr recordings [[PR-2144](https://github.com/meetecho/janus-gateway/pull/2144)]\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.9.4] - 2020-05-04\n\n- Updated code not to wait forever for local candidates when half-trickling and sending an SDP out\n- Fixed occasional CPU spiking issues when dealing with ICE failures (thanks @sjkummer!)\n- Fixed occasional stall when gathering ICE candidates (thanks @wheresjames!)\n- Fixed the incorrect value being set via DSCP, when configured\n- Fixed occasional race condition when hanging up VideoRoom subscribers\n- Fixed Audiobridge and Streaming plugins not playing the last chunk of .opus files (thanks @RSATom!)\n- Fixed duplicate subscriptions (and SRTP/SRTCP errors) on multiple watch requests in Streaming plugin\n- Updated Streaming and TextRoom plugins to stop using legacy datachannel negotiation\n- Fixed occasional crash in HTTP transport when dealing with unknown requests\n- Fixed occasional disconnect in WebSockets (thanks @tomnotcat!)\n- Made RabbitMQ exchange type configurable in both transport and event handler (thanks @voicenter!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.9.3] - 2020-04-22\n\n- Change libsrtp detection in the configure script to use pkg-config\n- Fixed compilation error with gcc10\n- Fixed RTCP issue that could occasionally lead to broken retransmissions when using rtx\n- Added option to specify DSCP Type of Service (ToS) for media streams\n- Fixed a couple of race conditions during renegotiations\n- Fixed VideoRoom and Streaming \"destroy\" not working properly when using string IDs\n- Fix occasional segfault in VideoRoom (thanks @cb22!)\n- Fixed AudioBridge \"create\" not working properly when using string IDs\n- Added support for playing Opus files in AudioBridge rooms\n- Added support to Opus files for file-based mountpoints in Streaming plugin\n- Added support for generic metadata to Streaming mountpoints\n- Streaming plugin now returns mountpoint IP address(es) in \"create\" and \"info\", when binding to specific IP/interface\n- Fixed occasional segfault when using helper threads in Streaming plugin\n- Fixed occasional race conditions in HTTP transport\n- Added support for specifying screensharing framerate in janus.js (thanks @agclark81!)\n- Cleaned up code in janus.js (thanks @alienpavlov!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.9.2] - 2020-03-26\n\n- Converted HTTP transport plugin to single thread (now requires libmicrohttpd >= 0.9.59)\n- Fixed .deb file packaging (thanks @FThrum!)\n- Added foundation for aiortc-based functional testing (python)\n- Fixed occasional audio/video desync\n- Added asynchronous resolution of mDNS candidates, and an option to automatically ignore them entirely\n- Updated default DTLS ciphers (thanks @fippo!)\n- Added option to generate ECDSA certificates at startup, instead of RSA (thanks @Sean-Der!)\n- Fixed rare race condition when claiming sessions\n- Fixed rare crash in ice.c (thanks @tmatth!)\n- Fixed dangerous typo in querylogger_parameters (copy/paste error)\n- Fixed occasional deadlocks in VideoRoom (thanks @mivuDing and @agclark81!)\n- Added support for RTSP Content-Base header to Streaming plugin\n- Fixed double unlock when listing private rooms in AudioBridge\n- Made AudioBridge prebuffering property configurable, both per-room and per-participant\n- Added G.711 support to AudioBridge (both participants and RTP forwarders)\n- Added called URI to 'incomingcall' and 'missed_call' events in SIP plugin (in case the registered user is associated with multiple public URIs)\n- Fixed race conditions and leaks in VideoCall and VoiceMail plugins\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.9.1] - 2020-03-10\n\n- Added configurable global prefix for log lines\n- Implemented better management of remote candidates with invalid addresses\n- Added subtype property to differentiate some macro-types in event handlers\n- Improved detection of H.264 keyframes (thanks @cameronlucas3!)\n- Added configurable support for strings as unique IDs in AudioBridge, VideoRoom, TextRoom and Streaming plugins\n- Fixed small memory leak when creating Streaming mountpoints dynamically\n- Fixed segfault when trying to start a SIP call with a non-existing refer_id (thanks @tmatth!)\n- Fixed errors negotiating video in SIP plugin when multiple video profiles are provided\n- Updated SIP plugin transfer code to answer with a 202 right away, instead of sending a 100 first (which won't work with proxies)\n- Added several features and fixes several nits in SIP demo UI\n- Fixed janus.js error callback not being invoked when an HTTP error happens trying to attach to a plugin (thanks @hxl-dy!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.9.0] - 2020-02-21\n\n- Refactored core-plugin callbacks\n- Added RTP extensions termination\n- Removed requirement to enable ICE Lite to use ICE-TCP, even though it may cause issues (thanks @sjkummer!)\n- Added support for transport-wide CC on outgoing streams (feedback still unused, though)\n- Dynamically update NACK queue size depending on RTT\n- Fixed risk of RTP header memory misalignment when dealing with rtx packets\n- Users muted in AudioBridge by an admin are now notified as well (thanks @klanjabrik!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.8.2] - 2020-02-04\n\n- Added Travis CI integration (thanks @fippo for kickstarting it!)\n- New configuration property to add protected folders not to save recordings and pcap captures to\n- Fixed rare race condition when joining and destroying a VideoRoom session\n- Improved parsing of headers in RTSP messages (thanks @kefir266!)\n- Fixed segfault in AudioBridge when leaving a room before PeerConnection is ready\n- Fixed '500' errors being sent in response to incoming OPTIONS in the SIP plugin (thanks @ycherniavskyi!)\n- Fixed helpers not being able to send SUBSCRIBE requests in SIP plugin\n- Added option to fix audio skew compensation, if present, to janus-pp-rec\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.8.1] - 2020-01-13\n\n- Added binary data support to data channels\n- Fixed segfault at startup if event handlers or loggers directory couldn't be opened (thanks @kazzmir!)\n- Fixed potential segfault when closing logging at shutdown\n- Allowed RTCP ports to be picked randomly using 0, in Streaming plugin\n- Fixed occasional memory leak when destroying mountpoints in Streaming plugin\n- Fixed memory leak in SIP plugin\n- Updated 'referred_by' field to contain the value of SIP referred-by header, and not just the URI (thanks @pawnnail!)\n- Don't keep TextRoom plugin loaded if data channels were not compiled\n- Removed SIPre plugin from the repo\n- Fixed late initialization of janus.js constructor callbacks\n- Changed janus.js to use sendBeacon instead of XHR when closing/refreshing page\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.8.0] - 2019-12-12\n\n- Added changelog file to the repo and docs (thanks @oscarvadillog!)\n- Added new category of plugins for modular logging (stdout and file still there, and part of the core)\n- Removed option to enable rtx (now always supported, when negotiated)\n- Added gzip compression helper method to the core utils\n- Fixed RTSP SETUP issues when url contains query string parameters\n- Added option to gzip events when using the Sample Event Handler\n- Streamlined janus.js (thanks @oscarvadillog!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.7.6] - 2019-11-27\n\n- Split SDP lines when parsing on line feed only, and trim carriage feed instead (\\n instead of \\r\\n)\n- Reduced default twcc_period (how often to send feedback when using Transport Side BWE) from 1s to 200ms\n- Added option to skip (and disable) unreachable STUN/TURN server at startup (thanks @sjkummer!)\n- Fixed video desynchronization when doing G.722/iSac audio\n- Other generic fixes on A/V desync\n- Added support for multiple concurrent calls for the same account to the SIP plugin\n- Added support for blind and attended transfers to the SIP plugin\n- Added way to inject custom Contact params in REGISTER to the SIP plugin\n- Added way to intercept non-standard headers in SIP messages to SIP plugin (thanks @ihusejnovic!)\n- Fixed missing SIP CANCEL when hanging up outgoing unanswered calls in SIP plugin\n- Added support for domain names (and IPv6) to RTP forwarders in AudioBridge and VideoRoom\n- Fixed broken b=TIAS SDP attribute support for Firefox in VideoRoom (thanks @MvEerd!)\n- Fixed and improved VP9 SVC support in VideoRoom and Streaming plugins\n- Added IPv6 support to Streaming plugin\n- Fixed potential segfault in Streaming plugin (thanks @garry81!)\n- Fixed occasional latching issues for RTSP in Streaming plugin\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.7.5] - 2019-10-28\n\n- Added warning at startup if libnice version is outdated (at least 0.1.15 recommended)\n- Added option to specify CWD when launching Janus as a daemon (thanks @l7s!)\n- Extended the STUN test via Admin API to support binding to a specific port, and return the public one\n- Fixed simulcast issue when needing to automatically drop to lower layers\n- Fixed potential endless loop when binding ports in the Streaming plugin\n- Made creating Streaming mountpoints more asynchronous (especially for RTSP)\n- Added support for SIP SUBSCRIBE/NOTIFY to SIP plugin\n- Added ability to add custom headers to SIP BYE (thanks @mmujic!)\n- Added option to specify IP to bind to for media in SIP plugin (thanks @razvancrainea!)\n- Fixed occasional segfault when leaving a VideoRoom\n- Added audio level dBov average to talk events in VideoRoom plugin (thanks @aconchillo!)\n- Added new synchronous API to mute other participants in the AudioBridge plugin (thanks @klanjabrik!)\n- Fixed typo in SDP processing in Duktape/JavaScript plugin, and tied Duktape logging to the one in the Janus core (thanks @l7s!)\n- Tied Lua logging to the one in the Janus core\n- Added command line option to janus-pp-rec to specify the output format (thanks @rscreene!)\n- Added new WebSocket and Nanomsg event handlers\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.7.4] - 2019-09-06\n\n- Fixed duplicate values in config that could result in wrong property being used\n- Fixed occasional race condition when processing SDPs (thanks @Bug-Fairy!)\n- Fixed broken SDP when rejecting audio/video m-line\n- Fixed Admin API not responding after sending messages to unresponsive plugins\n- Fixed some issues with RTSP support in Streaming plugin\n- Added option to keep recording Streaming mountpoints even when disabled\n- Allow SIP plugin to negotiate SRTP separately for audio and video\n- Fixed autoaccept_reinvites=FALSE not working when accepting calls in SIP plugin, and improved re-INVITEs support in general (thanks @pawnnail!)\n- Added possibility to have different addresses for remote audio and video in SIP, SIPre and NoSIP plugins (thanks @pawnnail!)\n- Make sure remote addresses are reset when call ends in SIP, SIPre and NoSIP plugins (thanks @pawnnail!)\n- Added SIP Reason Header (RFC3326) info to \"hangup\" event in SIP plugin, if available (thanks @ihusejnovic!)\n- Added method to list participants in a TextRoom (thanks @mtltechtemp!)\n- Added method to send a room announcement in TextRoom plugin\n- Fixed occasional segfault in TextRoom when using Admin API to send requests (thanks @MvEerd!)\n- Added support for MQTT v5, and fixed reconnection issue (thanks @feymartynov!)\n- Fixed occasional crashes when using more than one event handler at the same time\n- Added configurable bitrate values for rid-based simulcast to janus.js (thanks @vivaldi-va!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.7.3] - 2019-07-10\n\n- Added Admin API method to make synchronous requests to plugins\n- Fixed broken media when removing/adding it again in renegotiations\n- Fixed several issues related to datachannels\n- Fixed occasional memory leak in the core when ending sessions from plugins (thanks @uxmaster!)\n- Changed Janus API 'slowlink' event to use lost packets instead of NACKs, and made it configurable with a dynamic threshold\n- Fixed broken SDES length in compound RTCP packets (thanks @glenn-hpcnt!)\n- Fixed DTLS window size support in the core (thanks @garry81!)\n- Added status messages to MQTT transport (thanks @feymartynov!)\n- Changed default for sender-side bandwidth estimation in VideoRoom to TRUE\n- Fixed occasional segfaults when using RTP forwarders with RTCP support\n- Added VideoRoom RTP forwarder events to event handlers notifications\n- Added a configurable RTP range to the Streaming plugin settings\n- Fixed broken H.264 simulcast support in Streaming plugin\n- Refactored janus-pp-rec to support command line options\n- Fixed occasional segfault when post-processing VP8 recordings\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.7.2] - 2019-06-07\n\n- Removed requirement for both sdpMid and sdpMLineIndex to be in trickle messages\n- Set ICE remote credentials when receiving remote SDP, instead of later\n- Fixed occasional segfaults when using WebSocket as a transport\n- Fixed segfault in WebSocket transport when using ACL\n- Added new Admin API messages to destroy a session, detach a handle and hangup a PeerConnection (same as Janus API)\n- Fixed leak when RTP forwarding with RTCP feedback in the VideoRoom plugin\n- Added support for third spatial layer when using VP9 SVC in VideoRoom (assuming EnabledByFlag_3SL3TL is used)\n- Fixed segfault when changing rooms in AudioBridge\n- Made sure the SIP stack doesn't accept new calls until the previous one has freed all resources\n- Fixed occasional segfault when pushing SIP messages to event handlers\n- Added option to locally cleanup handles when destroying a session in janus.js\n- Fixed exception in janus.js when using datachannels\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.7.1] - 2019-05-20\n\n- Added experimental debug mode with disabled WebRTC encryption (to use with the --disable-webrtc-encryption in Chrome unstable)\n- Added Janus API ping/pong mechanism to Admin API as well\n- Added Admin API methods to check address resolving capabilities and test a provided STUN server\n- Added check on ICE gathering process start (fixes issue with exhausted port range)\n- Added support for temporal layer in H.264 simulcast via frame marking\n- Made sure a PLI is sent on all layers, when simulcast is used\n- Fixed a crash when using event handlers in SIP plugin\n- Fixed some race conditions on hangups in SIP plugin\n- Added option to lock RTP forwarding functionality via an admin key/secret (VideoRoom and AudioBridge)\n- Fixed regression in Streaming plugin RTCP support\n- Added option to override payload type for RTSP mountpoints in Streaming plugin\n- Fixed a few issues saving permanent mountpoints in Streaming plugin\n- Separated checks for PeerConnection and getUserMedia support in janus.js (since plain HTTP hides getUserMedia now)\n- Added sanity checks on createOffer/createAnswer in janus.js\n- Fixed regression in simulcasting when doing SDP munging in janus.js\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.7.0] - 2019-05-10\n\n- Added support for multiple datachannel streams in the same PeerConnection\n- Forced DTLS 1.2 on older OpenSSL versions\n- Added first integration of SDP support in the fuzzers\n- Fixed several leaks in SDP utils\n- Explicitly disabled support for encrypted RTP extensions (was causing SDP inconsistencies)\n- Added count of incoming retransmissions to Admin API and Event Handlers stats\n- Improved check for H.264 keyframe (thanks bwerther!)\n- Modified \"cap REMB\" behavior to \"replace REMB\"\n- Fixed missing notification of lurkers when first joining VideoRoom with notify_join=TRUE\n- Improved support for incoming re-INVITEs in SIP plugin\n- Fixed check in WebSocket transport that could lead to crashes\n- Fixed occasional segfaults when postprocessing H.264 recordings\n- Added new callback to janus.js to intercept the SDP before it is sent, e.g., for munging purposes (thx @carlcc!)\n- Fixed direction property error in janus.js on Safari (thx @alienpavlov!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.6.3] - 2019-03-20\n\n- Removed folder with self-signed certificate (unneeded and confusing)\n- Added many fixes and improvements to the RTCP code\n- Fixed typos that caused issues when sending retransmissions using RFC4588\n- Fixed typo when sending empty RR coupled with REMB\n- Made sure the CNAME is always the same for all m-lines in an SDP\n- Added support for mid RTP extension\n- Improved support for rid-based simulcasting\n- Fixed publish errors in MQTT transport and event handler\n- Fixed issue when switching Streaming mountpoints powered by helper threads\n- Added info on whether VideoRoom publisher is simulcasting to join events\n- Added option for new VideoRoom subscribers to specify simulcast substream/layer to subscribe to in join request (before it was configure-only)\n- Added type definitions for janus.js (thanks Elias!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.6.2] - 2019-03-04\n\n- Added RTP/RTCP fuzzing targets and tools\n- Fixed occasional crash when pushing the local SDP to event handlers, when enabled\n- Fixed NACK issue when receiving an out of order keyframe\n- Added option to configure the TWCC feedback period\n- Added option to include opaqueID in Janus API events\n- Added option to negotiate Opus inband FEC in the VideoRoom\n- Added option to specify temporary extension when recording AudioBridge rooms, and event handler notification for when recording is over\n- Fixed occasional playout issue after recording, using Record&Play demo\n- Fixed typo in janus.js that affected replacing audio tracks in renegotiations\n- Changed default maxev (number of events in long poll results) to 10 in janus.js\n- Updated path of getDisplayMedia in janus.js to reflect current spec (thanks cb22!)\n- Fixed ambiguous check in Janus.isWebrtcSupported in janus.js\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.6.1] - 2019-02-11\n\n- Added several fixes to RTP/RTCP parsing after fuzzing tests\n- Added fixes to keyframe detection after fuzzing tests\n- Fixed some demos not working after update to Chrome 72\n- Fixed occasional crashes when saving .jcfg files (e.g., saving permanent Streaming mountpoints)\n- Added new Admin API command to temporarily stop/resume accepting sessions (e.g., for draining servers)\n- Fixed recordings sometimes not closed/destroyed/renamed when hanging up SIP sessions\n- Added option to SIP/SIPre/NoSIP plugin to override c= IP in SDP\n- Fixed missing RTSP support in Streaming plugin if TURN REST API was disabled in configure\n- Fixed Streaming plugin not returning complete information on secret-less mountpoints (thanks @Musashi178!)\n- Fixed missing .jcfg support in Duktape plugin (thanks @fbertone!)\n- Updated janus.js to use transceivers for Chrome >=72\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.6.0] - 2019-01-07\n\n- Changed default configuration format to libconfig (INI still supported but deprecated)\n- Fixed several RTCP parsing issues that could lead to crashes (thanks to Fippo for bringing fuzzying to our attention!)\n- Added support to clang compiler (needed for fuzzying)\n- Fixed rtx packets ending up in retransmission buffer (thanks glenn-hpcnt!)\n- Fixed occasional crash when cleaning NACK buffer (thanks tmatth!)\n- Fixed loop termination warning when handling event handlers (thanks tmatth!)\n- Fixed occasional invalid rtx payload type\n- Fixed local SDP notification to event handlers\n- Fixed typo in link quality calculation\n- Fixed occasional crash in SIP plugin\n- Added option to provide custom headers in SIP 200 OK as well (thanks ihusejnovic!)\n- Fixed typo in Range header when sending RTSP PLAY in Streaming plugin (thanks Phil1972!)\n- Made MQTT and RabbitMQ configuration files more consistent with other ones (thanks manifest!)\n- Added support for Last Will and Testament to MQTT event handler (thanks 0nkery!)\n- Fixed broken video when post-processing recordings with high-profile H.264\n- Fixed missing success callback in sendDtmf JS method (thanks nevcos!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.5.0] - 2018-11-20\n\n- Refactored core to have a persistent GMainLoop/thread per handle\n- Added option to share static number of GMainLoop/thread instances for multiple handles\n- Better management of incoming RTCP packets before passing them to plugins\n- Updated TURN REST API to support both \"key\" and \"api\" as parameters\n- Added support for dumping directly to .pcap, rather than text first via text2pcap\n- Fixed occasional missing notifications of temporal layer changes, when doing simulcast\n- Fixed occasional crash in TextRoom plugin\n- Fixed crashes in Duktape plugin after some iterations\n- Added .mjr metadata to media files when postprocessing the recordings, if supported by the container\n- Fixed datachannels not working in Streaming demo, when configured\n- Fixed dangling \"Publish\" button in VideoRoom demo\n- Better management of timeout notifications when using websockets in janus.js (thanks @nevcos!)\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.4.5] - 2018-10-16\n\n- Switched to GMutex for locks by default (changeable in configure)\n- Fixed missing sdpMid in some trickle candidates, which could break full-trickle support\n- Fixed missing TWCC info when handling rtx duplicates (thanks garry81!)\n- Fixed H.264 keyframe detection and broken H.264 simulcast code\n- Fixed bug in skew compensation code\n- Fixed occasional crashes when closing PeerConnections in AudioBridge\n- Fixed broken Record-Route usage in SIP plugin (thanks Dan!)\n- Removed outdated autoack property from SIP plugin\n- Switched from SET_PARAMETER to OPTIONS as an RTSP keep-alive (thanks cnzjy!)\n- Fixed missing endianness for RTP packets in postprocessor, which caused problems on MacOS\n- Fixed crash in postprocessor when handling high(er) H.264 profiles (e.g., Safari 12)\n- Fixed multiple \"First keyframe\" log lines when postprocessing video\n- Added support for parsing a few RTP extensions in the postprocessor\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.4.4] - 2018-09-28\n\n- Added several important fixes to NACK and retransmission code\n- Fixed connectivity establishment when only available candidates are prflx\n- Fixed some leaks in TWCC code\n- Fixed missing information when reporting TWCC reports (thanks Kangsik!)\n- Made the timeout for trickle candidates configurable\n- Added support for mDNS candidates (see draft-ietf-rtcweb-mdns-ice-candidates)\n- Added option to configure the DTLS retransmission timer (BoringSSL only)\n- Optimized DTLS writes by removing a copy on each send (thanks Joachim!)\n- Added option to override codecs to negotiate in EchoTest\n- Added H.264 simulcasting support to plugins that did VP8 simulcast already\n- Added VP9/SVC support to the Streaming plugin\n- Improved the way simulcast streams can be recorded and forwarded\n- Added partial RTCP support to RTP forwarders (thanks Adam!)\n- Fixed occasional segfaults in the VideoRoom when forcing private IDs (thanks tugtugtug!)\n- Added option to use helper threads for Streaming plugin mountpoints\n- Fixed a couple of errors in the RTSP support of the Streaming plugin (thanks nu774!)\n- Several fixes in the NoSIP plugin (thanks Dmitry!)\n- Fixed broken SIP MESSAGE support in SIP plugin\n- Fixed occasional segfaults in SIP and SIPre plugins (thanks mharcar!)\n- Fixed broken recording support in the VideoCall plugin (thanks codebot!)\n- Fixed potential deadlock in Lua and Duktape plugins (thanks Gabriel!)\n- Fixed memory leaks in VideoRoom, AudioBridge and TextRoom\n- Added new MQTT event handler (thanks Olle!)\n- Made HTTP REST API optionally more consistent with other transports\n- Added new flag to postprocessor for just printing the JSON header\n- Fixed occasional segfaults when processing recordings\n- Added getDisplayMedia() support to janus.js\n- Added better support to constraints when screensharing (thanks Sol!)\n- Added better iOS devices support to janus.js and the demos\n- Other smaller fixes and improvements (thanks to all who contributed pull requests and reported issues!)\n\n\n## [v0.4.3] - 2018-08-27\n\n- Fixed occasional crash when closing PeerConnections\n- Fixed way of negotiating datachannels in Firefox Nightly\n- Fixed broken check when enabling TURN REST API\n- Fixed occasional crash when post-processing H.264 recordings (thanks Thomas!)\n- Fixed occasional issue when creating PID file\n- Fixed broken SDES generation (thanks Garry!)\n- Added new Duktape plugin to write plugin logic in JavaScript\n- Fixed occasional crash in VideoCall plugin when declining calls\n- Added basic RTCP support to the Streaming plugin (thanks Adam!)\n- Added basic RTCP support to RTP forwarders in the VideoRoom plugin\n- Added new Nanomsg transport\n- Changed the way libwebsockets logging is configured\n- Updated janus.js to use promises for WebRTC APIs (thanks Philipp!)\n- Some more bug fixes and improvements\n\n\n## [v0.4.2] - 2018-06-18\n\n- Fixed ICE loop not terminating at times, and spiking the CPU\n- Fixed compilation against older OpenSSL versions (thanks Joachim!)\n- Added option to statically enable locking debug via command line or configuration file\n- Fixed occasional crash in VideoRoom when destroying rooms\n- Fixed VideoRoom not closing subscribers PeerConnections when publisher goes away, if so configured\n- Fixed SRTP errors when resuming VideoRoom subscribers that were paused for a long time\n- Added new option to really force a cap on the bitrate in VideoRoom rooms\n- Fixed recording not being started for VideoRoom publishers media added in a renegotiation\n- Fixed occasional crash in AudioBridge when closing PeerConnections under load\n- Added Opus FEC support to AudioBridge (thanks Eric!)\n- Fixed pipe socket initialization in Streaming plugin (thanks Adam!)\n- Added systemd support to Unix Sockets transport plugin (thanks Adam!)\n- WebSocket connection is no longer torn down in case of a Janus session timeout\n- Added options to configure keep-alive and long-poll timers in janus.js\n- Some more bug fixes and improvements\n\n\n## [v0.4.1] - 2018-05-29\n\n- Single thread per PeerConnection, instead of two\n- Fixed issue with API secret, where sessions would be created anyway\n- Cleanup of ICE related code (thx Joachim!)\n- Removed ad-hoc thread for SCTP code\n- Fixed deadlock in VideoRoom plugin\n- Fixed segfault in SIPre plugin\n- Fixed leaks when using event handlers (thx zgjzzhw!)\n- Fixed some missing events when closing PeerConnections\n- Fixed broken dependencies mechanism in janus.js (thx Philippe!)\n- Some more bug fixes and improvements\n\n\n## [v0.4.0-broken] - 2018-05-22\n\n- Changed memory management to use reference counters\n- New plugin to write application logic in Lua\n- Added mechanism to reclaim sessions after a reconnection (thx Geige!)\n- Fixed broken renegotiations when upgrading from audio-only to audio-video\n- Fixed typo in evaluation of RTT from RTCP packets\n- Fixed crash when SRTP profile is missing in DTLS handshake\n- Improved and streamlined a few events (event handlers), e.g., selected-pair\n- Added new \"external\" events (event handlers), for events pushed via Admin API\n- Fixed deadlock when joining a VideoRoom with notify_join=true\n- Fixed some info not saved permanently in some plugins when editing\n- Added media latching to RTSP streams setup in the Streaming plugin\n- Fixed an issue with simulcast support in the Streaming plugin\n- Fixed occasional unexpected WebSockets disconnects when using the Streaming plugin\n- Fixed Streaming plugin not returning bound ports when creating mountpoints with random ones (port=0)\n- Improved and streamlined documentation for all plugins\n- Added option to limit ciphers/protocols in HTTP and WebSockets (thx Alexander!)\n- Added transceivers support to janus.js for proper renegotiations in Firefox\n- More bug fixing and general cleanup (thx to mtdxc, fancycode and others!)\n- Added a way to support other screensharing extensions in janus.js in a programmatic way (thx Sol!)\n\n\n## [v0.3.1] - 2018-04-04\n\n- Changed threading model for processing requests in the core\n- Added support for SRTP AES-GCM to core and SIP/SIPre/NoSIP plugins\n- Changed set of ciphers negotiated in DTLS, disabling weaker ones (thanks Chad!)\n- Added option to specify passphrase when dealing with certificates/keys\n- Added ability for Admin API requests to tweak Event Handlers\n- Integrated link quality stats info (thanks Piter!)\n- Added support for storage-less authentication via Signed Tokens (thanks Sol!)\n- Added option to force TCP for SIP messages in the SIP plugin\n- Added option to not fail RTSP mountpoint creation right away if backend is not up\n- Added SSL/TLS support to the MQTT transport (thanks Andrei!)\n- Added new request to edit some Streaming mountpoint properties (thanks Rob!)\n- Fixed management of DTMF in janus.js\n- Updated management of constraints in janus.js (thanks Igor!)\n- Bug fixing and general improvements\n\n\n## [v0.3.0] - 2018-02-23\n\n- Implemented renegotiations and ICE restarts\n- Bundle and rtcp-mux now are always forced\n- Added support to Transport Wide CC sender-side BWE (thanks Sergio!)\n- Added SRTP support to Streaming mountpoints\n- Implemented a skew compensation algorithm in the Streaming plugin\n- Added SRTP support to RTP forwarders\n- Implemented support for RFC4588 (rtx/90000 retransmissions)\n- Janus can now do full-trickle too\n- SIP plugin now supports 407 (proxy authentication)\n- Fixed post-processing of G.711 recordings\n- Added versioning info to janus-pp-rec\n- Several fixes and cleanup\n\n\n## [v0.2.6] - 2017-12-19\n\n- New SIP plugin based on libre, SIPre (janus.plugin.sipre), and related demo\n- New NoSIP plugin, that can be used with legacy applications (like SIP) without doing any signalling itself\n- VideoRoom can now support multiple codecs at the same time, instead of being forced to choose just one per media type\n- Plugins now record streams specifying the actual codec in use, instead of making assumptions (e.g., like Record&Play did with Opus and VP8)\n- Streaming plugin now allows you to temporarily pause audio and/or video delivery via \"configure\" requests\n- Removed RTCP BYE as a trigger to shutdown a PeerConnection (fixes Firefox 52 issues)\n- Added RTCP support for simulcast SSRCs\n- Fixed parsing of Firefox simulcast offer when order of attributes was different than expected\n- Improved RTP headers rewriting in case of SSRC changes (e.g., context switches)\n- Improved performance of the ICE send threads/loops and computation of transfer rates, by getting rid of all list traversals\n- Added support for MSG_EOR in SCTP datachannels\n- Added \"exchange\" support to RabbitMQ transport\n- Added new info to Event Handlers (server info in \"started\" event, and server name in \"emitter\")\n- Added RabbitMQ Event Handler\n- You can now add additional constraints for a PeerConnection when invoking createOffer and createAnswer in janus.js\n- Fixed occasional problems when postprocessing .mjr recordings, especially long ones, and Opus recordings\n- Several bug and typo fixes, in both core and janus.js\n\n\n## [v0.2.5] - 2017-10-23\n\n- VP8 simulcasting supported in a few plugins (you may have experimented with it on the online demos already);\n- VP9 SVC is also available (VideoRoom only);\n- VideoRoom and Streaming plugins allow you to subscribe to a subset of the feed's media (e.g., only get audio even though feed is audio/video);\n- automatic fallback in the VideoRoom to subset of the media in case of unsupported codecs (e.g., Safari joining VP8 room falls back to audio only);\n- added option to override rtpmap and fmtp SDP attributes for RTSP mountpoints in the Streaming plugin;\n- added support for other codecs besides opus and VP8 in Record&Play plugin;\n- added option to have a static RTP forwarder for an AudioBridge room in the configuration file;\n- added possibility to specify an RTP range to use in the SIP plugin;\n- implemented text2pcap support to dump incoming and outgoing unencrypted RTP/RTCP traffic for debugging purposes;\n- added support to G.722 in postprocessor;\n- made sure that each m-line now has its own a=end-of-candidates attribute;\n- fixed crash in websockets transport plugin when SSL was enabled on both APIs;\n- added support to ping/pong mechanism in websockets transport plugin;\n- switched from addstream to addtrack in janus.js;\n- decoupled the dependencies in janus.js to allow for dynamic override of some features;\n- added support to build JavaScript modules out of janus.js.\n\n\n## [v0.2.4] - 2017-07-28\n\n- binding to some or all interfaces/families has been fixed in the HTTP transport;\n- the Access-Control-Allow-Origin return value is now configurable in the HTTP transport;\n- fixed occasional slow WebSocket request management when DNS was involved;\n- there's a new timer before we return an ICE failed (as due to trickling there may be a success shortly after a temporary failure);\n- the frequency of media stats notifications (event 32) in event handlers has been made configurable (default is still 1s);\n- event handlers now notify about each local and remote candidate as well;\n- the admin.html demo page now prompts you with the password (although you can still hardcode it in the page, as before);\n- several changes in the SIP plugin: support for offerless INVITEs, early media (183+SDP), outbound proxies, and fixes to some POLLERR messages;\n- added support for LibreSSL as an alternative to OpenSSL and BoringSSL;\n- added a=end-of-candidates to all m-lines, since we half-trickle (fixes Edge support);\n- fixed a race condition in the TextRoom plugin;\n- fixed the way janus.js used getStats, in particular for Firefox;\n- fixed device selection demo;\n- several smaller fixes derived from a static analysis of the code via Coverity.\n\n\n## [v0.2.3] - 2017-06-12\n\n- A few janus.js fixes (among which a small fix to get it working with Safari, and the possibility to add mic audio when screensharing);\n- Several RTCP related enhancements in the Streaming plugin;\n- Support for on-hold in SIP plugin;\n- Fixed MQTT transport when credentials are needed;\n- Improved \"kick\" in VideoRoom (needs forcing of private_id when creating room);\n- Possibility to create Streaming mountpoints with random ports, instead of specifying them via API;\n- Optional \"talking\" events in AudioBridge and VideoRoom;\n- Possibility to force BUNDLE/rtcp-mux per handle via API (no need to wait for complete negotiation);\n- Several bug fixes, a couple of them to nasty race conditions that finally got solved.\n\n\n## [v0.2.2] - 2017-03-08\n\n- ACL/Kick support in VideoRoom/AudioBridge/TextRoom\n- Man pages for Janus and post-processor\n- Opaque identifiers for Event handlers + Transport related events\n- Ability to specify SSRC + payload type when using RTP forwarders\n- Ability to relay datachannels in Streaming plugin\n- Ability to send some TextRoom commands (e.g., create/list/etc.) via Janus API instead of only datachannels\n- Configurable session timeouts\n- Configurable \"no-media\" timeouts\n- Optional temporary extension for recordings until they're done\n- cleanup and bug fixing\n\n\n## [v0.2.1] - 2016-12-13\n\n- Missing info\n\n\n## [v0.2.0] - 2016-10-10\n\n- Missing info\n\n\n## [v0.1.2] - 2016-09-05\n\n- Missing info\n\n\n## [v0.1.1] - 2016-06-15\n\n- Missing info\n\n\n## [v0.1.0] - 2016-05-27\n\n- Missing info\n\n\n## [v0.0.9] - 2015-11-11\n\n- First release\n"
  },
  {
    "path": "COPYING",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n\n========================================================================\n\nException by Meetecho s.r.l.:\n\nIf you modify this Program, or any covered work, by linking or combining\nit with OpenSSL (or a modified version of that library), containing\nparts covered by the terms of OpenSSL License, the licensors of this\nProgram grant you additional permission to convey the resulting work.\nCorresponding Source for a non-source form of such a combination shall\ninclude the source code for the parts of OpenSSL used as well as that of\nthe covered work.\n"
  },
  {
    "path": "Makefile.am",
    "content": "ACLOCAL_AMFLAGS = -I m4\n\n# FIXME: These should be enabled once the code is safe for them. That requires\n# some fairly big refactoring though, which can wait.\n# AM_CFLAGS += -Wshadow -Wstrict-aliasing=2\n\n# FIXME: make docs work with distcheck\nDISTCHECK_CONFIGURE_FLAGS = --disable-docs --enable-post-processing\n\nEXTRA_DIST = $(NULL)\nCLEANFILES = $(NULL)\n\nconfdir = $(sysconfdir)/janus\nconf_DATA = conf/janus.jcfg.sample\n\nplugindir = $(libdir)/janus/plugins\ntransportdir = $(libdir)/janus/transports\neventdir = $(libdir)/janus/events\nloggerdir = $(libdir)/janus/loggers\n\nSUBDIRS = src html\ndist_html_DATA = README.md\n\ndemosdir = $(datadir)/janus/demos\ndemos_DATA = $(NULL)\n\njsmodulesdir = $(datadir)/janus/javascript\njsmodules_DATA = html/demos/janus.js\n\n%.sample: %.sample.in\n\t$(MKDIR_P) $(@D)\n\t$(AM_V_GEN) sed -e \"\\\n\t    s|[@]confdir[@]|$(confdir)|;\\\n\t    s|[@]plugindir[@]|$(plugindir)|;\\\n\t    s|[@]transportdir[@]|$(transportdir)|;\\\n\t    s|[@]eventdir[@]|$(eventdir)|;\\\n\t    s|[@]loggerdir[@]|$(loggerdir)|;\\\n\t    s|[@]recordingsdir[@]|$(recordingsdir)|;\\\n\t    s|[@]demosdir[@]|$(demosdir)|;\\\n\t    s|[@]streamdir[@]|$(streamdir)|; \\\n\t    s|[@]luadir[@]|$(luadir)|; \\\n\t    s|[@]duktapedir[@]|$(duktapedir)|\" \\\n\t$< > $@ || rm $@\n\nEXTRA_DIST += conf/janus.jcfg.sample.in\nCLEANFILES += conf/janus.jcfg.sample\n\n\n##\n# pkg-config file\n##\n\nEXTRA_DIST = janus-gateway.pc.in\npkgconfigdir = $(libdir)/pkgconfig\npkgconfig_DATA = janus-gateway.pc\nDISTCLEANFILES = janus-gateway.pc\n\n\n##\n# Fuzzers checking\n##\n\ncheck-fuzzers: FORCE\n\tCC=$(CC) SKIP_JANUS_BUILD=1 LIB_FUZZING_ENGINE=fuzzers/standalone.o ./fuzzers/build.sh\n\t./fuzzers/run.sh rtcp_fuzzer out/rtcp_fuzzer_seed_corpus\n\t./fuzzers/run.sh rtp_fuzzer out/rtp_fuzzer_seed_corpus\n\t./fuzzers/run.sh sdp_fuzzer out/sdp_fuzzer_seed_corpus\n\n.PHONY: FORCE\nFORCE:\n\n##\n# Docs\n##\n\nif ENABLE_DOCS\nSUBDIRS += docs\nendif\n\n##\n# JavaScript module flavours for janus.js\n##\n\nif ENABLE_JAVASCRIPT_ES_MODULE\njsmodules_DATA += npm/dist/janus.es.js\nendif\n\nif ENABLE_JAVASCRIPT_UMD_MODULE\njsmodules_DATA += npm/dist/janus.umd.js\nendif\n\nif ENABLE_JAVASCRIPT_IIFE_MODULE\njsmodules_DATA += npm/dist/janus.iife.js\nendif\n\nif ENABLE_JAVASCRIPT_COMMON_JS_MODULE\njsmodules_DATA += npm/dist/janus.cjs.js\nendif\n\nEXTRA_DIST += $(jsmodules_DATA)\n\nif ENABLE_JAVASCRIPT_MODULES\n\nnode_modules/rollup/dist/bin/rollup: package.json\n\t$(NPM) install && touch node_modules/rollup/dist/bin/rollup\n\nnpm/dist/janus.%.js: html/demos/janus.js node_modules/rollup/dist/bin/rollup npm/rollup.config.mjs npm/module.js\n\t$(NPM) run rollup -- --o $@ --f $*\n\nendif\n\n##\n# Configuration\n##\n\nconfigs:\n\t$(MKDIR_P) $(DESTDIR)$(confdir)\n\t$(foreach config,$(conf_DATA),cp \"$(CURDIR)/$(config)\" \"$(DESTDIR)$(confdir)/$(notdir $(basename $(config) .sample))\";)\n\t$(MAKE) -C src configs\n\n##\n# Extra cleanup\n##\n\nclean-local:\n\t-rm -f docs/doxygen_sqlite3.db\n"
  },
  {
    "path": "README.md",
    "content": "Janus WebRTC Server\n===================\n[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-brightgreen.svg)](COPYING)\n![janus-ci](https://github.com/meetecho/janus-gateway/workflows/janus-ci/badge.svg)\n[![Coverity Scan Build Status](https://scan.coverity.com/projects/13265/badge.svg)](https://scan.coverity.com/projects/meetecho-janus-gateway)\n[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/janus-gateway.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:janus-gateway)\n\nJanus is an open source, general purpose, WebRTC server designed and developed by [Meetecho](https://www.meetecho.com). This version of the server is tailored for Linux systems, although it can be compiled for, and installed on, MacOS machines as well. Windows is not supported, but if that's a requirement, Janus is known to work in the \"Windows Subsystem for Linux\" on Windows 10: do **NOT** trust repos that provide .exe builds of Janus, they are not official and will not be supported.\n\nFor some online demos and documentations, make sure you pay the [project website](https://janus.conf.meetecho.com/) a visit!\n\n> **Note well:** this is the main branch for the `multistream` version of Janus, which is the new version. If you want to check the legacy version of Janus instead (i.e., `0.x`, a.k.a. \"legacy\") click [here](https://github.com/meetecho/janus-gateway/tree/0.x) instead.\n\nIf you have questions on Janus, or wish to discuss Janus with us and other users, please join our [Community](https://janus.discourse.group/). If you encounter bugs, please submit an issue on [GitHub](https://github.com/meetecho/janus-gateway/issues): make sure you read the [guidelines](.github/CONTRIBUTING.md) before opening an issue or a pull request, though.\n\n\n## Dependencies\nTo install it, you'll need to satisfy the following dependencies:\n\n* [GLib](https://docs.gtk.org/glib/)\n* [zlib](https://zlib.net/)\n* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/)\n* [Jansson](https://github.com/akheron/jansson)\n* [libconfig](https://hyperrealm.github.io/libconfig/)\n* [libnice](https://libnice.freedesktop.org/) (at least v0.1.16 suggested, v0.1.18 recommended)\n* [OpenSSL](https://www.openssl.org/) (at least v1.0.1e)\n* [libsrtp](https://github.com/cisco/libsrtp) (at least v2.x suggested)\n\nThese are optional dependencies, depending on which features you're interested in:\n\n* [usrsctp](https://github.com/sctplab/usrsctp) (only needed if you are interested in Data Channels)\n* [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) (at least v0.9.59; only needed if you are interested in REST support for the Janus API)\n* [libwebsockets](https://libwebsockets.org/) (at least v4.x suggested; only needed if you are interested in WebSockets support for the Janus API)\n* [cmake](https://www.cmake.org/) (only needed if you are interested in WebSockets and/or BoringSSL support, as they make use of it)\n* [rabbitmq-c](https://github.com/alanxz/rabbitmq-c) (only needed if you are interested in RabbitMQ support for the Janus API or events)\n* [paho.mqtt.c](https://eclipse.org/paho/clients/c) (only needed if you are interested in MQTT support for the Janus API or events)\n* [nanomsg](https://nanomsg.org/) (only needed if you are interested in Nanomsg support for the Janus API)\n* [libcurl](https://curl.haxx.se/libcurl/) (only needed if you are interested in the TURN REST API support)\n\nA couple of plugins depend on a few more libraries (you only need to install the ones for the plugins you need):\n\n* [Sofia-SIP](https://github.com/freeswitch/sofia-sip) (only needed for the SIP plugin)\n* [libopus](https://opus-codec.org/) (only needed for the AudioBridge plugin)\n* [libogg](https://xiph.org/ogg/) (needed for the recordings post-processor, and optionally AudioBridge and Streaming plugins)\n* [libcurl](https://curl.haxx.se/libcurl/) (only needed if you are interested in RTSP support in the Streaming plugin or in the sample Event Handler plugin)\n* [Lua](https://www.lua.org/download.html) (only needed for the Lua plugin)\n* [Duktape](https://duktape.org/) (only needed for the Duktape plugin)\n\n\nAll of those libraries are usually available on most of the most common distributions. Installing these libraries on a recent Fedora, for instance, is very simple:\n\n    yum install libmicrohttpd-devel jansson-devel \\\n       openssl-devel libsrtp-devel sofia-sip-devel glib2-devel \\\n       opus-devel libogg-devel libcurl-devel pkgconfig \\\n       libconfig-devel libtool autoconf automake\n\nNotice that you may have to `yum install epel-release` as well if you're attempting an installation on a CentOS machine instead.\n\nOn Ubuntu or Debian, it would require something like this:\n\n\tapt install libmicrohttpd-dev libjansson-dev \\\n\t\tlibssl-dev libsofia-sip-ua-dev libglib2.0-dev \\\n\t\tlibopus-dev libogg-dev libcurl4-openssl-dev liblua5.3-dev \\\n\t\tlibconfig-dev pkg-config libtool automake\n\n* *Note:* please notice that libopus may not be available out of the box on your distro. In that case, you'll have to [install it manually](https://www.opus-codec.org).\n\nWhile `libnice` is typically available in most distros as a package, the version available out of the box in Ubuntu is known to cause problems. As such, we always recommend manually compiling and installing the master version of libnice.\nTo build libnice, you need Python 3, Meson and Ninja:\n\n\tgit clone https://gitlab.freedesktop.org/libnice/libnice\n\tcd libnice\n\tmeson --prefix=/usr build && ninja -C build && sudo ninja -C build install\n\n* *Note:* Make sure you remove the distro version first, or you'll cause conflicts between the installations. In case you want to keep both for some reason, for custom installations of libnice you can also run `pkg-config --cflags --libs nice` to make sure Janus can find the right installation. If that fails, you may need to set the `PKG_CONFIG_PATH` environment variable prior to compiling Janus, e.g., `export PKG_CONFIG_PATH=/path/to/libnice/lib/pkgconfig`\n\nIn case you're interested in compiling the sample Event Handler plugin, you'll need to install the development version of libcurl as well (usually `libcurl-devel` on Fedora/CentOS, `libcurl4-openssl-dev` on Ubuntu/Debian).\n\nIf your distro ships a pre-1.5 version of libsrtp, you'll have to uninstall that version and [install 1.5.x, 1.6.x or 2.x manually](https://github.com/cisco/libsrtp/releases). In fact, 1.4.x is known to cause several issues with WebRTC. While 1.5.x is supported, we recommend installing 2.x instead. Notice that the following steps are for version 2.2.0, but there may be more recent versions available:\n\n\twget https://github.com/cisco/libsrtp/archive/v2.2.0.tar.gz\n\ttar xfv v2.2.0.tar.gz\n\tcd libsrtp-2.2.0\n\t./configure --prefix=/usr --enable-openssl\n\tmake shared_library && sudo make install\n\nNotice that the `--enable-openssl` part is _important_, as it's needed for AES-GCM support. As an alternative, you can also pass `--enable-nss` to have libsrtp use NSS instead of OpenSSL. A failure to configure libsrtp with either might cause undefined references when starting Janus, as we'd be trying to use methods that aren't there.\n\nThe Janus configure script autodetects which one you have installed and links to the correct library automatically, choosing 2.x if both are installed. If you want 1.5 or 1.6 to be picked (which is NOT recommended), pass `--disable-libsrtp2` when configuring Janus to force it to use the older version instead.\n\n* *Note:* when installing libsrtp, no matter which version, you may need to pass `--libdir=/usr/lib64` to the configure script if you're installing on a x86_64 distribution.\n\nIf you want to make use of BoringSSL instead of OpenSSL (e.g., because you want to take advantage of `--enable-dtls-settimeout`), you'll have to manually install it to a specific location. Use the following steps:\n\n\tgit clone https://boringssl.googlesource.com/boringssl\n\tcd boringssl\n\t# Don't barf on errors\n\tsed -i s/\" -Werror\"//g CMakeLists.txt\n\t# Build\n\tmkdir -p build\n\tcd build\n\tcmake -DCMAKE_INSTALL_PREFIX=/opt/boringssl -DCMAKE_CXX_FLAGS=\"-lrt\" ..\n\tmake\n\tsudo make install\n\nOnce the library is installed, you'll have to pass an additional `--enable-boringssl` flag to the configure script, as by default Janus will be built assuming OpenSSL will be used. By default, Janus expects BoringSSL to be installed in `/opt/boringssl` -- if it's installed in another location, pass the path to the configure script as such: `--enable-boringssl=/path/to/boringssl` If you were using OpenSSL and want to switch to BoringSSL, make sure you also do a `make clean` in the Janus folder before compiling with the new BoringSSL support. If you enabled BoringSSL support and also want Janus to detect and react to DTLS timeouts with faster retransmissions, then pass `--enable-dtls-settimeout` to the configure script too.\n\n* *Note:* as explained in [this issue](https://github.com/meetecho/janus-gateway/issues/3456), building Janus with more recent versions of BoringSSL may require you to pass a `CCLD=c++` for any `make` command to build Janus itself.\n\nFor what concerns usrsctp, which is needed for Data Channels support, it is usually not available in repositories, so if you're interested in them (support is optional) you'll have to install it manually. It is a pretty easy and standard process:\n\n\tgit clone https://github.com/sctplab/usrsctp\n\tcd usrsctp\n\t./bootstrap\n\t./configure --prefix=/usr --disable-programs --disable-inet --disable-inet6\n\tmake && sudo make install\n\n* *Note:* you may need to pass `--libdir=/usr/lib64` to the configure script if you're installing on a x86_64 distribution.\n\nThe same applies for libwebsockets, which is needed for the optional WebSockets support. If you're interested in supporting WebSockets to control Janus, as an alternative (or replacement) to the default plain HTTP REST API, you'll have to install it manually:\n\n\tgit clone https://libwebsockets.org/repo/libwebsockets\n\tcd libwebsockets\n\t# If you want the stable version of libwebsockets, uncomment the next line\n\t# git checkout v4.3-stable\n\tmkdir build\n\tcd build\n\t# See https://github.com/meetecho/janus-gateway/issues/732 re: LWS_MAX_SMP\n\t# See https://github.com/meetecho/janus-gateway/issues/2476 re: LWS_WITHOUT_EXTENSIONS\n\tcmake -DLWS_MAX_SMP=1 -DLWS_WITHOUT_EXTENSIONS=0 -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_C_FLAGS=\"-fpic\" ..\n\tmake && sudo make install\n\n* *Note:* if libwebsockets.org is unreachable for any reason, replace the first line with this:\n\n\tgit clone https://github.com/warmcat/libwebsockets.git\n\nThe same applies for Eclipse Paho MQTT C client library, which is needed for the optional MQTT support. If you're interested in integrating MQTT channels as an alternative (or replacement) to HTTP and/or WebSockets to control Janus, or as a carrier of Janus Events, you can install the latest version with the following steps:\n\n\tgit clone https://github.com/eclipse/paho.mqtt.c.git\n\tcd paho.mqtt.c\n\tmake && sudo make install\n\n* *Note:* you may want to set up a different install path for the library, to achieve that, replace the last command by 'sudo prefix=/usr make install'.\n\nIn case you're interested in Nanomsg support, you'll need to install the related C library. It is usually available as an easily installable package in pretty much all repositories. The following is an example on how to install it on Ubuntu:\n\n\taptitude install libnanomsg-dev\n\nFinally, the same can be said for rabbitmq-c as well, which is needed for the optional RabbitMQ support. In fact, several different versions of the library can be found, and the versions usually available in most distribution repositories are not up-do-date with respect to the current state of the development. As such, if you're interested in integrating RabbitMQ queues as an alternative (or replacement) to HTTP and/or WebSockets to control Janus, you can install the latest version with the following steps:\n\n\tgit clone https://github.com/alanxz/rabbitmq-c\n\tcd rabbitmq-c\n\tgit submodule init\n\tgit submodule update\n\tmkdir build && cd build\n\tcmake -DCMAKE_INSTALL_PREFIX=/usr ..\n\tmake && sudo make install\n\n* *Note:* you may need to pass `--libdir=/usr/lib64` to the configure script if you're installing on a x86_64 distribution.\n\nTo conclude, should you be interested in building the Janus documentation as well, you'll need some additional tools too:\n\n* [Doxygen](https://www.doxygen.org)\n* [Graphviz](https://www.graphviz.org/)\n\nOn Fedora:\n\n\tyum install doxygen graphviz\n\nOn Ubuntu/Debian:\n\n\taptitude install doxygen graphviz\n\n\n## Compile\nOnce you have installed all the dependencies, get the code:\n\n\tgit clone https://github.com/meetecho/janus-gateway.git\n\tcd janus-gateway\n\nThen just use:\n\n\tsh autogen.sh\n\nto generate the configure file. After that, configure and compile as usual to start the whole compilation process:\n\n\t./configure --prefix=/opt/janus\n\tmake\n\tmake install\n\nSince Janus requires configuration files for both the core and its modules in order to work, you'll probably also want to install the default configuration files to use, which you can do this way:\n\n\tmake configs\n\nRemember to only do this once, or otherwise a subsequent `make configs` will overwrite any configuration file you may have modified in the meanwhile.\n\nIf you've installed the above libraries but are not interested, for instance, in Data Channels, WebSockets, MQTT and/or RabbitMQ, you can disable them when configuring:\n\n\t./configure --disable-websockets --disable-data-channels --disable-rabbitmq --disable-mqtt\n\nThere are configuration flags for pretty much all external modules and many of the features, so you may want to issue a `./configure --help` to dig through the available options. A summary of what's going to be built will always appear after you do a configure, allowing you to double check if what you need and don't need is there.\n\nIf Doxygen and graphviz are available, the process can also build the documentation for you. By default the compilation process will not try to build the documentation, so if you instead prefer to build it, use the `--enable-docs` configuration option:\n\n\t./configure --enable-docs\n\nYou can also selectively enable/disable other features (e.g., specific plugins you don't care about, or whether or not you want to build the recordings post-processor). Use the --help option when configuring for more info.\n\n### Building on FreeBSD\n* *Note*: rtp_forward of streams only works streaming to IPv6,\nbecause of #2051 and thus the feature is not supported on FreeBSD at the moment.\n\nWhen building on FreeBSD you can install the depencencies from ports or packages, here only pkg method is used. You also need to use `gmake` instead of `make`,\nsince it is a GNU makefile. `./configure` can be run without arguments since the default prefix is `/usr/local` which is your default `LOCALBASE`.\nNote that the `configure.ac` is coded to use openssl in base. If you wish to use openssl from ports or any other ssl you must change `configure.ac` accordingly.\n\n\tpkg install libsrtp2 libusrsctp jansson libnice libmicrohttpd libwebsockets curl opus sofia-sip libogg jansson libnice libconfig \\\n        libtool gmake autoconf autoconf-wrapper glib\n\n\n### Building on MacOS\nWhile most of the above instructions will work when compiling Janus on MacOS as well, there are a few aspects to highlight when doing that.\n\nFirst of all, you can use `brew` to install most of the dependencies:\n\n\tbrew install jansson libnice openssl srtp libusrsctp libmicrohttpd \\\n\t\tlibwebsockets cmake rabbitmq-c sofia-sip opus libogg curl glib \\\n\t\tlibconfig pkg-config autoconf automake libtool\n\nFor what concerns libwebsockets, though, make sure that the installed version is higher than `2.4.1`, or you might encounter the problems described in [this post](https://groups.google.com/forum/#!topic/meetecho-janus/HsFaEXBz4Cg). If `brew` doesn't provide a more recent version, you'll have to install the library manually.\n\nNotice that you may need to provide a custom `prefix` and `PKG_CONFIG_PATH` when configuring Janus as well, e.g.:\n\n\t./configure --prefix=/usr/local/janus PKG_CONFIG_PATH=/usr/local/opt/openssl/lib/pkgconfig\n\nEverything else works exactly the same way as on Linux.\n\n## Configure and start\nTo start the server, you can use the `janus` executable. There are several things you can configure, either in a configuration file:\n\n\t<installdir>/etc/janus/janus.jcfg\n\nor on the command line:\n\n\t<installdir>/bin/janus --help\n\n\tUsage: janus [OPTIONS]...\n\n\t-h, --help                    Print help and exit\n\t-V, --version                 Print version and exit\n\t-b, --daemon                  Launch Janus in background as a daemon\n                                  (default=off)\n\t-p, --pid-file=path           Open the specified PID file when starting Janus\n                                  (default=none)\n\t-N, --disable-stdout          Disable stdout based logging  (default=off)\n\t-L, --log-file=path           Log to the specified file (default=stdout only)\n\t-H  --cwd-path                Working directory for Janus daemon process\n\t                              (default=/)\n\t-i, --interface=ipaddress     Interface to use (will be the public IP)\n\t-P, --plugins-folder=path     Plugins folder (default=./plugins)\n\t-C, --config=filename         Configuration file to use\n\t-F, --configs-folder=path     Configuration files folder (default=./conf)\n\t-c, --cert-pem=filename       DTLS certificate\n\t-k, --cert-key=filename       DTLS certificate key\n\t-K, --cert-pwd=text           DTLS certificate key passphrase (if needed)\n\t-S, --stun-server=address:port\n                                  STUN server(:port) to use, if needed (e.g.,\n                                  Janus behind NAT, default=none)\n\t-1, --nat-1-1=ip              Public IP to put in all host candidates,\n                                  assuming a 1:1 NAT is in place (e.g., Amazon\n                                  EC2 instances, default=none)\n\t-2, --keep-private-host       When nat-1-1 is used (e.g., Amazon EC2\n                                  instances), don't remove the private host,\n                                  but keep both to simulate STUN  (default=off)\n\t-E, --ice-enforce-list=list   Comma-separated list of the only interfaces to\n                                  use for ICE gathering; partial strings are\n                                  supported (e.g., eth0 or eno1,wlan0,\n                                  default=none)\n\t-X, --ice-ignore-list=list    Comma-separated list of interfaces or IP\n                                  addresses to ignore for ICE gathering;\n                                  partial strings are supported (e.g.,\n                                  vmnet8,192.168.0.1,10.0.0.1 or\n                                  vmnet,192.168., default=vmnet)\n\t-6, --ipv6-candidates         Whether to enable IPv6 candidates or not\n                                  (experimental)  (default=off)\n\t-O, --ipv6-link-local         Whether IPv6 link-local candidates should be\n                                  gathered as well  (default=off)\n\t-f, --full-trickle            Do full-trickle instead of half-trickle\n                                  (default=off)\n\t-I, --ice-lite                Whether to enable the ICE Lite mode or not\n                                  (default=off)\n\t-T, --ice-tcp                 Whether to enable ICE-TCP or not (warning: only\n                                  works with ICE Lite)\n                                  (default=off)\n\t-Q, --min-nack-queue=number   Minimum size of the NACK queue (in ms) per user\n                                  for retransmissions, no matter the RTT\n\t-t, --no-media-timer=number   Time (in s) that should pass with no media\n                                  (audio or video) being received before Janus\n                                  notifies you about this\n\t-W, --slowlink-threshold=number\n                                  Number of lost packets (per s) that should\n                                  trigger a 'slowlink' Janus API event to users\n                                  (default=0, feature disabled)\n\t-r, --rtp-port-range=min-max  Port range to use for RTP/RTCP (only available\n\t\t\t\t\t\t\t\t  if the installed libnice supports it)\n\t-B, --twcc-period=number      How often (in ms) to send TWCC feedback back to\n                                  senders, if negotiated (default=200ms)\n\t-n, --server-name=name        Public name of this Janus instance\n                                  (default=MyJanusInstance)\n\t-s, --session-timeout=number  Session timeout value, in seconds (default=60)\n\t-m, --reclaim-session-timeout=number\n                                  Reclaim session timeout value, in seconds\n                                  (default=0)\n\t-d, --debug-level=1-7         Debug/logging level (0=disable debugging,\n                                  7=maximum debug level; default=4)\n\t-D, --debug-timestamps        Enable debug/logging timestamps  (default=off)\n\t-o, --disable-colors          Disable color in the logging  (default=off)\n\t-M, --debug-locks             Enable debugging of locks/mutexes (very\n                                  verbose!)  (default=off)\n\t-a, --apisecret=randomstring  API secret all requests need to pass in order\n                                  to be accepted by Janus (useful when wrapping\n                                  Janus API requests in a server, none by\n                                  default)\n\t-A, --token-auth              Enable token-based authentication for all\n                                  requests  (default=off)\n\t-e, --event-handlers          Enable event handlers  (default=off)\n\t-w, --no-webrtc-encryption    Disable WebRTC encryption, so no DTLS or SRTP\n                                  (only for debugging!)  (default=off)\n\n\nOptions passed through the command line have the precedence on those specified in the configuration file. To start the server, simply run:\n\n\t<installdir>/bin/janus\n\nThis will start the server, and have it look at the configuration file.\n\nMake sure you have a look at all of the configuration files, to tailor Janus to your specific needs: each configuration file is documented, so it shouldn't be hard to make changes according to your requirements. The repo comes with some defaults (assuming you issues `make configs` after installing the server) that tend to make sense for generic deployments, and also includes some sample configurations for all the plugins (e.g., web servers to listen on, conference rooms to create, streaming mountpoints to make available at startup, etc.).\n\nTo test whether it's working correctly, you can use the demos provided with this package in the `html` folder: these are exactly the same demos available online on the [project website](https://janus.conf.meetecho.com/). Just copy the file it contains in a webserver, or use a userspace webserver to serve the files in the `html` folder (e.g., with php or python), and open the `index.html` page in either Chrome or Firefox. A list of demo pages exploiting the different plugins will be available. Remember to edit the transport/port details in the demo JavaScript files if you changed any transport-related configuration from its defaults. Besides, the demos refer to the pre-configured plugin resources, so if you add some new resources (e.g., a new videoconference) you may have to tweak the demo pages to actually use them.\n\n## Documentation\nJanus is thoroughly documented. You can find the current documentation, automatically generated with Doxygen, on the [project website](https://janus.conf.meetecho.com/docs/).\n\n## Help us!\nAny thought, feedback or (hopefully not!) insult is welcome!\n\nDeveloped by [@meetecho](https://github.com/meetecho)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nNotice that, while there are multiple versions of Janus, they're simply\ntagged versions of the master Janus branch. As such, once they're tagged\nas versions, they won't receive security updates. The only way to get\nsecurity updates that are published after a specific version is to\neither use a more recent tagged version (if it includes the security\nfix) or refer to the master branch instead (which is where active\ndevelopment focuses on). Refer to the [CHANGELOG.md](CHANGELOG.md)\nfile for information of what each version adds, including security\nrelated fixes.\n\n## Reporting a Vulnerability\n\nIn case you find a vulnerability you think we should be aware of,\nplease use the contact form on the [Meetecho website](https://www.meetecho.com),\nusing the \"Not sure/Other\" service type in the dropdown menu. We\ntake vulnerability reports very seriously, and so we tend to\nprioritize the evaulation of such reports and, in case a fix\nis indeed warranted, the development of a fix.\n"
  },
  {
    "path": "autogen.sh",
    "content": "#!/bin/sh\n\nsrcdir=`dirname $0`\ntest -z \"$srcdir\" && srcdir=.\n\nmkdir -p m4\n\nautoreconf --verbose --force --install || exit 1\n"
  },
  {
    "path": "bower.json",
    "content": "{\n  \"name\": \"janus-gateway\",\n  \"version\": \"1.4.1\",\n  \"homepage\": \"https://github.com/meetecho/janus-gateway\",\n  \"authors\": [\n    \"Lorenzo Miniero <lorenzo@meetecho.com>\",\n    \"Philip Withnall <philip@tecnocode.co.uk>\",\n    \"Jack Leigh\",\n    \"Pierce Lopez <pierce.lopez@gmail.com>\",\n    \"Benjamin Trent <ben.w.trent@gmail.com>\",\n    \"Dustin Oprea <dustin@randomingenuity.com>\",\n    \"Maurizio Porrato\",\n    \"Giacomo Vacca\",\n    \"mrauhu\",\n    \"Min Wang\",\n    \"leonuh\",\n    \"Nicholas Wylie\",\n    \"Graeme Yeates <yeatesgraeme@gmail.com>\",\n    \"gatecrasher777\",\n    \"Damon Oehlman <damon.oehlman@gmail.com>\",\n    \"Scott <scottmortonashton@gmail.com>\"\n  ],\n  \"description\": \"A javascript library for interacting with the C based Janus WebRTC Server\",\n  \"main\": \"./html/janus.js\",\n  \"license\": \"GPLv3\",\n  \"ignore\": [\n    \"**/.*\",\n    \"**/*.alaw\",\n    \"**/*.mjr\",\n    \"node_modules\",\n    \"bower_components\",\n    \"test\",\n    \"tests\"\n  ],\n  \"dependencies\": {\n    \"webrtc-adapter\": \"9.0.3\"\n  }\n}\n"
  },
  {
    "path": "conf/janus.eventhandler.gelfevh.jcfg.sample",
    "content": "# This configures the GELF event handler. Appending necessary headers \n# and sending messages via TCP or UDP\n\ngeneral: {\n\tenabled = false\t\t\t\t\t\t# By default the module is not enabled\n\tevents = \"all\"\n\t\t\t\t\t\t\t\t\t\t# Comma separated list of the events mask you're interested\n\t\t\t\t\t\t\t\t\t\t# in. Valid values are none, sessions, handles, jsep, webrtc,\n\t\t\t\t\t\t\t\t\t\t# media, plugins, transports, core, external and all. By\n\t\t\t\t\t\t\t\t\t\t# default we subscribe to everything (all)\n\n\tbackend = \"your.graylog.server\"\t\t# DNS or IP of your Graylog server\n\tport = \"12201\"\t\t\t\t\t\t# Port Graylog server is listening on\n\tprotocol = \"tcp\"\t\t\t\t\t# tcp or udp transport type\n\tmax_message_len = 1024\t\t\t\t# Note that we add 12 bytes of headers + standard UDP headers (8 bytes) \n\t\t\t\t\t\t\t\t\t\t# when calculating packet size based on MTU   \n\n\t#compress = true\t\t\t\t\t# Optionally, only for UDP transport, JSON messages can be compressed using zlib\n\t#compression = 9\t\t\t\t\t# In case, you can specify the compression factor, where 1 is\n\t\t\t\t\t\t\t\t\t\t# the fastest (low compression), and 9 gives the best compression\n}\n"
  },
  {
    "path": "conf/janus.eventhandler.mqttevh.jcfg.sample",
    "content": "# This configures the MQTT event handler. Events are sent either on\n# one topic or on a topic per event type.\n#\n# By default, configuration topics for handle and webrtc event types\n# with the base topic are configured to /janus/events, e.g.:\n#\t/janus/events/handle\n#\t/janus/events/webrtc\n\ngeneral: {\n\tenabled = false\t\t\t# By default the module is not enabled\n\tevents = \"all\"\t\t\t# Comma separated list of the events mask you're interested\n\t\t\t\t\t\t\t# in. Valid values are none, sessions, handles, jsep, webrtc,\n\t\t\t\t\t\t\t# media, plugins, transports, core, external and all. By\n\t\t\t\t\t\t\t# default we subscribe to everything (all)\n\tjson = \"indented\"\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\n\turl = \"tcp://localhost:1883\"\t# The URL of the MQTT server. \"tcp://\" and \"ssl://\" protocols are supported.\n\t#mqtt_version = \"3.1.1\"\t\t\t# Protocol version. Available values: 3.1, 3.1.1 (default), 5.\n\tclient_id = \"janus.example.com\"\t# Janus client id. You have to configure a unique ID (default: guest).\n\t#keep_alive_interval = 20\t\t# Keep connection for N seconds (default: 30)\n\t#cleansession = 0\t\t\t\t# Clean session flag (default: off)\n\t#retain = 0\t\t\t\t\t\t# Default MQTT retain flag for published events\n\t#qos = 1\t\t\t\t\t\t# Default MQTT QoS for published events\n\t#max_inflight = 10\t\t\t\t# Maximum number of inflight messages\n\t#max_buffered = 100\t\t\t\t# Maximum number of buffered messages\n\t#disconnect_timeout = 100\t\t# Seconds to wait before destroying client\n\t#username = \"guest\"\t\t\t\t# Username for authentication (default: no authentication)\n\t#password = \"guest\"\t\t\t\t# Password for authentication (default: no authentication)\n\t#topic = \"/janus/events\"\t\t# Base topic (default: /janus/events)\n\t#addevent = true\t\t\t\t# Whether we should add the event type to the base topic\n\n\t#tls_enable = false\t\t\t\t# Whether TLS support must be enabled\n\n\t# Initial message sent to status topic\n\t#connect_status = \"{\\\"event\\\": \\\"connected\\\", \\\"eventhandler\\\": \\\"janus.eventhandler.mqttevh\\\"}\"\n\t# Message sent after disconnect or as LWT\n\t#disconnect_status = \"{\\\"event\\\": \\\"disconnected\\\"}\"\n\n\t#will_enabled = false\t\t\t\t\t\t\t# Whether to enable LWT (default: false)\n\t#will_retain = 1\t\t\t\t\t\t\t\t# Whether LWT should be retained (default: 1)\n\t#will_qos = 0\t\t\t\t\t\t\t\t\t# QoS for LWT (default: 0)\n\n\t# Additional parameters if \"mqtts://\" schema is used\n\t#tls_verify_peer = true\t\t\t# Whether peer verification must be enabled\n\t#tls_verify_hostname = true\t\t# Whether hostname verification must be enabled\n\n\t# Certificates to use when TLS support is enabled, if needed\n\t#tls_cacert = \"/path/to/cacert.pem\"\n\t#tls_client_cert = \"/path/to/cert.pem\"\n\t#tls_client_key = \"/path/to/key.pem\"\n\t#tls_ciphers\n\t#tls_version\n\n\t# These options work with MQTT 5 only.\n\t#add_user_properties = ()\t# List of user property [\"key\", \"value\"] pairs to add.\n}\n"
  },
  {
    "path": "conf/janus.eventhandler.nanomsgevh.jcfg.sample",
    "content": "# This configures the Nanomsg event handler. Since this plugin only\n# forwards each event it receives via Nanomsg, you simply need to\n# configure (i) which events to subscribe to, (ii) the address to use for\n# the communication, and (iii) whether the address should be used to bind\n# locally or to connect to a remote endpoint. Notice that the only supported\n# pattern is NN_PUBSUB, where the Nanomsg event handler is the publisher.\n\ngeneral: {\n\tenabled = false\t\t# By default the module is not enabled\n\tevents = \"all\"\t\t# Comma separated list of the events mask you're interested\n\t\t\t\t\t\t# in. Valid values are none, sessions, handles, jsep, webrtc,\n\t\t\t\t\t\t# media, plugins, transports, core, external and all. By\n\t\t\t\t\t\t# default we subscribe to everything (all)\n\tgrouping = true\t\t# Whether events should be sent individually (one per\n\t\t\t\t\t\t# HTTP POST, JSON object), or if it's ok to group them\n\t\t\t\t\t\t# (one or more per HTTP POST, JSON array with objects)\n\t\t\t\t\t\t# The default is 'yes' to limit the number of connections.\n\n\t\t\t\t\t\t# Address the plugin will send all events to as HTTP POST\n\t\t\t\t\t\t# requests with an application/json payload. In case\n\t\t\t\t\t\t# authentication is required to contact the backend, set\n\t\t\t\t\t\t# the credentials as well (basic authentication only).\n\tjson = \"indented\"\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\n\t#mode = \"bind\"\t\t\t\t\t\t# Whether we should 'bind' to the specified\n\t\t\t\t\t\t\t\t\t\t# address, or connect to it if remote (default)\n\taddress = \"ipc:///tmp/janusevh.ipc\"\t# Address to use, refer to the Nanomsg documentation\n\t\t\t\t\t\t\t\t\t\t# for more info on different transports you can use here\n}\n"
  },
  {
    "path": "conf/janus.eventhandler.rabbitmqevh.jcfg.sample",
    "content": "# This configures the RabbitMQ event handler.\n\ngeneral: {\n\tenabled = false\t\t\t\t\t# By default the module is not enabled\n\tevents = \"all\"\t\t\t\t\t# Comma separated list of the events mask you're interested\n\t\t\t\t\t\t\t\t\t# in. Valid values are none, sessions, handles, jsep, webrtc,\n\t\t\t\t\t\t\t\t\t# media, plugins, transports, core, external and all. By\n\t\t\t\t\t\t\t\t\t# default we subscribe to everything (all)\n\tgrouping = true\t\t\t\t\t# Whether events should be sent individually , or if it's ok\n\t\t\t\t\t\t\t\t\t# to group them. The default is 'yes' to limit the number of\n\t\t\t\t\t\t\t\t\t# messages\n\tjson = \"indented\"\t\t\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\n\thost = \"localhost\"\t\t\t\t# The address of the RabbitMQ server\n\t#port = 5672\t\t\t\t\t# The port of the RabbitMQ server (5672 by default)\n\t#username = \"guest\"\t\t\t\t# Username to use to authenticate, if needed\n\t#password = \"guest\"\t\t\t\t# Password to use to authenticate, if needed\n\t#vhost = \"/\"\t\t\t\t\t# Virtual host to specify when logging in, if needed\n\t#exchange = \"janus-exchange\"\n\troute_key = \"janus-events\"\t\t# Routing key to use when publishing messages\n\t#exchange_type = \"fanout\" \t\t# Rabbitmq exchange_type can be one of the available types: direct, topic, headers and fanout (fanout by defualt).\n\t#heartbeat = 60 \t\t\t\t# Defines the seconds without communication that should pass before considering the TCP connection unreachable.\n\t#declare_outgoing_queue = true # By default (for backwards compatibility), we declare an outgoing queue. Set this to false to disable that behavior\n\n\t#ssl_enable = false\t\t\t\t# Whether ssl support must be enabled\n\t#ssl_verify_peer = true\t\t\t# Whether peer verification must be enabled\n\t#ssl_verify_hostname = true\t\t# Whether hostname verification must be enabled\n\n\t# Certificates to use when SSL support is enabled, if needed\n\t#ssl_cacert = \"/path/to/cacert.pem\"\n\t#ssl_cert = \"/path/to/cert.pem\"\n\t#ssl_key = \"/path/to/key.pem\"\n}\n"
  },
  {
    "path": "conf/janus.eventhandler.sampleevh.jcfg.sample",
    "content": "# This configures the sample event handler. Since this plugin simply\n# forwards each event it receives via HTTP POST, you simply need to\n# configure (i) which events to subscribe to, and (ii) the address of\n# the web server which will receive the requests.\n\ngeneral: {\n\tenabled = false\t\t# By default the module is not enabled\n\tevents = \"all\"\t\t# Comma separated list of the events mask you're interested\n\t\t\t\t\t\t# in. Valid values are none, sessions, handles, jsep, webrtc,\n\t\t\t\t\t\t# media, plugins, transports, core, external and all. By\n\t\t\t\t\t\t# default we subscribe to everything (all)\n\tgrouping = true\t\t# Whether events should be sent individually (one per\n\t\t\t\t\t\t# HTTP POST, JSON object), or if it's ok to group them\n\t\t\t\t\t\t# (one or more per HTTP POST, JSON array with objects)\n\t\t\t\t\t\t# The default is 'true' to limit the number of connections.\n\tjson = \"indented\"\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\n\t#compress = true\t# Optionally, the JSON messages can be compressed using zlib\n\t#compression = 9\t# In case, you can specify the compression factor, where 1 is\n\t\t\t\t\t\t# the fastest (low compression), and 9 gives the best compression\n\n\t\t\t\t\t\t# Address the plugin will send all events to as HTTP POST\n\t\t\t\t\t\t# requests with an application/json payload. In case\n\t\t\t\t\t\t# authentication is required to contact the backend, set\n\t\t\t\t\t\t# the credentials as well (basic authentication only).\n\tbackend = \"http://your.webserver.here/and/a/path\"\n\t#backend_user = \"myuser\"\n\t#backend_pwd = \"mypwd\"\n\n\t\t\t\t\t\t# You can also configure how retransmissions should\n\t\t\t\t\t\t# happen, after a failed attempt to deliver an event.\n\t\t\t\t\t\t# Specifically, you can specify how many times a\n\t\t\t\t\t\t# retransmission should be attempted (default=5) and\n\t\t\t\t\t\t# which step is used, in milliseconds, for the exponential\n\t\t\t\t\t\t# backoff before retrying (e.g, if step=100ms, then the\n\t\t\t\t\t\t# the first retry will happen after 100ms, the second\n\t\t\t\t\t\t# after 200ms, then 400ms, and so on). If the event cannot\n\t\t\t\t\t\t# be retransmitted after the maximum number of attemps\n\t\t\t\t\t\t# is reached, then it's lost. Beware that retransmissions\n\t\t\t\t\t\t# will also delay pending events and increase the queue.\n\t#max_retransmissions = 5\n\t#retransmissions_backoff = 100\n}\n"
  },
  {
    "path": "conf/janus.eventhandler.wsevh.jcfg.sample",
    "content": "# This configures the WebSockets event handler. Since this plugin only\n# forwards each event it receives via WebSockets, you simply need to\n# configure (i) which events to subscribe to, and (ii) the address of\n# the WebSockets server which will receive the requests.\n\ngeneral: {\n\tenabled = false\t\t# By default the module is not enabled\n\tevents = \"all\"\t\t# Comma separated list of the events mask you're interested\n\t\t\t\t\t\t# in. Valid values are none, sessions, handles, jsep, webrtc,\n\t\t\t\t\t\t# media, plugins, transports, core, external and all. By\n\t\t\t\t\t\t# default we subscribe to everything (all)\n\tgrouping = true\t\t# Whether events should be sent individually (one per\n\t\t\t\t\t\t# HTTP POST, JSON object), or if it's ok to group them\n\t\t\t\t\t\t# (one or more per HTTP POST, JSON array with objects)\n\t\t\t\t\t\t# The default is 'yes' to limit the number of connections.\n\n\tjson = \"indented\"\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\n\t\t\t\t\t\t# Address the plugin will send all events to as WebSocket\n\t\t\t\t\t\t# messages. In case authentication is required to contact\n\t\t\t\t\t\t# the backend, set the credentials as well.\n\tbackend = \"ws://your.websocket.here\"\n\t# subprotocol = \"your-subprotocol\"\n\n\t\t\t\t\t\t# If the WebSocket server isn't reachable or the client has\n\t\t\t\t\t\t# to reconnect, the default behaviour of the handler plugin\n\t\t\t\t\t\t# is to retry with an exponential back-off, all while buffering\n\t\t\t\t\t\t# events that Janus may keep on pushing. Buffering has no\n\t\t\t\t\t\t# limit, so if reconnecting takes a long time (or forever)\n\t\t\t\t\t\t# memory usage will keep on growing; besides, it may cause\n\t\t\t\t\t\t# a network spike when eventually reconnected, as all stored\n\t\t\t\t\t\t# events would need to be sent to the backend before new\n\t\t\t\t\t\t# ones can be relayed as well. You can prune queued events\n\t\t\t\t\t\t# and put a cap on the amount of buffering to perform when\n\t\t\t\t\t\t# reconnecting by setting the 'events_cap_on_reconnect'\n\t\t\t\t\t\t# property accordingly: any number you set will be the\n\t\t\t\t\t\t# maximum number of events stored in memory until we\n\t\t\t\t\t\t# reconnect, which means older packets will be discarded\n\t\t\t\t\t\t# if the cap is exceeded. Notice that setting a value\n\t\t\t\t\t\t# of 0 will not mean \"drop all packets\", but will disable\n\t\t\t\t\t\t# the cap (default behaviour), which means the minimum\n\t\t\t\t\t\t# possible value is 1. Also notice that, when the cap is\n\t\t\t\t\t\t# enabled, this means the event receiver may end up missing key\n\t\t\t\t\t\t# events when a reconnection actually ends up taking place.\n\t# events_cap_on_reconnect = 10\n\n\t\t\t\t\t\t# In case you need to debug connection issues, you can configure\n\t\t\t\t\t\t# the libwebsockets debugging level as a comma separated list of things\n\t\t\t\t\t\t# to debug, supported values: err, warn, notice, info, debug, parser,\n\t\t\t\t\t\t# header, ext, client, latency, user, count (plus 'none' and 'all')\n\t#ws_logging = \"err,warn\"\n}\n"
  },
  {
    "path": "conf/janus.jcfg.sample.in",
    "content": "# General configuration: folders where the configuration and the plugins\n# can be found, how output should be logged, whether Janus should run as\n# a daemon or in foreground, default interface to use, debug/logging level\n# and, if needed, shared apisecret and/or token authentication mechanism\n# between application(s) and Janus.\ngeneral: {\n\tconfigs_folder = \"@confdir@\"\t\t\t# Configuration files folder\n\tplugins_folder = \"@plugindir@\"\t\t\t# Plugins folder\n\ttransports_folder = \"@transportdir@\"\t# Transports folder\n\tevents_folder = \"@eventdir@\"\t\t\t# Event handlers folder\n\tloggers_folder = \"@loggerdir@\"\t\t\t# External loggers folder\n\n\t\t# The next settings configure logging\n\t#log_to_stdout = false\t\t\t\t\t# Whether the Janus output should be written\n\t\t\t\t\t\t\t\t\t\t\t# to stdout or not (default=true)\n\t#log_to_file = \"/path/to/janus.log\"\t\t# Whether to use a log file or not\n\t#log_rotate_sig = \"SIGUSR1\"\t\t\t\t# Signal to handle for log rotation, valid values\n\t\t\t\t\t\t\t\t\t\t\t# are \"SIGUSR1\" and \"SIGHUP\".\n\t\t\t\t\t\t\t\t\t\t\t# Default is no setting, which disables the\n\t\t\t\t\t\t\t\t\t\t\t# signal handler for log rotation.\n\tdebug_level = 4\t\t\t\t\t\t\t# Debug/logging level, valid values are 0-7\n\t#debug_timestamps = true\t\t\t\t# Whether to show a timestamp for each log line\n\t#debug_colors = false\t\t\t\t\t# Whether colors should be disabled in the log\n\t#debug_locks = true\t\t\t\t\t\t# Whether to enable debugging of locks (very verbose!)\n\t#log_prefix = \"[janus] \"\t\t\t\t# In case you want log lines to be prefixed by some\n\t\t\t\t\t\t\t\t\t\t\t# custom text, you can use the 'log_prefix' property.\n\t\t\t\t\t\t\t\t\t\t\t# It supports terminal colors, meaning something like\n\t\t\t\t\t\t\t\t\t\t\t# \"[\\x1b[32mjanus\\x1b[0m] \" would show a green \"janus\"\n\t\t\t\t\t\t\t\t\t\t\t# string in square brackets (assuming debug_colors=true).\n\n\t\t# This is what you configure if you want to launch Janus as a daemon\n\t#daemonize = true\t\t\t\t\t\t# Whether Janus should run as a daemon\n\t\t\t\t\t\t\t\t\t\t\t# or not (default=run in foreground)\n\t#pid_file = \"/path/to/janus.pid\"\t\t# PID file to create when Janus has been\n\t\t\t\t\t\t\t\t\t\t\t# started, and to destroy at shutdown\n\n\t\t# There are different ways you can authenticate the Janus and Admin APIs\n\t#api_secret = \"janusrocks\"\t\t# String that all Janus requests must contain\n\t\t\t\t\t\t\t\t\t# to be accepted/authorized by the Janus core.\n\t\t\t\t\t\t\t\t\t# Useful if you're wrapping all Janus API requests\n\t\t\t\t\t\t\t\t\t# in your servers (that is, not in the browser,\n\t\t\t\t\t\t\t\t\t# where you do the things your way) and you\n\t\t\t\t\t\t\t\t\t# don't want other application to mess with\n\t\t\t\t\t\t\t\t\t# this Janus instance.\n\t#token_auth = true\t\t\t\t# Enable a token based authentication\n\t\t\t\t\t\t\t\t\t# mechanism to force users to always provide\n\t\t\t\t\t\t\t\t\t# a valid token in all requests. Useful if\n\t\t\t\t\t\t\t\t\t# you want to authenticate requests from web\n\t\t\t\t\t\t\t\t\t# users.\n\t#token_auth_secret = \"janus\"\t# Use HMAC-SHA1 signed tokens (with token_auth). Note that\n\t\t\t\t\t\t\t\t\t# without this, the Admin API MUST\n\t\t\t\t\t\t\t\t\t# be enabled, as tokens are added and removed\n\t\t\t\t\t\t\t\t\t# through messages sent there.\n\tadmin_secret = \"janusoverlord\"\t# String that all Janus requests must contain\n\t\t\t\t\t\t\t\t\t# to be accepted/authorized by the admin/monitor.\n\t\t\t\t\t\t\t\t\t# only needed if you enabled the admin API\n\t\t\t\t\t\t\t\t\t# in any of the available transports.\n\n\t\t# Generic settings\n\t#interface = \"1.2.3.4\"\t\t\t# Interface to use (will be used in SDP)\n\t#server_name = \"MyJanusInstance\"# Public name of this Janus instance\n\t\t\t\t\t\t\t\t\t# as it will appear in an info request\n\t#session_timeout = 60\t\t\t# How long (in seconds) we should wait before\n\t\t\t\t\t\t\t\t\t# deciding a Janus session has timed out. A\n\t\t\t\t\t\t\t\t\t# session times out when no request is received\n\t\t\t\t\t\t\t\t\t# for session_timeout seconds (default=60s).\n\t\t\t\t\t\t\t\t\t# Setting this to 0 will disable the timeout\n\t\t\t\t\t\t\t\t\t# mechanism, which is NOT suggested as it may\n\t\t\t\t\t\t\t\t\t# risk having orphaned sessions (sessions not\n\t\t\t\t\t\t\t\t\t# controlled by any transport and never freed).\n\t\t\t\t\t\t\t\t\t# To avoid timeouts, keep-alives can be used.\n\t#candidates_timeout = 45\t\t# How long (in seconds) we should keep hold of\n\t\t\t\t\t\t\t\t\t# pending (trickle) candidates before discarding\n\t\t\t\t\t\t\t\t\t# them (default=45s). Notice that setting this\n\t\t\t\t\t\t\t\t\t# to 0 will NOT disable the timeout, but will\n\t\t\t\t\t\t\t\t\t# be considered an invalid value and ignored.\n\t#reclaim_session_timeout = 0\t# How long (in seconds) we should wait for a\n\t\t\t\t\t\t\t\t\t# janus session to be reclaimed after the transport\n\t\t\t\t\t\t\t\t\t# is gone. After the transport is gone, a session\n\t\t\t\t\t\t\t\t\t# times out when no request is received for\n\t\t\t\t\t\t\t\t\t# reclaim_session_timeout seconds (default=0s).\n\t\t\t\t\t\t\t\t\t# Setting this to 0 will disable the timeout\n\t\t\t\t\t\t\t\t\t# mechanism, and sessions will be destroyed immediately\n\t\t\t\t\t\t\t\t\t# if the transport is gone.\n\t#recordings_tmp_ext = \"tmp\"\t\t# The extension for recordings, in Janus, is\n\t\t\t\t\t\t\t\t\t# .mjr, a custom format we devised ourselves.\n\t\t\t\t\t\t\t\t\t# By default, we save to .mjr directly. If you'd\n\t\t\t\t\t\t\t\t\t# rather the recording filename have a temporary\n\t\t\t\t\t\t\t\t\t# extension while it's being saved, and only\n\t\t\t\t\t\t\t\t\t# have the .mjr extension when the recording\n\t\t\t\t\t\t\t\t\t# is over (e.g., to automatically trigger some\n\t\t\t\t\t\t\t\t\t# external scripts), then uncomment and set the\n\t\t\t\t\t\t\t\t\t# recordings_tmp_ext property to the extension\n\t\t\t\t\t\t\t\t\t# to add to the base (e.g., tmp --> .mjr.tmp).\n\t#event_loops = 8\t\t\t\t# By default, Janus handles each have their own\n\t\t\t\t\t\t\t\t\t# event loop and related thread for all the media\n\t\t\t\t\t\t\t\t\t# routing and management. If for some reason you'd\n\t\t\t\t\t\t\t\t\t# rather limit the number of loop/threads, and\n\t\t\t\t\t\t\t\t\t# you want handles to share those, you can do that\n\t\t\t\t\t\t\t\t\t# configuring the event_loops property: this will\n\t\t\t\t\t\t\t\t\t# spawn the specified amount of threads at startup,\n\t\t\t\t\t\t\t\t\t# run a separate event loop on each of them, and\n\t\t\t\t\t\t\t\t\t# add new handles to one of them when attaching.\n\t\t\t\t\t\t\t\t\t# Notice that, while cutting the number of threads\n\t\t\t\t\t\t\t\t\t# and possibly reducing context switching, this\n\t\t\t\t\t\t\t\t\t# might have an impact on the media delivery,\n\t\t\t\t\t\t\t\t\t# especially if the available loops can't take\n\t\t\t\t\t\t\t\t\t# care of all the handles and their media in time.\n\t\t\t\t\t\t\t\t\t# As such, if you want to use this you should\n\t\t\t\t\t\t\t\t\t# provision the correct value according to the\n\t\t\t\t\t\t\t\t\t# available resources (e.g., CPUs available).\n\t#allow_loop_indication = true\t# In case a static number of event loops is\n\t\t\t\t\t\t\t\t\t# configured as explained above, by default\n\t\t\t\t\t\t\t\t\t# new handles will be allocated on one loop or\n\t\t\t\t\t\t\t\t\t# another by the Janus core itself. In some cases\n\t\t\t\t\t\t\t\t\t# it may be helpful to manually tell the Janus\n\t\t\t\t\t\t\t\t\t# core which loop a handle should be added to,\n\t\t\t\t\t\t\t\t\t# e.g., to group viewers of the same stream on\n\t\t\t\t\t\t\t\t\t# the same loop. This is possible via the Janus\n\t\t\t\t\t\t\t\t\t# API when performing the 'attach' request, but\n\t\t\t\t\t\t\t\t\t# only if allow_loop_indication is set to true;\n\t\t\t\t\t\t\t\t\t# it's set to false by default to avoid abuses.\n\t\t\t\t\t\t\t\t\t# Don't change if you don't know what you're doing!\n\t#task_pool_size = 100\t\t\t# By default, while the Janus core is single thread\n\t\t\t\t\t\t\t\t\t# when it comes to processing incoming messages, it\n\t\t\t\t\t\t\t\t\t# also uses a task pool with an indefinite amount\n\t\t\t\t\t\t\t\t\t# of helper threads spawned on demand to handle\n\t\t\t\t\t\t\t\t\t# messages addressed to plugins. If you want to\n\t\t\t\t\t\t\t\t\t# limit this task pool size with a maximum number\n\t\t\t\t\t\t\t\t\t# of concurrent threads, set the 'task_pool_size'\n\t\t\t\t\t\t\t\t\t# property accordingly: a value of '0' means\n\t\t\t\t\t\t\t\t\t# 'indefinite' and is the default. Notice that\n\t\t\t\t\t\t\t\t\t# threads are automatically destroyed when unused\n\t\t\t\t\t\t\t\t\t# for a while, so whatever value you choose simply\n\t\t\t\t\t\t\t\t\t# puts a cap on the maximum concurrency.\n\t\t\t\t\t\t\t\t\t# Don't change if you don't know what you're doing!\n\t#opaqueid_in_api = true\t\t\t# Opaque IDs set by applications are typically\n\t\t\t\t\t\t\t\t\t# only passed to event handlers for correlation\n\t\t\t\t\t\t\t\t\t# purposes, but not sent back to the user or\n\t\t\t\t\t\t\t\t\t# application in the related Janus API responses\n\t\t\t\t\t\t\t\t\t# or events; in case you need them to be in the\n\t\t\t\t\t\t\t\t\t# Janus API too, set this property to 'true'.\n\t#hide_dependencies = true\t\t# By default, a call to the \"info\" endpoint of\n\t\t\t\t\t\t\t\t\t# either the Janus or Admin API now also returns\n\t\t\t\t\t\t\t\t\t# the versions of the main dependencies (e.g.,\n\t\t\t\t\t\t\t\t\t# libnice, libsrtp, which crypto library is in\n\t\t\t\t\t\t\t\t\t# use and so on). Should you want that info not\n\t\t\t\t\t\t\t\t\t# to be disclose, set 'hide_dependencies' to true.\n\t#exit_on_dl_error = false\t\t# If a Janus shared libary cannot be loaded or an expected\n\t\t\t\t\t\t\t\t\t# symbol is not found, exit immediately.\n\n\t\t# The following is ONLY useful when debugging RTP/RTCP packets,\n\t\t# e.g., to look at unencrypted live traffic with a browser. By\n\t\t# default it is obviously disabled, as WebRTC mandates encryption.\n\t#no_webrtc_encryption = true\n\n\t\t# Janus provides ways via its API to specify custom paths to save\n\t\t# files to (e.g., recordings, pcap captures and the like). In order\n\t\t# to avoid people can mess with folders they're not supposed to,\n\t\t# you can configure an array of folders that Janus should prevent\n\t\t# creating files in. If the 'protected_folder' property below is\n\t\t# commented, no folder is protected.\n\t\t# Notice that at the moment this only covers attempts to start\n\t\t# an .mjr recording and pcap/text2pcap packet captures.\n\tprotected_folders = [\n\t\t\"/bin\",\n\t\t\"/boot\",\n\t\t\"/dev\",\n\t\t\"/etc\",\n\t\t\"/initrd\",\n\t\t\"/lib\",\n\t\t\"/lib32\",\n\t\t\"/lib64\",\n\t\t\"/proc\",\n\t\t\"/sbin\",\n\t\t\"/sys\",\n\t\t\"/usr\",\n\t\t\"/var\",\n\t\t\t# We add what are usually the folders Janus is installed to\n\t\t\t# as well: we don't just put \"/opt/janus\" because that would\n\t\t\t# include folders like \"/opt/janus/share\" that is where\n\t\t\t# recordings might be saved to by some plugins\n\t\t\"/opt/janus/bin\",\n\t\t\"/opt/janus/etc\",\n\t\t\"/opt/janus/include\",\n\t\t\"/opt/janus/lib\",\n\t\t\"/opt/janus/lib32\",\n\t\t\"/opt/janus/lib64\",\n\t\t\"/opt/janus/sbin\"\n\t]\n}\n\n# Certificate and key to use for DTLS (and passphrase if needed). If missing,\n# Janus will autogenerate a self-signed certificate to use. Notice that\n# self-signed certificates are fine for the purpose of WebRTC DTLS\n# connectivity, for the time being, at least until Identity Providers\n# are standardized and implemented in browsers. If for some reason you\n# want to enforce the DTLS stack in Janus to enforce valid certificates\n# from peers, though, you can do that setting 'dtls_accept_selfsigned' to\n# 'false' below: DO NOT TOUCH THAT IF YOU DO NOT KNOW WHAT YOU'RE DOING!\n# You can also configure the DTLS ciphers to offer: the default if not\n# set is \"DEFAULT:!NULL:!aNULL:!SHA256:!SHA384:!aECDH:!AESGCM+AES256:!aPSK\"\n# Finally, by default NIST P-256 certificates are generated (see #1997),\n# but RSA generation is still supported if you set 'rsa_private_key' to 'true'.\ncertificates: {\n\t#cert_pem = \"/path/to/certificate.pem\"\n\t#cert_key = \"/path/to/key.pem\"\n\t#cert_pwd = \"secretpassphrase\"\n\t#dtls_accept_selfsigned = false\n\t#dtls_ciphers = \"your-desired-openssl-ciphers\"\n\t#rsa_private_key = false\n}\n\n# Media-related stuff: you can configure whether if you want to enable IPv6\n# support (and link-local IPs), the minimum size of the NACK queue (in ms,\n# defaults to 200ms) for retransmissions no matter the RTT, the range of\n# ports to use for RTP and RTCP (by default, no range is envisaged), the\n# starting MTU for DTLS (1200 by default, it adapts automatically),\n# how much time, in seconds, should pass with no media (audio or\n# video) being received before Janus notifies you about this (default=1s,\n# 0 disables these events entirely), how many lost packets should trigger a\n# 'slowlink' event to users (default=0, disabled), and how often, in milliseconds,\n# to send the Transport Wide Congestion Control feedback information back\n# to senders, if negotiated (default=200ms). Finally, if you're using BoringSSL\n# you can customize the frequency of retransmissions: OpenSSL has a fixed\n# value of 1 second (the default), while BoringSSL can override that. Notice\n# that lower values (e.g., 100ms) will typically get you faster connection\n# times, but may not work in case the RTT of the user is high: as such,\n# you should pick a reasonable trade-off (usually 2*max expected RTT).\nmedia: {\n\t#ipv6 = true\n\t#ipv6_linklocal = true\n\t#min_nack_queue = 500\n\t#rtp_port_range = \"20000-40000\"\n\t#dtls_mtu = 1200\n\t#no_media_timer = 1\n\t#slowlink_threshold = 4\n\t#twcc_period = 100\n\t#dtls_timeout = 500\n\n\t# Janus can do some optimizations on the NACK queue, specifically when\n\t# keyframes are involved. Namely, you can configure Janus so that any\n\t# time a keyframe is sent to a user, the NACK buffer for that connection\n\t# is emptied. This allows Janus to ignore NACK requests for packets\n\t# sent shortly before the keyframe was sent, since it can be assumed\n\t# that the keyframe will restore a complete working image for the user\n\t# anyway (which is the main reason why video retransmissions are typically\n\t# required). While this optimization is known to work fine in most cases,\n\t# it can backfire in some edge cases, and so is disabled by default.\n\t#nack_optimizations = true\n\n\t# If you need DSCP packet marking and prioritization, you can configure\n\t# the 'dscp' property to a specific values, and Janus will try to\n\t# set it on all outgoing packets using libnice. Normally, the specs\n\t# suggest to use different values depending on whether audio, video\n\t# or data are used, but since all PeerConnections in Janus are bundled,\n\t# we can only use one. You can refer to this document for more info:\n\t# https://tools.ietf.org/html/draft-ietf-tsvwg-rtcweb-qos-18#page-6\n\t# That said, DON'T TOUCH THIS IF YOU DON'T KNOW WHAT IT MEANS!\n\t#dscp = 46\n}\n\n# NAT-related stuff: specifically, you can configure the STUN/TURN\n# servers to use to gather candidates if the gateway is behind a NAT,\n# and srflx/relay candidates are needed. In case STUN is not enough and\n# this is needed (it shouldn't), you can also configure Janus to use a\n# TURN server# please notice that this does NOT refer to TURN usage in\n# browsers, but in the gathering of relay candidates by Janus itself,\n# e.g., if you want to limit the ports used by a Janus instance on a\n# private machine. Furthermore, you can choose whether Janus should be\n# configured to do full-trickle (Janus also trickles its candidates to\n# users) rather than the default half-trickle (Janus supports trickle\n# candidates from users, but sends its own within the SDP), and whether\n# it should work in ICE-Lite mode (by default it doesn't). If libnice is\n# at least 0.1.15, you can choose which ICE nomination mode to use: valid\n# values are \"regular\" and \"aggressive\" (the default depends on the libnice\n# version itself; if we can set it, we set aggressive nomination). If\n# libnice is at least 0.1.19, you can enable consent freshness checks for\n# PeerConnections as well: this will issue regular checks to check whether\n# or not the WebRTC peer isn't available anymore. Enabling consent freshness\n# will automatically also enable using connectivity checks as keep-alives, which\n# might help detecting when a peer is no longer available (notice that\n# current libnice master is breaking connections after 50 seconds when\n# keepalive-conncheck is being used, so if you want to use it, better\n# sticking to 0.1.18 until the issue is addressed upstream). Finally,\n# you can also enable ICE-TCP support (beware that this may lead to problems\n# if you do not enable ICE Lite as well), choose which interfaces should\n# be used for gathering candidates.\nnat: {\n\t#stun_server = \"stun.voip.eutelia.it\"\n\t#stun_port = 3478\n\t#nice_debug = \"nice_debug option is NOT SUPPORTED ANYMORE! Please set NICE_DEBUG and G_MESSAGES_DEBUG env vars when starting Janus\"\n\t#full_trickle = true\n\t#ice_nomination = \"regular\"\n\t#ice_consent_freshness = true\n\t#ice_keepalive_conncheck = true\n\t#ice_lite = true\n\t#ice_tcp = true\n\n\t# By default, Janus implements a grace period when detecting ICE\n\t# failures in PeerConnections, to give time to applications to react\n\t# to that, e.g., by enforcing an ICE restart. If you want an ICE\n\t# failure to result in the PeerConnection being closed right away\n\t# (e.g., with the help of consent freshness) then you can do that\n\t# by uncommenting the following property and set it to true\n\t#hangup_on_failed = true\n\n\t# By default Janus tries to resolve mDNS (.local) candidates: even\n\t# though this is now done asynchronously and shouldn't keep the API\n\t# busy, even in case mDNS resolution takes a long time to timeout,\n\t# you can choose to drop all .local candidates instead, which is\n\t# helpful in case you know clients will never be in the same private\n\t# network as the one the Janus instance is running from. Notice that\n\t# this will cause ICE to fail if mDNS is the only way to connect!\n\t#ignore_mdns = true\n\n\t# In case you're deploying Janus on a server which is configured with\n\t# a 1:1 NAT (e.g., Amazon EC2), you might want to also specify the public\n\t# address of the machine using the setting below. This will result in\n\t# all host candidates (which normally have a private IP address) to\n\t# be rewritten with the public address provided in the settings. As\n\t# such, use the option with caution and only if you know what you're doing.\n\t# Make sure you keep ICE Lite disabled, though, as it's not strictly\n\t# speaking a publicly reachable server, and a NAT is still involved.\n\t# If you'd rather keep the private IP address in place, rather than\n\t# replacing it (and so have both of them as advertised candidates),\n\t# then set the 'keep_private_host' property to true.\n\t# Multiple public IP addresses can be specified as a comma separated list\n\t# if the Janus is deployed in a DMZ between two 1-1 NAT for internal and\n\t# external users.\n\t#nat_1_1_mapping = \"1.2.3.4\"\n\t#keep_private_host = true\n\n\t# You can configure a TURN server in two different ways: specifying a\n\t# statically configured TURN server, and thus provide the address of the\n\t# TURN server, the transport (udp/tcp/tls) to use, and a set of valid\n\t# credentials to authenticate. Notice that you should NEVER configure\n\t# a TURN server for Janus unless it's really what you want! If you want\n\t# *users* to use TURN, then you need to configure that on the client\n\t# side, and NOT in Janus. The following TURN configuration should ONLY\n\t# be enabled when Janus itself is sitting behind a restrictive firewall\n\t# (e.g., it's part of a service installed on a box in a private home).\n\t#turn_server = \"myturnserver.com\"\n\t#turn_port = 3478\n\t#turn_type = \"udp\"\n\t#turn_user = \"myuser\"\n\t#turn_pwd = \"mypassword\"\n\n\t# You can also make use of the TURN REST API to get info on one or more\n\t# TURN services dynamically. This makes use of the proposed standard of\n\t# such an API (https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00)\n\t# which is currently available in both rfc5766-turn-server and coturn.\n\t# You enable this by specifying the address of your TURN REST API backend,\n\t# the HTTP method to use (GET or POST) and, if required, the API key Janus\n\t# must provide. The timeout can be configured in seconds, with a default of\n\t# 10 seconds and a minimum of 1 second. Notice that the 'opaque_id' provided\n\t# via Janus API will be used as the username for a specific PeerConnection\n\t# by default; if that one is missing, the 'session_id' will be used as the\n\t# username instead.\n\t#turn_rest_api = \"http://yourbackend.com/path/to/api\"\n\t#turn_rest_api_key = \"anyapikeyyoumayhaveset\"\n\t#turn_rest_api_method = \"GET\"\n\t#turn_rest_api_timeout = 10\n\n\t# In case a TURN server is provided, you can allow applications to force\n\t# Janus to use TURN (https://github.com/meetecho/janus-gateway/pull/2774).\n\t# This is NOT allowed by default: only enable it if you know what you're doing.\n\t#allow_force_relay = true\n\n\t# You can also choose which interfaces should be explicitly used by the\n\t# gateway for the purpose of ICE candidates gathering, thus excluding\n\t# others that may be available. To do so, use the 'ice_enforce_list'\n\t# setting and pass it a comma-separated list of interfaces or IP addresses\n\t# to enforce. This is especially useful if the server hosting the gateway\n\t# has several interfaces, and you only want a subset to be used. Any of\n\t# the following examples are valid:\n\t#     ice_enforce_list = \"eth0\"\n\t#     ice_enforce_list = \"eth0,eth1\"\n\t#     ice_enforce_list = \"eth0,192.168.\"\n\t#     ice_enforce_list = \"eth0,192.168.0.1\"\n\t# By default, no interface is enforced, meaning Janus will try to use them all.\n\t#ice_enforce_list = \"eth0\"\n\n\t# In case you don't want to specify specific interfaces to use, but would\n\t# rather tell Janus to use all the available interfaces except some that\n\t# you don't want to involve, you can also choose which interfaces or IP\n\t# addresses should be excluded and ignored by the gateway for the purpose\n\t# of ICE candidates gathering. To do so, use the 'ice_ignore_list' setting\n\t# and pass it a comma-separated list of interfaces or IP addresses to\n\t# ignore. This is especially useful if the server hosting the gateway\n\t# has several interfaces you already know will not be used or will simply\n\t# always slow down ICE (e.g., virtual interfaces created by VMware).\n\t# Partial strings are supported, which means that any of the following\n\t# examples are valid:\n\t#     ice_ignore_list = \"vmnet8,192.168.0.1,10.0.0.1\"\n\t#     ice_ignore_list = \"vmnet,192.168.\"\n\t# Just beware that the ICE ignore list is not used if an enforce list\n\t# has been configured. By default, Janus ignores all interfaces whose\n\t# name starts with 'vmnet', to skip VMware interfaces:\n\tice_ignore_list = \"vmnet\"\n\n\t# In case you want to allow Janus to start even if the configured STUN or TURN\n\t# server is unreachable, you can set 'ignore_unreachable_ice_server' to true.\n\t# WARNING: We do not recommend to ignore reachability problems, particularly\n\t# if you run Janus in the cloud. Before enabling this flag, make sure your\n\t# system is correctly configured and Janus starts after the network layer of\n\t# your machine is ready. Note that Linux distributions offer such directives.\n\t# You could use the following directive in systemd: 'After=network-online.target'\n\t# https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before=\n\t#ignore_unreachable_ice_server = true\n}\n\n# You can choose which of the available plugins should be\n# enabled or not. Use the 'disable' directive to prevent Janus from\n# loading one or more plugins: use a comma separated list of plugin file\n# names to identify the plugins to disable. By default all available\n# plugins are enabled and loaded at startup.\nplugins: {\n\t#disable = \"libjanus_echotest.so,libjanus_recordplay.so\"\n}\n\n# You can choose which of the available transports should be enabled or\n# not. Use the 'disable' directive to prevent Janus from loading one\n# or more transport: use a comma separated list of transport file names\n# to identify the transports to disable. By default all available\n# transports are enabled and loaded at startup.\ntransports: {\n\t#disable = \"libjanus_rabbitmq.so\"\n}\n\n# As a core feature, Janus can log either on the standard output, or to\n# a local file. Should you need more advanced logging functionality, you\n# can make use of one of the custom loggers, or write one yourself. Use the\n# 'disable' directive to prevent Janus from loading one or more loggers:\n# use a comma separated list of logger file names to identify the loggers\n# to disable. By default all available loggers are enabled and loaded at startup.\nloggers: {\n\t#disable = \"libjanus_jsonlog.so\"\n}\n\n# Event handlers allow you to receive live events from Janus happening\n# in core and/or plugins. Since this can require some more resources,\n# the feature is disabled by default. Setting 'broadcast' to 'true' will\n# enable them. You can then choose which of the available event handlers\n# should be loaded or not. Use the 'disable' directive to prevent Janus\n# from loading one or more event handlers: use a comma separated list of\n# file names to identify the event handlers to disable. By default, if\n# broadcast is set to 'true' all available event handlers are enabled and\n# loaded at startup. Finally, you can choose how often media statistics\n# (packets sent/received, losses, etc.) should be sent: by default it's\n# once per second (audio and video statistics sent separately), but may\n# considered too verbose, or you may want to limit the number of events,\n# especially if you have many PeerConnections active. To change this,\n# just set 'stats_period' to the number of seconds that should pass in\n# between statistics for each handle. Setting it to 0 disables them (but\n# not other media-related events). By default Janus sends single media\n# statistic events per media (audio, video and simulcast layers as separate\n# events): if you'd rather receive a single containing all media stats in a\n# single array, set 'combine_media_stats' to 'true'.\nevents: {\n\t#broadcast = true\n\t#combine_media_stats = true\n\t#disable = \"libjanus_sampleevh.so\"\n\t#stats_period = 5\n}\n"
  },
  {
    "path": "conf/janus.logger.jsonlog.jcfg.sample",
    "content": "# This configures the JSON-based file logger. This is a very simple logger\n# with no particular advantage over the existing, integrated, logging\n# functionality Janus provides, and so it's configuration is quite basic\n# as well: it's here mostly to provide a reference implementation for\n# developers willing to provide additional, and more complex, external loggers.\n\ngeneral: {\n\tenabled = false\t\t# By default the module is not enabled\n\n\tjson = \"indented\"\t# Since this logger simply writes each log line as\n\t\t\t\t\t\t# a JSON object to a file, you can configure whether\n\t\t\t\t\t\t# the JSON log lines should be indented (default),\n\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\n\tfilename = \"/tmp/janus-log.json\"\t# Filename to save to\n}\n"
  },
  {
    "path": "conf/janus.plugin.audiobridge.jcfg.sample",
    "content": "# room-<unique room ID>: {\n# description = \"This is my awesome room\"\n# is_private = true|false (whether this room should be in the public list, default=true)\n# secret = \"<optional password needed for manipulating (e.g. destroying) the room>\"\n# pin = \"<optional password needed for joining the room>\"\n# sampling_rate = <sampling rate> (e.g., 16000 for wideband mixing)\n# spatial_audio = true|false (if true, the mix will be stereo to spatially place users, default=false)\n# audiolevel_ext = true|false (whether the ssrc-audio-level RTP extension must\n#\t\tbe negotiated/used or not for new joins, default=true)\n# audiolevel_event = true|false (whether to emit event to other users or not, default=false)\n# audio_active_packets = 100 (number of packets with audio level, default=100, 2 seconds)\n# audio_level_average = 25 (average value of audio level, 127=muted, 0='too loud', default=25)\n# default_expectedloss = percent of packets we expect participants may miss, to help with outgoing FEC (default=0, max=20; automatically used for forwarders too)\n# default_bitrate = default bitrate in bps to use for the all participants (default=0, which means libopus decides; automatically used for forwarders too)\n# denoise = true|false (whether denoising via RNNoise should be performed for each participant by default)\n# record = true|false (whether this room should be recorded, default=false)\n# record_file = \"/path/to/recording.wav\" (where to save the recording)\n# record_dir = \"/path/to/\" (path to save the recording to, makes record_file a relative path if provided)\n# mjrs = true|false (whether all participants in the room should be individually recorded to mjr files, default=false)\n# mjrs_dir = \"/path/to/\" (path to save the mjr files to)\n# allow_rtp_participants = true|false (whether participants should be allowed to join\n#\t\tvia plain RTP as well, rather than just WebRTC, default=false)\n# groups = optional, non-hierarchical, array of groups to tag participants, for external forwarding purposes only\n#\n#     The following lines are only needed if you want the mixed audio\n#     to be automatically forwarded via plain RTP to an external component\n#     (e.g., an ffmpeg script, or a gstreamer pipeline) for processing\n#     By default plain RTP is used, SRTP must be configured if needed\n# rtp_forward_id = numeric RTP forwarder ID for referencing it via API (optional: random ID used if missing)\n# rtp_forward_host = \"<host address to forward RTP packets of mixed audio to>\"\n# rtp_forward_host_family = \"<ipv4|ipv6; by default, first family returned by DNS request>\"\n# rtp_forward_port = port to forward RTP packets of mixed audio to\n# rtp_forward_ssrc = SSRC to use to use when streaming (optional: stream_id used if missing)\n# rtp_forward_codec = opus (default), pcma (A-Law) or pcmu (mu-Law)\n# rtp_forward_ptype = payload type to use when streaming (optional: only read for Opus, 100 used if missing)\n# rtp_forward_group = group of participants to forward, if enabled in the room (optional: forwards full mix if missing)\n# rtp_forward_srtp_suite = length of authentication tag (32 or 80)\n# rtp_forward_srtp_crypto = \"<key to use as crypto (base64 encoded key as in SDES)>\"\n# rtp_forward_always_on = true|false, whether silence should be forwarded when the room is empty (optional: false used if missing)\n#}\n\ngeneral: {\n\t#admin_key = \"supersecret\"\t\t# If set, rooms can be created via API only\n\t\t\t\t\t\t\t\t\t# if this key is provided in the request\n\t#lock_rtp_forward = true\t\t# Whether the admin_key above should be\n\t\t\t\t\t\t\t\t\t# enforced for RTP forwarding requests too\n\t#lock_play_file = true\t\t\t# Whether the admin_key above should be\n\t\t\t\t\t\t\t\t\t# enforced for playing .opus files too\n\t#record_tmp_ext = \"tmp\"\t\t\t# Optional temporary extension to add to filenames\n\t\t\t\t\t\t\t\t\t# while recording: e.g., setting \"tmp\" would mean\n\t\t\t\t\t\t\t\t\t# .wav --> .wav.tmp until the file is closed\n\t#events = false\t\t\t\t\t# Whether events should be sent to event\n\t\t\t\t\t\t\t\t\t# handlers (default=true)\n\n\t# By default, integers are used as a unique ID for both rooms and participants.\n\t# In case you want to use strings instead (e.g., a UUID), set string_ids to true.\n\t#string_ids = true\n\n\t# Normally, all AudioBridge participants will join by negotiating a WebRTC\n\t# PeerConnection: the plugin also supports adding participants that will\n\t# use plain RTP, though, be it for supporting legacy users (e.g., SIP\n\t# participants who an orchestrator can add to the bridge) or more simply\n\t# to temporarily inject external audio in a room from a live source. To\n\t# support plain RTP, the plugin needs to have a range of ports it can bind\n\t# to: notice this should be configured so that it doesn't conflict with other\n\t# plugins (e.g., Streaming, SIP, NoSIP) and applications (e.g., Janus itself).\n\t# The default if you don't specify anything is 10000-60000.\n\t#rtp_port_range = \"50000-60000\"\n\t# In case we need to support plain RTP participants, we'll also need to know\n\t# what local IP address to bind to for media. If no address is set in the\n\t# property below, then one will be automatically guessed from the system.\n\t#local_ip = \"1.2.3.4\"\n\n}\n\nroom-1234: {\n\tdescription = \"Demo Room\"\n\tsecret = \"adminpwd\"\n\tsampling_rate = 16000\n\trecord = false\n\t#record_dir = \"/path/to/\"\n\t#record_file = \"recording.wav\"\n}\n"
  },
  {
    "path": "conf/janus.plugin.duktape.jcfg.sample.in",
    "content": "# The only things you configure in here are which JavaScipt file to load and,\n# optionally, the paths to add for searching libraries and a configuration\n# file, if the script will need it. For what concerns the libraries path,\n# by default this configuration file adds a path to where the JS samples\n# have been installed, as it contains a couple of helper libraries the\n# samples use; should you be interested in adding more, just add other\n# paths separated by a semicolon. Due to the syntax of the configuration\n# file, make sure you escape all semicolons with a trailing slash, in case.\n# The 'config' property is entirely script specific, instead: if your\n# script will need to rely on an XML configuration file in its initialization,\n# for instance, then set the 'config' property as the path to the file;\n# it will be passed, as is, to your script in the init() call. None of\n# the samples use this property, which is why it's commented out. \n\ngeneral: {\n\tpath = \"@duktapedir@\"\n\tscript = \"@duktapedir@/echotest.js\"\n\t#script = \"@duktapedir@/videoroom.js\"\n\t#config = \"/path/to/configfile\"\n}\n"
  },
  {
    "path": "conf/janus.plugin.echotest.jcfg.sample",
    "content": "# events = true|false, whether events should be sent to event handlers\n\ngeneral: {\n\t#events = false\n}\n"
  },
  {
    "path": "conf/janus.plugin.lua.jcfg.sample.in",
    "content": "# The only things you configure in here are which lua script to load and,\n# optionally, the paths to add for searching libraries and a configuration\n# file, if the script will need it. For what concerns the libraries path,\n# by default this configuration file adds a path to where the Lua samples\n# have been installed, as it contains a couple of helper libraries the\n# samples use; should you be interested in adding more, just add other\n# paths separated by a semicolon. Due to the syntax of the configuration\n# file, make sure you escape all semicolons with a trailing slash, in case.\n# The 'config' property is entirely script specific, instead: if your\n# script will need to rely on an XML configuration file in its initialization,\n# for instance, then set the 'config' property as the path to the file;\n# it will be passed, as is, to your script in the init() call. None of\n# the samples use this property, which is why it's commented out. \n\ngeneral: {\n\tpath = \"@luadir@\"\n\tscript = \"@luadir@/echotest.lua\"\n\t#script = \"@luadir@/videoroom.lua\"\n\t#config = \"/path/to/configfile\"\n}\n"
  },
  {
    "path": "conf/janus.plugin.nosip.jcfg.sample",
    "content": "general: {\n\t# Specify which local IP address to bind to for media.\n\t# If not set it will be automatically guessed from the system\n\t#local_ip = \"1.2.3.4\"\n\n\t# Specify which (public) IP address to advertise in the SDP.\n\t# If not set, the value above or anything autodetected will be used\n\t#sdp_ip = \"1.2.3.4\"\n\n\t# Range of ports to use for RTP/RTCP (default=10000-60000)\n\trtp_port_range = \"20000-40000\"\n\n\t# Whether events should be sent to event handlers (default=true)\n\t#events = false\n\n\t# If you need DSCP packet marking and prioritization, you can configure\n\t# the 'dscp_audio_rtp' and/or 'dscp_video_rtp' property to specific values,\n\t# and the plugin will set it on all outgoing audio/video RTP packets.\n\t# No packet marking is done if this parameter is undefined or equal to 0\n\t#dscp_audio_rtp = 46\n\t#dscp_video_rtp = 26\n\n}\n"
  },
  {
    "path": "conf/janus.plugin.recordplay.jcfg.sample.in",
    "content": "# path = where to place recordings in the file system\n# admin_key = plugin management secret; if set, private recordings can\n#             be listed only if this key is provided in the request\n# private = true|false, whether recordings should be marked as private by\n#           default, meaning they're not returned to users connecting to the\n#           plugin unless they provide the right 'admin_key' in the request\n# events = true|false, whether events should be sent to event handlers\n\ngeneral: {\n\tpath = \"@recordingsdir@\"\n\t#admin_key = \"supersecret\"\n\t#private = true\n\t#events = false\n}\n"
  },
  {
    "path": "conf/janus.plugin.sip.jcfg.sample",
    "content": "general: {\n\t# Specify which local IP address to bind to for SIP stack.\n\t# If not set it will be automatically guessed from the system\n\t#local_ip = \"1.2.3.4\"\n\n\t# Specify which local IP address to bind for the media stack.\n\t# If not set it will be automatically set to the value of local_ip\n\t#local_media_ip = \"1.2.3.4\"\n\n\t# Specify which (public) IP address to advertise in the SDP.\n\t# If not set, the value above or anything autodetected will be used\n\t#sdp_ip = \"1.2.3.4\"\n\n\t# Enable local keep-alives to keep the registration open. Keep-alives are\n\t# sent in the form of OPTIONS requests, at the given interval inseconds.\n\t# (0 to disable)\n\tkeepalive_interval = 120\n\n\t# Indicate if the server is behind NAT. If so, the server will use STUN\n\t# to guess its own public IP address and use it in the Contact header of\n\t# outgoing requests\n\tbehind_nat = false\n\n\t# User-Agent string to be used\n\t# user_agent = \"Cool WebRTC Gateway\"\n\n\t# Expiration time for registrations\n\tregister_ttl = 3600\n\n\t# Range of ports to use for RTP/RTCP (default=10000-60000)\n\trtp_port_range = \"20000-40000\"\n\n\t# Whether events should be sent to event handlers (default=true)\n\t#events = false\n\n\t# If you need DSCP packet marking and prioritization, you can configure\n\t# the 'dscp_audio_rtp' and/or 'dscp_video_rtp' property to specific values,\n\t# and the plugin will set it on all outgoing audio/video RTP packets.\n\t# No packet marking is done if this parameter is undefined or equal to 0\n\t#dscp_audio_rtp = 46\n\t#dscp_video_rtp = 26\n\n\t# In case you want to use SIPS for some sessions, Sofia may need to\n\t# have access to a certificate to use: this is especially true for\n\t# Sofia >= 1.13, which will fail to create the agent if no certificate\n\t# is available. By default, Sofia looks for 'agent.pem' and 'cafile.pem'\n\t# in the '$HOME/.sip/auth' folder, but you can specify a different\n\t# one by uncommenting and setting the property below.\n\t#sips_certs_dir = \"/etc/sip/certs\"\n\n\t# Set the T1x64 timeout value (in milliseconds) used by the SIP transaction\n\t# engine (default 32000 milliseconds)\n\tsip_timer_t1x64 = 32000\n\n}\n"
  },
  {
    "path": "conf/janus.plugin.streaming.jcfg.sample.in",
    "content": "# You can configure static mountpoints that should be made available\n# when Janus starts in this configuration file. The syntax and the\n# available properties are listed below. Notice that, for the sake of\n# simplicity, only the global and legacy properties are listed in the\n# following text: you can use the new stream-based syntax as well (see\n# the 'multistream-test' example below for reference), but in that case\n# all properties whose names start with audio/video/data should be\n# renamed to have that prefix removed. Please refer to the Streaming\n# plugin documentation for more details, as the static configuration\n# in that case mirrors the dynamic API syntax.\n#\n# stream-name: {\n# type = rtp|live|ondemand|rtsp\n#        rtp = stream originated by an external tool (e.g., gstreamer or\n#              ffmpeg) and sent to the plugin via RTP\n#        live = local file streamed live to multiple listeners\n#               (multiple listeners = same streaming context)\n#        ondemand = local file streamed on-demand to a single listener\n#                   (multiple listeners = different streaming contexts)\n#        rtsp = stream originated by an external RTSP feed (only\n#               available if libcurl support was compiled)\n# id = <unique numeric ID> (if missing, a random one will be generated)\n# description = This is my awesome stream\n# metadata = An optional string that can contain any metadata (e.g., JSON)\n#\t\t   associated with the stream you want users to receive\n# is_private = true|false (private streams don't appear when you do a 'list'\n#\t\t\trequest)\n# secret = <optional password needed for manipulating (e.g., destroying\n#\t\t\tor enabling/disabling) the stream>\n# pin = <optional password needed for watching the stream>\n# filename = path to the local file to stream (only for live/ondemand)\n# audio = true|false (do/don't stream audio)\n# video = true|false (do/don't stream video)\n#    The following options are only valid for the 'rtp' type:\n# data = true|false (do/don't stream text via datachannels)\n# audioport = local port for receiving audio frames\n# audiortcpport = local port, if any, for receiving and sending audio RTCP feedback\n# audiomcast = multicast group port for receiving audio frames, if any\n# audioiface = network interface or IP address to bind to, if any (binds to all otherwise)\n# audiopt = <audio RTP payload type> (e.g., 111)\n# audiocodec = name of the audio codec (e.g., opus)\n# audioskew = true|false (whether the plugin should perform skew\n#\t\tanalisys and compensation on incoming audio RTP stream, EXPERIMENTAL)\n# videoport = local port for receiving video frames\n# videortcpport = local port, if any, for receiving and sending video RTCP feedback\n# videomcast = multicast group port for receiving video frames, if any\n# videoiface = network interface or IP address to bind to, if any (binds to all otherwise)\n# videopt = <video RTP payload type> (e.g., 100)\n# videocodec = name of the video codec (e.g., vp8)\n# videosimulcast = true|false (do|don't enable video simulcasting)\n# videoport2 = second local port for receiving video frames (only for rtp, and simulcasting)\n# videoport3 = third local port for receiving video frames (only for rtp, and simulcasting)\n# videoskew = true|false (whether the plugin should perform skew\n#\t\tanalisys and compensation on incoming video RTP stream, EXPERIMENTAL)\n# videosvc = true|false (whether the video will have SVC support; works only for VP9-SVC, default=false)\n# h264sps = if using H.264 as a video codec, value of the sprop-parameter-sets\n#\t\tthat would normally be sent via SDP, but that we'll use to instead\n#\t\tmanually ingest SPS and PPS packets via RTP for streams that miss it\n# collision = in case of collision (more than one SSRC hitting the same port), the plugin\n#\t\twill discard incoming RTP packets with a new SSRC unless this many milliseconds\n#\t\tpassed, which would then change the current SSRC (0=disabled)\n# dataport = local port for receiving data messages to relay\n# dataiface = network interface or IP address to bind to, if any (binds to all otherwise)\n# datatype = text|binary (type of data this mountpoint will relay, default=text)\n# databuffermsg = true|false (whether the plugin should store the latest\n#\t\tmessage and send it immediately for new viewers)\n# threads = number of threads to assist with the relaying part, which can help\n#\t\tif you expect a lot of viewers that may cause the RTP receiving part\n#\t\tin the Streaming plugin to slow down and fail to catch up (default=0)\n#\n# Note: by default, the Streaming plugin only forwards the latest packets\n# it receives, never performing any buffering. This means that, for video\n# streams, new viewers may initially start receiving frames they cannot\n# decode right away, since they'd refer to keyframes that were sent before\n# they joined. In such scenarios, they'd have to wait until the next keyframe\n# arrives before video can be decoded and displayed, which could take a\n# while depending on the frequency of keyframes encoded by the source.\n# For forwarded streams (e.g., from the VideoRoom) this can be easily\n# addressed by using the RTCP support. For sources that can't or won't\n# honour dynamic keyframe requests, a partial and experimental solution\n# might be storing the latest keyframe and the following deltas, to send\n# to new viewers before new live packet are delivered. This feature can\n# be enabled in the Streaming plugin using the 'bufferkf_ms' and/or\n# the 'bufferkf_bytes' properties, which configure how many milliseconds\n# or how many bytes (in total) should be stored any time a keyframe is\n# received: data exceeding those limits won't be stored, until a new\n# keyframe arrives. The two properties are not mutually exclusive, and\n# can be configured at the same time: in that case, the first one that\n# hits the limit stops the buffering of the current keyframe. Notice\n# that, again, this feature should be considered highly experimental,\n# and that it comes with a few considerable drawbacks: the most obvious\n# one is that, depending on how many packets were stored, new viewers\n# may be hit with a considerable burst of data as soon as they connect,\n# which may negatively impact performance or even cause issues of its own.\n# Also notice that this is a global, and not per-stream, feature: this\n# means that if you create a mountpoint with two video streams, the\n# feature will impact both of them, and both streams will have a buffer\n# of their own. This also works for RTSP mountpoints.\n#\tbufferkf_ms = how many milliseconds of packets to store, starting\n#\t\t\t\t\tfrom a new keyframe (default=0)\n#\tbufferkf_bytes = how many bytes of packets to store, starting\n#\t\t\t\t\tfrom a new keyframe (default=0)\n#\n# In case you want to use SRTP for your RTP-based mountpoint, you'll need\n# to configure the SRTP-related properties as well, namely the suite to\n# use for hashing (32 or 80) and the crypto information for decrypting\n# the stream (as a base64 encoded string the way SDES does it). Notice\n# that with SRTP involved you'll have to pay extra attention to what you\n# feed the mountpoint, as you may risk getting SRTP decrypt errors:\n# srtpsuite = 32\n# srtpcrypto = WbTBosdVUZqEb6Htqhn+m3z7wUh4RJVR8nE15GbN\n#\n# The Streaming plugin can also be used to (re)stream media that has been\n# encrypted using something that can be consumed via Insertable Streams.\n# In that case, we only need to be aware of it, so that we can send the\n# info along with the SDP. How to decrypt the media is out of scope, and\n# up to the application since, again, this is end-to-end encryption and\n# so neither Janus nor the Streaming plugin have access to anything.\n# DO NOT SET THIS PROPERTY IF YOU DON'T KNOW WHAT YOU'RE DOING!\n# e2ee = true\n#\n# To allow mountpoints to negotiate the playout-delay RTP extension,\n# you can set the 'playoutdelay_ext' property to true: this way, any\n# subscriber can customize the playout delay of incoming video streams,\n# assuming the browser supports the RTP extension in the first place.\n# playoutdelay_ext = true\n#\n# To allow mountpoints to negotiate the abs-capture-time RTP extension,\n# you can set the 'abscapturetime_src_ext_id' property to value in range 1..14 inclusive: this way, any\n# subscriber can receive the abs-capture-time of incoming RTP streams,\n# assuming the browser supports the RTP extension in the first place.\n# Incoming RTP stream should provide abs-capture-time exactly in the same header id.\n# abscapturetime_src_ext_id = 1\n#\n# The following options are only valid for the 'rtsp' type:\n# url = RTSP stream URL (only for restreaming RTSP)\n# rtsp_user = RTSP authorization username (only if type=rtsp)\n# rtsp_pwd = RTSP authorization password (only if type=rtsp)\n# rtsp_quirk = Some RTSP servers offer the stream using only the path, instead of the fully qualified URL.\n#\t\tIf set true, this boolean informs Janus that we should try a path-only DESCRIBE request if the initial request returns 404.\n# rtsp_failcheck = whether an error should be returned if connecting to the RTSP server fails (default=true)\n# rtspiface = network interface or IP address to bind to, if any (binds to all otherwise), when receiving RTSP streams\n# rtsp_reconnect_delay = after n seconds passed and no media assumed, the RTSP server has gone and schedule a reconnect (default=5s)\n# rtsp_session_timeout = by default the streaming plugin will check the RTSP connection with an OPTIONS query,\n# \t\tthe value of the timeout comes from the RTSP session initializer and by default\n# \t\tthis session timeout is the half of this value In some cases this value can be too high (for example more than one minute)\n# \t\tbecause of the media server. In that case this plugin will calculate the timeout with this\n# \t\tformula: timeout = min(session_timeout, rtsp_session_timeout / 2). (default=0s)\n# rtsp_timeout = communication timeout (CURLOPT_TIMEOUT) for cURL call gathering the RTSP information (default=10s)\n# rtsp_conn_timeout = connection timeout for cURL (CURLOPT_CONNECTTIMEOUT) call gathering the RTSP information (default=5s)\n# rtsp_notify_changes = if set to true, will send an event to connected users when the RTSP session\n#\t\tgets disconnected, and when it's reconnected (default=false)\n#\n# Notice that, for 'rtsp' mountpoints, normally the plugin uses the exact\n# SDP codec and fmtp attributes the remote camera or RTSP server sent.\n# In case the values set remotely are known to conflict with WebRTC viewers,\n# you can override both using the settings introduced above.\n#\n# To test the 'gstreamer-sample' example, check the test_gstreamer.sh\n# script in the plugins/streams folder. The live and on-demand  audio\n# file streams, use a couple of files (radio.alaw, music.mulaw) that are\n# provided in the plugins/streams folder.\n#}\n\ngeneral: {\n\t#admin_key = \"supersecret\"\t\t# If set, mountpoints can be created via API\n\t\t\t\t\t\t\t\t\t# only if this key is provided in the request\n\t#rtp_port_range = \"20000-40000\"\t# Range of ports to use for RTP/RTCP when '0' is\n\t\t\t\t\t\t\t\t\t# passed as port for a mountpoint (default=10000-60000)\n\t#events = false\t\t\t\t\t# Whether events should be sent to event\n\t\t\t\t\t\t\t\t\t# handlers (default=true)\n\n\t# By default, integers are used as a unique ID for both mountpoints. In case\n\t# you want to use strings instead (e.g., a UUID), set string_ids to true.\n\t#string_ids = true\n}\n\n#\n# This is an example of an RTP source stream, which is what you'll need\n# in the vast majority of cases: here, the Streaming plugin will bind to\n# some ports, and expect media to be sent by an external source (e.g.,\n# FFmpeg or Gstreamer). This sample listens on 5002 for audio (Opus) and\n# 5004 for video (VP8), which is what the sample gstreamer script in the\n# plugins/streams folder sends to. Whatever is sent to those ports will\n# be the source of a WebRTC broadcast users can subscribe to.\n#\nrtp-sample: {\n\ttype = \"rtp\"\n\tid = 1\n\tdescription = \"Opus/VP8 live stream coming from external source\"\n\tmetadata = \"You can use this metadata section to put any info you want!\"\n\taudio = true\n\tvideo = true\n\taudioport = 5002\n\taudiopt = 111\n\taudiocodec = \"opus\"\n\tvideoport = 5004\n\tvideopt = 100\n\tvideocodec = \"vp8\"\n\tsecret = \"adminpwd\"\n}\n\n#\n# This is a better example that uses the new settings to configure a live\n# mountpoint to send multiple streams of the same type at the same time:\n# that is, not simulcasting, but different streams (e.g., two audio\n# streams and two video streams). To do so, you don't set the audio,\n# video and data properties inline, but use an array of properties instead,\n# each identifying a single stream to add, that will then translate to\n# a dedicated m-line in the SDP. For each stream, you specify the type,\n# a unique ID (mid), and can provide a short description (label) so that\n# the client side can know what's what when rendering the streams;\n# optionally, a msid to add to the SDP m-line can be provided as well. Notice\n# how the port/pt/codec/fmtp/etc. stuff is called just like that, without\n# any audio/video/data prefix: in fact, each media stream can be configured\n# the same way, and it's the type that allows us to differentiate them.\n# As such, you can use the same approach for creating regular mountpoints\n# as well (e.g., 1 audio and 1 video) in a much clearer, and cleaner, way.\n#\nmultistream-test: {\n\ttype = \"rtp\"\n\tid = 123\n\tdescription = \"Multistream test (1 audio, 2 video)\"\n\tmetadata = \"This is an example of a multistream mountpoint: you'll get an audio stream and two video feeds\"\n\tmedia = (\n\t\t{\n\t\t\ttype = \"audio\"\n\t\t\tmid = \"a\"\n\t\t\tlabel = \"Audio stream\"\n\t\t\tport = 5102\n\t\t\tpt = 111\n\t\t\tcodec = \"opus\"\n\t\t},\n\t\t{\n\t\t\ttype = \"video\"\n\t\t\tmid = \"v1\"\n\t\t\tlabel = \"Video stream #1\"\n\t\t\tport = 5104\n\t\t\tpt = 100\n\t\t\tcodec = \"vp8\"\n\t\t},\n\t\t{\n\t\t\ttype = \"video\"\n\t\t\tmid = \"v2\"\n\t\t\tlabel = \"Video stream #2\"\n\t\t\tport = 5106\n\t\t\tpt = 100\n\t\t\tcodec = \"vp8\"\n\t\t}\n\t)\n\tsecret = \"adminpwd\"\n}\n\n#\n# This is a sample of the file-based streaming support. Specifically,\n# this simulates a radio broadcast by streaming (in a loop) raw a-Law\n# (that is, G.711) frames. Since type is \"live\", anyone subscribing to\n# this mountpoint will listen to the same broadcast as if it were live.\n# Notice that file-based streaming supports Opus files too, but no video.\n#\nfile-live-sample: {\n\ttype = \"live\"\n\tid = 2\n\tdescription = \"Pseudo-live radio broadcast (a-law file source)\"\n\tmetadata = \"This is a pseudo-live stream, whose source is actually a static Opus file on the disk: the Streaming plugin goes through the file in real-time, and sends the frames via RTP. It's pseudo-live because it progresses even when there are no subscribers, and all subscribers get the same audio.\"\n\tfilename = \"@streamdir@/radio.alaw\"\n\taudio = true\n\tvideo = false\n\tsecret = \"adminpwd\"\n}\n\n#\n# This is another sample of the file-based streaming support, but using\n# the \"ondemand\" type instead. In this case, the file we're streaming\n# is an Opus file containing stereo opus frames. Since this is \"ondemand\",\n# anyone subscribing to this mountpoint will listen to their own version\n# of the stream, meaning that it will start from the beginning and then\n# loop when it's over. On-demand streaming supports G.711 files as well.\n#\nfile-ondemand-sample: {\n\ttype = \"ondemand\"\n\tid = 3\n\tdescription = \"Remembrance, by Lorenzo Miniero (stereo music, Opus)\"\n\tmetadata = \"This is an on-demand stream, whose source is actually a static Opus file on the disk: when a subscriber opens it, the Streaming plugin starts from the beginning and goes through the file in real-time, and sends the frames via RTP. Unlike the pseudo-live sample, each subscriber opening it will experience a personal playout.\"\n\tfilename = \"@streamdir@/remembrance.opus\"\n\taudio = true\n\taudiopt = 100\n\taudiortpmap = \"opus/48000/2\"\n\taudiofmtp = \"stereo=1\"\n\tvideo = false\n\tsecret = \"adminpwd\"\n}\n\n#\n# All browsers also support H.264, often through Cisco's OpenH264 plugin.\n# The only profile that is definitely supported is the baseline one, which\n# means that if you try a higher one it might or might not work. No matter\n# which profile you encode, though, you can put a custom one in the SDP if\n# you override the fmtp SDP attribute via 'videofmtp'. The following is an\n# example of how to create a simple H.264 mountpoint: you can feed it via\n# an x264enc+rtph264pay pipeline in gstreamer, an ffmpeg script or other.\n#\n#h264-sample: {\n\t#type = \"rtp\"\n\t#id = 10\n\t#description = \"H.264 live stream coming from gstreamer\"\n\t#audio = false\n\t#video = true\n\t#videoport = 8004\n\t#videopt = 126\n\t#videocodec = \"h264\"\n\t#videofmtp = \"profile-level-id=42e01f;packetization-mode=1\"\n\t#secret = \"adminpwd\"\n#}\n\n#\n# The Streaming plugin also supports the broadcasting of datachannel\n# messages, either by themselves or along other audio/video streams (e.g.,\n# to add a subtitle to a stream you're sending). The following is an\n# example of how you can create a datachannel-only mountpoint: you can\n# feed it with any tool that can send UDP datagrams, e.g., netcat.\n# Notice that the 'rtp' type just indicates this is a live mountpoint:\n# datachannel messages will be sent as usual, and not use RTP at all.\n#\n#data-example: {\n\t#type = \"rtp\"\n\t#id = 15\n\t#description = \"Datachannel stream from an UDP source\"\n\t#audio = false\n\t#video = false\n\t#data = true\n\t#dataport = 5008\n\t#datatype = \"text\"\n\t#secret = \"adminpwd\"\n#}\n\n#\n# This is a variation of the rtp-sample configuration for Opus/VP8 shown\n# before, where multicast support is used to receive the streams. You\n# need an external script to feed data on those ports, of course.\n#\n#rtp-multicast: {\n\t#type = \"rtp\"\n\t#id = 20\n\t#description = \"Opus/VP8 live multicast stream sample\"\n\t#audio = true\n\t#video = true\n\t#audioport = 5002\n\t#audiomcast = \"232.3.4.5\"\n\t#audiopt = 111\n\t#audiocodec = \"opus\"\n\t#videoport = 5004\n\t#videomcast = \"232.3.4.5\"\n\t#videopt = 100\n\t#videocodec = \"vp8\"\n\t#secret = \"adminpwd\"\n#}\n\n#\n# This is a sample configuration for an RTSP stream: you can specify\n# the url to connect to and whether or not authentication is needed\n# using the url/rtsp_user/rtsp_pwd settings (but notice that digest\n# authentication will only work if you installed libcurl >= 7.45.0)\n# NOTE WELL: the plugin does NOT transcode, so the RTSP stream MUST be\n# in a format the browser can digest (e.g., VP8 or H.264 baseline for video)\n# Again, you can override payload type, codec and/or fmtp, if needed.\n#\n#rtsp-test: {\n\t#type = \"rtsp\"\n\t#id = 99\n\t#description = \"RTSP Test\"\n\t#audio = false\n\t#video = true\n\t#url = \"rtsp://127.0.0.1:8554/unicast\"\n\t#rtsp_user = \"username\"\n\t#rtsp_pwd = \"password\"\n\t#secret = \"adminpwd\"\n\t#rtsp_reconnect_delay = 5\n\t#rtsp_session_timeout = 0\n\t#rtsp_timeout = 10\n\t#rtsp_conn_timeout = 5\n#}\n"
  },
  {
    "path": "conf/janus.plugin.textroom.jcfg.sample",
    "content": "# room-<unique room ID>: {\n# description = This is my awesome room\n# is_private = true|false (whether this room should be in the public list, default=true)\n# secret = <optional password needed for manipulating (e.g. destroying) the room>\n# pin = <optional password needed for joining the room>\n# history = <number of messages to store as a history, and send back to new participants (default=0, no history)>\n# post = <optional backend to contact via HTTP post for all incoming messages>\n#}\n\ngeneral: {\n\t#admin_key = \"supersecret\"\t\t# If set, rooms can be created via API only\n\t\t\t\t\t\t\t\t\t# if this key is provided in the request\n\tjson = \"indented\"\t\t\t\t# Whether the data channel JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\t#events = false\t\t\t\t\t# Whether events should be sent to event\n\t\t\t\t\t\t\t\t\t# handlers (default=true)\n\n\t# By default, integers are used as a unique ID for rooms. In case you\n\t# want to use strings instead (e.g., a UUID), set string_ids to true.\n\t#string_ids = true\n}\n\nroom-1234: {\n\tdescription = \"Demo Room\"\n\t# is_private = true\n\tsecret = \"adminpwd\"\n\t# pin = \"roompwd\"\n\t# history = 10\n\t# post = \"http://example.com/forward/here\"\n}\n"
  },
  {
    "path": "conf/janus.plugin.videocall.jcfg.sample",
    "content": "# events = true|false, whether events should be sent to event handlers\n\ngeneral: {\n\t#events = false\n}\n"
  },
  {
    "path": "conf/janus.plugin.videoroom.jcfg.sample",
    "content": "# room-<unique room ID>: {\n# description = This is my awesome room\n# is_private = true|false (whether this room should be in the public list, default=true)\n# secret = <optional password needed for manipulating (e.g. destroying) the room>\n# pin = <optional password needed for joining the room>\n# require_pvtid = true|false (whether subscriptions are required to provide a valid private_id\n#\t\t\tto associate with a publisher, default=false)\n# signed_tokens = true|false (whether access to the room requires signed tokens; default=false,\n#\t\t\tonly works if signed tokens are used in the core as well)\n# publishers = <max number of concurrent senders> (e.g., 6 for a video\n#              conference or 1 for a webinar)\n# bitrate = <max video bitrate for senders> (e.g., 128000)\n# bitrate_cap = true|false (whether the above cap should act as a hard limit to\n#\t\t\tdynamic bitrate changes by publishers; default=false, publishers can go beyond that)\n# fir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)\n# audiocodec = opus|g722|pcmu|pcma|isac32|isac16 (audio codec(s) to force on publishers, default=opus\n#\t\t\tcan be a comma separated list in order of preference, e.g., opus,pcmu)\n# videocodec = vp8|vp9|h264|av1|h265 (video codec(s) to force on publishers, default=vp8\n#\t\t\tcan be a comma separated list in order of preference, e.g., vp9,vp8,h264)\n# vp9_profile = VP9-specific profile to prefer (e.g., \"2\" for \"profile-id=2\")\n# h264_profile = H.264-specific profile to prefer (e.g., \"42e01f\" for \"profile-level-id=42e01f\")\n# opus_fec = true|false (whether inband FEC must be negotiated; only works for Opus, default=true)\n# opus_dtx = true|false (whether DTX must be negotiated; only works for Opus, default=false)\n# audiolevel_ext = true|false (whether the ssrc-audio-level RTP extension must\n#\t\tbe negotiated/used or not for new publishers, default=true)\n# audiolevel_event = true|false (whether to emit event to other users or not, default=false)\n# audio_active_packets = 100 (number of packets with audio level, default=100, 2 seconds)\n# audio_level_average = 25 (average value of audio level, 127=muted, 0='too loud', default=25)\n# videoorient_ext = true|false (whether the video-orientation RTP extension must\n#\t\tbe negotiated/used or not for new publishers, default=true)\n# playoutdelay_ext = true|false (whether the playout-delay RTP extension must\n#\t\tbe negotiated/used or not for new publishers, default=true)\n# transport_wide_cc_ext = true|false (whether the transport wide CC RTP extension must be\n#\t\tnegotiated/used or not for new publishers, default=true)\n# record = true|false (whether this room should be recorded, default=false)\n# rec_dir = <folder where recordings should be stored, when enabled>\n# lock_record = true|false (whether recording can only be started/stopped if the secret\n#            is provided, or using the global enable_recording request, default=false)\n# notify_joining = true|false (optional, whether to notify all participants when a new\n#               participant joins the room. The Videoroom plugin by design only notifies\n#               new feeds (publishers), and enabling this may result extra notification\n#               traffic. This flag is particularly useful when enabled with require_pvtid\n#               for admin to manage listening only participants. default=false)\n# require_e2ee = true|false (whether all participants are required to publish and subscribe\n#             using end-to-end media encryption, e.g., via Insertable Streams; default=false)\n# dummy_publisher = true|false (whether a dummy publisher should be created in this room,\n#                 with one separate m-line for each codec supported in the room; this is\n#                 useful when there's a need to create subscriptions with placeholders\n#                 for some or all m-lines, even when they aren't used yet; default=false)\n# dummy_streams = in case dummy_publisher is set to true, array of codecs to offer,\n#\t\t\t\toptionally with a fmtp attribute to match (codec/fmtp properties).\n#\t\t\t\tIf not provided, all codecs enabled in the room are offered, with no fmtp.\n#\t\t\t\tNotice that the fmtp is parsed, and only a few codecs are supported.\n# dummy_e2ee = true|false (whether the dummy publisher should be advertised as supporting\n#\t\t\tend-to-end encryption; default=value of room's require_e2ee)\n# threads = number of threads to assist with the relaying of publishers in the room; as\n#\t\t\tin the Streaming plugin, this setting can help if you expect a lot of subscribers\n#\t\t\tthat may cause the plugin to slow down and fail to catch up (default=0)\n#}\n\ngeneral: {\n\t#admin_key = \"supersecret\"\t\t# If set, rooms can be created via API only\n\t\t\t\t\t\t\t\t\t# if this key is provided in the request\n\t#lock_rtp_forward = true\t\t# Whether the admin_key above should be\n\t\t\t\t\t\t\t\t\t# enforced for RTP forwarding requests too\n\t#events = false\t\t\t\t\t# Whether events should be sent to event\n\t\t\t\t\t\t\t\t\t# handlers (default=true)\n\n\t# By default, integers are used as a unique ID for both rooms and participants.\n\t# In case you want to use strings instead (e.g., a UUID), set string_ids to true.\n\t#string_ids = true\n}\n\nroom-1234: {\n\tdescription = \"Demo Room\"\n\tsecret = \"adminpwd\"\n\tpublishers = 6\n\tbitrate = 128000\t# This is a low cap, increase if you want to use simulcast or SVC\n\tfir_freq = 10\n\t#audiocodec = \"opus\"\n\t#videocodec = \"vp8\"\n\trecord = false\n\t#rec_dir = \"/path/to/recordings-folder\"\n}\n"
  },
  {
    "path": "conf/janus.transport.http.jcfg.sample",
    "content": "# Web server stuff: whether any should be enabled, which ports they\n# should use, whether security should be handled directly or demanded to\n# an external application (e.g., web frontend) and what should be the\n# base path for the Janus API protocol. Notice that by default\n# all the web servers will try and bind on both IPv4 and IPv6: if you\n# want to only bind to IPv4 addresses (e.g., because your system does not\n# support IPv6), you should set the web server 'ip' property to '0.0.0.0'.\n# To see debug logs from the HTTP server library, set 'mhd_debug'.\ngeneral: {\n\t#events = true\t\t\t\t\t# Whether to notify event handlers about transport events (default=true)\n\tjson = \"indented\"\t\t\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\tbase_path = \"/janus\"\t\t\t# Base path to bind to in the web server (plain HTTP only)\n\thttp = true\t\t\t\t\t\t# Whether to enable the plain HTTP interface\n\tport = 8088\t\t\t\t\t\t# Web server HTTP port\n\t#interface = \"eth0\"\t\t\t\t# Whether we should bind this server to a specific interface only\n\t#ip = \"192.168.0.1\"\t\t\t\t# Whether we should bind this server to a specific IP address (v4 or v6) only\n\thttps = false\t\t\t\t\t# Whether to enable HTTPS (default=false)\n\t#secure_port = 8089\t\t\t\t# Web server HTTPS port, if enabled\n\t#secure_interface = \"eth0\"\t\t# Whether we should bind this server to a specific interface only\n\t#secure_ip = \"192.168.0.1\"\t\t# Whether we should bind this server to a specific IP address (v4 or v6) only\n\t#acl = \"127.,192.168.0.\"\t\t# Only allow requests coming from this comma separated list of addresses\n\t#acl_forwarded = true\t\t\t# Whether we should check the X-Forwarded-For header too for the ACL\n\t\t\t\t\t\t\t\t\t# (default=false, since without a proxy in the middle this could be abused)\n\t#mhd_connection_limit = 1020\t\t# Open connections limit in libmicrohttpd (default=1020)\n\t#mhd_debug = false\t\t\t\t\t# Ask libmicrohttpd to write warning and error messages to stderr (default=false)\n}\n\n# Janus can also expose an admin/monitor endpoint, to allow you to check\n# which sessions are up, which handles they're managing, their current\n# status and so on. This provides a useful aid when debugging potential\n# issues in Janus. The configuration is pretty much the same as the one\n# already presented above for the webserver stuff, as the API is very\n# similar: choose the base bath for the admin/monitor endpoint (/admin\n# by default), ports, etc. Besides, you can specify\n# a secret that must be provided in all requests as a crude form of\n# authorization mechanism, and partial or full source IPs if you want to\n# limit access basing on IP addresses. For security reasons, this\n# endpoint is disabled by default, enable it by setting admin_http=true.\nadmin: {\n\tadmin_base_path = \"/admin\"\t\t\t# Base path to bind to in the admin/monitor web server (plain HTTP only)\n\tadmin_http = false\t\t\t\t\t# Whether to enable the plain HTTP interface\n\tadmin_port = 7088\t\t\t\t\t# Admin/monitor web server HTTP port\n\t#admin_interface = \"eth0\"\t\t\t# Whether we should bind this server to a specific interface only\n\t#admin_ip = \"192.168.0.1\"\t\t\t# Whether we should bind this server to a specific IP address (v4 or v6) only\n\tadmin_https = false\t\t\t\t\t# Whether to enable HTTPS (default=false)\n\t#admin_secure_port = 7889\t\t\t# Admin/monitor web server HTTPS port, if enabled\n\t#admin_secure_interface = \"eth0\"\t# Whether we should bind this server to a specific interface only\n\t#admin_secure_ip = \"192.168.0.1\"\t# Whether we should bind this server to a specific IP address (v4 or v6) only\n\t#admin_acl = \"127.,192.168.0.\"\t\t# Only allow requests coming from this comma separated list of addresses\n\t#admin_acl_forwarded = true\t\t\t# Whether we should check the X-Forwarded-For header too for the admin ACL\n\t\t\t\t\t\t\t\t\t\t# (default=false, since without a proxy in the middle this could be abused)\n}\n\n# The HTTP servers created in Janus support CORS out of the box, but by\n# default they return a wildcard (*) in the 'Access-Control-Allow-Origin'\n# header. This works fine in most situations, except when we have to\n# respond to a credential request (withCredentials=true in the XHR). If\n# you need that, uncomment and set the 'allow_origin' below to specify\n# what must be returned in 'Access-Control-Allow-Origin'. More details:\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS\n# In case you want to enforce the Origin validation, rather than leave\n# it to browsers, you can set 'enforce_cors' to 'true' to have Janus\n# return a '403 Forbidden' for all requests that don't comply.\ncors: {\n\t#allow_origin = \"http://foo.example\"\n\t#enforce_cors = true\n}\n\n# Certificate and key to use for HTTPS, if enabled (and passphrase if needed).\n# You can also disable insecure protocols and ciphers by configuring the\n# 'ciphers' property accordingly (no limitation by default).\ncertificates: {\n\t#cert_pem = \"/path/to/cert.pem\"\n\t#cert_key = \"/path/to/key.pem\"\n\t#cert_pwd = \"secretpassphrase\"\n\t#ciphers = \"PFS:-VERS-TLS1.0:-VERS-TLS1.1:-3DES-CBC:-ARCFOUR-128\"\n}\n"
  },
  {
    "path": "conf/janus.transport.mqtt.jcfg.sample",
    "content": "# Configuration of the MQTT additional transport for the Janus API.\ngeneral: {\n\tenabled = false\t\t\t\t\t\t# Whether the support must be enabled\n\t#events = true\t\t\t\t\t\t# Whether to notify event handlers about transport events (default=true)\n\tjson = \"indented\"\t\t\t\t\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\n\turl = \"tcp://localhost:1883\"\t\t# The connection URL of the MQTT broker: if you want\n\t\t\t\t\t\t\t\t\t\t# to use SSL, make sure you type ssl:// instead of tcp://,\n\t\t\t\t\t\t\t\t\t\t# and that you configure the SSL settings below\n\t#mqtt_version = \"3.1.1\"\t\t\t\t# Protocol version. Available values: 3.1, 3.1.1 (default), 5.\n\t#client_id = \"guest\"\t\t\t\t# Client identifier\n\t#username = \"guest\"\t\t\t\t\t# Username to use to authenticate, if needed\n\t#password = \"guest\"\t\t\t\t\t# Password to use to authenticate, if needed\n\t#keep_alive_interval = 20\t\t\t# Keep connection for N seconds\n\t#cleansession = 0\t\t\t\t\t# Clean session flag\n\t#max_inflight = 10\t\t\t\t\t# Maximum number of inflight messages\n\t#max_buffered = 100\t\t\t\t\t# Maximum number of buffered messages\n\t#disconnect_timeout = 100\t\t\t# Milliseconds to wait before destroying client\n\tsubscribe_topic = \"to-janus\"\t\t# Topic for incoming messages\n\t#subscribe_qos = 1\t\t\t\t\t# QoS for incoming messages\n\tpublish_topic = \"from-janus\"\t\t# Topic for outgoing messages\n\t#publish_qos = 1\t\t\t\t\t# QoS for outgoing messages\n\n\t#ssl_enabled = true\t\t\t\t\t# Whether ssl support must be enabled\n\t#verify_peer = true\t\t\t\t\t# Whether peer verification must be enabled\n\n\t# Certificates to use when SSL support is enabled, if needed\n\t#cacertfile = /path/to/cacert.pem\n\t#certfile = /path/to/cert.pem\n\t#keyfile = /path/to/key.pem\n\n\t# These options work with MQTT 5 only.\n\t#vacuum_interval = 60\t\t\t\t\t# Interval for removing old transaction states in seconds.\n\t#proxy_transaction_user_properties = []\t# Array of user property names to copy from the incoming message.\n\t#add_transaction_user_properties = ()\t# List of user property [\"key\", \"value\"] pairs to add.\n}\n\nadmin: {\n\t#admin_enabled = false\t\t\t\t# Whether the support must be enabled\n\tsubscribe_topic = \"to-janus-admin\"\t# Topic for incoming admin messages\n\t#subscribe_qos = 1\t\t\t\t\t# QoS for incoming admin messages\n\tpublish_topic = \"from-janus-admin\"\t# Topic for outgoing admin messages\n\t#publish_qos = 1\t\t\t\t\t# QoS for outgoing admin messages\n}\n\nstatus: {\n\tenabled = false \t\t\t# Whether status messages must be enabled (default: false)\n\n\t# Initial message sent to status topic. Nothing is being sent if not set.\n\t#connect_message = \"{\\\"online\\\": true}\"\n\n\t# Message sent after disconnect or as LWT. Nothing is being sent if not set.\n\t#disconnect_message = \"{\\\"online\\\": false}\"\n\n\t#topic = \"status\"\t\t\t# Status topic (default: \"status\")\n\t#qos = 1\t\t\t\t  \t# QoS for status messages (default: 1)\n\t#retain = false\t\t\t\t# Whether status messages should be retained (default: false)\n}\n"
  },
  {
    "path": "conf/janus.transport.nanomsg.jcfg.sample",
    "content": "# You can also control a Janus instance using Nanomsg sockets. The only\n# aspect you need to configure here is the address to use for the\n# communication, and whether the address should be used to bind locally\n# or to connect to a remote endpoint. Notice that the only supported\n# pattern is NN_PAIR, so you'll only be able to have a single client\n# controlling the API with this plugin. As usual, both Janus API and Admin\n# API endpoints can be configured.\ngeneral: {\n\tenabled = true\t\t\t\t\t\t# Whether to enable the Nanomsg interface\n\t\t\t\t\t\t\t\t\t\t# for Janus API clients\n\t#events = true\t\t\t\t\t\t# Whether to notify event handlers about transport events (default=true)\n\tjson = \"indented\"\t\t\t\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\t#mode = \"bind\"\t\t\t\t\t\t# Whether we should 'bind' to the specified\n\t\t\t\t\t\t\t\t\t\t# address (default), or connect to it if remote\n\taddress = \"ipc:///tmp/janus.ipc\"\t# Address to use (Janus API), refer\n\t\t\t\t\t\t\t\t\t\t# to the Nanomsg documentation for more info\n\t\t\t\t\t\t\t\t\t\t# on different transports you can use here\n}\n\n# As with other transport plugins, you can use Nanomsg to interact with\n# the Admin API as well: in case you're interested in it, a different\n# address needs to be provided.\nadmin: {\n\tadmin_enabled = false\t\t\t\t# Whether to enable the Nanomsg interface\n\t\t\t\t\t\t\t\t\t\t# for Admin API clients\n\t#admin_mode = \"bind\"\n\t#admin_address = \"ipc:///tmp/janus-admin.ipc\"\n}\n"
  },
  {
    "path": "conf/janus.transport.pfunix.jcfg.sample",
    "content": "# You can also control a Janus instance using Unix Sockets. The only\n# aspect you need to configure here is the path of the Unix Sockets\n# server. Notice that by default the interface is disabled, as you need\n# to specify the path(s) to bind to for the API(s).\ngeneral: {\n\tenabled = false\t\t\t\t\t# Whether to enable the Unix Sockets interface\n\t\t\t\t\t\t\t\t\t# for Janus API clients\n\t#events = true\t\t\t\t\t# Whether to notify event handlers about transport events (default=true)\n\tjson = \"indented\"\t\t\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\t#path = \"/path/to/ux-janusapi\"\t# Path to bind to (Janus API)\n\t#type = \"SOCK_SEQPACKET\"\t\t# SOCK_SEQPACKET (default) or SOCK_DGRAM?\n}\n\n# As with other transport plugins, you can use Unix Sockets to interact\n# with the Admin API as well: in case you're interested in it, a different\n# path needs to be provided.\nadmin: {\n\tadmin_enabled = false\t\t\t\t\t# Whether to enable the Unix Sockets interface\n\t\t\t\t\t\t\t\t\t\t\t# for Admin API clients\n\t#admin_path = \"/path/to/ux-janusadmin\"\t# Path to bind to (Admin API)\n\t#admin_type = \"SOCK_SEQPACKET\"\t\t\t# SOCK_SEQPACKET (default) or SOCK_DGRAM?\n}\n"
  },
  {
    "path": "conf/janus.transport.rabbitmq.jcfg.sample",
    "content": "# Configuration of the RabbitMQ additional transport for the Janus API.\n# This is only useful when you're wrapping Janus requests in your server\n# application, and handling the communication with clients your own way.\n# At the moment, only a single \"application\" can be handled at the same\n# time, meaning that Janus won't implement multiple queues to handle\n# multiple concurrent \"application servers\" taking advantage of its\n# features. Support for this is planned, though (e.g., through some kind\n# of negotiation to create queues on the fly). Right now, you can only\n# configure the address of the RabbitMQ server to use, and the queues to\n# make use of to receive (to-janus) and send (from-janus) messages\n# from/to an external application. If you're using the same RabbitMQ\n# server instance for multiple Janus instances, make sure you configure\n# different queues for each of them (e.g., from-janus-1/to-janus-1 and\n# from-janus-2/to-janus-2), or otherwise both the instances will make\n# use of the same queues and messages will get lost. The integration\n# is disabled by default, so set enabled=true if you want to use it.\ngeneral: {\n\tenabled = false\t\t\t\t\t\t# Whether the support must be enabled\n\t#events = true\t\t\t\t\t\t# Whether to notify event handlers about transport events (default=true)\n\tjson = \"indented\"\t\t\t\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\thost = \"localhost\"\t\t\t\t\t# The address of the RabbitMQ server\n\t#port = 5672\t\t\t\t\t\t# The port of the RabbitMQ server (5672 by default)\n\t#username = \"guest\"\t\t\t\t\t# Username to use to authenticate, if needed\n\t#password = \"guest\"\t\t\t\t\t# Password to use to authenticate, if needed\n\t#vhost = \"/\"\t\t\t\t\t\t# Virtual host to specify when logging in, if needed\n\n\t#janus_exchange = \"janus-exchange\"\t# Exchange for outgoing messages, using default if not provided\n\t#janus_exchange_type = \"fanout\"\t\t# Rabbitmq exchange_type can be one of the available types: direct, topic, headers and fanout (fanout by defualt).\n\t#queue_name = \"janus-gateway\"\t\t# Queue name for incoming messages (if set and janus_exchange_type is topic/direct, to_janus will be the routing key the queue is bound to the exchange on)\n\tto_janus = \"to-janus\"\t\t\t\t# Name of the queue for incoming messages if queue_name isn't set, otherwise, the routing key that queue_name is bound to\n\tfrom_janus = \"from-janus\"\t\t\t# Routing key of the message sent from janus (as well as the name of the outgoing queue if declare_outgoing_queue = true)\n\t#declare_outgoing_queue = true\t\t# By default (for backwards compatibility), we declare an outgoing queue. Set this to false to disable that behavior\n\t#queue_durable = false\t\t\t\t# Whether or not incoming queue should remain after a RabbitMQ reboot\n\t#queue_autodelete = false\t\t\t# Whether or not incoming queue should autodelete after janus disconnects from RabbitMQ\n\t#queue_exclusive = false\t\t\t# Whether or not incoming queue should only allow one subscriber\n\t#heartbeat = 60 \t\t\t\t# Defines the seconds without communication that should pass before considering the TCP connection unreachable.\n\n\t#ssl_enabled = false\t\t\t\t# Whether ssl support must be enabled\n\t#ssl_verify_peer = true\t\t\t\t# Whether peer verification must be enabled\n\t#ssl_verify_hostname = true\t\t\t# Whether hostname verification must be enabled\n\n\t# Certificates to use when SSL support is enabled, if needed\n\t#ssl_cacert = \"/path/to/cacert.pem\"\n\t#ssl_cert = \"/path/to/cert.pem\"\n\t#ssl_key = \"/path/to/key.pem\"\n}\n\n# If you want to expose the Admin API via RabbitMQ as well, you need to\n# specify a different set of queues, as you cannot mix Janus API and\n# Admin API messaging. The same RabbitMQ server is supposed to be used.\n# Notice that by default the Admin API support via RabbitMQ is disabled.\nadmin: {\n\t#admin_enabled = false\t\t\t\t\t\t# Whether the support must be enabled\n\n\t#queue_name_admin = \"janus-gateway-admin\"\t# Queue name for incoming admin messages (if set and janus_exchange_type is topic/direct, to_janus_admin will be the the routing key the queue is bound to the exchange on)\n\t#to_janus_admin = \"to-janus-admin\"\t\t\t# Name of the queue for incoming messages if queue_name_admin isn't set, otherwise, the routing key that queue_name_admin is bound to\n\t#from_janus_admin = \"from-janus-admin\"\t\t# Routing key of the message sent from janus  (as well as the name of the outgoing queue if declare_outgoing_queue_admin = true)\n\t#declare_outgoing_queue_admin = true\t\t# By default (for backwards compatibility), we declare an outgoing queue. Set this to false to disable that behavior\n\t#queue_durable_admin = false\t\t\t\t# Whether or not incoming queue should remain after a RabbitMQ reboot\n\t#queue_autodelete_admin = false\t\t\t\t# Whether or not incoming queue should autodelete after janus disconnects from RabbitMQ\n\t#queue_exclusive_admin = false\t\t\t\t# Whether or not incoming queue should only allow one subscriber\n\n}\n"
  },
  {
    "path": "conf/janus.transport.websockets.jcfg.sample",
    "content": "# WebSockets stuff: whether they should be enabled, which ports they\n# should use, and so on.\ngeneral: {\n\t#events = true\t\t\t\t\t# Whether to notify event handlers about transport events (default=true)\n\tjson = \"indented\"\t\t\t\t# Whether the JSON messages should be indented (default),\n\t\t\t\t\t\t\t\t\t# plain (no indentation) or compact (no indentation and no spaces)\n\t#pingpong_trigger = 30\t\t\t# After how many seconds of idle, a PING should be sent\n\t#pingpong_timeout = 10\t\t\t# After how many seconds of not getting a PONG, a timeout should be detected\n\n\tws = true\t\t\t\t\t\t# Whether to enable the WebSockets API\n\tws_port = 8188\t\t\t\t\t# WebSockets server port\n\t#ws_interface = \"eth0\"\t\t\t# Whether we should bind this server to a specific interface only\n\t#ws_ip = \"192.168.0.1\"\t\t\t# Whether we should bind this server to a specific IP address only\n\t#ws_unix = \"/run/ws.sock\"\t\t# Use WebSocket server over UNIX socket instead of TCP\n\twss = false\t\t\t\t\t\t# Whether to enable secure WebSockets\n\t#wss_port = 8989\t\t\t\t# WebSockets server secure port, if enabled\n\t#wss_interface = \"eth0\"\t\t\t# Whether we should bind this server to a specific interface only\n\t#wss_ip = \"192.168.0.1\"\t\t\t# Whether we should bind this server to a specific IP address only\n\t#wss_unix = \"/run/wss.sock\"\t\t# Use WebSocket server over UNIX socket instead of TCP\n\t#ws_logging = \"err,warn\"\t\t# libwebsockets debugging level as a comma separated list of things\n\t\t\t\t\t\t\t\t\t# to debug, supported values: err, warn, notice, info, debug, parser,\n\t\t\t\t\t\t\t\t\t# header, ext, client, latency, user, count (plus 'none' and 'all')\n\t#ws_acl = \"127.,192.168.0.\"\t\t# Only allow requests coming from this comma separated list of addresses\n\t#ws_acl_forwarded = true\t\t# Whether we should check the X-Forwarded-For header too for the ACL\n\t\t\t\t\t\t\t\t\t# (default=false, since without a proxy in the middle this could be abused)\n}\n\n# If you want to expose the Admin API via WebSockets as well, you need to\n# specify a different server instance, as you cannot mix Janus API and\n# Admin API messaging. Notice that by default the Admin API support via\n# WebSockets is disabled.\nadmin: {\n\tadmin_ws = false\t\t\t\t\t# Whether to enable the Admin API WebSockets API\n\tadmin_ws_port = 7188\t\t\t\t# Admin API WebSockets server port, if enabled\n\t#admin_ws_interface = \"eth0\"\t\t# Whether we should bind this server to a specific interface only\n\t#admin_ws_ip = \"192.168.0.1\"\t\t# Whether we should bind this server to a specific IP address only\n\t#admin_ws_unix = \"/run/aws.sock\"\t# Use WebSocket server over UNIX socket instead of TCP\n\tadmin_wss = false\t\t\t\t\t# Whether to enable the Admin API secure WebSockets\n\t#admin_wss_port = 7989\t\t\t\t# Admin API WebSockets server secure port, if enabled\n\t#admin_wss_interface = \"eth0\"\t\t# Whether we should bind this server to a specific interface only\n\t#admin_wss_ip = \"192.168.0.1\"\t\t# Whether we should bind this server to a specific IP address only\n\t#admin_wss_unix = \"/run/awss.sock\"\t# Use WebSocket server over UNIX socket instead of TCP\n\t#admin_ws_acl = \"127.,192.168.0.\"\t# Only allow requests coming from this comma separated list of addresses\n\t#admin_ws_acl_forwarded = true\t\t# Whether we should check the X-Forwarded-For header too for the ACL\n\t\t\t\t\t\t\t\t\t\t# (default=false, since without a proxy in the middle this could be abused)\n}\n\n# The HTTP servers created in Janus support CORS out of the box, but by\n# default they return a wildcard (*) in the 'Access-Control-Allow-Origin'\n# header. This works fine in most situations, except when we have to\n# respond to a credential request (withCredentials=true in the XHR). If\n# you need that, uncomment and set the 'allow_origin' below to specify\n# what must be returned in 'Access-Control-Allow-Origin'. More details:\n# https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS\n# In case you want to enforce the Origin validation, rather than leave\n# it to browsers, you can set 'enforce_cors' to 'true' to have Janus\n# return a '403 Forbidden' for all requests that don't comply.\ncors: {\n\t#allow_origin = \"http://foo.example\"\n\t#enforce_cors = true\n}\n\n# Certificate and key to use for any secure WebSocket server, if enabled (and passphrase if needed).\n# You can also disable insecure protocols and ciphers by configuring the\n# 'ciphers' property accordingly (no limitation by default).\n# Examples of recommended cipher strings at https://cheatsheetseries.owasp.org/cheatsheets/TLS_Cipher_String_Cheat_Sheet.html\ncertificates: {\n\t#cert_pem = \"/path/to/cert.pem\"\n\t#cert_key = \"/path/to/key.pem\"\n\t#cert_pwd = \"secretpassphrase\"\n\t#ciphers = \"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256\"\n}\n"
  },
  {
    "path": "configure.ac",
    "content": "AC_INIT([Janus WebRTC Server],[1.4.1],[https://github.com/meetecho/janus-gateway],[janus-gateway],[https://janus.conf.meetecho.com])\nAC_LANG(C)\nAC_CONFIG_AUX_DIR([.])\nAC_CONFIG_MACRO_DIR([m4])\n\nAC_ENABLE_SHARED(yes)\nAC_ENABLE_STATIC(no)\n\nAM_INIT_AUTOMAKE([foreign subdir-objects])\nAM_SILENT_RULES([yes])\n\nAC_USE_SYSTEM_EXTENSIONS\n\nAC_PROG_CC\n\nLT_PREREQ([2.2])\nLT_INIT\n\n# Common CFLAGS\nCFLAGS=\"$CFLAGS \\\n\t-fPIC \\\n\t-fstack-protector-all \\\n\t-fstrict-aliasing \\\n\t-pthread \\\n\t-Wall \\\n\t-Warray-bounds \\\n\t-Wextra \\\n\t-Wformat-nonliteral \\\n\t-Wformat-security \\\n\t-Wformat=2 \\\n\t-Winit-self \\\n\t-Wlarger-than=2097152 \\\n\t-Wmissing-declarations \\\n\t-Wmissing-format-attribute \\\n\t-Wmissing-include-dirs \\\n\t-Wmissing-noreturn \\\n\t-Wmissing-prototypes \\\n\t-Wnested-externs \\\n\t-Wold-style-definition \\\n\t-Wpacked \\\n\t-Wpointer-arith \\\n\t-Wsign-compare \\\n\t-Wstrict-prototypes \\\n\t-Wswitch-default \\\n\t-Wunused \\\n\t-Wno-unused-parameter \\\n\t-Wno-unused-result \\\n\t-Wwrite-strings \\\n\t-Werror=implicit-function-declaration\"\n\ncase \"$CC\" in\n*clang*)\n\t# Specific clang flags\n\tCFLAGS=\"$CFLAGS \\\n\t\t-Wno-initializer-overrides \\\n\t\t-Wno-missing-noreturn\"\n;;\ncc*)\n\tCFLAGS=\"$CFLAGS \\\n\t\t-Wno-cast-align \\\n\t\t-Wno-initializer-overrides\"\n;;\n*)\n\t# Specific gcc flags\n\tCFLAGS=\"$CFLAGS \\\n\t\t-Wno-override-init \\\n\t\t-Wunsafe-loop-optimizations \\\n\t\t-Wunused-but-set-variable\"\nesac\n\nJANUS_VERSION=1401\nAC_SUBST(JANUS_VERSION)\nJANUS_VERSION_STRING=\"1.4.1\"\nAC_SUBST(JANUS_VERSION_STRING)\nJANUS_VERSION_SO=\"2:10:0\"\nAC_SUBST(JANUS_VERSION_SO)\n\ncase \"$host_os\" in\ndarwin*)\n\tCFLAGS=\"$CFLAGS -I/usr/local/opt/openssl/include -I/usr/local/include\"\n\t# add rdynamic to LDFLAGS\n\tLDFLAGS=\"$LDFLAGS -Wl,-export_dynamic\"\n\tLDFLAGS=\"$LDFLAGS -L/usr/local/lib -L/usr/local/opt/openssl/lib -L/opt/local/lib -L/usr/local/libsrtp/lib\"\n\tAM_CONDITIONAL([DARWIN_OS], true)\n;;\nfreebsd*)\n\tCFLAGS=\"$CFLAGS -I/usr/include/openssl\"\n\tLDFLAGS=\"$LDFLAGS -Xlinker --export-dynamic\"\n\tLDFLAGS=\"$LDFLAGS -L/usr/lib/openssl -lcrypto -lssl -L/usr/local/lib\"\n\tAM_CONDITIONAL([DARWIN_OS], false)\n;;\n*)\n\tLDFLAGS=\"$LDFLAGS -Wl,--export-dynamic\"\n\tAM_CONDITIONAL([DARWIN_OS], false)\n\tAC_DEFINE(HAS_DTLS_WINDOW_SIZE, 1)\nesac\n\nglib_version=2.34\nssl_version=1.0.1\njansson_version=2.5\n\nJANUS_PACKAGES_PUBLIC=\"glib-2.0 >= $glib_version, jansson >= $jansson_version\"\nAC_SUBST(JANUS_PACKAGES_PUBLIC)\n\n##\n# Janus\n##\n\nAC_ARG_ENABLE([docs],\n              [AS_HELP_STRING([--enable-docs],\n                              [Enable building documentation])],\n              [],\n              [enable_docs=no])\n\nAC_ARG_ENABLE([data-channels],\n              [AS_HELP_STRING([--disable-data-channels],\n                              [Disable DataChannels])],\n              [],\n              [enable_data_channels=maybe])\n\nAC_ARG_ENABLE([boringssl],\n              [AS_HELP_STRING([--enable-boringssl],\n                              [Use BoringSSL instead of OpenSSL])],\n              [\n                case \"${enableval}\" in\n                  yes) boringssl_dir=/opt/boringssl ;;\n                  no)  boringssl_dir= ;;\n                  *) boringssl_dir=${enableval} ;;\n                esac\n              ],\n              [boringssl_dir=])\n\nAC_ARG_ENABLE([libsrtp2],\n              [AS_HELP_STRING([--enable-libsrtp2],\n                              [Use libsrtp 2.0.x instead of libsrtp 1.5.x])],\n              [],\n              [enable_libsrtp2=maybe])\n\nAC_ARG_ENABLE([aes-gcm],\n              [AS_HELP_STRING([--disable-aes-gcm],\n                              [Disable AES-GCM support in libsrtp(2)])],\n              [],\n              [AC_DEFINE(HAVE_SRTP_AESGCM)])\n\nAC_ARG_ENABLE([dtls-settimeout],\n              [AS_HELP_STRING([--enable-dtls-settimeout],\n                              [Use DTLSv1_set_initial_timeout_duration (only available in recent BoringSSL versions)])],\n              [],\n              [enable_dtls_settimeout=no])\n\nAC_ARG_ENABLE([pthread-mutex],\n              [AS_HELP_STRING([--enable-pthread-mutex],\n                              [Use pthread_mutex instead of GMutex (see #1397)])],\n              [],\n              [enable_pthread_mutex=no])\n\nAC_ARG_ENABLE([turn-rest-api],\n              [AS_HELP_STRING([--disable-turn-rest-api],\n                              [Disable TURN REST API client (via libcurl)])],\n              [],\n              [enable_turn_rest_api=maybe])\n\nAC_ARG_ENABLE([all-plugins],\n              [AS_HELP_STRING([--disable-all-plugins],\n                              [Disable building all plugins (except those manually enabled)])],\n              [\n               AS_IF([test \"x$enable_plugin_audiobridge\" != \"xyes\"],\n                     [enable_plugin_audiobridge=no])\n               AS_IF([test \"x$enable_plugin_duktape\" != \"xyes\"],\n                     [enable_plugin_duktape=no])\n               AS_IF([test \"x$enable_plugin_echotest\" != \"xyes\"],\n                     [enable_plugin_echotest=no])\n               AS_IF([test \"x$enable_plugin_lua\" != \"xyes\"],\n                     [enable_plugin_lua=no])\n               AS_IF([test \"x$enable_plugin_recordplay\" != \"xyes\"],\n                     [enable_plugin_recordplay=no])\n               AS_IF([test \"x$enable_plugin_sip\" != \"xyes\"],\n                     [enable_plugin_sip=no])\n               AS_IF([test \"x$enable_plugin_nosip\" != \"xyes\"],\n                     [enable_plugin_nosip=no])\n               AS_IF([test \"x$enable_plugin_streaming\" != \"xyes\"],\n                     [enable_plugin_streaming=no])\n               AS_IF([test \"x$enable_plugin_textroom\" != \"xyes\"],\n                     [enable_plugin_textroom=no])\n               AS_IF([test \"x$enable_plugin_videocall\" != \"xyes\"],\n                     [enable_plugin_videocall=no])\n               AS_IF([test \"x$enable_plugin_videoroom\" != \"xyes\"],\n                     [enable_plugin_videoroom=no])\n              ],\n              [])\n\nAC_ARG_ENABLE([all-transports],\n              [AS_HELP_STRING([--disable-all-transports],\n                              [Disable building all transports (except those manually enabled)])],\n              [\n               AS_IF([test \"x$enable_rest\" != \"xyes\"],\n                     [enable_rest=no])\n               AS_IF([test \"x$enable_websockets\" != \"xyes\"],\n                     [enable_websockets=no])\n               AS_IF([test \"x$enable_rabbitmq\" != \"xyes\"],\n                     [enable_rabbitmq=no])\n               AS_IF([test \"x$enable_mqtt\" != \"xyes\"],\n                     [enable_mqtt=no])\n               AS_IF([test \"x$enable_unix_sockets\" != \"xyes\"],\n                     [enable_unix_sockets=no])\n               AS_IF([test \"x$enable_nanomsg\" != \"xyes\"],\n                     [enable_nanomsg=no])\n              ],\n              [])\n\nAC_ARG_ENABLE([all-handlers],\n              [AS_HELP_STRING([--disable-all-handlers],\n                              [Disable building all event handlers (except those manually enabled)])],\n              [\n               AS_IF([test \"x$enable_sample_event_handler\" != \"xyes\"],\n                     [enable_sample_event_handler=no])\n               AS_IF([test \"x$enable_websockets_event_handler\" != \"xyes\"],\n                     [enable_websockets_event_handler=no])\n               AS_IF([test \"x$enable_rabbitmq_event_handler\" != \"xyes\"],\n                     [enable_rabbitmq_event_handler=no])\n               AS_IF([test \"x$enable_mqtt_event_handler\" != \"xyes\"],\n                     [enable_mqtt_event_handler=no])\n               AS_IF([test \"x$enable_nanomsg_event_handler\" != \"xyes\"],\n                     [enable_nanomsg_event_handler=no])\n               AS_IF([test \"x$enable_gelf_event_handler\" != \"xyes\"],\n                     [enable_gelf_event_handler=no])\n              ],\n              [])\n\nAC_ARG_ENABLE([all-loggers],\n              [AS_HELP_STRING([--disable-all-loggers],\n                              [Disable building all loggers (except those manually enabled)])],\n              [\n               AS_IF([test \"x$enable_json_logger\" != \"xyes\"],\n                     [enable_json_logger=no])\n              ],\n              [])\n\nAC_ARG_ENABLE([all-js-modules],\n              [AS_HELP_STRING([--enable-all-js-modules],\n                              [Build all the JavaScript modules (instead of manually enabling them one by one)])],\n              [\n               enable_javascript_es_module=yes\n               enable_javascript_umd_module=yes\n               enable_javascript_iife_module=yes\n               enable_javascript_common_js_module=yes\n              ],\n              [])\n\nAC_ARG_ENABLE([rest],\n              [AS_HELP_STRING([--disable-rest],\n                              [Disable REST (HTTP/HTTPS) support])],\n              [AS_IF([test \"x$enable_rest\" != \"xyes\"],\n                     [enable_rest=no])],\n              [enable_rest=maybe])\n\nAC_ARG_ENABLE([websockets],\n              [AS_HELP_STRING([--disable-websockets],\n                              [Disable WebSockets support])],\n              [AS_IF([test \"x$enable_websockets\" != \"xyes\"],\n                     [enable_websockets=no])],\n              [enable_websockets=maybe])\n\nAC_ARG_ENABLE([rabbitmq],\n              [AS_HELP_STRING([--disable-rabbitmq],\n                              [Disable RabbitMQ integration])],\n              [AS_IF([test \"x$enable_rabbitmq\" != \"xyes\"],\n                     [enable_rabbitmq=no])],\n              [enable_rabbitmq=maybe])\n\nAC_ARG_ENABLE([mqtt],\n              [AS_HELP_STRING([--disable-mqtt],\n                              [Disable MQTT integration])],\n              [AS_IF([test \"x$enable_mqtt\" != \"xyes\"],\n                     [enable_mqtt=no])],\n              [enable_mqtt=maybe])\n\nAC_ARG_ENABLE([unix-sockets],\n              [AS_HELP_STRING([--disable-unix-sockets],\n                              [Disable Unix Sockets integration])],\n              [AS_IF([test \"x$enable_unix_sockets\" != \"xyes\"],\n                     [enable_unix_sockets=no])],\n              [enable_unix_sockets=maybe])\n\nAC_ARG_ENABLE([nanomsg],\n              [AS_HELP_STRING([--disable-nanomsg],\n                              [Disable Nanomsg integration])],\n              [AS_IF([test \"x$enable_nanomsg\" != \"xyes\"],\n                     [enable_nanomsg=no])],\n              [enable_nanomsg=maybe])\n\nAC_ARG_ENABLE([sample-event-handler],\n              [AS_HELP_STRING([--disable-sample-event-handler],\n                              [Disable sample event handler (HTTP POST) ])],\n              [AS_IF([test \"x$enable_sample_event_handler\" != \"xyes\"],\n                     [enable_sample_event_handler=no])],\n              [enable_sample_event_handler=maybe])\n\nAC_ARG_ENABLE([websockets-event-handler],\n              [AS_HELP_STRING([--disable-websockets-event-handler],\n                              [Disable WebSockets event handler ])],\n              [AS_IF([test \"x$enable_websockets_event_handler\" != \"xyes\"],\n                     [enable_websockets_event_handler=no])],\n              [enable_websockets_event_handler=maybe])\n\nAC_ARG_ENABLE([rabbitmq-event-handler],\n              [AS_HELP_STRING([--disable-rabbitmq-event-handler],\n                              [Disable RabbitMQ event handler ])],\n              [AS_IF([test \"x$enable_rabbitmq_event_handler\" != \"xyes\"],\n                     [enable_rabbitmq_event_handler=no])],\n              [enable_rabbitmq_event_handler=maybe])\n\nAC_ARG_ENABLE([mqtt-event-handler],\n              [AS_HELP_STRING([--disable-mqtt-event-handler],\n                              [Disable MQTT event handler ])],\n              [AS_IF([test \"x$enable_mqtt_event_handler\" != \"xyes\"],\n                     [enable_mqtt_event_handler=no])],\n              [enable_mqtt_event_handler=maybe])\n\nAC_ARG_ENABLE([nanomsg-event-handler],\n              [AS_HELP_STRING([--disable-nanomsg-event-handler],\n                              [Disable Nanomsg event handler ])],\n              [AS_IF([test \"x$enable_nanomsg_event_handler\" != \"xyes\"],\n                     [enable_nanomsg_event_handler=no])],\n              [enable_nanomsg_event_handler=maybe])\n\nAC_ARG_ENABLE([gelf-event-handler],\n              [AS_HELP_STRING([--disable-gelf-event-handler],\n                              [Disable gelf event handler ])],\n              [AS_IF([test \"x$enable_gelf_event_handler\" != \"xyes\"],\n                     [enable_gelf_event_handler=no])],\n              [enable_gelf_event_handler=yes])\n\nAC_ARG_ENABLE([json-logger],\n              [AS_HELP_STRING([--enable-json-logger],\n                              [Enable external JSON file logger ])],\n              [AS_IF([test \"x$enable_json_logger\" != \"xyes\"],\n                     [enable_json_logger=no])],\n              [enable_json_logger=no])\n\nAC_ARG_ENABLE([systemd-sockets],\n              [AS_HELP_STRING([--enable-systemd-sockets],\n                              [Enable Systemd Unix Sockets management])],\n              [],\n              [enable_systemd_sockets=no])\n\ncase \"$host_os\" in\nfreebsd*)\n\tPKGCHECKMODULES=\"glib-2.0 >= $glib_version\n                    gio-2.0 >= $glib_version\n                    libconfig\n                    nice\n                    jansson >= $jansson_version\n                    zlib\"\n;;\n*)\n\tPKGCHECKMODULES=\"glib-2.0 >= $glib_version\n                    gio-2.0 >= $glib_version\n                    libconfig\n                    nice\n                    jansson >= $jansson_version\n                    libssl >= $ssl_version\n                    libcrypto\n                    zlib\"\nesac\nPKG_CHECK_MODULES([JANUS],\"$PKGCHECKMODULES\")\n\nJANUS_MANUAL_LIBS=\"${JANUS_MANUAL_LIBS} -lm\"\nAC_SUBST(JANUS_MANUAL_LIBS)\n\nAS_IF([test \"x${boringssl_dir}\" != \"x\"],\n\t  [echo \"Trying to use BoringSSL instead of OpenSSL...\";\n\t   AC_MSG_NOTICE([BoringSSL directory is ${boringssl_dir}])\n\t   CFLAGS=\"$CFLAGS -I${boringssl_dir}/include\";\n\t   BORINGSSL_CFLAGS=\" -I${boringssl_dir}/include\";\n       AC_SUBST(BORINGSSL_CFLAGS)\n\t   BORINGSSL_LIBS=\" -L${boringssl_dir}/lib -lstdc++\";\n       AC_SUBST(BORINGSSL_LIBS)\n\t   AC_CHECK_HEADERS([${boringssl_dir}/include/openssl/opensslconf.h],\n\t                    [AC_DEFINE(HAVE_BORINGSSL)],\n\t                    [AC_MSG_ERROR([BoringSSL headers not found in ${boringssl_dir}, use --disable-boringssl if you want to use OpenSSL instead])])\n      ])\nAM_CONDITIONAL([ENABLE_BORINGSSL], [test \"x${boringssl_dir}\" != \"x\"])\n\nAS_IF([test \"x$enable_dtls_settimeout\" = \"xyes\"],\n      [\n      AC_DEFINE(HAVE_DTLS_SETTIMEOUT)\n      AC_MSG_NOTICE([Assuming DTLSv1_set_initial_timeout_duration is available])\n      ])\nAM_CONDITIONAL([ENABLE_DTLS_SETTIMEOUT], [test \"x$enable_dtls_settimeout\" = \"xyes\"])\n\nAS_IF([test \"x$enable_pthread_mutex\" = \"xyes\"],\n      [\n      AC_DEFINE(USE_PTHREAD_MUTEX)\n      AC_MSG_NOTICE([Will use pthread_mutex instead of GMutex])\n      ])\nAM_CONDITIONAL([ENABLE_PTHREAD_MUTEX], [test \"x$enable_pthread_mutex\" = \"xyes\"])\n\nAC_SEARCH_LIBS([tls_config_set_ca_mem],[tls],\n             [AM_CONDITIONAL([LIBRESSL_DETECTED], true)],\n             [AM_CONDITIONAL([LIBRESSL_DETECTED], false)]\n             )\n\nAC_CHECK_LIB([nice],\n             [nice_agent_set_port_range],\n             [AC_DEFINE(HAVE_PORTRANGE)],\n             [AC_MSG_NOTICE([libnice version does not have nice_agent_set_port_range])]\n             )\n\nAC_CHECK_LIB([nice],\n             [nice_address_equal_no_port],\n             [AC_DEFINE(HAVE_LIBNICE_TCP)],\n             [AC_MSG_NOTICE([libnice version does not support TCP candidates])]\n             )\n\nAC_CHECK_LIB([nice],\n             [nice_agent_close_async],\n             [AC_DEFINE(HAVE_CLOSE_ASYNC)],\n             [AC_MSG_NOTICE([libnice version does not have nice_agent_close_async])]\n             )\n\nAC_CHECK_LIB([nice],\n             [nice_agent_new_full],\n             [AC_DEFINE(HAVE_ICE_NOMINATION)],\n             [AC_MSG_NOTICE([libnice version does not have nice_agent_new_full])]\n             )\n\nAC_CHECK_LIB([nice],\n             [nice_agent_consent_lost],\n             [AC_DEFINE(HAVE_CONSENT_FRESHNESS)],\n             [AC_MSG_NOTICE([libnice version does not have nice_agent_consent_lost])]\n             )\n\nAC_CHECK_LIB([dl],\n             [dlopen],\n             [JANUS_MANUAL_LIBS=\"${JANUS_MANUAL_LIBS} -ldl\"],\n             [AC_MSG_ERROR([libdl not found.])])\n\nAM_CONDITIONAL([ENABLE_LIBSRTP_2], false)\nAS_IF([test \"x$enable_libsrtp2\" != \"xno\"],\n      [PKG_CHECK_MODULES([LIBSRTP],\n                         [libsrtp2],\n                         [\n                          AC_DEFINE(HAVE_SRTP_2)\n                          enable_libsrtp2=yes\n                          AM_CONDITIONAL([ENABLE_LIBSRTP_2], true)\n                         ],\n                         [\n                          AS_IF([test \"x$enable_libsrtp2\" = \"xyes\"],\n                                [AC_MSG_ERROR([libsrtp2 headers not found. See README.md for installation instructions or use --disable-libsrtp-2 to try and autodetect libsrtp 1.5.x instead])])\n                         ])\n      ])\nAM_COND_IF([ENABLE_LIBSRTP_2],\n           [],\n           [PKG_CHECK_MODULES([LIBSRTP],\n                              [libsrtp >= 1.5.0],\n                              [enable_libsrtp2=no],\n                              [AC_MSG_ERROR([libsrtp and libsrtp2 not found. See README.md for installation instructions])\n                              ])\n           ])\n\nAC_CHECK_LIB([usrsctp],\n             [usrsctp_finish],\n             [\n               AS_IF([test \"x$enable_data_channels\" != \"xno\"],\n               [\n                  AC_DEFINE(HAVE_SCTP)\n                  JANUS_MANUAL_LIBS=\"${JANUS_MANUAL_LIBS} -lusrsctp\"\n                  enable_data_channels=yes\n               ])\n             ],\n             [\n               AS_IF([test \"x$enable_data_channels\" = \"xyes\"],\n                     [AC_MSG_ERROR([libusrsctp not found. See README.md for installation instructions or use --disable-data-channels])])\n             ])\nAM_CONDITIONAL([ENABLE_SCTP], [test \"x$enable_data_channels\" = \"xyes\"])\n\nPKG_CHECK_MODULES([LIBCURL],\n                  [libcurl],\n                  [\n                    AC_DEFINE(HAVE_LIBCURL)\n                    AS_IF(\n                      [test \"x$enable_turn_rest_api\" != \"xno\"],\n                      [\n                        AC_DEFINE(HAVE_TURNRESTAPI)\n                        enable_turn_rest_api=yes\n                      ])\n                    AS_IF([test \"x$enable_sample_event_handler\" != \"xno\"],\n                      [\n                        AC_DEFINE(HAVE_SAMPLEEVH)\n                        enable_sample_event_handler=yes\n                      ])\n                  ],\n                  [\n                    AS_IF([test \"x$enable_turn_rest_api\" = \"xyes\"],\n                          [AC_MSG_ERROR([libcurl not found. See README.md for installation instructions or use --disable-turn-rest-api])])\n                    AS_IF([test \"x$enable_sample_event_handler\" = \"xyes\"],\n                          [AC_MSG_ERROR([libcurl not found. See README.md for installation instructions or use --disable-sample-event-handler])])\n                  ])\nAM_CONDITIONAL([ENABLE_TURN_REST_API], [test \"x$enable_turn_rest_api\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_SAMPLEEVH], [test \"x$enable_sample_event_handler\" = \"xyes\"])\n\nAC_CHECK_PROG([DOXYGEN],\n              [doxygen],\n              [doxygen])\nAC_CHECK_PROG([DOT],\n              [dot],\n              [dot])\nAS_IF([test -z \"$DOXYGEN\" -o -z \"$DOT\"],\n      [\n        AS_IF([test \"x$enable_docs\" = \"xyes\"],\n              [AC_MSG_ERROR([doxygen or dot not found. See README.md for installation instructions or remove --enable-docs])])\n      ])\nAM_CONDITIONAL([ENABLE_DOCS], [test \"x$enable_docs\" = \"xyes\"])\nif test \"x$enable_docs\" = \"xyes\"; then\n\tdoxygen_version=$($DOXYGEN --version)\n\tAS_VERSION_COMPARE([$doxygen_version], [1.8.11],\n                           [],\n                           [],\n                           [\n                            AS_VERSION_COMPARE([$doxygen_version], [1.8.14],\n                                               [AC_MSG_ERROR([Doxygen $doxygen_version not usable: versions between 1.8.12 and 1.8.14 are known to render poorly.])],\n                                               [],\n                                               []\n                                              )\n                           ]\n                          )\nfi\n\n\n##\n# Transports\n##\n\nPKG_CHECK_MODULES([TRANSPORTS],\n                  [\n                    glib-2.0 >= $glib_version\n                    jansson >= $jansson_version\n                  ])\n\nPKG_CHECK_MODULES([MHD],\n                  [libmicrohttpd >= 0.9.59],\n                  [\n                    AS_IF([test \"x$enable_rest\" = \"xmaybe\"],\n                          [enable_rest=yes])\n                  ],\n                  [\n                    AS_IF([test \"x$enable_rest\" = \"xyes\"],\n                          [AC_MSG_ERROR([libmicrohttpd not found. See README.md for installation instructions or use --disable-rest])])\n                  ])\nAC_SUBST([MHD_CFLAGS])\nAC_SUBST([MHD_LIBS])\nAM_CONDITIONAL([ENABLE_REST], [test \"x$enable_rest\" = \"xyes\"])\n\nAC_CHECK_LIB([websockets],\n\t\t\t [lws_create_vhost],\n\t\t\t [\n\t\t\t   AS_IF([test \"x$enable_websockets\" != \"xno\"],\n\t\t\t   [\n\t\t\t\t  AC_DEFINE(HAVE_WEBSOCKETS)\n\t\t\t\t  WS_MANUAL_LIBS=\"-lwebsockets\"\n\t\t\t\t  enable_websockets=yes\n\t\t\t\t  AC_CHECK_LIB([websockets],\n\t\t\t\t\t\t\t   [lws_get_peer_simple],\n\t\t\t\t\t\t\t   [AC_DEFINE(HAVE_LIBWEBSOCKETS_PEER_SIMPLE)]\n\t\t\t\t\t\t\t  )\n\t\t\t   ])\n               AS_IF([test \"x$enable_websockets_event_handler\" != \"xno\"],\n               [\n                 AC_DEFINE(HAVE_WSEVH)\n                 WS_MANUAL_LIBS=\"-lwebsockets\"\n                 enable_websockets_event_handler=yes\n               ])\n\t\t\t ],\n\t\t\t [\n\t\t\t   AS_IF([test \"x$enable_websockets\" = \"xyes\"],\n\t\t\t\t\t [AC_MSG_ERROR([libwebsockets not found. See README.md for installation instructions or use --disable-websockets])])\n\t\t\t ])\nAM_CONDITIONAL([ENABLE_WEBSOCKETS], [test \"x$enable_websockets\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_WSEVH], [test \"x$enable_websockets_event_handler\" = \"xyes\"])\nAC_SUBST(WS_MANUAL_LIBS)\n\nAC_CHECK_LIB([rabbitmq],\n             [amqp_error_string2],\n             [\n               AS_IF([test \"x$enable_rabbitmq\" != \"xno\"],\n               [\n                  AC_DEFINE(HAVE_RABBITMQ)\n                  enable_rabbitmq=yes\n               ])\n               AS_IF([test \"x$enable_rabbitmq_event_handler\" != \"xno\"],\n               [\n                 AC_DEFINE(HAVE_RABBITMQEVH)\n                 enable_rabbitmq_event_handler=yes\n               ])\n               AC_CHECK_HEADERS([rabbitmq-c/amqp.h])\n             ],\n             [\n               AS_IF([test \"x$enable_rabbitmq\" = \"xyes\"],\n                     [AC_MSG_ERROR([rabbitmq-c not found. See README.md for installation instructions or use --disable-rabbitmq])])\n               AS_IF([test \"x$enable_rabbitmq_event_handler\" = \"xyes\"],\n                     [AC_MSG_ERROR([rabbitmq-c not found. See README.md for installation instructions or use --disable-rabbitmq-event-handler])])\n             ])\nAC_CHECK_LIB([paho-mqtt3a],\n             [MQTTAsync_create],\n             [\n               AS_IF([test \"x$enable_mqtt\" != \"xno\"],\n               [\n                  AC_DEFINE(HAVE_MQTT)\n                  enable_mqtt=yes\n               ])\n               AS_IF([test \"x$enable_mqtt_event_handler\" != \"xno\"],\n               [\n                 AC_DEFINE(HAVE_MQTTEVH)\n                 enable_mqtt_event_handler=yes\n               ])\n             ],\n             [\n               AS_IF([test \"x$enable_mqtt\" = \"xyes\"],\n                     [AC_MSG_ERROR([paho c client not found. See README.md for installation instructions or use --disable-mqtt])])\n               AS_IF([test \"x$enable_mqtt_event_handler\" = \"xyes\"],\n                     [AC_MSG_ERROR([paho c not found. See README.md for installation instructions or use --disable-mqtt-event-handler])])\n             ])\nAC_CHECK_LIB([nanomsg],\n             [nn_socket],\n             [\n               AS_IF([test \"x$enable_nanomsg\" != \"xno\"],\n               [\n                  AC_DEFINE(HAVE_NANOMSG)\n                  enable_nanomsg=yes\n               ])\n               AS_IF([test \"x$enable_nanomsg_event_handler\" != \"xno\"],\n               [\n                 AC_DEFINE(HAVE_NANOMSGEVH)\n                 enable_nanomsg_event_handler=yes\n               ])\n             ],\n             [\n               AS_IF([test \"x$enable_nanomsg\" = \"xyes\"],\n                     [AC_MSG_ERROR([nanomsg not found. See README.md for installation instructions or use --disable-nanomsg])])\n               AS_IF([test \"x$enable_nanomsg_event_handler\" = \"xyes\"],\n                     [AC_MSG_ERROR([nanomsg not found. See README.md for installation instructions or use --disable-nanomsg-event-handler])])\n             ])\nAM_CONDITIONAL([ENABLE_RABBITMQ], [test \"x$enable_rabbitmq\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_RABBITMQEVH], [test \"x$enable_rabbitmq_event_handler\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_MQTT], [test \"x$enable_mqtt\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_MQTTEVH], [test \"x$enable_mqtt_event_handler\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_NANOMSG], [test \"x$enable_nanomsg\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_NANOMSGEVH], [test \"x$enable_nanomsg_event_handler\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_GELFEVH], [test \"x$enable_gelf_event_handler\" = \"xyes\"])\n\nAM_CONDITIONAL([ENABLE_JSONLOGGER], [test \"x$enable_json_logger\" = \"xyes\"])\n\nAC_COMPILE_IFELSE([AC_LANG_PROGRAM([[\n               #include <stdlib.h>\n               #include <sys/socket.h>\n               #include <sys/un.h>\n               void test() {\n                 int pfd = socket(PF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK, 0);\n                 if(pfd < 0)\n                   exit(1);\n               }]], [[]])],[\n                 AS_IF([test \"x$enable_unix_sockets\" != \"xno\"],\n                 [\n                    AC_DEFINE(HAVE_PFUNIX)\n                    enable_unix_sockets=yes\n                 ])\n               ],[\n                 AS_IF([test \"x$enable_unix_sockets\" = \"xyes\"],\n                       [AC_MSG_ERROR([SOCK_SEQPACKET not defined in your OS. Use --disable-unix-sockets])])\n               ])\nAM_CONDITIONAL([ENABLE_PFUNIX], [test \"x$enable_unix_sockets\" = \"xyes\"])\n\nAS_IF([test \"x$enable_systemd_sockets\" = \"xyes\"],\n      [PKG_CHECK_MODULES([LIBSYSTEMD],\n                          [libsystemd],\n                          [\n                            AC_DEFINE(HAVE_LIBSYSTEMD)\n                          ],\n                          [AC_MSG_ERROR([libsystemd not found. systemd unix domain socket service not supported])])\n      ])\n\n\n##\n# Plugins\n##\n\nPKG_CHECK_MODULES([PLUGINS],\n                  [\n                    glib-2.0 >= $glib_version\n                    jansson >= $jansson_version\n                  ])\n\nAC_ARG_ENABLE([plugin-audiobridge],\n              [AS_HELP_STRING([--disable-plugin-audiobridge],\n                              [Disable audiobridge plugin])],\n              [AS_IF([test \"x$enable_plugin_audiobridge\" != \"xyes\"],\n                     [enable_plugin_audiobridge=no])],\n              [enable_plugin_audiobridge=maybe])\n\nAC_ARG_ENABLE([plugin-duktape],\n              [AS_HELP_STRING([--enable-plugin-duktape],\n                              [Enable duktape plugin])],\n              [AS_IF([test \"x$enable_plugin_duktape\" != \"xyes\"],\n                     [enable_plugin_duktape=no])],\n              [enable_plugin_duktape=no])\n\nAC_ARG_ENABLE([plugin-echotest],\n              [AS_HELP_STRING([--disable-plugin-echotest],\n                              [Disable echotest plugin])],\n              [AS_IF([test \"x$enable_plugin_echotest\" != \"xyes\"],\n                     [enable_plugin_echotest=no])],\n              [enable_plugin_echotest=yes])\n\nAC_ARG_ENABLE([plugin-lua],\n              [AS_HELP_STRING([--enable-plugin-lua],\n                              [Enable lua plugin])],\n              [AS_IF([test \"x$enable_plugin_lua\" != \"xyes\"],\n                     [enable_plugin_lua=no])],\n              [enable_plugin_lua=no])\n\nAC_ARG_ENABLE([plugin-recordplay],\n              [AS_HELP_STRING([--disable-plugin-recordplay],\n                              [Disable record&play plugin])],\n              [AS_IF([test \"x$enable_plugin_recordplay\" != \"xyes\"],\n                     [enable_plugin_recordplay=no])],\n              [enable_plugin_recordplay=yes])\n\nAC_ARG_ENABLE([plugin-sip],\n              [AS_HELP_STRING([--disable-plugin-sip],\n                              [Disable sip plugin])],\n              [AS_IF([test \"x$enable_plugin_sip\" != \"xyes\"],\n                     [enable_plugin_sip=no])],\n              [enable_plugin_sip=maybe])\n\nAC_ARG_ENABLE([plugin-nosip],\n              [AS_HELP_STRING([--disable-plugin-nosip],\n                              [Disable nosip plugin])],\n              [AS_IF([test \"x$enable_plugin_nosip\" != \"xyes\"],\n                     [enable_plugin_nosip=no])],\n              [enable_plugin_nosip=yes])\n\nAC_ARG_ENABLE([plugin-streaming],\n              [AS_HELP_STRING([--disable-plugin-streaming],\n                              [Disable streaming plugin])],\n              [AS_IF([test \"x$enable_plugin_streaming\" != \"xyes\"],\n                     [enable_plugin_streaming=no])],\n              [enable_plugin_streaming=yes])\n\nAC_ARG_ENABLE([plugin-textroom],\n              [AS_HELP_STRING([--disable-plugin-textroom],\n                              [Disable textroom plugin])],\n              [AS_IF([test \"x$enable_plugin_textroom\" != \"xyes\"],\n                     [enable_plugin_textroom=no])],\n              [enable_plugin_textroom=yes])\n\nAC_ARG_ENABLE([plugin-videocall],\n              [AS_HELP_STRING([--disable-plugin-videocall],\n                              [Disable videocall plugin])],\n              [AS_IF([test \"x$enable_plugin_videocall\" != \"xyes\"],\n                     [enable_plugin_videocall=no])],\n              [enable_plugin_videocall=yes])\n\nAC_ARG_ENABLE([plugin-videoroom],\n              [AS_HELP_STRING([--disable-plugin-videoroom],\n                              [Disable videoroom plugin])],\n              [AS_IF([test \"x$enable_plugin_videoroom\" != \"xyes\"],\n                     [enable_plugin_videoroom=no])],\n              [enable_plugin_videoroom=yes])\n\nPKG_CHECK_MODULES([SOFIA],\n                  [sofia-sip-ua],\n                  [\n                    AS_IF([test \"x$enable_plugin_sip\" = \"xmaybe\"],\n                          [enable_plugin_sip=yes])\n                  ],\n                  [\n                    AS_IF([test \"x$enable_plugin_sip\" = \"xyes\"],\n                          [AC_MSG_ERROR([sofia-sip-ua not found. See README.md for installation instructions or use --disable-plugin-sip])])\n                  ])\nAC_SUBST([SOFIA_CFLAGS])\nAC_SUBST([SOFIA_LIBS])\n\nPKG_CHECK_MODULES([OPUS],\n                  [\n                   opus\n                  ],\n                  [\n                    AS_IF([test \"x$enable_plugin_audiobridge\" = \"xmaybe\"],\n                          [\n                           enable_plugin_audiobridge=yes\n                          ])\n                    AS_IF([test \"x$enable_plugin_audiobridge\" = \"xyes\"],\n                          [\n                           AC_DEFINE(FLOATING_POINT)\n                           AC_DEFINE([EXPORT], [], [Symbol visibility prefix])\n                          ])\n                  ],\n                  [\n                    AS_IF([test \"x$enable_plugin_audiobridge\" = \"xyes\"],\n                          [AC_MSG_ERROR([libopus not found. See README.md for installation instructions or use --disable-plugin-audiobridge])])\n                  ])\nAC_SUBST([OPUS_CFLAGS])\nAC_SUBST([OPUS_LIBS])\n\nPKG_CHECK_MODULES([OGG],\n                  [ogg],\n                  [\n                    AC_DEFINE(HAVE_LIBOGG)\n                  ],\n                  [\n                  ])\nAC_SUBST([OGG_CFLAGS])\nAC_SUBST([OGG_LIBS])\n\nPKG_CHECK_MODULES([RNNOISE],\n                  [rnnoise],\n                  [\n                    AC_DEFINE(HAVE_RNNOISE)\n                  ],\n                  [\n                  ])\nAC_SUBST([RNNOISE_CFLAGS])\nAC_SUBST([RNNOISE_LIBS])\n\nPKG_CHECK_MODULES([LUA],\n                  [lua],\n                  [\n                    AS_IF([test \"x$enable_plugin_lua\" = \"xmaybe\"],\n                          [enable_plugin_lua=yes])\n                  ],\n                  [PKG_CHECK_MODULES([LUA],\n                                     [lua5.3],\n                                     [\n                                       AS_IF([test \"x$enable_plugin_lua\" = \"xmaybe\"],\n                                             [enable_plugin_lua=yes])\n                                     ],\n                                     [\n                                       AS_IF([test \"x$enable_plugin_lua\" = \"xyes\"],\n                                             [AC_MSG_ERROR([lua-libs not found. See README.md for installation instructions or use --disable-plugin-lua])])\n                                     ])\n                  ])\nAC_SUBST([LUA_CFLAGS])\nAC_SUBST([LUA_LIBS])\n\nPKG_CHECK_MODULES([DUKTAPE],\n                  [duktape],\n                  [\n                    AS_IF([test \"x$enable_plugin_duktape\" = \"xmaybe\"],\n                          [enable_plugin_duktape=yes])\n                  ],\n                  [\n                    AS_IF([test \"x$enable_plugin_duktape\" = \"xyes\"],\n                          [AC_MSG_ERROR([duktape not found. See README.md for installation instructions or use --disable-plugin-duktape])])\n                  ])\nAC_SUBST([DUKTAPE_CFLAGS])\nAC_SUBST([DUKTAPE_LIBS])\n\nAM_CONDITIONAL([ENABLE_PLUGIN_AUDIOBRIDGE], [test \"x$enable_plugin_audiobridge\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_DUKTAPE], [test \"x$enable_plugin_duktape\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_ECHOTEST], [test \"x$enable_plugin_echotest\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_LUA], [test \"x$enable_plugin_lua\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_RECORDPLAY], [test \"x$enable_plugin_recordplay\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_SIP], [test \"x$enable_plugin_sip\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_NOSIP], [test \"x$enable_plugin_nosip\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_STREAMING], [test \"x$enable_plugin_streaming\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_VIDEOCALL], [test \"x$enable_plugin_videocall\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_VIDEOROOM], [test \"x$enable_plugin_videoroom\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PLUGIN_TEXTROOM], [test \"x$enable_plugin_textroom\" = \"xyes\"])\n\n\n##\n# Event handlers\n##\n\nPKG_CHECK_MODULES([EVENTS],\n                  [\n                    glib-2.0 >= $glib_version\n                    jansson >= $jansson_version\n                  ])\n\n\n##\n# Loggers\n##\n\nPKG_CHECK_MODULES([LOGGERS],\n                  [\n                    glib-2.0 >= $glib_version\n                    jansson >= $jansson_version\n                  ])\n\n\n##\n# JavaScript modules\n##\n\nAC_ARG_ENABLE([javascript-es-module],\n              [AS_HELP_STRING([--enable-javascript-es-module],\n                              [Generate an ECMAScript style module from janus.js])],\n              [AS_IF([test \"x$enable_javascript_es_module\" = \"xyes\"],\n                     [enable_javascript_es_module=yes])],\n              [enable_javascript_es_module=no])\nAM_CONDITIONAL([ENABLE_JAVASCRIPT_ES_MODULE], [test \"x$enable_javascript_es_module\" = \"xyes\" ])\n\nAC_ARG_ENABLE([javascript-umd-module],\n              [AS_HELP_STRING([--enable-javascript-umd-module],\n                              [Generate an UMD style module from janus.js])],\n              [AS_IF([test \"x$enable_javascript_umd_module\" = \"xyes\"],\n                     [enable_javascript_umd_module=yes])],\n              [enable_javascript_umd_module=no])\nAM_CONDITIONAL([ENABLE_JAVASCRIPT_UMD_MODULE], [test \"x$enable_javascript_umd_module\" = \"xyes\" ])\n\nAC_ARG_ENABLE([javascript-iife-module],\n              [AS_HELP_STRING([--enable-javascript-iife-module],\n                              [Generate an IIFE style wrapper around janus.js])],\n              [AS_IF([test \"x$enable_javascript_iife_module\" = \"xyes\"],\n                     [enable_javascript_iife_module=yes])],\n              [enable_javascript_iife_module=no])\nAM_CONDITIONAL([ENABLE_JAVASCRIPT_IIFE_MODULE], [test \"x$enable_javascript_iife_module\" = \"xyes\" ])\n\nAC_ARG_ENABLE([javascript-common-js-module],\n              [AS_HELP_STRING([--enable-javascript-common-js-module],\n                              [Generate an CommonJS style module from janus.js])],\n              [AS_IF([test \"x$enable_javascript_common_js_module\" = \"xyes\"],\n                     [enable_javascript_common_js_module=yes])],\n              [enable_javascript_common_js_module=no])\nAM_CONDITIONAL([ENABLE_JAVASCRIPT_COMMON_JS_MODULE], [test \"x$enable_javascript_common_js_module\" = \"xyes\" ])\n\ncase \"-${enable_javascript_common_js_module}-${enable_javascript_iife_module}-${enable_javascript_umd_module}-${enable_javascript_es_module}-\" in\n    *-yes*)\n        AM_CONDITIONAL([ENABLE_JAVASCRIPT_MODULES], true)\n    ;;\n    *)\n        AM_CONDITIONAL([ENABLE_JAVASCRIPT_MODULES], false)\n    ;;\nesac\n\nAC_ARG_VAR(NPM,\"npm executable to use\")\nAC_PATH_PROG(NPM,npm,,)\nAM_CONDITIONAL([NPM_FOUND], [test \"x$NPM\" != \"x\" ])\nAM_COND_IF([ENABLE_JAVASCRIPT_MODULES],[\n    AM_COND_IF([NPM_FOUND],,[AC_MSG_ERROR([npm not found])])\n])\n\n\n##\n# Post-processing\n##\n\nAC_ARG_ENABLE([post-processing],\n              [AS_HELP_STRING([--enable-post-processing],\n                              [Enable building post-processing utility])],\n              [],\n              [enable_post_processing=no])\n\nAS_IF([test \"x$enable_post_processing\" = \"xyes\"],\n      [PKG_CHECK_MODULES([POST_PROCESSING],\n                         [\n                           glib-2.0 >= $glib_version\n                           jansson >= $jansson_version\n                           libssl >= $ssl_version\n                           libcrypto\n                           libavutil\n                           libavcodec\n                           libavformat\n                           ogg\n                           zlib\n                         ])\n      ])\n\nPKG_CHECK_MODULES([PCAP],\n                  [libpcap],\n                  [\n                   AC_DEFINE(HAVE_LIBPCAP)\n                   enable_pcap2mjr=yes\n                  ],\n                  [\n                   enable_pcap2mjr=no\n                  ])\nAC_SUBST([PCAP_CFLAGS])\nAC_SUBST([PCAP_LIBS])\n\nAM_CONDITIONAL([WITH_SOURCE_DATE_EPOCH], [test \"x$SOURCE_DATE_EPOCH\" != \"x\"])\nAM_CONDITIONAL([ENABLE_POST_PROCESSING], [test \"x$enable_post_processing\" = \"xyes\"])\nAM_CONDITIONAL([ENABLE_PCAP2MJR], [test \"x$enable_pcap2mjr\" = \"xyes\"])\n\nAC_CONFIG_FILES([\n  Makefile\n  src/Makefile\n  html/Makefile\n  docs/Makefile\n  janus-gateway.pc\n])\n\nJANUS_MANUAL_LIBS+=\" -pthread\"\n\nAC_OUTPUT\n\n##\n# Summary\n##\necho\necho \"Compiler:                  $CC\"\nAM_COND_IF([ENABLE_LIBSRTP_2],\n\t[echo \"libsrtp version:           2.x\"],\n\t[echo \"libsrtp version:           1.5.x\"])\nAM_COND_IF([ENABLE_BORINGSSL],\n\t[echo \"SSL/crypto library:        BoringSSL\"\n\t AM_COND_IF([ENABLE_DTLS_SETTIMEOUT],\n\t\t[echo \"DTLS set-timeout:          yes\"],\n\t\t[echo \"DTLS set-timeout:          no\"])\n\t],\n\t[AM_COND_IF([LIBRESSL_DETECTED],\n\t\t[echo \"SSL/crypto library:        LibreSSL\"],\n\t\t[echo \"SSL/crypto library:        OpenSSL\"])\n\t echo \"DTLS set-timeout:          not available\"])\nAM_COND_IF([ENABLE_PTHREAD_MUTEX],\n\t[echo \"Mutex implementation:      pthread mutex\"],\n\t[echo \"Mutex implementation:      GMutex (native futex on Linux)\"])\nAM_COND_IF([ENABLE_SCTP],\n\t[echo \"DataChannels support:      yes\"],\n\t[echo \"DataChannels support:      no\"])\nAM_COND_IF([ENABLE_POST_PROCESSING],\n\t[echo \"Recordings post-processor: yes\"],\n\t[echo \"Recordings post-processor: no\"])\nAM_COND_IF([ENABLE_TURN_REST_API],\n\t[echo \"TURN REST API client:      yes\"],\n\t[echo \"TURN REST API client:      no\"])\nAM_COND_IF([ENABLE_DOCS],\n\t[echo \"Doxygen documentation:     yes\"],\n\t[echo \"Doxygen documentation:     no\"])\necho \"Transports:\"\nAM_COND_IF([ENABLE_REST],\n\t[echo \"    REST (HTTP/HTTPS):     yes\"],\n\t[echo \"    REST (HTTP/HTTPS):     no\"])\nAM_COND_IF([ENABLE_WEBSOCKETS],\n\t[echo \"    WebSockets:            yes\"],\n\t[echo \"    WebSockets:            no\"])\nAM_COND_IF([ENABLE_RABBITMQ],\n\t[echo \"    RabbitMQ:              yes\"],\n\t[echo \"    RabbitMQ:              no\"])\nAM_COND_IF([ENABLE_MQTT],\n\t[echo \"    MQTT:                  yes\"],\n\t[echo \"    MQTT:                  no\"])\nAM_COND_IF([ENABLE_PFUNIX],\n\t[echo \"    Unix Sockets:          yes\"],\n\t[echo \"    Unix Sockets:          no\"])\nAM_COND_IF([ENABLE_NANOMSG],\n\t[echo \"    Nanomsg:               yes\"],\n\t[echo \"    Nanomsg:               no\"])\necho \"Plugins:\"\nAM_COND_IF([ENABLE_PLUGIN_ECHOTEST],\n\t[echo \"    Echo Test:             yes\"],\n\t[echo \"    Echo Test:             no\"])\nAM_COND_IF([ENABLE_PLUGIN_STREAMING],\n\t[echo \"    Streaming:             yes\"],\n\t[echo \"    Streaming:             no\"])\nAM_COND_IF([ENABLE_PLUGIN_VIDEOCALL],\n\t[echo \"    Video Call:            yes\"],\n\t[echo \"    Video Call:            no\"])\nAM_COND_IF([ENABLE_PLUGIN_SIP],\n\t[echo \"    SIP Gateway:           yes\"],\n\t[echo \"    SIP Gateway:           no\"])\nAM_COND_IF([ENABLE_PLUGIN_NOSIP],\n\t[echo \"    NoSIP (RTP Bridge):    yes\"],\n\t[echo \"    NoSIP (RTP Bridge):    no\"])\nAM_COND_IF([ENABLE_PLUGIN_AUDIOBRIDGE],\n\t[echo \"    Audio Bridge:          yes\"],\n\t[echo \"    Audio Bridge:          no\"])\nAM_COND_IF([ENABLE_PLUGIN_VIDEOROOM],\n\t[echo \"    Video Room:            yes\"],\n\t[echo \"    Video Room:            no\"])\nAM_COND_IF([ENABLE_PLUGIN_RECORDPLAY],\n\t[echo \"    Record&Play:           yes\"],\n\t[echo \"    Record&Play:           no\"])\nAM_COND_IF([ENABLE_PLUGIN_TEXTROOM],\n\t[echo \"    Text Room:             yes\"],\n\t[echo \"    Text Room:             no\"])\nAM_COND_IF([ENABLE_PLUGIN_LUA],\n\t[echo \"    Lua Interpreter:       yes\"],\n\t[echo \"    Lua Interpreter:       no\"])\nAM_COND_IF([ENABLE_PLUGIN_DUKTAPE],\n\t[echo \"    Duktape Interpreter:   yes\"],\n\t[echo \"    Duktape Interpreter:   no\"])\necho \"Event handlers:\"\nAM_COND_IF([ENABLE_SAMPLEEVH],\n\t[echo \"    Sample event handler:  yes\"],\n\t[echo \"    Sample event handler:  no\"])\nAM_COND_IF([ENABLE_WSEVH],\n\t[echo \"    WebSocket ev. handler: yes\"],\n\t[echo \"    WebSocket ev. handler: no\"])\nAM_COND_IF([ENABLE_RABBITMQEVH],\n\t[echo \"    RabbitMQ event handler:yes\"],\n\t[echo \"    RabbitMQ event handler:no\"])\nAM_COND_IF([ENABLE_MQTTEVH],\n\t[echo \"    MQTT event handler:    yes\"],\n\t[echo \"    MQTT event handler:    no\"])\nAM_COND_IF([ENABLE_NANOMSGEVH],\n\t[echo \"    Nanomsg event handler: yes\"],\n\t[echo \"    Nanomsg event handler: no\"])\nAM_COND_IF([ENABLE_GELFEVH],\n\t[echo \"    GELF event handler:    yes\"],\n\t[echo \"    GELF event handler:    no\"])\necho \"External loggers:\"\nAM_COND_IF([ENABLE_JSONLOGGER],\n\t[echo \"    JSON file logger:      yes\"],\n\t[echo \"    JSON file logger:      no\"])\nAM_COND_IF([ENABLE_JAVASCRIPT_MODULES], [\n\techo \"JavaScript modules:        yes\"\n\techo \"    Using npm:             $NPM\"\n\tAM_COND_IF([ENABLE_JAVASCRIPT_ES_MODULE],\n\t\t[echo \"    ES syntax:             yes\"],\n\t\t[echo \"    ES syntax:             no\"])\n\tAM_COND_IF([ENABLE_JAVASCRIPT_IIFE_MODULE],\n\t\t[echo \"    IIFE syntax:           yes\"],\n\t\t[echo \"    IIFE syntax:           no\"])\n\tAM_COND_IF([ENABLE_JAVASCRIPT_UMD_MODULE],\n\t\t[echo \"    UMD syntax:            yes\"],\n\t\t[echo \"    UMD syntax:            no\"])\n\tAM_COND_IF([ENABLE_JAVASCRIPT_COMMON_JS_MODULE],\n\t\t[echo \"    CommonJS syntax:       yes\"],\n\t\t[echo \"    CommonJS syntax:       no\"])\n\t],\n\t[echo \"JavaScript modules:        no\"])\n\necho\necho \"If this configuration is ok for you, do a 'make' to start building Janus. A 'make install' will install Janus and its plugins to the specified prefix. Finally, a 'make configs' will install some sample configuration files too (something you'll only want to do the first time, though).\"\necho\n"
  },
  {
    "path": "docs/Makefile.am",
    "content": "if ENABLE_DOCS\n\ndoxygendir = $(htmldir)/janus-gateway-$(VERSION)\n\nEXTRA_DIST = html\n\nall: html-local\n\nhtml-local:\n\tdoxygen janus-doxygen.cfg\n\tcp doxy-boot.js html/\n\tmkdir -p html/css\n\tcp ../html/css/demo.css html/css/\n\tmkdir -p html/js\n\tcp ../html/favicon.ico html/\n\tcp ../html/meetecho-logo.png html/\n\tcp ../html/forkme_left_darkblue_121621.png html/\n\ninstall-data-local: html-local\n\t$(MKDIR_P) $(DESTDIR)$(doxygendir)\n\tcp -r html/ $(DESTDIR)$(doxygendir)\n\nuninstall-local:\n\trm -rf $(DESTDIR)$(doxygendir)\n\nclean-local:\n\trm -rf $(builddir)/html\n\nendif\n"
  },
  {
    "path": "docs/doxy-boot.js",
    "content": "$(document).ready(function() {\n\n\t$(\"div.headertitle\").addClass(\"pb-2 mt-4 mb-2 border-bottom\");\n\t$(\"div.title\").addClass(\"h1\");\n\n\t$('li > a[href=\"index.html\"] > span').before(\"<i class='fa-solid fa-gear'></i> \");\n\t// $('li > a[href=\"index.html\"] > span').text(\"CoActionOS\");\n\t$('li > a[href=\"modules.html\"] > span').before(\"<i class='fa-solid fa-square'></i> \");\n\t$('li > a[href=\"namespaces.html\"] > span').before(\"<i class='fa-solid fa-bars'></i> \");\n\t$('li > a[href=\"annotated.html\"] > span').before(\"<i class='fa-solid fa-list-ul'></i> \");\n\t$('li > a[href=\"classes.html\"] > span').before(\"<i class='fa-solid fa-book'></i> \");\n\t$('li > a[href=\"inherits.html\"] > span').before(\"<i class='fa-solid fa-sitemap'></i> \");\n\t$('li > a[href=\"functions.html\"] > span').before(\"<i class='fa-solid fa-list'></i> \");\n\t$('li > a[href=\"functions_func.html\"] > span').before(\"<i class='fa-solid fa-list'></i> \");\n\t$('li > a[href=\"functions_vars.html\"] > span').before(\"<i class='fa-solid fa-list'></i> \");\n\t$('li > a[href=\"functions_enum.html\"] > span').before(\"<i class='fa-solid fa-list'></i> \");\n\t$('li > a[href=\"functions_eval.html\"] > span').before(\"<i class='fa-solid fa-list'></i> \");\n\t$('img[src=\"ftv2ns.png\"]').replaceWith('<span class=\"badge bg-danger\">N</span> ');\n\t$('img[src=\"ftv2cl.png\"]').replaceWith('<span class=\"badge bg-danger\">C</span> ');\n\n\t$(\"ul.tablist\").addClass(\"nav nav-pills nav-fill\");\n\t$(\"ul.tablist\").css(\"margin-top\", \"0.5em\");\n\t$(\"ul.tablist\").css(\"margin-bottom\", \"0.5em\");\n\t$(\"ul.tablist > li\").addClass(\"nav-item\");\n\t$(\"ul.tablist > li > a\").addClass(\"nav-link\");\n\t$(\"li.current\").children().addClass(\"active\");\n\t$(\"iframe\").attr(\"scrolling\", \"yes\");\n\n\t$(\"#nav-path > ul\").addClass(\"breadcrumb\");\n\n\t$(\"table.params\").addClass(\"table\");\n\t$(\"div.ingroups\").wrapInner(\"<small></small>\");\n\t$(\"div.ingroups > small > a\").addClass(\"text-muted\");\n\t$(\"div.levels\").css(\"margin\", \"0.5em\");\n\t$(\"div.levels > span\").addClass(\"btn btn-secondary btn-sm\");\n\t$(\"div.levels > span\").css(\"margin-right\", \"0.25em\");\n\n\t$(\"table.directory\").addClass(\"table table-striped\");\n\t$(\"div.summary > a\").addClass(\"btn btn-secondary btn-sm\");\n\t$(\"table.fieldtable\").addClass(\"table\");\n\t$(\".fragment\").addClass(\"card card-body bg-gray\");\n\t$(\".memitem\").addClass(\"card\");\n\t$(\".memproto\").addClass(\"card-header\");\n\t$(\".memdoc\").addClass(\"card-body\");\n\t$(\"span.mlabel\").addClass(\"badge bg-info\");\n\n\t$(\"table.memberdecls\").addClass(\"table\");\n\t$(\"[class^=memitem]\").addClass(\"active\");\n\n\t$(\"div.ah\").addClass(\"btn btn-secondary\");\n\t$(\"span.mlabels\").addClass(\"pull-right\");\n\t$(\"table.mlabels\").css(\"width\", \"100%\")\n\t$(\"td.mlabels-right\").addClass(\"pull-right\");\n\n\t$(\"div.ttc\").addClass(\"card card-info\");\n\t$(\"div.ttname\").addClass(\"card-header\");\n\t$(\"div.ttdef,div.ttdoc,div.ttdeci\").addClass(\"card-body\");\n\n\t$('div.tabs').addClass('container card card-body bg-gray mb-3');\n\t$('div.tabs2').addClass('container card card-body bg-gray mb-3');\n\t$('div.tabs3').addClass('container card card-body bg-gray mb-3');\n\t$('div.header').addClass('container');\n\t$('div.contents').addClass('container');\n\t$('div.groupHeader').addClass('alert-link').parent().parent().addClass('alert alert-info');\n\n\t$('#MSearchBox').remove();//.parent().appendTo('#topmenu');\n\n\t$('code').each(function() { $(this).html($(this).html().replace(\"–\", \"--\")); } );\n});\n"
  },
  {
    "path": "docs/footer.html",
    "content": "<!-- HTML footer for doxygen 1.8.18-->\n<!-- start footer part -->\n<!--BEGIN GENERATE_TREEVIEW-->\n<div id=\"nav-path\" class=\"navpath\"><!-- id is needed for treeview function! -->\n  <ul>\n    $navpath\n    <li class=\"footer\">\nJanus WebRTC Server &copy; <a target=\"_blank\" href=\"http://www.meetecho.com/\">Meetecho</a> 2014-2026\n    </li>\n  </ul>\n</div>\n<!--END GENERATE_TREEVIEW-->\n<!--BEGIN !GENERATE_TREEVIEW-->\n<div class=\"footer container\">\n<hr class=\"footer\"/>\nLast updated on $date &mdash; Janus WebRTC Server &copy; <a target=\"_blank\" href=\"http://www.meetecho.com/\">Meetecho</a> 2014-2026\n</div>\n<!--END !GENERATE_TREEVIEW-->\n</body>\n</html>\n"
  },
  {
    "path": "docs/header.html",
    "content": "<!-- HTML header for doxygen 1.8.18-->\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/xhtml;charset=UTF-8\"/>\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=9\"/>\n<meta name=\"generator\" content=\"Doxygen $doxygenversion\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>$title</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"$relpath^dynsections.js\"></script>\n$treeview\n$search\n$mathjax\n$extrastylesheet\n<link href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"css/demo.css\" rel=\"stylesheet\">\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"doxy-boot.js\"></script>\n</head>\n<body>\n<div id=\"top\"><!-- do not remove this div, it is closed by doxygen! -->\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n<div class=\"container\">\n\t<a class=\"navbar-brand\" href=\"https://janus.conf.meetecho.com/\">$projectname</a>\n\t<button type=\"button\" class=\"navbar-toggler\" data-toggle=\"collapse\" data-target=\".navbar-collapse\" data-target=\"#navbarResponsive\" aria-controls=\"navbarResponsive\" aria-expanded=\"false\">\n\t\t<span class=\"navbar-toggler-icon\"></span>\n\t</button>\n\t<div class=\"navbar-collapse collapse\" id=\"navbarResponsive\">\n\t\t<ul class=\"navbar-nav\">\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus.conf.meetecho.com/\">Home</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus.conf.meetecho.com/demos/\">Demos</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link active\" href=\"index.html\">Documentation</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus.conf.meetecho.com/citeus.html\">Papers</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus.conf.meetecho.com/support.html\">Need help?</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus-legacy.conf.meetecho.com/\">Janus (0.x)</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link januscon\" target=\"_blank\" href=\"https://januscon.it\">JanusCon!</a></li>\n\t\t</ul>\n\t\t<ul class=\"navbar-nav ms-auto\">\n\t\t\t<li class=\"nav-item\">\n\t\t\t\t<a class=\"nav-link meetecho-logo\" target=\"_blank\" href=\"https://www.meetecho.com\">\n\t\t\t\t\t<img src=\"meetecho-logo.png\"/>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</div>\n</div>\n"
  },
  {
    "path": "docs/janus-doxygen.cfg",
    "content": "# Doxyfile 1.8.18\n\n# This file describes the settings to be used by the documentation system\n# doxygen (www.doxygen.org) for a project.\n#\n# All text after a double hash (##) is considered a comment and is placed in\n# front of the TAG it is preceding.\n#\n# All text after a single hash (#) is considered a comment and will be ignored.\n# The format is:\n# TAG = value [value, ...]\n# For lists, items can also be appended using:\n# TAG += value [value, ...]\n# Values that contain spaces should be placed between quotes (\\\" \\\").\n\n#---------------------------------------------------------------------------\n# Project related configuration options\n#---------------------------------------------------------------------------\n\n# This tag specifies the encoding used for all characters in the configuration\n# file that follow. The default is UTF-8 which is also the encoding used for all\n# text before the first occurrence of this tag. Doxygen uses libiconv (or the\n# iconv built into libc) for the transcoding. See\n# https://www.gnu.org/software/libiconv/ for the list of possible encodings.\n# The default value is: UTF-8.\n\nDOXYFILE_ENCODING      = UTF-8\n\n# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by\n# double-quotes, unless you are using Doxywizard) that should identify the\n# project for which the documentation is generated. This name is used in the\n# title of most generated pages and in a few other places.\n# The default value is: My Project.\n\nPROJECT_NAME           = \"Janus (multistream)\"\n\n# The PROJECT_NUMBER tag can be used to enter a project or revision number. This\n# could be handy for archiving the generated documentation or if some version\n# control system is used.\n\nPROJECT_NUMBER         = 1.4.1\n\n# Using the PROJECT_BRIEF tag one can provide an optional one line description\n# for a project that appears at the top of each page and should give viewer a\n# quick idea about the purpose of the project. Keep the description short.\n\nPROJECT_BRIEF          = \"Janus WebRTC Server (multistream)\"\n\n# With the PROJECT_LOGO tag one can specify a logo or an icon that is included\n# in the documentation. The maximum height of the logo should not exceed 55\n# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy\n# the logo to the output directory.\n\nPROJECT_LOGO           =\n\n# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path\n# into which the generated documentation will be written. If a relative path is\n# entered, it will be relative to the location where doxygen was started. If\n# left blank the current directory will be used.\n\nOUTPUT_DIRECTORY       =\n\n# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-\n# directories (in 2 levels) under the output directory of each output format and\n# will distribute the generated files over these directories. Enabling this\n# option can be useful when feeding doxygen a huge amount of source files, where\n# putting all generated files in the same directory would otherwise causes\n# performance problems for the file system.\n# The default value is: NO.\n\nCREATE_SUBDIRS         = NO\n\n# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII\n# characters to appear in the names of generated files. If set to NO, non-ASCII\n# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode\n# U+3044.\n# The default value is: NO.\n\nALLOW_UNICODE_NAMES    = NO\n\n# The OUTPUT_LANGUAGE tag is used to specify the language in which all\n# documentation generated by doxygen is written. Doxygen will use this\n# information to generate all constant output in the proper language.\n# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,\n# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),\n# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,\n# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),\n# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,\n# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,\n# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,\n# Ukrainian and Vietnamese.\n# The default value is: English.\n\nOUTPUT_LANGUAGE        = English\n\n# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all\n# documentation generated by doxygen is written. Doxygen will use this\n# information to generate all generated output in the proper direction.\n# Possible values are: None, LTR, RTL and Context.\n# The default value is: None.\n\nOUTPUT_TEXT_DIRECTION  = None\n\n# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member\n# descriptions after the members that are listed in the file and class\n# documentation (similar to Javadoc). Set to NO to disable this.\n# The default value is: YES.\n\nBRIEF_MEMBER_DESC      = YES\n\n# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief\n# description of a member or function before the detailed description\n#\n# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the\n# brief descriptions will be completely suppressed.\n# The default value is: YES.\n\nREPEAT_BRIEF           = YES\n\n# This tag implements a quasi-intelligent brief description abbreviator that is\n# used to form the text in various listings. Each string in this list, if found\n# as the leading text of the brief description, will be stripped from the text\n# and the result, after processing the whole list, is used as the annotated\n# text. Otherwise, the brief description is used as-is. If left blank, the\n# following values are used ($name is automatically replaced with the name of\n# the entity):The $name class, The $name widget, The $name file, is, provides,\n# specifies, contains, represents, a, an and the.\n\nABBREVIATE_BRIEF       =\n\n# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then\n# doxygen will generate a detailed section even if there is only a brief\n# description.\n# The default value is: NO.\n\nALWAYS_DETAILED_SEC    = NO\n\n# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all\n# inherited members of a class in the documentation of that class as if those\n# members were ordinary class members. Constructors, destructors and assignment\n# operators of the base classes will not be shown.\n# The default value is: NO.\n\nINLINE_INHERITED_MEMB  = NO\n\n# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path\n# before files name in the file list and in the header files. If set to NO the\n# shortest path that makes the file name unique will be used\n# The default value is: YES.\n\nFULL_PATH_NAMES        = NO\n\n# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.\n# Stripping is only done if one of the specified strings matches the left-hand\n# part of the path. The tag can be used to show relative paths in the file list.\n# If left blank the directory from which doxygen is run is used as the path to\n# strip.\n#\n# Note that you can specify absolute paths here, but also relative paths, which\n# will be relative from the directory where doxygen is started.\n# This tag requires that the tag FULL_PATH_NAMES is set to YES.\n\nSTRIP_FROM_PATH        =\n\n# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the\n# path mentioned in the documentation of a class, which tells the reader which\n# header file to include in order to use a class. If left blank only the name of\n# the header file containing the class definition is used. Otherwise one should\n# specify the list of include paths that are normally passed to the compiler\n# using the -I flag.\n\nSTRIP_FROM_INC_PATH    =\n\n# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but\n# less readable) file names. This can be useful is your file systems doesn't\n# support long names like on DOS, Mac, or CD-ROM.\n# The default value is: NO.\n\nSHORT_NAMES            = NO\n\n# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the\n# first line (until the first dot) of a Javadoc-style comment as the brief\n# description. If set to NO, the Javadoc-style will behave just like regular Qt-\n# style comments (thus requiring an explicit @brief command for a brief\n# description.)\n# The default value is: NO.\n\nJAVADOC_AUTOBRIEF      = NO\n\n# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line\n# such as\n# /***************\n# as being the beginning of a Javadoc-style comment \"banner\". If set to NO, the\n# Javadoc-style will behave just like regular comments and it will not be\n# interpreted by doxygen.\n# The default value is: NO.\n\nJAVADOC_BANNER         = NO\n\n# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first\n# line (until the first dot) of a Qt-style comment as the brief description. If\n# set to NO, the Qt-style will behave just like regular Qt-style comments (thus\n# requiring an explicit \\brief command for a brief description.)\n# The default value is: NO.\n\nQT_AUTOBRIEF           = NO\n\n# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a\n# multi-line C++ special comment block (i.e. a block of //! or /// comments) as\n# a brief description. This used to be the default behavior. The new default is\n# to treat a multi-line C++ comment block as a detailed description. Set this\n# tag to YES if you prefer the old behavior instead.\n#\n# Note that setting this tag to YES also means that rational rose comments are\n# not recognized any more.\n# The default value is: NO.\n\nMULTILINE_CPP_IS_BRIEF = NO\n\n# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the\n# documentation from any documented member that it re-implements.\n# The default value is: YES.\n\nINHERIT_DOCS           = YES\n\n# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new\n# page for each member. If set to NO, the documentation of a member will be part\n# of the file/class/namespace that contains it.\n# The default value is: NO.\n\nSEPARATE_MEMBER_PAGES  = NO\n\n# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen\n# uses this value to replace tabs by spaces in code fragments.\n# Minimum value: 1, maximum value: 16, default value: 4.\n\nTAB_SIZE               = 8\n\n# This tag can be used to specify a number of aliases that act as commands in\n# the documentation. An alias has the form:\n# name=value\n# For example adding\n# \"sideeffect=@par Side Effects:\\n\"\n# will allow you to put the command \\sideeffect (or @sideeffect) in the\n# documentation, which will result in a user-defined paragraph with heading\n# \"Side Effects:\". You can put \\n's in the value part of an alias to insert\n# newlines (in the resulting output). You can put ^^ in the value part of an\n# alias to insert a newline as if a physical newline was in the original file.\n# When you need a literal { or } or , in the value part of an alias you have to\n# escape them by means of a backslash (\\), this can lead to conflicts with the\n# commands \\{ and \\} for these it is advised to use the version @{ and @} or use\n# a double escape (\\\\{ and \\\\})\n\nALIASES                =\n\n# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources\n# only. Doxygen will then generate output that is more tailored for C. For\n# instance, some of the names that are used will be different. The list of all\n# members will be omitted, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_FOR_C  = YES\n\n# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or\n# Python sources only. Doxygen will then generate output that is more tailored\n# for that language. For instance, namespaces will be presented as packages,\n# qualified scopes will look different, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_JAVA   = NO\n\n# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran\n# sources. Doxygen will then generate output that is tailored for Fortran.\n# The default value is: NO.\n\nOPTIMIZE_FOR_FORTRAN   = NO\n\n# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL\n# sources. Doxygen will then generate output that is tailored for VHDL.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_VHDL   = NO\n\n# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice\n# sources only. Doxygen will then generate output that is more tailored for that\n# language. For instance, namespaces will be presented as modules, types will be\n# separated into more groups, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_SLICE  = NO\n\n# Doxygen selects the parser to use depending on the extension of the files it\n# parses. With this tag you can assign which parser to use for a given\n# extension. Doxygen has a built-in mapping, but you can override or extend it\n# using this tag. The format is ext=language, where ext is a file extension, and\n# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,\n# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,\n# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:\n# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser\n# tries to guess whether the code is fixed or free formatted code, this is the\n# default for Fortran type files). For instance to make doxygen treat .inc files\n# as Fortran files (default is PHP), and .f files as C (default is Fortran),\n# use: inc=Fortran f=C.\n#\n# Note: For files without extension you can use no_extension as a placeholder.\n#\n# Note that for custom extensions you also need to set FILE_PATTERNS otherwise\n# the files are not read by doxygen.\n\nEXTENSION_MAPPING      =\n\n# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments\n# according to the Markdown format, which allows for more readable\n# documentation. See https://daringfireball.net/projects/markdown/ for details.\n# The output of markdown processing is further processed by doxygen, so you can\n# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in\n# case of backward compatibilities issues.\n# The default value is: YES.\n\nMARKDOWN_SUPPORT       = YES\n\n# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up\n# to that level are automatically included in the table of contents, even if\n# they do not have an id attribute.\n# Note: This feature currently applies only to Markdown headings.\n# Minimum value: 0, maximum value: 99, default value: 5.\n# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.\n\nTOC_INCLUDE_HEADINGS   = 5\n\n# When enabled doxygen tries to link words that correspond to documented\n# classes, or namespaces to their corresponding documentation. Such a link can\n# be prevented in individual cases by putting a % sign in front of the word or\n# globally by setting AUTOLINK_SUPPORT to NO.\n# The default value is: YES.\n\nAUTOLINK_SUPPORT       = YES\n\n# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want\n# to include (a tag file for) the STL sources as input, then you should set this\n# tag to YES in order to let doxygen match functions declarations and\n# definitions whose arguments contain STL classes (e.g. func(std::string);\n# versus func(std::string) {}). This also make the inheritance and collaboration\n# diagrams that involve STL classes more complete and accurate.\n# The default value is: NO.\n\nBUILTIN_STL_SUPPORT    = NO\n\n# If you use Microsoft's C++/CLI language, you should set this option to YES to\n# enable parsing support.\n# The default value is: NO.\n\nCPP_CLI_SUPPORT        = NO\n\n# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:\n# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen\n# will parse them like normal C++ but will assume all classes use public instead\n# of private inheritance when no explicit protection keyword is present.\n# The default value is: NO.\n\nSIP_SUPPORT            = NO\n\n# For Microsoft's IDL there are propget and propput attributes to indicate\n# getter and setter methods for a property. Setting this option to YES will make\n# doxygen to replace the get and set methods by a property in the documentation.\n# This will only work if the methods are indeed getting or setting a simple\n# type. If this is not the case, or you want to show the methods anyway, you\n# should set this option to NO.\n# The default value is: YES.\n\nIDL_PROPERTY_SUPPORT   = YES\n\n# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC\n# tag is set to YES then doxygen will reuse the documentation of the first\n# member in the group (if any) for the other members of the group. By default\n# all members of a group must be documented explicitly.\n# The default value is: NO.\n\nDISTRIBUTE_GROUP_DOC   = NO\n\n# If one adds a struct or class to a group and this option is enabled, then also\n# any nested class or struct is added to the same group. By default this option\n# is disabled and one has to add nested compounds explicitly via \\ingroup.\n# The default value is: NO.\n\nGROUP_NESTED_COMPOUNDS = NO\n\n# Set the SUBGROUPING tag to YES to allow class member groups of the same type\n# (for instance a group of public functions) to be put as a subgroup of that\n# type (e.g. under the Public Functions section). Set it to NO to prevent\n# subgrouping. Alternatively, this can be done per class using the\n# \\nosubgrouping command.\n# The default value is: YES.\n\nSUBGROUPING            = YES\n\n# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions\n# are shown inside the group in which they are included (e.g. using \\ingroup)\n# instead of on a separate page (for HTML and Man pages) or section (for LaTeX\n# and RTF).\n#\n# Note that this feature does not work in combination with\n# SEPARATE_MEMBER_PAGES.\n# The default value is: NO.\n\nINLINE_GROUPED_CLASSES = NO\n\n# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions\n# with only public data fields or simple typedef fields will be shown inline in\n# the documentation of the scope in which they are defined (i.e. file,\n# namespace, or group documentation), provided this scope is documented. If set\n# to NO, structs, classes, and unions are shown on a separate page (for HTML and\n# Man pages) or section (for LaTeX and RTF).\n# The default value is: NO.\n\nINLINE_SIMPLE_STRUCTS  = NO\n\n# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or\n# enum is documented as struct, union, or enum with the name of the typedef. So\n# typedef struct TypeS {} TypeT, will appear in the documentation as a struct\n# with name TypeT. When disabled the typedef will appear as a member of a file,\n# namespace, or class. And the struct will be named TypeS. This can typically be\n# useful for C code in case the coding convention dictates that all compound\n# types are typedef'ed and only the typedef is referenced, never the tag name.\n# The default value is: NO.\n\nTYPEDEF_HIDES_STRUCT   = NO\n\n# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This\n# cache is used to resolve symbols given their name and scope. Since this can be\n# an expensive process and often the same symbol appears multiple times in the\n# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small\n# doxygen will become slower. If the cache is too large, memory is wasted. The\n# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range\n# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536\n# symbols. At the end of a run doxygen will report the cache usage and suggest\n# the optimal cache size from a speed point of view.\n# Minimum value: 0, maximum value: 9, default value: 0.\n\nLOOKUP_CACHE_SIZE      = 0\n\n#---------------------------------------------------------------------------\n# Build related configuration options\n#---------------------------------------------------------------------------\n\n# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in\n# documentation are documented, even if no documentation was available. Private\n# class members and static file members will be hidden unless the\n# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.\n# Note: This will also disable the warnings about undocumented members that are\n# normally produced when WARNINGS is set to YES.\n# The default value is: NO.\n\nEXTRACT_ALL            = YES\n\n# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will\n# be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PRIVATE        = NO\n\n# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual\n# methods of a class will be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PRIV_VIRTUAL   = NO\n\n# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal\n# scope will be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PACKAGE        = NO\n\n# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be\n# included in the documentation.\n# The default value is: NO.\n\nEXTRACT_STATIC         = NO\n\n# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined\n# locally in source files will be included in the documentation. If set to NO,\n# only classes defined in header files are included. Does not have any effect\n# for Java sources.\n# The default value is: YES.\n\nEXTRACT_LOCAL_CLASSES  = YES\n\n# This flag is only useful for Objective-C code. If set to YES, local methods,\n# which are defined in the implementation section but not in the interface are\n# included in the documentation. If set to NO, only methods in the interface are\n# included.\n# The default value is: NO.\n\nEXTRACT_LOCAL_METHODS  = NO\n\n# If this flag is set to YES, the members of anonymous namespaces will be\n# extracted and appear in the documentation as a namespace called\n# 'anonymous_namespace{file}', where file will be replaced with the base name of\n# the file that contains the anonymous namespace. By default anonymous namespace\n# are hidden.\n# The default value is: NO.\n\nEXTRACT_ANON_NSPACES   = NO\n\n# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all\n# undocumented members inside documented classes or files. If set to NO these\n# members will be included in the various overviews, but no documentation\n# section is generated. This option has no effect if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_MEMBERS     = NO\n\n# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all\n# undocumented classes that are normally visible in the class hierarchy. If set\n# to NO, these classes will be included in the various overviews. This option\n# has no effect if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_CLASSES     = NO\n\n# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend\n# declarations. If set to NO, these declarations will be included in the\n# documentation.\n# The default value is: NO.\n\nHIDE_FRIEND_COMPOUNDS  = NO\n\n# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any\n# documentation blocks found inside the body of a function. If set to NO, these\n# blocks will be appended to the function's detailed documentation block.\n# The default value is: NO.\n\nHIDE_IN_BODY_DOCS      = NO\n\n# The INTERNAL_DOCS tag determines if documentation that is typed after a\n# \\internal command is included. If the tag is set to NO then the documentation\n# will be excluded. Set it to YES to include the internal documentation.\n# The default value is: NO.\n\nINTERNAL_DOCS          = NO\n\n# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file\n# names in lower-case letters. If set to YES, upper-case letters are also\n# allowed. This is useful if you have classes or files whose names only differ\n# in case and if your file system supports case sensitive file names. Windows\n# (including Cygwin) ands Mac users are advised to set this option to NO.\n# The default value is: system dependent.\n\nCASE_SENSE_NAMES       = YES\n\n# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with\n# their full class and namespace scopes in the documentation. If set to YES, the\n# scope will be hidden.\n# The default value is: NO.\n\nHIDE_SCOPE_NAMES       = NO\n\n# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will\n# append additional text to a page's title, such as Class Reference. If set to\n# YES the compound reference will be hidden.\n# The default value is: NO.\n\nHIDE_COMPOUND_REFERENCE= NO\n\n# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of\n# the files that are included by a file in the documentation of that file.\n# The default value is: YES.\n\nSHOW_INCLUDE_FILES     = YES\n\n# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each\n# grouped member an include statement to the documentation, telling the reader\n# which file to include in order to use the member.\n# The default value is: NO.\n\nSHOW_GROUPED_MEMB_INC  = NO\n\n# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include\n# files with double quotes in the documentation rather than with sharp brackets.\n# The default value is: NO.\n\nFORCE_LOCAL_INCLUDES   = NO\n\n# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the\n# documentation for inline members.\n# The default value is: YES.\n\nINLINE_INFO            = YES\n\n# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the\n# (detailed) documentation of file and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order.\n# The default value is: YES.\n\nSORT_MEMBER_DOCS       = YES\n\n# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief\n# descriptions of file, namespace and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order. Note that\n# this will also influence the order of the classes in the class list.\n# The default value is: NO.\n\nSORT_BRIEF_DOCS        = NO\n\n# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the\n# (brief and detailed) documentation of class members so that constructors and\n# destructors are listed first. If set to NO the constructors will appear in the\n# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.\n# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief\n# member documentation.\n# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting\n# detailed member documentation.\n# The default value is: NO.\n\nSORT_MEMBERS_CTORS_1ST = NO\n\n# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy\n# of group names into alphabetical order. If set to NO the group names will\n# appear in their defined order.\n# The default value is: NO.\n\nSORT_GROUP_NAMES       = NO\n\n# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by\n# fully-qualified names, including namespaces. If set to NO, the class list will\n# be sorted only by class name, not including the namespace part.\n# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.\n# Note: This option applies only to the class list, not to the alphabetical\n# list.\n# The default value is: NO.\n\nSORT_BY_SCOPE_NAME     = NO\n\n# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper\n# type resolution of all parameters of a function it will reject a match between\n# the prototype and the implementation of a member function even if there is\n# only one candidate or it is obvious which candidate to choose by doing a\n# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still\n# accept a match between prototype and implementation in such cases.\n# The default value is: NO.\n\nSTRICT_PROTO_MATCHING  = NO\n\n# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo\n# list. This list is created by putting \\todo commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TODOLIST      = NO\n\n# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test\n# list. This list is created by putting \\test commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TESTLIST      = NO\n\n# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug\n# list. This list is created by putting \\bug commands in the documentation.\n# The default value is: YES.\n\nGENERATE_BUGLIST       = NO\n\n# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)\n# the deprecated list. This list is created by putting \\deprecated commands in\n# the documentation.\n# The default value is: YES.\n\nGENERATE_DEPRECATEDLIST= NO\n\n# The ENABLED_SECTIONS tag can be used to enable conditional documentation\n# sections, marked by \\if <section_label> ... \\endif and \\cond <section_label>\n# ... \\endcond blocks.\n\nENABLED_SECTIONS       =\n\n# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the\n# initial value of a variable or macro / define can have for it to appear in the\n# documentation. If the initializer consists of more lines than specified here\n# it will be hidden. Use a value of 0 to hide initializers completely. The\n# appearance of the value of individual variables and macros / defines can be\n# controlled using \\showinitializer or \\hideinitializer command in the\n# documentation regardless of this setting.\n# Minimum value: 0, maximum value: 10000, default value: 30.\n\nMAX_INITIALIZER_LINES  = 30\n\n# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at\n# the bottom of the documentation of classes and structs. If set to YES, the\n# list will mention the files that were used to generate the documentation.\n# The default value is: YES.\n\nSHOW_USED_FILES        = YES\n\n# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This\n# will remove the Files entry from the Quick Index and from the Folder Tree View\n# (if specified).\n# The default value is: YES.\n\nSHOW_FILES             = YES\n\n# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces\n# page. This will remove the Namespaces entry from the Quick Index and from the\n# Folder Tree View (if specified).\n# The default value is: YES.\n\nSHOW_NAMESPACES        = NO\n\n# The FILE_VERSION_FILTER tag can be used to specify a program or script that\n# doxygen should invoke to get the current version for each file (typically from\n# the version control system). Doxygen will invoke the program by executing (via\n# popen()) the command command input-file, where command is the value of the\n# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided\n# by doxygen. Whatever the program writes to standard output is used as the file\n# version. For an example see the documentation.\n\nFILE_VERSION_FILTER    =\n\n# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed\n# by doxygen. The layout file controls the global structure of the generated\n# output files in an output format independent way. To create the layout file\n# that represents doxygen's defaults, run doxygen with the -l option. You can\n# optionally specify a file name after the option, if omitted DoxygenLayout.xml\n# will be used as the name of the layout file.\n#\n# Note that if you run doxygen from a directory containing a file called\n# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE\n# tag is left empty.\n\nLAYOUT_FILE            =\n\n# The CITE_BIB_FILES tag can be used to specify one or more bib files containing\n# the reference definitions. This must be a list of .bib files. The .bib\n# extension is automatically appended if omitted. This requires the bibtex tool\n# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.\n# For LaTeX the style of the bibliography can be controlled using\n# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the\n# search path. See also \\cite for info how to create references.\n\nCITE_BIB_FILES         =\n\n#---------------------------------------------------------------------------\n# Configuration options related to warning and progress messages\n#---------------------------------------------------------------------------\n\n# The QUIET tag can be used to turn on/off the messages that are generated to\n# standard output by doxygen. If QUIET is set to YES this implies that the\n# messages are off.\n# The default value is: NO.\n\nQUIET                  = NO\n\n# The WARNINGS tag can be used to turn on/off the warning messages that are\n# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES\n# this implies that the warnings are on.\n#\n# Tip: Turn warnings on while writing the documentation.\n# The default value is: YES.\n\nWARNINGS               = YES\n\n# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate\n# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag\n# will automatically be disabled.\n# The default value is: YES.\n\nWARN_IF_UNDOCUMENTED   = YES\n\n# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for\n# potential errors in the documentation, such as not documenting some parameters\n# in a documented function, or documenting parameters that don't exist or using\n# markup commands wrongly.\n# The default value is: YES.\n\nWARN_IF_DOC_ERROR      = YES\n\n# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that\n# are documented, but have no documentation for their parameters or return\n# value. If set to NO, doxygen will only warn about wrong or incomplete\n# parameter documentation, but not about the absence of documentation. If\n# EXTRACT_ALL is set to YES then this flag will automatically be disabled.\n# The default value is: NO.\n\nWARN_NO_PARAMDOC       = NO\n\n# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when\n# a warning is encountered.\n# The default value is: NO.\n\nWARN_AS_ERROR          = NO\n\n# The WARN_FORMAT tag determines the format of the warning messages that doxygen\n# can produce. The string should contain the $file, $line, and $text tags, which\n# will be replaced by the file and line number from which the warning originated\n# and the warning text. Optionally the format may contain $version, which will\n# be replaced by the version of the file (if it could be obtained via\n# FILE_VERSION_FILTER)\n# The default value is: $file:$line: $text.\n\nWARN_FORMAT            = \"$file:$line: $text\"\n\n# The WARN_LOGFILE tag can be used to specify a file to which warning and error\n# messages should be written. If left blank the output is written to standard\n# error (stderr).\n\nWARN_LOGFILE           =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the input files\n#---------------------------------------------------------------------------\n\n# The INPUT tag is used to specify the files and/or directories that contain\n# documented source files. You may enter file names like myfile.cpp or\n# directories like /usr/src/myproject. Separate the files or directories with\n# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING\n# Note: If this tag is empty the current directory is searched.\n\nINPUT                  = ../src \\\n                         ../src/plugins \\\n                         ../src/transports \\\n                         ../src/events \\\n                         ../src/loggers \\\n                         ../src/postprocessing\n\n# This tag can be used to specify the character encoding of the source files\n# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses\n# libiconv (or the iconv built into libc) for the transcoding. See the libiconv\n# documentation (see: https://www.gnu.org/software/libiconv/) for the list of\n# possible encodings.\n# The default value is: UTF-8.\n\nINPUT_ENCODING         = UTF-8\n\n# If the value of the INPUT tag contains directories, you can use the\n# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and\n# *.h) to filter out the source-files in the directories.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# read by doxygen.\n#\n# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,\n# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,\n# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,\n# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),\n# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen\n# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,\n# *.vhdl, *.ucf, *.qsf and *.ice.\n\nFILE_PATTERNS          = *.c \\\n                         *.h \\\n                         *.dox\n\n# The RECURSIVE tag can be used to specify whether or not subdirectories should\n# be searched for input files as well.\n# The default value is: NO.\n\nRECURSIVE              = NO\n\n# The EXCLUDE tag can be used to specify files and/or directories that should be\n# excluded from the INPUT source files. This way you can easily exclude a\n# subdirectory from a directory tree whose root is specified with the INPUT tag.\n#\n# Note that relative paths are relative to the directory from which doxygen is\n# run.\n\nEXCLUDE                = ../cmdline.c \\\n                         ../cmdline.h \\\n                         ../postprocessing/pp-cmdline.c \\\n                         ../postprocessing/pp-cmdline.h \\\n                         ../postprocessing/p2m-cmdline.c \\\n                         ../postprocessing/p2m-cmdline.h\n\n# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or\n# directories that are symbolic links (a Unix file system feature) are excluded\n# from the input.\n# The default value is: NO.\n\nEXCLUDE_SYMLINKS       = NO\n\n# If the value of the INPUT tag contains directories, you can use the\n# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude\n# certain files from those directories.\n#\n# Note that the wildcards are matched against the file with absolute path, so to\n# exclude all test directories for example use the pattern */test/*\n\nEXCLUDE_PATTERNS       =\n\n# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names\n# (namespaces, classes, functions, etc.) that should be excluded from the\n# output. The symbol name can be a fully qualified name, a word, or if the\n# wildcard * is used, a substring. Examples: ANamespace, AClass,\n# AClass::ANamespace, ANamespace::*Test\n#\n# Note that the wildcards are matched against the file with absolute path, so to\n# exclude all test directories use the pattern */test/*\n\nEXCLUDE_SYMBOLS        =\n\n# The EXAMPLE_PATH tag can be used to specify one or more files or directories\n# that contain example code fragments that are included (see the \\include\n# command).\n\nEXAMPLE_PATH           = ..\n\n# If the value of the EXAMPLE_PATH tag contains directories, you can use the\n# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and\n# *.h) to filter out the source-files in the directories. If left blank all\n# files are included.\n\nEXAMPLE_PATTERNS       =\n\n# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be\n# searched for input files to be used with the \\include or \\dontinclude commands\n# irrespective of the value of the RECURSIVE tag.\n# The default value is: NO.\n\nEXAMPLE_RECURSIVE      = NO\n\n# The IMAGE_PATH tag can be used to specify one or more files or directories\n# that contain images that are to be included in the documentation (see the\n# \\image command).\n\nIMAGE_PATH             =\n\n# The INPUT_FILTER tag can be used to specify a program that doxygen should\n# invoke to filter for each input file. Doxygen will invoke the filter program\n# by executing (via popen()) the command:\n#\n# <filter> <input-file>\n#\n# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the\n# name of an input file. Doxygen will then use the output that the filter\n# program writes to standard output. If FILTER_PATTERNS is specified, this tag\n# will be ignored.\n#\n# Note that the filter must not add or remove lines; it is applied before the\n# code is scanned, but not when the output code is generated. If lines are added\n# or removed, the anchors will not be placed correctly.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nINPUT_FILTER           =\n\n# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern\n# basis. Doxygen will compare the file name with each pattern and apply the\n# filter if there is a match. The filters are a list of the form: pattern=filter\n# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how\n# filters are used. If the FILTER_PATTERNS tag is empty or if none of the\n# patterns match the file name, INPUT_FILTER is applied.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nFILTER_PATTERNS        =\n\n# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using\n# INPUT_FILTER) will also be used to filter the input files that are used for\n# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).\n# The default value is: NO.\n\nFILTER_SOURCE_FILES    = NO\n\n# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file\n# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and\n# it is also possible to disable source filtering for a specific pattern using\n# *.ext= (so without naming a filter).\n# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.\n\nFILTER_SOURCE_PATTERNS =\n\n# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that\n# is part of the input, its contents will be placed on the main page\n# (index.html). This can be useful if you have a project on for instance GitHub\n# and want to reuse the introduction page also for the doxygen output.\n\nUSE_MDFILE_AS_MAINPAGE =\n\n#---------------------------------------------------------------------------\n# Configuration options related to source browsing\n#---------------------------------------------------------------------------\n\n# If the SOURCE_BROWSER tag is set to YES then a list of source files will be\n# generated. Documented entities will be cross-referenced with these sources.\n#\n# Note: To get rid of all source code in the generated output, make sure that\n# also VERBATIM_HEADERS is set to NO.\n# The default value is: NO.\n\nSOURCE_BROWSER         = NO\n\n# Setting the INLINE_SOURCES tag to YES will include the body of functions,\n# classes and enums directly into the documentation.\n# The default value is: NO.\n\nINLINE_SOURCES         = NO\n\n# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any\n# special comment blocks from generated source code fragments. Normal C, C++ and\n# Fortran comments will always remain visible.\n# The default value is: YES.\n\nSTRIP_CODE_COMMENTS    = YES\n\n# If the REFERENCED_BY_RELATION tag is set to YES then for each documented\n# entity all documented functions referencing it will be listed.\n# The default value is: NO.\n\nREFERENCED_BY_RELATION = NO\n\n# If the REFERENCES_RELATION tag is set to YES then for each documented function\n# all documented entities called/used by that function will be listed.\n# The default value is: NO.\n\nREFERENCES_RELATION    = NO\n\n# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set\n# to YES then the hyperlinks from functions in REFERENCES_RELATION and\n# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will\n# link to the documentation.\n# The default value is: YES.\n\nREFERENCES_LINK_SOURCE = YES\n\n# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the\n# source code will show a tooltip with additional information such as prototype,\n# brief description and links to the definition and documentation. Since this\n# will make the HTML file larger and loading of large files a bit slower, you\n# can opt to disable this feature.\n# The default value is: YES.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nSOURCE_TOOLTIPS        = YES\n\n# If the USE_HTAGS tag is set to YES then the references to source code will\n# point to the HTML generated by the htags(1) tool instead of doxygen built-in\n# source browser. The htags tool is part of GNU's global source tagging system\n# (see https://www.gnu.org/software/global/global.html). You will need version\n# 4.8.6 or higher.\n#\n# To use it do the following:\n# - Install the latest version of global\n# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file\n# - Make sure the INPUT points to the root of the source tree\n# - Run doxygen as normal\n#\n# Doxygen will invoke htags (and that will in turn invoke gtags), so these\n# tools must be available from the command line (i.e. in the search path).\n#\n# The result: instead of the source browser generated by doxygen, the links to\n# source code will now point to the output of htags.\n# The default value is: NO.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nUSE_HTAGS              = NO\n\n# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a\n# verbatim copy of the header file for each class for which an include is\n# specified. Set to NO to disable this.\n# See also: Section \\class.\n# The default value is: YES.\n\nVERBATIM_HEADERS       = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to the alphabetical class index\n#---------------------------------------------------------------------------\n\n# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all\n# compounds will be generated. Enable this if the project contains a lot of\n# classes, structs, unions or interfaces.\n# The default value is: YES.\n\nALPHABETICAL_INDEX     = YES\n\n# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in\n# which the alphabetical index list will be split.\n# Minimum value: 1, maximum value: 20, default value: 5.\n# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.\n\nCOLS_IN_ALPHA_INDEX    = 5\n\n# In case all classes in a project start with a common prefix, all classes will\n# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag\n# can be used to specify a prefix (or a list of prefixes) that should be ignored\n# while generating the index headers.\n# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.\n\nIGNORE_PREFIX          =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the HTML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output\n# The default value is: YES.\n\nGENERATE_HTML          = YES\n\n# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_OUTPUT            = html\n\n# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each\n# generated HTML page (for example: .htm, .php, .asp).\n# The default value is: .html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FILE_EXTENSION    = .html\n\n# The HTML_HEADER tag can be used to specify a user-defined HTML header file for\n# each generated HTML page. If the tag is left blank doxygen will generate a\n# standard header.\n#\n# To get valid HTML the header file that includes any scripts and style sheets\n# that doxygen needs, which is dependent on the configuration options used (e.g.\n# the setting GENERATE_TREEVIEW). It is highly recommended to start with a\n# default header using\n# doxygen -w html new_header.html new_footer.html new_stylesheet.css\n# YourConfigFile\n# and then modify the file new_header.html. See also section \"Doxygen usage\"\n# for information on how to generate the default header that doxygen normally\n# uses.\n# Note: The header is subject to change so you typically have to regenerate the\n# default header when upgrading to a newer version of doxygen. For a description\n# of the possible markers and block names see the documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_HEADER            = header.html\n\n# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each\n# generated HTML page. If the tag is left blank doxygen will generate a standard\n# footer. See HTML_HEADER for more information on how to generate a default\n# footer and what special commands can be used inside the footer. See also\n# section \"Doxygen usage\" for information on how to generate the default footer\n# that doxygen normally uses.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FOOTER            = footer.html\n\n# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style\n# sheet that is used by each HTML page. It can be used to fine-tune the look of\n# the HTML output. If left blank doxygen will generate a default style sheet.\n# See also section \"Doxygen usage\" for information on how to generate the style\n# sheet that doxygen normally uses.\n# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as\n# it is more robust and this tag (HTML_STYLESHEET) will in the future become\n# obsolete.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_STYLESHEET        =\n\n# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# cascading style sheets that are included after the standard style sheets\n# created by doxygen. Using this option one can overrule certain style aspects.\n# This is preferred over using HTML_STYLESHEET since it does not replace the\n# standard style sheet and is therefore more robust against future updates.\n# Doxygen will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list). For an example see the documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_STYLESHEET  =\n\n# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the HTML output directory. Note\n# that these files will be copied to the base HTML output directory. Use the\n# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these\n# files. In the HTML_STYLESHEET file, use the file name only. Also note that the\n# files will be copied as-is; there are no commands or markers available.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_FILES       =\n\n# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen\n# will adjust the colors in the style sheet and background images according to\n# this color. Hue is specified as an angle on a colorwheel, see\n# https://en.wikipedia.org/wiki/Hue for more information. For instance the value\n# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300\n# purple, and 360 is red again.\n# Minimum value: 0, maximum value: 359, default value: 220.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_HUE    = 220\n\n# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors\n# in the HTML output. For a value of 0 the output will use grayscales only. A\n# value of 255 will produce the most vivid colors.\n# Minimum value: 0, maximum value: 255, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_SAT    = 100\n\n# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the\n# luminance component of the colors in the HTML output. Values below 100\n# gradually make the output lighter, whereas values above 100 make the output\n# darker. The value divided by 100 is the actual gamma applied, so 80 represents\n# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not\n# change the gamma.\n# Minimum value: 40, maximum value: 240, default value: 80.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_GAMMA  = 80\n\n# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML\n# page will contain the date and time when the page was generated. Setting this\n# to YES can help to show when doxygen was last run and thus if the\n# documentation is up to date.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_TIMESTAMP         = NO\n\n# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML\n# documentation will contain a main index with vertical navigation menus that\n# are dynamically created via JavaScript. If disabled, the navigation index will\n# consists of multiple levels of tabs that are statically embedded in every HTML\n# page. Disable this option to support browsers that do not have JavaScript,\n# like the Qt help browser.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_DYNAMIC_MENUS     = NO\n\n# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML\n# documentation will contain sections that can be hidden and shown after the\n# page has loaded.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_DYNAMIC_SECTIONS  = NO\n\n# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries\n# shown in the various tree structured indices initially; the user can expand\n# and collapse entries dynamically later on. Doxygen will expand the tree to\n# such a level that at most the specified number of entries are visible (unless\n# a fully collapsed tree already exceeds this amount). So setting the number of\n# entries 1 will produce a full collapsed tree by default. 0 is a special value\n# representing an infinite number of entries and will result in a full expanded\n# tree by default.\n# Minimum value: 0, maximum value: 9999, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_INDEX_NUM_ENTRIES = 100\n\n# If the GENERATE_DOCSET tag is set to YES, additional index files will be\n# generated that can be used as input for Apple's Xcode 3 integrated development\n# environment (see: https://developer.apple.com/xcode/), introduced with OSX\n# 10.5 (Leopard). To create a documentation set, doxygen will generate a\n# Makefile in the HTML output directory. Running make will produce the docset in\n# that directory and running make install will install the docset in\n# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at\n# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy\n# genXcode/_index.html for more information.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_DOCSET        = NO\n\n# This tag determines the name of the docset feed. A documentation feed provides\n# an umbrella under which multiple documentation sets from a single provider\n# (such as a company or product suite) can be grouped.\n# The default value is: Doxygen generated docs.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_FEEDNAME        = \"Doxygen generated docs\"\n\n# This tag specifies a string that should uniquely identify the documentation\n# set bundle. This should be a reverse domain-name style string, e.g.\n# com.mycompany.MyDocSet. Doxygen will append .docset to the name.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_BUNDLE_ID       = com.meetecho.janus\n\n# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify\n# the documentation publisher. This should be a reverse domain-name style\n# string, e.g. com.mycompany.MyDocSet.documentation.\n# The default value is: org.doxygen.Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_ID    = com.meetecho.janus\n\n# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.\n# The default value is: Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_NAME  = Meetecho\n\n# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three\n# additional HTML index files: index.hhp, index.hhc, and index.hhk. The\n# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop\n# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on\n# Windows.\n#\n# The HTML Help Workshop contains a compiler that can convert all HTML output\n# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML\n# files are now used as the Windows 98 help format, and will replace the old\n# Windows help format (.hlp) on all Windows platforms in the future. Compressed\n# HTML files also contain an index, a table of contents, and you can search for\n# words in the documentation. The HTML workshop also contains a viewer for\n# compressed HTML files.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_HTMLHELP      = NO\n\n# The CHM_FILE tag can be used to specify the file name of the resulting .chm\n# file. You can add a path in front of the file if the result should not be\n# written to the html output directory.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_FILE               =\n\n# The HHC_LOCATION tag can be used to specify the location (absolute path\n# including file name) of the HTML help compiler (hhc.exe). If non-empty,\n# doxygen will try to run the HTML help compiler on the generated index.hhp.\n# The file has to be specified with full path.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nHHC_LOCATION           =\n\n# The GENERATE_CHI flag controls if a separate .chi index file is generated\n# (YES) or that it should be included in the master .chm file (NO).\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nGENERATE_CHI           = NO\n\n# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)\n# and project file content.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_INDEX_ENCODING     =\n\n# The BINARY_TOC flag controls whether a binary table of contents is generated\n# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it\n# enables the Previous and Next buttons.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nBINARY_TOC             = NO\n\n# The TOC_EXPAND flag can be set to YES to add extra items for group members to\n# the table of contents of the HTML help documentation and to the tree view.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nTOC_EXPAND             = NO\n\n# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and\n# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that\n# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help\n# (.qch) of the generated HTML documentation.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_QHP           = NO\n\n# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify\n# the file name of the resulting .qch file. The path specified is relative to\n# the HTML output folder.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQCH_FILE               =\n\n# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help\n# Project output. For more information please see Qt Help Project / Namespace\n# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_NAMESPACE          = org.doxygen.Project\n\n# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt\n# Help Project output. For more information please see Qt Help Project / Virtual\n# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-\n# folders).\n# The default value is: doc.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_VIRTUAL_FOLDER     = doc\n\n# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom\n# filter to add. For more information please see Qt Help Project / Custom\n# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-\n# filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_NAME   =\n\n# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the\n# custom filter to add. For more information please see Qt Help Project / Custom\n# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-\n# filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_ATTRS  =\n\n# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this\n# project's filter section matches. Qt Help Project / Filter Attributes (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_SECT_FILTER_ATTRS  =\n\n# The QHG_LOCATION tag can be used to specify the location of Qt's\n# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the\n# generated .qhp file.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHG_LOCATION           =\n\n# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be\n# generated, together with the HTML files, they form an Eclipse help plugin. To\n# install this plugin and make it available under the help contents menu in\n# Eclipse, the contents of the directory containing the HTML and XML files needs\n# to be copied into the plugins directory of eclipse. The name of the directory\n# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.\n# After copying Eclipse needs to be restarted before the help appears.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_ECLIPSEHELP   = NO\n\n# A unique identifier for the Eclipse help plugin. When installing the plugin\n# the directory name containing the HTML and XML files should also have this\n# name. Each documentation set should have its own identifier.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.\n\nECLIPSE_DOC_ID         = org.doxygen.Project\n\n# If you want full control over the layout of the generated HTML pages it might\n# be necessary to disable the index and replace it with your own. The\n# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top\n# of each HTML page. A value of NO enables the index and the value YES disables\n# it. Since the tabs in the index contain the same information as the navigation\n# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nDISABLE_INDEX          = NO\n\n# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index\n# structure should be generated to display hierarchical information. If the tag\n# value is set to YES, a side panel will be generated containing a tree-like\n# index structure (just like the one that is generated for HTML Help). For this\n# to work a browser that supports JavaScript, DHTML, CSS and frames is required\n# (i.e. any modern browser). Windows users are probably better off using the\n# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can\n# further fine-tune the look of the index. As an example, the default style\n# sheet generated by doxygen has an example that shows how to put an image at\n# the root of the tree instead of the PROJECT_NAME. Since the tree basically has\n# the same information as the tab index, you could consider setting\n# DISABLE_INDEX to YES when enabling this option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_TREEVIEW      = NO\n\n# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that\n# doxygen will group on one line in the generated HTML documentation.\n#\n# Note that a value of 0 will completely suppress the enum values from appearing\n# in the overview section.\n# Minimum value: 0, maximum value: 20, default value: 4.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nENUM_VALUES_PER_LINE   = 4\n\n# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used\n# to set the initial width (in pixels) of the frame in which the tree is shown.\n# Minimum value: 0, maximum value: 1500, default value: 250.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nTREEVIEW_WIDTH         = 250\n\n# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to\n# external symbols imported via tag files in a separate window.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nEXT_LINKS_IN_WINDOW    = NO\n\n# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg\n# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see\n# https://inkscape.org) to generate formulas as SVG images instead of PNGs for\n# the HTML output. These images will generally look nicer at scaled resolutions.\n# Possible values are: png The default and svg Looks nicer but requires the\n# pdf2svg tool.\n# The default value is: png.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FORMULA_FORMAT    = png\n\n# Use this tag to change the font size of LaTeX formulas included as images in\n# the HTML documentation. When you change the font size after a successful\n# doxygen run you need to manually remove any form_*.png images from the HTML\n# output directory to force them to be regenerated.\n# Minimum value: 8, maximum value: 50, default value: 10.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFORMULA_FONTSIZE       = 10\n\n# Use the FORMULA_TRANSPARENT tag to determine whether or not the images\n# generated for formulas are transparent PNGs. Transparent PNGs are not\n# supported properly for IE 6.0, but are supported on all modern browsers.\n#\n# Note that when changing this option you need to delete any form_*.png files in\n# the HTML output directory before the changes have effect.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFORMULA_TRANSPARENT    = YES\n\n# The FORMULA_MACROFILE can contain LaTeX \\newcommand and \\renewcommand commands\n# to create new LaTeX commands to be used in formulas as building blocks. See\n# the section \"Including formulas\" for details.\n\nFORMULA_MACROFILE      =\n\n# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see\n# https://www.mathjax.org) which uses client side JavaScript for the rendering\n# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX\n# installed or if you want to formulas look prettier in the HTML output. When\n# enabled you may also need to install MathJax separately and configure the path\n# to it using the MATHJAX_RELPATH option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nUSE_MATHJAX            = NO\n\n# When MathJax is enabled you can set the default output format to be used for\n# the MathJax output. See the MathJax site (see:\n# http://docs.mathjax.org/en/latest/output.html) for more details.\n# Possible values are: HTML-CSS (which is slower, but has the best\n# compatibility), NativeMML (i.e. MathML) and SVG.\n# The default value is: HTML-CSS.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_FORMAT         = HTML-CSS\n\n# When MathJax is enabled you need to specify the location relative to the HTML\n# output directory using the MATHJAX_RELPATH option. The destination directory\n# should contain the MathJax.js script. For instance, if the mathjax directory\n# is located at the same level as the HTML output directory, then\n# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax\n# Content Delivery Network so you can quickly see the result without installing\n# MathJax. However, it is strongly recommended to install a local copy of\n# MathJax from https://www.mathjax.org before deployment.\n# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest\n\n# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax\n# extension names that should be enabled during MathJax rendering. For example\n# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_EXTENSIONS     =\n\n# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces\n# of code that will be used on startup of the MathJax code. See the MathJax site\n# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an\n# example see the documentation.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_CODEFILE       =\n\n# When the SEARCHENGINE tag is enabled doxygen will generate a search box for\n# the HTML output. The underlying search engine uses javascript and DHTML and\n# should work on any modern browser. Note that when using HTML help\n# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)\n# there is already a search function so this one should typically be disabled.\n# For large projects the javascript based search engine can be slow, then\n# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to\n# search using the keyboard; to jump to the search box use <access key> + S\n# (what the <access key> is depends on the OS and browser, but it is typically\n# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down\n# key> to jump into the search results window, the results can be navigated\n# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel\n# the search. The filter options can be selected when the cursor is inside the\n# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>\n# to select a filter and <Enter> or <escape> to activate or cancel the filter\n# option.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nSEARCHENGINE           = YES\n\n# When the SERVER_BASED_SEARCH tag is enabled the search engine will be\n# implemented using a web server instead of a web client using JavaScript. There\n# are two flavors of web server based searching depending on the EXTERNAL_SEARCH\n# setting. When disabled, doxygen will generate a PHP script for searching and\n# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing\n# and searching needs to be provided by external tools. See the section\n# \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSERVER_BASED_SEARCH    = NO\n\n# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP\n# script for searching. Instead the search results are written to an XML file\n# which needs to be processed by an external indexer. Doxygen will invoke an\n# external search engine pointed to by the SEARCHENGINE_URL option to obtain the\n# search results.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see: https://xapian.org/).\n#\n# See the section \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH        = NO\n\n# The SEARCHENGINE_URL should point to a search engine hosted by a web server\n# which will return the search results when EXTERNAL_SEARCH is enabled.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see: https://xapian.org/). See the section \"External Indexing and\n# Searching\" for details.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHENGINE_URL       =\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed\n# search data is written to a file for indexing by an external tool. With the\n# SEARCHDATA_FILE tag the name of this file can be specified.\n# The default file is: searchdata.xml.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHDATA_FILE        = searchdata.xml\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the\n# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is\n# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple\n# projects and redirect the results back to the right project.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH_ID     =\n\n# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen\n# projects other than the one defined by this configuration file, but that are\n# all added to the same external search index. Each project needs to have a\n# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of\n# to a relative location where the documentation can be found. The format is:\n# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTRA_SEARCH_MAPPINGS  =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the LaTeX output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.\n# The default value is: YES.\n\nGENERATE_LATEX         = NO\n\n# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: latex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_OUTPUT           = latex\n\n# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be\n# invoked.\n#\n# Note that when not enabling USE_PDFLATEX the default is latex when enabling\n# USE_PDFLATEX the default is pdflatex and when in the later case latex is\n# chosen this is overwritten by pdflatex. For specific output languages the\n# default can have been set differently, this depends on the implementation of\n# the output language.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_CMD_NAME         = latex\n\n# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate\n# index for LaTeX.\n# Note: This tag is used in the Makefile / make.bat.\n# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file\n# (.tex).\n# The default file is: makeindex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nMAKEINDEX_CMD_NAME     = makeindex\n\n# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to\n# generate index for LaTeX. In case there is no backslash (\\) as first character\n# it will be automatically added in the LaTeX code.\n# Note: This tag is used in the generated output file (.tex).\n# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.\n# The default value is: makeindex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_MAKEINDEX_CMD    = makeindex\n\n# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nCOMPACT_LATEX          = NO\n\n# The PAPER_TYPE tag can be used to set the paper type that is used by the\n# printer.\n# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x\n# 14 inches) and executive (7.25 x 10.5 inches).\n# The default value is: a4.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPAPER_TYPE             = a4\n\n# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names\n# that should be included in the LaTeX output. The package can be specified just\n# by its name or with the correct syntax as to be used with the LaTeX\n# \\usepackage command. To get the times font for instance you can specify :\n# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}\n# To use the option intlimits with the amsmath package you can specify:\n# EXTRA_PACKAGES=[intlimits]{amsmath}\n# If left blank no extra packages will be included.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nEXTRA_PACKAGES         =\n\n# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the\n# generated LaTeX document. The header should contain everything until the first\n# chapter. If it is left blank doxygen will generate a standard header. See\n# section \"Doxygen usage\" for information on how to let doxygen write the\n# default header to a separate file.\n#\n# Note: Only use a user-defined header if you know what you are doing! The\n# following commands have a special meaning inside the header: $title,\n# $datetime, $date, $doxygenversion, $projectname, $projectnumber,\n# $projectbrief, $projectlogo. Doxygen will replace $title with the empty\n# string, for the replacement values of the other commands the user is referred\n# to HTML_HEADER.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HEADER           =\n\n# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the\n# generated LaTeX document. The footer should contain everything after the last\n# chapter. If it is left blank doxygen will generate a standard footer. See\n# LATEX_HEADER for more information on how to generate a default footer and what\n# special commands can be used inside the footer.\n#\n# Note: Only use a user-defined footer if you know what you are doing!\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_FOOTER           =\n\n# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# LaTeX style sheets that are included after the standard style sheets created\n# by doxygen. Using this option one can overrule certain style aspects. Doxygen\n# will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list).\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_STYLESHEET =\n\n# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the LATEX_OUTPUT output\n# directory. Note that the files will be copied as-is; there are no commands or\n# markers available.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_FILES      =\n\n# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is\n# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will\n# contain links (just like the HTML output) instead of page references. This\n# makes the output suitable for online browsing using a PDF viewer.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPDF_HYPERLINKS         = YES\n\n# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate\n# the PDF file directly from the LaTeX files. Set this option to YES, to get a\n# higher quality PDF documentation.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nUSE_PDFLATEX           = YES\n\n# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode\n# command to the generated LaTeX files. This will instruct LaTeX to keep running\n# if errors occur, instead of asking the user for help. This option is also used\n# when generating formulas in HTML.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BATCHMODE        = NO\n\n# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the\n# index chapters (such as File Index, Compound Index, etc.) in the output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HIDE_INDICES     = NO\n\n# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source\n# code with syntax highlighting in the LaTeX output.\n#\n# Note that which sources are shown also depends on other settings such as\n# SOURCE_BROWSER.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_SOURCE_CODE      = NO\n\n# The LATEX_BIB_STYLE tag can be used to specify the style to use for the\n# bibliography, e.g. plainnat, or ieeetr. See\n# https://en.wikipedia.org/wiki/BibTeX and \\cite for more info.\n# The default value is: plain.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BIB_STYLE        = plain\n\n# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated\n# page will contain the date and time when the page was generated. Setting this\n# to NO can help when comparing the output of multiple runs.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_TIMESTAMP        = NO\n\n# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)\n# path from which the emoji images will be read. If a relative path is entered,\n# it will be relative to the LATEX_OUTPUT directory. If left blank the\n# LATEX_OUTPUT directory will be used.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EMOJI_DIRECTORY  =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the RTF output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The\n# RTF output is optimized for Word 97 and may not look too pretty with other RTF\n# readers/editors.\n# The default value is: NO.\n\nGENERATE_RTF           = NO\n\n# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: rtf.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_OUTPUT             = rtf\n\n# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nCOMPACT_RTF            = NO\n\n# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will\n# contain hyperlink fields. The RTF file will contain links (just like the HTML\n# output) instead of page references. This makes the output suitable for online\n# browsing using Word or some other Word compatible readers that support those\n# fields.\n#\n# Note: WordPad (write) and others do not support links.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_HYPERLINKS         = NO\n\n# Load stylesheet definitions from file. Syntax is similar to doxygen's\n# configuration file, i.e. a series of assignments. You only have to provide\n# replacements, missing definitions are set to their default value.\n#\n# See also section \"Doxygen usage\" for information on how to generate the\n# default style sheet that doxygen normally uses.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_STYLESHEET_FILE    =\n\n# Set optional variables used in the generation of an RTF document. Syntax is\n# similar to doxygen's configuration file. A template extensions file can be\n# generated using doxygen -e rtf extensionFile.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_EXTENSIONS_FILE    =\n\n# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code\n# with syntax highlighting in the RTF output.\n#\n# Note that which sources are shown also depends on other settings such as\n# SOURCE_BROWSER.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_SOURCE_CODE        = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the man page output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for\n# classes and files.\n# The default value is: NO.\n\nGENERATE_MAN           = NO\n\n# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it. A directory man3 will be created inside the directory specified by\n# MAN_OUTPUT.\n# The default directory is: man.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_OUTPUT             = man\n\n# The MAN_EXTENSION tag determines the extension that is added to the generated\n# man pages. In case the manual section does not start with a number, the number\n# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is\n# optional.\n# The default value is: .3.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_EXTENSION          = .3\n\n# The MAN_SUBDIR tag determines the name of the directory created within\n# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by\n# MAN_EXTENSION with the initial . removed.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_SUBDIR             =\n\n# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it\n# will generate one additional man file for each entity documented in the real\n# man page(s). These additional files only source the real man page, but without\n# them the man command would be unable to find the correct page.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_LINKS              = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the XML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that\n# captures the structure of the code including all documentation.\n# The default value is: NO.\n\nGENERATE_XML           = NO\n\n# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: xml.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_OUTPUT             = xml\n\n# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program\n# listings (including syntax highlighting and cross-referencing information) to\n# the XML output. Note that enabling this will significantly increase the size\n# of the XML output.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_PROGRAMLISTING     = YES\n\n# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include\n# namespace members in file scope as well, matching the HTML output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_NS_MEMB_FILE_SCOPE = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the DOCBOOK output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files\n# that can be used to generate PDF.\n# The default value is: NO.\n\nGENERATE_DOCBOOK       = NO\n\n# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.\n# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in\n# front of it.\n# The default directory is: docbook.\n# This tag requires that the tag GENERATE_DOCBOOK is set to YES.\n\nDOCBOOK_OUTPUT         = docbook\n\n# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the\n# program listings (including syntax highlighting and cross-referencing\n# information) to the DOCBOOK output. Note that enabling this will significantly\n# increase the size of the DOCBOOK output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_DOCBOOK is set to YES.\n\nDOCBOOK_PROGRAMLISTING = NO\n\n#---------------------------------------------------------------------------\n# Configuration options for the AutoGen Definitions output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an\n# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures\n# the structure of the code including all documentation. Note that this feature\n# is still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_AUTOGEN_DEF   = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the Perl module output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module\n# file that captures the structure of the code including all documentation.\n#\n# Note that this feature is still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_PERLMOD       = NO\n\n# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary\n# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI\n# output from the Perl module output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_LATEX          = NO\n\n# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely\n# formatted so it can be parsed by a human reader. This is useful if you want to\n# understand what is going on. On the other hand, if this tag is set to NO, the\n# size of the Perl module output will be much smaller and Perl will parse it\n# just the same.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_PRETTY         = YES\n\n# The names of the make variables in the generated doxyrules.make file are\n# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful\n# so different doxyrules.make files included by the same Makefile don't\n# overwrite each other's variables.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_MAKEVAR_PREFIX =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the preprocessor\n#---------------------------------------------------------------------------\n\n# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all\n# C-preprocessor directives found in the sources and include files.\n# The default value is: YES.\n\nENABLE_PREPROCESSING   = YES\n\n# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names\n# in the source code. If set to NO, only conditional compilation will be\n# performed. Macro expansion can be done in a controlled way by setting\n# EXPAND_ONLY_PREDEF to YES.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nMACRO_EXPANSION        = NO\n\n# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then\n# the macro expansion is limited to the macros specified with the PREDEFINED and\n# EXPAND_AS_DEFINED tags.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_ONLY_PREDEF     = NO\n\n# If the SEARCH_INCLUDES tag is set to YES, the include files in the\n# INCLUDE_PATH will be searched if a #include is found.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSEARCH_INCLUDES        = YES\n\n# The INCLUDE_PATH tag can be used to specify one or more directories that\n# contain include files that are not input files but should be processed by the\n# preprocessor.\n# This tag requires that the tag SEARCH_INCLUDES is set to YES.\n\nINCLUDE_PATH           =\n\n# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard\n# patterns (like *.h and *.hpp) to filter out the header-files in the\n# directories. If left blank, the patterns specified with FILE_PATTERNS will be\n# used.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nINCLUDE_FILE_PATTERNS  =\n\n# The PREDEFINED tag can be used to specify one or more macro names that are\n# defined before the preprocessor is started (similar to the -D option of e.g.\n# gcc). The argument of the tag is a list of macros of the form: name or\n# name=definition (no spaces). If the definition and the \"=\" are omitted, \"=1\"\n# is assumed. To prevent a macro definition from being undefined via #undef or\n# recursively expanded use the := operator instead of the = operator.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nPREDEFINED             =\n\n# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this\n# tag can be used to specify a list of macro names that should be expanded. The\n# macro definition that is found in the sources will be used. Use the PREDEFINED\n# tag if you want to use a different macro definition that overrules the\n# definition found in the source code.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_AS_DEFINED      =\n\n# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will\n# remove all references to function-like macros that are alone on a line, have\n# an all uppercase name, and do not end with a semicolon. Such function macros\n# are typically used for boiler-plate code, and will confuse the parser if not\n# removed.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSKIP_FUNCTION_MACROS   = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to external references\n#---------------------------------------------------------------------------\n\n# The TAGFILES tag can be used to specify one or more tag files. For each tag\n# file the location of the external documentation should be added. The format of\n# a tag file without this location is as follows:\n# TAGFILES = file1 file2 ...\n# Adding location for the tag files is done as follows:\n# TAGFILES = file1=loc1 \"file2 = loc2\" ...\n# where loc1 and loc2 can be relative or absolute paths or URLs. See the\n# section \"Linking to external documentation\" for more information about the use\n# of tag files.\n# Note: Each tag file must have a unique name (where the name does NOT include\n# the path). If a tag file is not located in the directory in which doxygen is\n# run, you must also specify the path to the tagfile here.\n\nTAGFILES               =\n\n# When a file name is specified after GENERATE_TAGFILE, doxygen will create a\n# tag file that is based on the input files it reads. See section \"Linking to\n# external documentation\" for more information about the usage of tag files.\n\nGENERATE_TAGFILE       =\n\n# If the ALLEXTERNALS tag is set to YES, all external class will be listed in\n# the class index. If set to NO, only the inherited external classes will be\n# listed.\n# The default value is: NO.\n\nALLEXTERNALS           = NO\n\n# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed\n# in the modules index. If set to NO, only the current project's groups will be\n# listed.\n# The default value is: YES.\n\nEXTERNAL_GROUPS        = YES\n\n# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in\n# the related pages index. If set to NO, only the current project's pages will\n# be listed.\n# The default value is: YES.\n\nEXTERNAL_PAGES         = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to the dot tool\n#---------------------------------------------------------------------------\n\n# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram\n# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to\n# NO turns the diagrams off. Note that this option also works with HAVE_DOT\n# disabled, but it is recommended to install and use dot, since it yields more\n# powerful graphs.\n# The default value is: YES.\n\nCLASS_DIAGRAMS         = YES\n\n# You can include diagrams made with dia in doxygen documentation. Doxygen will\n# then run dia to produce the diagram and insert it in the documentation. The\n# DIA_PATH tag allows you to specify the directory where the dia binary resides.\n# If left empty dia is assumed to be found in the default search path.\n\nDIA_PATH               =\n\n# If set to YES the inheritance and collaboration graphs will hide inheritance\n# and usage relations if the target is undocumented or is not a class.\n# The default value is: YES.\n\nHIDE_UNDOC_RELATIONS   = YES\n\n# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is\n# available from the path. This tool is part of Graphviz (see:\n# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent\n# Bell Labs. The other options in this section have no effect if this option is\n# set to NO\n# The default value is: NO.\n\nHAVE_DOT               = YES\n\n# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed\n# to run in parallel. When set to 0 doxygen will base this on the number of\n# processors available in the system. You can set it explicitly to a value\n# larger than 0 to get control over the balance between CPU load and processing\n# speed.\n# Minimum value: 0, maximum value: 32, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_NUM_THREADS        = 0\n\n# When you want a differently looking font in the dot files that doxygen\n# generates you can specify the font name using DOT_FONTNAME. You need to make\n# sure dot is able to find the font, which can be done by putting it in a\n# standard location or by setting the DOTFONTPATH environment variable or by\n# setting DOT_FONTPATH to the directory containing the font.\n# The default value is: Helvetica.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTNAME           = Helvetica\n\n# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of\n# dot graphs.\n# Minimum value: 4, maximum value: 24, default value: 10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTSIZE           = 10\n\n# By default doxygen will tell dot to use the default font as specified with\n# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set\n# the path where dot can find it using this tag.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTPATH           =\n\n# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for\n# each documented class showing the direct and indirect inheritance relations.\n# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCLASS_GRAPH            = YES\n\n# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a\n# graph for each documented class showing the direct and indirect implementation\n# dependencies (inheritance, containment, and class references variables) of the\n# class with other documented classes.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCOLLABORATION_GRAPH    = YES\n\n# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for\n# groups, showing the direct groups dependencies.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGROUP_GRAPHS           = YES\n\n# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and\n# collaboration diagrams in a style similar to the OMG's Unified Modeling\n# Language.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nUML_LOOK               = NO\n\n# If the UML_LOOK tag is enabled, the fields and methods are shown inside the\n# class node. If there are many fields or methods and many nodes the graph may\n# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the\n# number of items for each type to make the size more manageable. Set this to 0\n# for no limit. Note that the threshold may be exceeded by 50% before the limit\n# is enforced. So when you set the threshold to 10, up to 15 fields may appear,\n# but if the number exceeds 15, the total amount of fields shown is limited to\n# 10.\n# Minimum value: 0, maximum value: 100, default value: 10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nUML_LIMIT_NUM_FIELDS   = 10\n\n# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and\n# collaboration graphs will show the relations between templates and their\n# instances.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nTEMPLATE_RELATIONS     = NO\n\n# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to\n# YES then doxygen will generate a graph for each documented file showing the\n# direct and indirect include dependencies of the file with other documented\n# files.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDE_GRAPH          = YES\n\n# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are\n# set to YES then doxygen will generate a graph for each documented file showing\n# the direct and indirect include dependencies of the file with other documented\n# files.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDED_BY_GRAPH      = YES\n\n# If the CALL_GRAPH tag is set to YES then doxygen will generate a call\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable call graphs for selected\n# functions only using the \\callgraph command. Disabling a call graph can be\n# accomplished by means of the command \\hidecallgraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALL_GRAPH             = NO\n\n# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable caller graphs for selected\n# functions only using the \\callergraph command. Disabling a caller graph can be\n# accomplished by means of the command \\hidecallergraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALLER_GRAPH           = NO\n\n# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical\n# hierarchy of all classes instead of a textual one.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGRAPHICAL_HIERARCHY    = YES\n\n# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the\n# dependencies a directory has on other directories in a graphical way. The\n# dependency relations are determined by the #include relations between the\n# files in the directories.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDIRECTORY_GRAPH        = YES\n\n# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images\n# generated by dot. For an explanation of the image formats see the section\n# output formats in the documentation of the dot tool (Graphviz (see:\n# http://www.graphviz.org/)).\n# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order\n# to make the SVG files visible in IE 9+ (other browsers do not have this\n# requirement).\n# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,\n# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and\n# png:gdiplus:gdiplus.\n# The default value is: png.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_IMAGE_FORMAT       = png\n\n# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to\n# enable generation of interactive SVG images that allow zooming and panning.\n#\n# Note that this requires a modern browser other than Internet Explorer. Tested\n# and working are Firefox, Chrome, Safari, and Opera.\n# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make\n# the SVG files visible. Older versions of IE do not have SVG support.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINTERACTIVE_SVG        = NO\n\n# The DOT_PATH tag can be used to specify the path where the dot tool can be\n# found. If left blank, it is assumed the dot tool can be found in the path.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_PATH               =\n\n# The DOTFILE_DIRS tag can be used to specify one or more directories that\n# contain dot files that are included in the documentation (see the \\dotfile\n# command).\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOTFILE_DIRS           =\n\n# The MSCFILE_DIRS tag can be used to specify one or more directories that\n# contain msc files that are included in the documentation (see the \\mscfile\n# command).\n\nMSCFILE_DIRS           =\n\n# The DIAFILE_DIRS tag can be used to specify one or more directories that\n# contain dia files that are included in the documentation (see the \\diafile\n# command).\n\nDIAFILE_DIRS           =\n\n# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the\n# path where java can find the plantuml.jar file. If left blank, it is assumed\n# PlantUML is not used or called during a preprocessing step. Doxygen will\n# generate a warning when it encounters a \\startuml command in this case and\n# will not generate output for the diagram.\n\nPLANTUML_JAR_PATH      =\n\n# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a\n# configuration file for plantuml.\n\nPLANTUML_CFG_FILE      =\n\n# When using plantuml, the specified paths are searched for files specified by\n# the !include statement in a plantuml block.\n\nPLANTUML_INCLUDE_PATH  =\n\n# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes\n# that will be shown in the graph. If the number of nodes in a graph becomes\n# larger than this value, doxygen will truncate the graph, which is visualized\n# by representing a node as a red box. Note that doxygen if the number of direct\n# children of the root node in a graph is already larger than\n# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that\n# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.\n# Minimum value: 0, maximum value: 10000, default value: 50.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_GRAPH_MAX_NODES    = 100\n\n# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs\n# generated by dot. A depth value of 3 means that only nodes reachable from the\n# root by following a path via at most 3 edges will be shown. Nodes that lay\n# further from the root node will be omitted. Note that setting this option to 1\n# or 2 may greatly reduce the computation time needed for large code bases. Also\n# note that the size of a graph can be further restricted by\n# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.\n# Minimum value: 0, maximum value: 1000, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nMAX_DOT_GRAPH_DEPTH    = 0\n\n# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent\n# background. This is disabled by default, because dot on Windows does not seem\n# to support this out of the box.\n#\n# Warning: Depending on the platform used, enabling this option may lead to\n# badly anti-aliased labels on the edges of a graph (i.e. they become hard to\n# read).\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_TRANSPARENT        = NO\n\n# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output\n# files in one run (i.e. multiple -o and -T options on the command line). This\n# makes dot run faster, but since only newer versions of dot (>1.8.10) support\n# this, this feature is disabled by default.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_MULTI_TARGETS      = NO\n\n# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page\n# explaining the meaning of the various boxes and arrows in the dot generated\n# graphs.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGENERATE_LEGEND        = YES\n\n# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot\n# files that are used to generate the various graphs.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_CLEANUP            = YES\n"
  },
  {
    "path": "emacs.el",
    "content": ";; Include this in your ~/.emacs file for editing Janus code in Emacs.\n\n;; Run \"M-x janus-mode\" to set the coding style for Janus.\n(defun janus-mode ()\n  \"Sets the Janus coding style in c-mode.\"\n  (interactive)\n  ;; c-basic-offset and tab-width can be set to personal preference as long as\n  ;; they are the same. They will expand to the single tab required by the Janus\n  ;; coding style.\n  (setq c-basic-offset 2\n        tab-width 2\n        indent-tabs-mode t)\n  (c-set-offset 'arglist-intro '+)\n  (c-set-offset 'arglist-cont-nonempty '+)\n  (c-set-offset 'case-label '+))\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import globals from 'globals';\nimport js from '@eslint/js';\nimport html from 'eslint-plugin-html'\n\nexport default [\n\t{\n\t\tfiles: [\n\t\t\t'**/*.html',\n\t\t\t'**/*.js'\n\t\t],\n\t\tplugins: {\n\t\t\thtml\n\t\t},\n\t\tlanguageOptions: {\n\t\t\tecmaVersion: 'latest',\n\t\t\tglobals: {\n\t\t\t\t...globals.browser,\n\t\t\t\t...globals.jquery,\n\t\t\t\t'adapter': 'readonly',\n\t\t\t\t'bootbox': 'readonly',\n\t\t\t\t'define': 'readonly',\n\t\t\t\t'module': 'readonly',\n\t\t\t\t'toastr': 'readonly',\n\t\t\t}\n\t\t},\n\t\trules: {\n\t\t\t...js.configs.recommended.rules,\n\t\t\t'no-empty': 'off',\n\t\t\t'no-unused-vars': [\n\t\t\t\t'warn',\n\t\t\t\t{\n\t\t\t\t\t'args': 'all',\n\t\t\t\t\t'vars': 'all',\n\t\t\t\t\t'caughtErrors': 'all',\n\t\t\t\t}\n\t\t\t],\n\t\t\t'indent': [\n\t\t\t\t'error',\n\t\t\t\t'tab',\n\t\t\t\t{\n\t\t\t\t\t'SwitchCase': 1\n\t\t\t\t}\n\t\t\t],\n\t\t}\n\t}\n];"
  },
  {
    "path": "fuzzers/build.sh",
    "content": "#!/bin/bash -eu\n\n# Load script configuration\nsource $(dirname $0)/config.sh\n\n# Set fuzzing environment\n# Fallback to local\nFUZZ_ENV=${FUZZ_ENV-$DEFAULT_ENV}\n\n# Set working paths from the environment\n# Fallback to values used for local testing\nSRC=${SRC-$DEFAULT_SRC}\nOUT=${OUT-$DEFAULT_OUT}\nWORK=${WORK-$DEFAULT_WORK}\nJANUSGW=${JANUSGW-$DEFAULT_JANUSGW}\n\n# Set compiler from the environment\n# Fallback to clang\nFUZZ_CC=${CC-$DEFAULT_CC}\n\n# Set linker from the environment (CXX is used as linker in oss-fuzz)\n# Fallback to clang\nFUZZ_CCLD=${CXX-${CC-$DEFAULT_CCLD}}\n\n# Set CFLAGS from the environment\n# Fallback to using address and undefined behaviour sanitizers\nFUZZ_CFLAGS=${CFLAGS-$DEFAULT_CFLAGS}\n# Allow users to optionally append extra CFLAGS\nECFLAGS=${ECFLAGS-\"\"}\nFUZZ_CFLAGS=\"${FUZZ_CFLAGS} ${ECFLAGS}\"\n\n# Set LDFLAGS from the environment (CXXFLAGS var is used for linker flags in oss-fuzz)\n# Fallback to using address and undefined behaviour sanitizers\nFUZZ_LDFLAGS=${CXXFLAGS-${LDFLAGS-$DEFAULT_LDFLAGS}}\n# Allow users to optionally append extra LDFLAGS\nELDFLAGS=${ELDFLAGS-\"\"}\nFUZZ_LDFLAGS=\"${FUZZ_LDFLAGS} ${ELDFLAGS}\"\n\n# Set fuzzing engine from the environment (optional)\nFUZZ_ENGINE=${LIB_FUZZING_ENGINE-\"\"}\n\n# Use shared libraries in local execution\nFUZZ_DEPS=\"$DEPS_LIB\"\nif [[ $FUZZ_ENV == \"local\" ]]; then\n\tFUZZ_DEPS=\"$DEPS_LIB_SHARED\"\nfi\n# Mess with the flags only in local execution\nif [[ $FUZZ_ENV == \"local\" &&  $FUZZ_CC == clang* ]]; then\n\t# For coverage testing with clang uncomment\n\t# \tFUZZ_CFLAGS=\"$COVERAGE_CFLAGS\"\n\t# \tFUZZ_LDFLAGS=\"$COVERAGE_LDFLAGS\"\n\n\t# Add fuzzer CFLAG only if not present\n\tif [[ ! $FUZZ_CFLAGS =~ .*-fsanitize=([^\\s].*)*fuzzer(-.*)* ]]; then\n\t\tFUZZ_CFLAGS=\"$FUZZ_CFLAGS -fsanitize=fuzzer-no-link\"\n\tfi\n\t# Add fuzzer LDFLAG only if not present\n\tif [[ ! $FUZZ_LDFLAGS =~ .*-fsanitize=([^\\s].*)*fuzzer(-.*)* ]]; then\n\t\t# Link against libFuzzer only if FUZZ_ENGINE has not been set\n\t\tif [[ ! -z $FUZZ_ENGINE ]]; then\n\t\t\tFUZZ_LDFLAGS=\"$FUZZ_LDFLAGS -fsanitize=fuzzer-no-link\"\n\t\telse\n\t\t\tFUZZ_LDFLAGS=\"$FUZZ_LDFLAGS -fsanitize=fuzzer\"\n\t\tfi\n\tfi\nfi\n\nrm -f $WORK/*.a $WORK/*.o\n\n# Build and archive necessary Janus objects\nJANUS_LIB=\"$WORK/janus-lib.a\"\npushd $SRC/$JANUSGW\n# Use this variable to skip Janus objects building\nSKIP_JANUS_BUILD=${SKIP_JANUS_BUILD-\"0\"}\nif [ \"$SKIP_JANUS_BUILD\" -eq \"0\" ]; then\n\techo \"Building Janus objects\"\n\t./autogen.sh\n\t./configure CC=\"$FUZZ_CC\" CFLAGS=\"$FUZZ_CFLAGS\" $JANUS_CONF_FLAGS\n\tpushd src\n\tmake clean\n\tmake -j$(nproc) $JANUS_OBJECTS\n\tpopd\nfi\npushd src\nar rcs $JANUS_LIB $JANUS_OBJECTS\npopd\npopd\n\n# Build standalone fuzzing engines\nengines=$(find $SRC/$JANUSGW/fuzzers/engines/ -name \"*.c\")\nfor sourceFile in $engines; do\n  name=$(basename $sourceFile .c)\n  echo \"Building engine: $name\"\n  $FUZZ_CC -c $FUZZ_CFLAGS $sourceFile -o $WORK/$name.o\ndone\n\n# Build Fuzzers\nmkdir -p $OUT\nfuzzers=$(find $SRC/$JANUSGW/fuzzers/ -name \"*.c\" | grep -v \"engines/\")\nfor sourceFile in $fuzzers; do\n  name=$(basename $sourceFile .c)\n  echo \"Building fuzzer: $name\"\n\n  $FUZZ_CC -c $FUZZ_CFLAGS $DEPS_CFLAGS -I. -I$SRC/$JANUSGW/src $sourceFile -o $WORK/$name.o\n  $FUZZ_CCLD $FUZZ_LDFLAGS $WORK/${name}.o -o $OUT/${name} $FUZZ_ENGINE $JANUS_LIB $FUZZ_DEPS\n\n  if [ -d \"$SRC/$JANUSGW/fuzzers/corpora/${name}\" ]; then\n\techo \"Exporting corpus: $name \"\n\tzip -jqr --exclude=*LICENSE* $OUT/${name}_seed_corpus.zip $SRC/$JANUSGW/fuzzers/corpora/${name}\n  fi\ndone\n"
  },
  {
    "path": "fuzzers/config.sh",
    "content": "#!/bin/bash\n\nSCRIPTPATH=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\n\n# Default environment\nDEFAULT_ENV=\"local\"\n\n# Working paths\nDEFAULT_SRC=\"$(dirname $(dirname $SCRIPTPATH))\"\nDEFAULT_OUT=\"$SCRIPTPATH/out\"\nDEFAULT_WORK=\"$SCRIPTPATH\"\nDEFAULT_JANUSGW=\"janus-gateway\"\n\n# CFLAGS and LDFLAGS for local fuzzing\nDEFAULT_CC=\"clang\"\nDEFAULT_CCLD=$DEFAULT_CC\nDEFAULT_CFLAGS=\"-O1 -fno-omit-frame-pointer -g -ggdb3 -fsanitize=address,undefined -fsanitize-address-use-after-scope -fno-sanitize-recover=undefined -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION\"\nDEFAULT_LDFLAGS=\"-O1 -fno-omit-frame-pointer -g -ggdb3 -fsanitize=address,undefined -fno-sanitize-recover=undefined -fsanitize-address-use-after-scope\"\nCOVERAGE_CFLAGS=\"-O1 -fno-omit-frame-pointer -g -ggdb3 -fprofile-instr-generate -fcoverage-mapping -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION\"\nCOVERAGE_LDFLAGS=\"-O1 -fno-omit-frame-pointer -g -ggdb3 -fprofile-instr-generate -fcoverage-mapping -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION\"\n\n# Janus configure flags\nJANUS_CONF_FLAGS=\"--disable-docs --disable-post-processing --disable-turn-rest-api --disable-all-transports --disable-all-plugins --disable-all-handlers --disable-data-channels\"\n\n# Janus objects needed for fuzzing\nJANUS_OBJECTS=\"janus-log.o janus-utils.o janus-rtcp.o janus-rtp.o janus-sdp-utils.o\"\n\n# CFLAGS for fuzzer dependencies\nDEPS_CFLAGS=\"$(pkg-config --cflags glib-2.0)\"\n\n# Libraries to link in with fuzzers\nDEPS_LIB=\"-Wl,-Bstatic $(pkg-config --libs glib-2.0 jansson) -pthread -Wl,-Bdynamic\"\nDEPS_LIB_SHARED=\"$(pkg-config --libs glib-2.0 jansson zlib) -pthread -lm\"\n"
  },
  {
    "path": "fuzzers/corpora/rtcp_fuzzer/2webrtc/47.rtcp",
    "content": ""
  },
  {
    "path": "fuzzers/corpora/rtcp_fuzzer/2webrtc/55.rtcp",
    "content": ""
  },
  {
    "path": "fuzzers/corpora/rtcp_fuzzer/2webrtc/LICENSE",
    "content": "Copyright (c) 2011, The WebRTC project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n  * Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n  * Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in\n    the documentation and/or other materials provided with the\n    distribution.\n  * Neither the name of Google nor the names of its contributors may\n    be used to endorse or promote products derived from this software\n    without specific prior written permission.\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "fuzzers/corpora/rtp_fuzzer/1meetecho/rtp_fuzzer-crash-7e2d460edd5d5d7f5548922f10489f468d1638bf",
    "content": "(print_fusDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/1meetecho/sdp_fuzzer-timeout-26f23f1364a16565e2f339c3297f71c275b6b0cf",
    "content": "v=0\ns=~\no=R -32767-4INIP4 \na=d\na=Y\na=d\na=d\na=d\na=0\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=e\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=RTP=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\n=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=RTP=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=da=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=dd\na=d\na=a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\n󠁗a=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na==\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\nt=-1-1\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na =d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=d\na=l\na=d\nm=video  1 \u0004-"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/1meetecho/sdp_fuzzer-timeout-80e8c427e72e9f33f02c8a088caab69d9d97702c",
    "content": "v=0.t\nm=audio 9 UDP/T                                                                                                                                                                                                                                       atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                                                                                                                                                                        atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                           \u0018                                                             :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                       - Y                      0                                                                                                                                                                                                                                                                                                                  - Y                                                      `                                                                                                                                                                                                                       :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                           - Y                                                      `                                                                                                                               :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                                                                                                                                                                                                          :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                                                                                                                                                                        atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                           \u0018                                                             :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                       - Y                      0                                                                                                                                                                                                                                                                                                                  - Y                                                      `                                                                                                                                                                                                                       :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                           - Y                                                      `                                                                                                                               :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                           - Y                                                      `                                                                                                                               :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                                                                                                                                                                                                          :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                                                                                                                                                                        atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                           \u0018                                                             :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                       - Y                      0                                                                                                                                                                                                                                                                                                                  - Y                                                      `                                                                                                                                                                                                                       :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                           - Y                                                      `                                                                                                                               :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                        - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                                                                                         :                                                                                                                                       - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                       - Y                      0                                                                                                                                           :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                              - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                        - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                                                                                         :                                                                                                                                       - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                       - Y                      0                                                                                                                                           :                                                              `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                           - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                        - Y                                                      `                                                                                                /recvonly                                                                                                                                    - Y                                                      `                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       0                                                                                                                                                                                                                                                                                         :                                                              `                                                                                                                                                                                                                                                                                                                                                                                              atdi9bd-3                                                                                                                                                                           :                                                              `                                                                                                                                       1                                                                                                                                                                                                                                                                                         :                                                                                                                                       - Y                                                      `                                                                                                                                                                                                                                    - Y                                                      `                         tvion                                                                                                                                                                                                                                                                                                                                          - Y                      0                                                                                                                                                n\u0007"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/10.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 97\na=rtpmap:0 PCMU/8000\na=rtpmap:97 iLBC/8000\nm=audio 49172 RTP/AVP 98\na=rtpmap:98 telephone-event/8000\na=sendonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/11.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=audio 49174 RTP/AVP 98\na=rtpmap:98 telephone-event/8000\na=recvonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/12.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 51372 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/13.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49174 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 49170 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/14.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 newhost.biloxi.example.com\nt=0 0\nm=audio 49178 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 49188 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/15.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 51372 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/16.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=audio 51372 RTP/AVP 97 101\na=rtpmap:97 iLBC/8000\na=rtpmap:101 telephone-event/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/17.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 0 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=audio 49170 RTP/AVP 97 101\na=rtpmap:97 iLBC/8000\na=rtpmap:101 telephone-event/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/18.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 99\na=rtpmap:99 iLBC/8000\nm=video 51372 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/19.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 99\na=rtpmap:99 iLBC/8000\nm=video 51374 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/2.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 8 97\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=rtpmap:97 iLBC/8000\nm=video 51372 RTP/AVP 31 32\na=rtpmap:31 H261/90000\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/20.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 99\na=rtpmap:99 iLBC/8000\nm=video 51372 RTP/AVP 31 32\na=rtpmap:31 H261/90000\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/21.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 99\na=rtpmap:99 iLBC/8000\nm=video 51374 RTP/AVP 31 32\na=rtpmap:31 H261/90000\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/22.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 8 97\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=rtpmap:97 iLBC/8000\nm=video 51372 RTP/AVP 31 32\na=rtpmap:31 H261/90000\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/23.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49174 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=video 49172 RTP/AVP 32\nc=IN IP4 otherhost.biloxi.example.com\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/24.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 97\na=rtpmap:0 PCMU/8000\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/25.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 placeholder.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 97\na=rtpmap:97 iLBC/8000\na=sendonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/26.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/27.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49178 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/28.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 97\na=rtpmap:0 PCMU/8000\na=rtpmap:97 iLBC/8000\nm=audio 49172 RTP/AVP 98\na=rtpmap:98 telephone-event/8000\na=sendonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/29.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=audio 49174 RTP/AVP 98\na=rtpmap:98 telephone-event/8000\na=recvonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/3.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49174 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=video 49170 RTP/AVP 32\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/30.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 97\na=rtpmap:97 iLBC/8000\na=sendonly\nm=audio 49174 RTP/AVP 98\na=rtpmap:98 telephone-event/8000\na=recvonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/31.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 97\na=rtpmap:0 PCMU/8000\na=rtpmap:97 iLBC/8000\nm=audio 49172 RTP/AVP 98\na=rtpmap:98 telephone-event/8000\na=sendonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/32.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 97\na=rtpmap:0 PCMU/8000\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/33.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/34.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=audio 48282 RTP/AVP 98\nc=IN IP4 mediaserver.biloxi.example.com\na=rtpmap:98 telephone-event/8000\na=recvonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/35.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=audio 49172 RTP/AVP 98\nc=IN IP4 host.atlanta.example.com\na=rtpmap:98 telephone-event/8000\na=sendonly\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/36.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0\na=rtpmap:0 PCMU/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/37.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 0\na=rtpmap:0 PCMU/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/38.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=video 49172 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/39.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=video 49168 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/4.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 8 97\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=rtpmap:97 iLBC/8000\nm=video 51372 RTP/AVP 31 32\na=rtpmap:31 H261/90000\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/40.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 51372 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/41.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49174 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 49170 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/42.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49174 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 0 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/43.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\nm=video 0 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/44.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/45.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/46.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/47.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/48.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 0.0.0.0\nt=0 0\nm=audio 23442 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/49.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/5.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 0 8\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\nm=video 0 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/50.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/51.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/52.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/53.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 0.0.0.0\nt=0 0\nm=audio 9322 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/54.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/55.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 97\na=rtpmap:97 iLBC/8000\n\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/6.sdp",
    "content": "v=0\no=alice 2890844526 2890844527 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 51372 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=video 0 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/7.sdp",
    "content": "v=0\no=bob 2808844564 2808844565 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 0\na=rtpmap:0 PCMU/8000\nm=video 0 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/8.sdp",
    "content": "v=0\no=alice 2890844526 2890844526 IN IP4 host.atlanta.example.com\ns=\nc=IN IP4 host.atlanta.example.com\nt=0 0\nm=audio 49170 RTP/AVP 0 8 97\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=rtpmap:97 iLBC/8000\nm=video 51372 RTP/AVP 31 32\na=rtpmap:31 H261/90000\na=rtpmap:32 MPV/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/9.sdp",
    "content": "v=0\no=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com\ns=\nc=IN IP4 host.biloxi.example.com\nt=0 0\nm=audio 49172 RTP/AVP 99\na=rtpmap:99 iLBC/8000\nm=video 51374 RTP/AVP 31\na=rtpmap:31 H261/90000\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/firefox-1.sdp",
    "content": "v=0\no=mozilla...THIS_IS_SDPARTA-46.0.1 5115930144083302970 0 IN IP4 0.0.0.0\ns=-\nt=0 0\na=fingerprint:sha-256 24:67:5E:1B:9A:B9:CF:36:C5:30:8F:35:F7:B1:50:66:88:81:92:CB:29:BA:53:A5:02:C8:0A:A5:4E:9C:AE:D9\na=group:BUNDLE sdparta_0 sdparta_1 sdparta_2\na=ice-options:trickle\na=msid-semantic:WMS *\nm=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8\nc=IN IP4 0.0.0.0\na=sendrecv\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\na=fmtp:109 maxplaybackrate=48000;stereo=1\na=ice-pwd:b9f3911b591ae61e2d7f6af0531fd2a3\na=ice-ufrag:3edc9012\na=mid:sdparta_0\na=msid:{258e92fb-547c-40ca-92e9-efe0cedb4cba} {bd1fafff-bfd0-40d4-b0a3-2a87cff307ee}\na=rtcp-mux\na=rtpmap:109 opus/48000/2\na=rtpmap:9 G722/8000/1\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=setup:actpass\na=ssrc:2121669360 cname:{387b0735-bde2-43a4-8484-7f5663b60f24}\nm=video 9 UDP/TLS/RTP/SAVPF 120 126 97\nc=IN IP4 0.0.0.0\na=sendrecv\na=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1\na=fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1\na=fmtp:120 max-fs=12288;max-fr=60\na=ice-pwd:b9f3911b591ae61e2d7f6af0531fd2a3\na=ice-ufrag:3edc9012\na=mid:sdparta_1\na=msid:{258e92fb-547c-40ca-92e9-efe0cedb4cba} {9e8f5867-a9aa-4489-8bd4-3a8a57a5e592}\na=rtcp-fb:120 nack\na=rtcp-fb:120 nack pli\na=rtcp-fb:120 ccm fir\na=rtcp-fb:126 nack\na=rtcp-fb:126 nack pli\na=rtcp-fb:126 ccm fir\na=rtcp-fb:97 nack\na=rtcp-fb:97 nack pli\na=rtcp-fb:97 ccm fir\na=rtcp-mux\na=rtpmap:120 VP8/90000\na=rtpmap:126 H264/90000\na=rtpmap:97 H264/90000\na=setup:actpass\na=ssrc:2158832026 cname:{387b0735-bde2-43a4-8484-7f5663b60f24}\nm=application 9 DTLS/SCTP 5000\nc=IN IP4 0.0.0.0\na=sendrecv\na=ice-pwd:b9f3911b591ae61e2d7f6af0531fd2a3\na=ice-ufrag:3edc9012\na=mid:sdparta_2\na=sctpmap:5000 webrtc-datachannel 256\na=setup:actpass\na=ssrc:2670959794 cname:{387b0735-bde2-43a4-8484-7f5663b60f24}\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/firefox-2.sdp",
    "content": "v=0\no=mozilla...THIS_IS_SDPARTA-46.0.1 3068771576687940834 0 IN IP4 0.0.0.0\ns=-\nt=0 0\na=fingerprint:sha-256 AD:87:B3:11:E4:E2:BA:EF:D2:3F:2E:AC:24:57:8E:DC:1F:67:41:29:44:C4:96:E3:62:90:CC:90:59:CA:2C:84\na=group:BUNDLE sdparta_0 sdparta_1 sdparta_2\na=ice-options:trickle\na=msid-semantic:WMS *\nm=audio 9 UDP/TLS/RTP/SAVPF 109\nc=IN IP4 0.0.0.0\na=recvonly\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\na=fmtp:109 maxplaybackrate=48000;stereo=1\na=ice-pwd:ff4c4dc6fe92e1f22d2c10352d8967d5\na=ice-ufrag:a539544b\na=mid:sdparta_0\na=rtcp-mux\na=rtpmap:109 opus/48000/2\na=setup:active\na=ssrc:600995474 cname:{5b598a29-a81b-4ffe-a2c5-507778057e7a}\nm=video 9 UDP/TLS/RTP/SAVPF 120\nc=IN IP4 0.0.0.0\na=recvonly\na=fmtp:120 max-fs=12288;max-fr=60\na=ice-pwd:ff4c4dc6fe92e1f22d2c10352d8967d5\na=ice-ufrag:a539544b\na=mid:sdparta_1\na=rtcp-fb:120 nack\na=rtcp-fb:120 nack pli\na=rtcp-fb:120 ccm fir\na=rtcp-mux\na=rtpmap:120 VP8/90000\na=setup:active\na=ssrc:3480150809 cname:{5b598a29-a81b-4ffe-a2c5-507778057e7a}\nm=application 9 DTLS/SCTP 5000\nc=IN IP4 0.0.0.0\na=sendrecv\na=ice-pwd:ff4c4dc6fe92e1f22d2c10352d8967d5\na=ice-ufrag:a539544b\na=mid:sdparta_2\na=sctpmap:5000 webrtc-datachannel 256\na=setup:active\na=ssrc:3021788991 cname:{5b598a29-a81b-4ffe-a2c5-507778057e7a}\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/opera-1.sdp",
    "content": "v=0\no=- 1656229333038673902 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE audio video data\na=msid-semantic: WMS Ppsa09YmDLBombOh5e8HqfqxEIPF69a46Hd4\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:1Jyk4q3nLIL5NiMx\na=ice-pwd:GL8/iarMqPIhImfnsG2dyXlH\na=fingerprint:sha-256 5A:16:96:94:B2:AC:60:27:64:C5:FE:46:6C:02:C0:CD:49:E3:E2:0B:5B:C9:D4:86:C4:B3:A4:F2:23:80:7A:DA\na=setup:actpass\na=mid:audio\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=sendrecv\na=rtcp-mux\na=rtpmap:111 opus/48000/2\na=rtcp-fb:111 transport-cc\na=fmtp:111 minptime=10; useinbandfec=1\na=rtpmap:103 ISAC/16000\na=rtpmap:104 ISAC/32000\na=rtpmap:9 G722/8000\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=rtpmap:106 CN/32000\na=rtpmap:105 CN/16000\na=rtpmap:13 CN/8000\na=rtpmap:126 telephone-event/8000\na=maxptime:60\na=ssrc:2233075910 cname:VhHMGYCjn4alR9zP\na=ssrc:2233075910 msid:Ppsa09YmDLBombOh5e8HqfqxEIPF69a46Hd4 689d3496-0896-4d52-bce6-8e90512a368b\na=ssrc:2233075910 mslabel:Ppsa09YmDLBombOh5e8HqfqxEIPF69a46Hd4\na=ssrc:2233075910 label:689d3496-0896-4d52-bce6-8e90512a368b\nm=video 9 UDP/TLS/RTP/SAVPF 100 101 116 117 96 97 98\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:1Jyk4q3nLIL5NiMx\na=ice-pwd:GL8/iarMqPIhImfnsG2dyXlH\na=fingerprint:sha-256 5A:16:96:94:B2:AC:60:27:64:C5:FE:46:6C:02:C0:CD:49:E3:E2:0B:5B:C9:D4:86:C4:B3:A4:F2:23:80:7A:DA\na=setup:actpass\na=mid:video\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=extmap:4 urn:3gpp:video-orientation\na=sendrecv\na=rtcp-mux\na=rtpmap:100 VP8/90000\na=rtcp-fb:100 ccm fir\na=rtcp-fb:100 nack\na=rtcp-fb:100 nack pli\na=rtcp-fb:100 goog-remb\na=rtcp-fb:100 transport-cc\na=rtpmap:101 VP9/90000\na=rtcp-fb:101 ccm fir\na=rtcp-fb:101 nack\na=rtcp-fb:101 nack pli\na=rtcp-fb:101 goog-remb\na=rtcp-fb:101 transport-cc\na=rtpmap:116 red/90000\na=rtpmap:117 ulpfec/90000\na=rtpmap:96 rtx/90000\na=fmtp:96 apt=100\na=rtpmap:97 rtx/90000\na=fmtp:97 apt=101\na=rtpmap:98 rtx/90000\na=fmtp:98 apt=116\na=ssrc-group:FID 50498894 2399294607\na=ssrc:50498894 cname:VhHMGYCjn4alR9zP\na=ssrc:50498894 msid:Ppsa09YmDLBombOh5e8HqfqxEIPF69a46Hd4 1aef96f4-fc4c-4f86-98f3-0fbf4f625f70\na=ssrc:50498894 mslabel:Ppsa09YmDLBombOh5e8HqfqxEIPF69a46Hd4\na=ssrc:50498894 label:1aef96f4-fc4c-4f86-98f3-0fbf4f625f70\na=ssrc:2399294607 cname:VhHMGYCjn4alR9zP\na=ssrc:2399294607 msid:Ppsa09YmDLBombOh5e8HqfqxEIPF69a46Hd4 1aef96f4-fc4c-4f86-98f3-0fbf4f625f70\na=ssrc:2399294607 mslabel:Ppsa09YmDLBombOh5e8HqfqxEIPF69a46Hd4\na=ssrc:2399294607 label:1aef96f4-fc4c-4f86-98f3-0fbf4f625f70\nm=application 9 DTLS/SCTP 5000\nc=IN IP4 0.0.0.0\na=ice-ufrag:1Jyk4q3nLIL5NiMx\na=ice-pwd:GL8/iarMqPIhImfnsG2dyXlH\na=fingerprint:sha-256 5A:16:96:94:B2:AC:60:27:64:C5:FE:46:6C:02:C0:CD:49:E3:E2:0B:5B:C9:D4:86:C4:B3:A4:F2:23:80:7A:DA\na=setup:actpass\na=mid:data\na=sctpmap:5000 webrtc-datachannel 1024\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/opera-2.sdp",
    "content": "v=0\no=- 2013283641453412290 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE audio video data\na=msid-semantic: WMS\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:YVa3KTlFDCwsfPOQ\na=ice-pwd:ByUn1Od88VokVM0rtQ/bbeZa\na=fingerprint:sha-256 5A:16:96:94:B2:AC:60:27:64:C5:FE:46:6C:02:C0:CD:49:E3:E2:0B:5B:C9:D4:86:C4:B3:A4:F2:23:80:7A:DA\na=setup:active\na=mid:audio\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=recvonly\na=rtcp-mux\na=rtpmap:111 opus/48000/2\na=rtcp-fb:111 transport-cc\na=fmtp:111 minptime=10; useinbandfec=1\na=rtpmap:103 ISAC/16000\na=rtpmap:104 ISAC/32000\na=rtpmap:9 G722/8000\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=rtpmap:106 CN/32000\na=rtpmap:105 CN/16000\na=rtpmap:13 CN/8000\na=rtpmap:126 telephone-event/8000\na=maxptime:60\nm=video 9 UDP/TLS/RTP/SAVPF 100 101 116 117 96 97 98\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:YVa3KTlFDCwsfPOQ\na=ice-pwd:ByUn1Od88VokVM0rtQ/bbeZa\na=fingerprint:sha-256 5A:16:96:94:B2:AC:60:27:64:C5:FE:46:6C:02:C0:CD:49:E3:E2:0B:5B:C9:D4:86:C4:B3:A4:F2:23:80:7A:DA\na=setup:active\na=mid:video\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=extmap:4 urn:3gpp:video-orientation\na=recvonly\na=rtcp-mux\na=rtpmap:100 VP8/90000\na=rtcp-fb:100 ccm fir\na=rtcp-fb:100 nack\na=rtcp-fb:100 nack pli\na=rtcp-fb:100 goog-remb\na=rtcp-fb:100 transport-cc\na=rtpmap:101 VP9/90000\na=rtcp-fb:101 ccm fir\na=rtcp-fb:101 nack\na=rtcp-fb:101 nack pli\na=rtcp-fb:101 goog-remb\na=rtcp-fb:101 transport-cc\na=rtpmap:116 red/90000\na=rtpmap:117 ulpfec/90000\na=rtpmap:96 rtx/90000\na=fmtp:96 apt=100\na=rtpmap:97 rtx/90000\na=fmtp:97 apt=101\na=rtpmap:98 rtx/90000\na=fmtp:98 apt=116\nm=application 9 DTLS/SCTP 5000\nc=IN IP4 0.0.0.0\nb=AS:30\na=ice-ufrag:YVa3KTlFDCwsfPOQ\na=ice-pwd:ByUn1Od88VokVM0rtQ/bbeZa\na=fingerprint:sha-256 5A:16:96:94:B2:AC:60:27:64:C5:FE:46:6C:02:C0:CD:49:E3:E2:0B:5B:C9:D4:86:C4:B3:A4:F2:23:80:7A:DA\na=setup:active\na=mid:data\na=sctpmap:5000 webrtc-datachannel 1024\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-1.sdp",
    "content": "v=0\no=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\ns=-\nt=0 0\na=msid-semantic: WMS local_stream_1\nm=audio 2345 RTP/SAVPF 111 103 104\nc=IN IP4 74.125.127.126\na=rtcp:2347 IN IP4 74.125.127.126\na=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host generation 2\na=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1235 typ host generation 2\na=candidate:a0+B/2 1 udp 2130706432 ::1 1238 typ host generation 2\na=candidate:a0+B/2 2 udp 2130706432 ::1 1239 typ host generation 2\na=candidate:a0+B/3 1 udp 2130706432 74.125.127.126 2345 typ srflx raddr 192.168.1.5 rport 2346 generation 2\na=candidate:a0+B/3 2 udp 2130706432 74.125.127.126 2347 typ srflx raddr 192.168.1.5 rport 2348 generation 2\na=ice-ufrag:ufrag_voice\na=ice-pwd:pwd_voice\na=mid:audio_content_name\na=sendrecv\na=rtcp-mux\na=rtcp-rsize\na=crypto:1 AES_CM_128_HMAC_SHA1_32 inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 dummy_session_params\na=rtpmap:111 opus/48000/2\na=rtpmap:103 ISAC/16000\na=rtpmap:104 ISAC/32000\na=ssrc:1 cname:stream_1_cname\na=ssrc:1 msid:local_stream_1 audio_track_id_1\na=ssrc:1 mslabel:local_stream_1\na=ssrc:1 label:audio_track_id_1\nm=video 3457 RTP/SAVPF 120\nc=IN IP4 74.125.224.39\na=rtcp:3456 IN IP4 74.125.224.39\na=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1236 typ host generation 2\na=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1237 typ host generation 2\na=candidate:a0+B/2 2 udp 2130706432 ::1 1240 typ host generation 2\na=candidate:a0+B/2 1 udp 2130706432 ::1 1241 typ host generation 2\na=candidate:a0+B/4 2 udp 2130706432 74.125.224.39 3456 typ relay generation 2\na=candidate:a0+B/4 1 udp 2130706432 74.125.224.39 3457 typ relay generation 2\na=ice-ufrag:ufrag_video\na=ice-pwd:pwd_video\na=mid:video_content_name\na=sendrecv\na=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\na=rtpmap:120 VP8/90000\na=ssrc-group:FEC 2 3\na=ssrc:2 cname:stream_1_cname\na=ssrc:2 msid:local_stream_1 video_track_id_1\na=ssrc:2 mslabel:local_stream_1\na=ssrc:2 label:video_track_id_1\na=ssrc:3 cname:stream_1_cname\na=ssrc:3 msid:local_stream_1 video_track_id_1\na=ssrc:3 mslabel:local_stream_1\na=ssrc:3 label:video_track_id_1\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-2.sdp",
    "content": "v=0\no=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\ns=-\nt=0 0\na=msid-semantic: WMS local_stream_1\nm=audio 9 RTP/SAVPF 111 103 104\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:ufrag_voice\na=ice-pwd:pwd_voice\na=mid:audio_content_name\na=sendrecv\na=rtcp-mux\na=rtcp-rsize\na=crypto:1 AES_CM_128_HMAC_SHA1_32 inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 dummy_session_params\na=rtpmap:111 opus/48000/2\na=rtpmap:103 ISAC/16000\na=rtpmap:104 ISAC/32000\na=ssrc:1 cname:stream_1_cname\na=ssrc:1 msid:local_stream_1 audio_track_id_1\na=ssrc:1 mslabel:local_stream_1\na=ssrc:1 label:audio_track_id_1\nm=video 9 RTP/SAVPF 120\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:ufrag_video\na=ice-pwd:pwd_video\na=mid:video_content_name\na=sendrecv\na=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\na=rtpmap:120 VP8/90000\na=ssrc-group:FEC 2 3\na=ssrc:2 cname:stream_1_cname\na=ssrc:2 msid:local_stream_1 video_track_id_1\na=ssrc:2 mslabel:local_stream_1\na=ssrc:2 label:video_track_id_1\na=ssrc:3 cname:stream_1_cname\na=ssrc:3 msid:local_stream_1 video_track_id_1\na=ssrc:3 mslabel:local_stream_1\na=ssrc:3 label:video_track_id_1\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-3.sdp",
    "content": "m=application 9 RTP/SAVPF 101\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:ufrag_data\na=ice-pwd:pwd_data\na=mid:data_content_name\na=sendrecv\na=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:FvLcvU2P3ZWmQxgPAgcDu7Zl9vftYElFOjEzhWs5\na=rtpmap:101 google-data/90000\na=ssrc:10 cname:data_channel_cname\na=ssrc:10 msid:data_channel data_channeld0\na=ssrc:10 mslabel:data_channel\na=ssrc:10 label:data_channeld0\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-4.sdp",
    "content": "v=0\no=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\ns=-\nt=0 0\na=msid-semantic: WMS\nm=audio 9 RTP/SAVPF 111 103 104\nc=IN IP4 0.0.0.0\na=x-google-flag:conference\nm=video 9 RTP/SAVPF 120\nc=IN IP4 0.0.0.0\na=x-google-flag:conference\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-5.sdp",
    "content": "v=0\no=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\ns=-\nt=0 0\na=msid-semantic: WMS local_stream\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-6.sdp",
    "content": "m=audio 9 RTP/SAVPF 111\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:ufrag_voice\na=ice-pwd:pwd_voice\na=mid:audio_content_name\na=sendrecv\na=rtpmap:111 opus/48000/2\na=ssrc:1 cname:stream_1_cname\na=ssrc:1 msid:local_stream audio_track_id_1\na=ssrc:1 mslabel:local_stream\na=ssrc:1 label:audio_track_id_1\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-7.sdp",
    "content": "m=video 9 RTP/SAVPF 120\nc=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0\na=ice-ufrag:ufrag_video\na=ice-pwd:pwd_video\na=mid:video_content_name\na=sendrecv\na=rtpmap:120 VP8/90000\na=ssrc:2 cname:stream_1_cname\na=ssrc:2 msid:local_stream video_track_id_1\na=ssrc:2 mslabel:local_stream\na=ssrc:2 label:video_track_id_1\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-8.sdp",
    "content": "v=0\no=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\ns=-\nt=0 0\na=msid-semantic: WMS local_stream_1 local_stream_2\nm=audio 2345 RTP/SAVPF 111 103 104\nc=IN IP4 74.125.127.126\na=rtcp:2347 IN IP4 74.125.127.126\na=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host generation 2\na=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1235 typ host generation 2\na=candidate:a0+B/2 1 udp 2130706432 ::1 1238 typ host generation 2\na=candidate:a0+B/2 2 udp 2130706432 ::1 1239 typ host generation 2\na=candidate:a0+B/3 1 udp 2130706432 74.125.127.126 2345 typ srflx raddr 192.168.1.5 rport 2346 generation 2\na=candidate:a0+B/3 2 udp 2130706432 74.125.127.126 2347 typ srflx raddr 192.168.1.5 rport 2348 generation 2\na=ice-ufrag:ufrag_voice\na=ice-pwd:pwd_voice\na=mid:audio_content_name\na=sendrecv\na=rtcp-mux\na=rtcp-rsize\na=crypto:1 AES_CM_128_HMAC_SHA1_32 inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 dummy_session_params\na=rtpmap:111 opus/48000/2\na=rtpmap:103 ISAC/16000\na=rtpmap:104 ISAC/32000\na=ssrc:1 cname:stream_1_cname\na=ssrc:1 msid:local_stream_1 audio_track_id_1\na=ssrc:1 mslabel:local_stream_1\na=ssrc:1 label:audio_track_id_1\na=ssrc:4 cname:stream_2_cname\na=ssrc:4 msid:local_stream_2 audio_track_id_2\na=ssrc:4 mslabel:local_stream_2\na=ssrc:4 label:audio_track_id_2\nm=video 3457 RTP/SAVPF 120\nc=IN IP4 74.125.224.39\na=rtcp:3456 IN IP4 74.125.224.39\na=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1236 typ host generation 2\na=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1237 typ host generation 2\na=candidate:a0+B/2 2 udp 2130706432 ::1 1240 typ host generation 2\na=candidate:a0+B/2 1 udp 2130706432 ::1 1241 typ host generation 2\na=candidate:a0+B/4 2 udp 2130706432 74.125.224.39 3456 typ relay generation 2\na=candidate:a0+B/4 1 udp 2130706432 74.125.224.39 3457 typ relay generation 2\na=ice-ufrag:ufrag_video\na=ice-pwd:pwd_video\na=mid:video_content_name\na=sendrecv\na=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\na=rtpmap:120 VP8/90000\na=ssrc-group:FEC 2 3\na=ssrc:2 cname:stream_1_cname\na=ssrc:2 msid:local_stream_1 video_track_id_1\na=ssrc:2 mslabel:local_stream_1\na=ssrc:2 label:video_track_id_1\na=ssrc:3 cname:stream_1_cname\na=ssrc:3 msid:local_stream_1 video_track_id_1\na=ssrc:3 mslabel:local_stream_1\na=ssrc:3 label:video_track_id_1\na=ssrc:5 cname:stream_2_cname\na=ssrc:5 msid:local_stream_2 video_track_id_2\na=ssrc:5 mslabel:local_stream_2\na=ssrc:5 label:video_track_id_2\na=ssrc:6 cname:stream_2_cname\na=ssrc:6 msid:local_stream_2 video_track_id_3\na=ssrc:6 mslabel:local_stream_2\na=ssrc:6 label:video_track_id_3\n"
  },
  {
    "path": "fuzzers/corpora/sdp_fuzzer/2webrtc/unittest-9.sdp",
    "content": "v=0\no=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\ns=-\nt=0 0\na=msid-semantic: WMS local_stream_1 local_stream_2\nm=audio 2345 RTP/SAVPF 111 103 104\nc=IN IP4 74.125.127.126\na=rtcp:2347 IN IP4 74.125.127.126\na=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host generation 2\na=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1235 typ host generation 2\na=candidate:a0+B/2 1 udp 2130706432 ::1 1238 typ host generation 2\na=candidate:a0+B/2 2 udp 2130706432 ::1 1239 typ host generation 2\na=candidate:a0+B/3 1 udp 2130706432 74.125.127.126 2345 typ srflx raddr 192.168.1.5 rport 2346 generation 2\na=candidate:a0+B/3 2 udp 2130706432 74.125.127.126 2347 typ srflx raddr 192.168.1.5 rport 2348 generation 2\na=ice-ufrag:ufrag_voice\na=ice-pwd:pwd_voice\na=mid:audio_content_name\na=msid:local_stream_1 audio_track_id_1\na=sendrecv\na=rtcp-mux\na=rtcp-rsize\na=crypto:1 AES_CM_128_HMAC_SHA1_32 inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 dummy_session_params\na=rtpmap:111 opus/48000/2\na=rtpmap:103 ISAC/16000\na=rtpmap:104 ISAC/32000\na=ssrc:1 cname:stream_1_cname\na=ssrc:1 msid:local_stream_1 audio_track_id_1\na=ssrc:1 mslabel:local_stream_1\na=ssrc:1 label:audio_track_id_1\na=ssrc:4 cname:stream_2_cname\na=ssrc:4 msid:local_stream_2 audio_track_id_2\na=ssrc:4 mslabel:local_stream_2\na=ssrc:4 label:audio_track_id_2\nm=video 3457 RTP/SAVPF 120\nc=IN IP4 74.125.224.39\na=rtcp:3456 IN IP4 74.125.224.39\na=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1236 typ host generation 2\na=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1237 typ host generation 2\na=candidate:a0+B/2 2 udp 2130706432 ::1 1240 typ host generation 2\na=candidate:a0+B/2 1 udp 2130706432 ::1 1241 typ host generation 2\na=candidate:a0+B/4 2 udp 2130706432 74.125.224.39 3456 typ relay generation 2\na=candidate:a0+B/4 1 udp 2130706432 74.125.224.39 3457 typ relay generation 2\na=ice-ufrag:ufrag_video\na=ice-pwd:pwd_video\na=mid:video_content_name\na=msid:local_stream_1 video_track_id_1\na=sendrecv\na=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\na=rtpmap:120 VP8/90000\na=ssrc-group:FEC 2 3\na=ssrc:2 cname:stream_1_cname\na=ssrc:2 msid:local_stream_1 video_track_id_1\na=ssrc:2 mslabel:local_stream_1\na=ssrc:2 label:video_track_id_1\na=ssrc:3 cname:stream_1_cname\na=ssrc:3 msid:local_stream_1 video_track_id_1\na=ssrc:3 mslabel:local_stream_1\na=ssrc:3 label:video_track_id_1\na=ssrc:5 cname:stream_2_cname\na=ssrc:5 msid:local_stream_2 video_track_id_2\na=ssrc:5 mslabel:local_stream_2\na=ssrc:5 label:video_track_id_2\na=ssrc:6 cname:stream_2_cname\na=ssrc:6 msid:local_stream_2 video_track_id_3\na=ssrc:6 mslabel:local_stream_2\na=ssrc:6 label:video_track_id_3\n"
  },
  {
    "path": "fuzzers/engines/standalone.c",
    "content": "#include <stdint.h>\n#include <stddef.h>\n#include <stdio.h>\n#include <stdlib.h>\n\nextern int LLVMFuzzerTestOneInput(const unsigned char *data, size_t size);\n\nint main(int argc, char **argv) {\n\tfor (int i = 1; i < argc; i++) {\n\t\tfprintf(stderr, \"Running: %s\\n\", argv[i]);\n\t\tFILE *f = fopen(argv[i], \"r\");\n\t\tfseek(f, 0, SEEK_END);\n\t\tsize_t len = ftell(f);\n\t\tfseek(f, 0, SEEK_SET);\n\t\tunsigned char *buf = (unsigned char*)malloc(len);\n\t\tsize_t n_read = fread(buf, 1, len, f);\n\t\tLLVMFuzzerTestOneInput(buf, len);\n\t\tfree(buf);\n\t\tfprintf(stderr, \"Done:    %s: (%zd bytes)\\n\", argv[i], n_read);\n\t}\n}\n"
  },
  {
    "path": "fuzzers/rtcp_fuzzer.c",
    "content": "#include <stdint.h>\n#include <stddef.h>\n#include <stdlib.h>\n\n#include <glib.h>\n#include \"../src/debug.h\"\n#include \"../src/rtcp.h\"\n#include \"../src/rtp.h\"\n\nint janus_log_level = LOG_NONE;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = FALSE;\nchar *janus_log_global_prefix = NULL;\nint lock_debug = 0;\n\n/* This is to avoid linking with openSSL */\nint RAND_bytes(uint8_t *key, int len) {\n\treturn 0;\n}\n\nint LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {\n\t/* Sanity Checks */\n\t/* Max UDP payload with MTU=1500 */\n\tif (size > 1472) return 0;\n\t/* libnice checks that a packet length is positive */\n\tif (size <= 0) return 0;\n\t/* Janus checks for a minimum COMPOUND packet length\n\t * and the RTP header type value */\n\tif (!janus_is_rtcp((char *)data, size)) return 0;\n\t/* libsrtp checks that an entire COMPOUND packet must\n\t * contain at least a full RTCP header */\n\tif (size < 8) return 0;\n\n\t/* Test context setup */\n\t/* Do some copies of input data */\n\tuint8_t copy_data0[size], copy_data1[size],\n\t\tcopy_data2[size], copy_data3[size],\n\t\t\tcopy_data4[size], copy_data5[size];\n\tuint8_t *copy_data[6] = { copy_data0, copy_data1,\n\t\t\tcopy_data2, copy_data3,\n\t\t\t\tcopy_data4, copy_data5 };\n\tint idx, newlen;\n\tfor (idx=0; idx < 6; idx++) {\n\t\tmemcpy(copy_data[idx], data, size);\n\t}\n\tidx = 0;\n\t/* Create some void RTCP contexts */\n\tjanus_rtcp_context ctx0, ctx1;\n\tmemset(&ctx0, 0, sizeof(janus_rtcp_context));\n\tmemset(&ctx1, 0, sizeof(janus_rtcp_context));\n\n\t/* Targets */\n\t/* Functions that just read data */\n\tjanus_rtcp_has_bye((char *)data, size);\n\tjanus_rtcp_has_fir((char *)data, size);\n\tjanus_rtcp_has_pli((char *)data, size);\n\tjanus_rtcp_get_receiver_ssrc((char *)data, size);\n\tjanus_rtcp_get_remb((char *)data, size);\n\tjanus_rtcp_get_sender_ssrc((char *)data, size);\n\t/* Functions that alter input data */\n\tjanus_rtcp_cap_remb((char *)copy_data[idx++], size, 256000);\n\tjanus_rtcp_swap_report_blocks((char *)copy_data[idx++], size, 2);\n\tjanus_rtcp_fix_report_data((char *)copy_data[idx++], size, 2000, 1000, 2, 2, 2, TRUE);\n\tjanus_rtcp_fix_ssrc(&ctx0, (char *)copy_data[idx++], size, 1, 2, 2);\n\tjanus_rtcp_parse(&ctx1, (char *)copy_data[idx++], size);\n\tjanus_rtcp_remove_nacks((char *)copy_data[idx++], size);\n\t/* Functions that allocate new memory */\n\tchar *output_data = janus_rtcp_filter((char *)data, size, &newlen);\n\tGQueue *queue = g_queue_new();\n\tjanus_rtcp_get_nacks((char *)data, size, queue);\n\n\t/* Free resources */\n\tg_free(output_data);\n\tg_queue_free(queue);\n\treturn 0;\n}\n"
  },
  {
    "path": "fuzzers/rtp_fuzzer.c",
    "content": "#include <stdint.h>\n#include <stddef.h>\n#include <stdlib.h>\n\n#include <glib.h>\n#include \"../src/debug.h\"\n#include \"../src/utils.h\"\n#include \"../src/rtp.h\"\n\nint janus_log_level = LOG_NONE;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = FALSE;\nchar *janus_log_global_prefix = NULL;\nint lock_debug = 0;\n\n/* This is to avoid linking with openSSL */\nint RAND_bytes(uint8_t *key, int len) {\n\treturn 0;\n}\n\n/* Clone libsrtp srtp_validate_rtp_header */\n#define octets_in_rtp_header 12\n#define uint32s_in_rtp_header 3\n#define octets_in_rtp_extn_hdr 4\n\nstatic int srtp_validate_rtp_header(char *data, int pkt_octet_len) {\n    if (pkt_octet_len < octets_in_rtp_header)\n        return -1;\n\n    janus_rtp_header *hdr = (janus_rtp_header *)data;\n\n    /* Check RTP header length */\n    int rtp_header_len = octets_in_rtp_header + 4 * hdr->csrccount;\n    if (hdr->extension == 1)\n        rtp_header_len += octets_in_rtp_extn_hdr;\n\n    if (pkt_octet_len < rtp_header_len)\n        return -1;\n\n    /* Verifing profile length. */\n    if (hdr->extension == 1) {\n    \tjanus_rtp_header_extension *xtn_hdr =\n            (janus_rtp_header_extension *)((uint32_t *)hdr + uint32s_in_rtp_header +\n                                hdr->csrccount);\n        int profile_len = ntohs(xtn_hdr->length);\n        rtp_header_len += profile_len * 4;\n        /* profile length counts the number of 32-bit words */\n        if (pkt_octet_len < rtp_header_len)\n            return -1;\n    }\n    return 0;\n}\n\nint LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {\n\t/* Sanity Checks */\n\t/* Max UDP payload with MTU=1500 */\n\tif (size > 1472) return 0;\n\t/* libnice checks that a packet length is positive */\n\tif (size <= 0) return 0;\n\t/* Janus checks for a minimum packet length\n\t * and the RTP header type value */\n\tif (!janus_is_rtp((char *)data, size)) return 0;\n\n\tchar sdes_item[16];\n\tjanus_rtp_header_extension_parse_rid((char *)data, size, 1, sdes_item, sizeof(sdes_item));\n\tjanus_rtp_header_extension_parse_mid((char *)data, size, 1, sdes_item, sizeof(sdes_item));\n\n\t/* Do same checks that libsrtp does */\n\tif (srtp_validate_rtp_header((char *)data, size) < 0) return 0;\n\n\t/* RTP extensions parsers */\n\tguint16 transport_seq_num;\n\tgboolean c, f, r1, r0;\n\tuint8_t dd[256];\n\tint sizedd = sizeof(dd);\n\tjanus_rtp_header_extension_parse_audio_level((char *)data, size, 1, NULL, NULL);\n\tjanus_rtp_header_extension_parse_playout_delay((char *)data, size, 1, NULL, NULL);\n\tjanus_rtp_header_extension_parse_transport_wide_cc((char *)data, size, 1, &transport_seq_num);\n\tjanus_rtp_header_extension_parse_abs_send_time((char *)data, size, 1, NULL);\n\tjanus_rtp_header_extension_parse_abs_capture_time((char *)data, size, 1, NULL);\n\tjanus_rtp_header_extension_parse_video_orientation((char * )data, size, 1, &c, &f, &r1, &r0);\n\tjanus_rtp_header_extension_parse_dependency_desc((char *)data, size, 1, (uint8_t *)&dd, &sizedd);\n\tjanus_rtp_header_extension_parse_video_layers_allocation((char *)data, size, 1, NULL, NULL);\n\n\t/* Extract codec payload */\n\tint plen = 0;\n\tchar *payload = janus_rtp_payload((char *)data, size, &plen);\n\tif (!payload) return 0;\n\t/* Make a copy of payload */\n\tchar copy_payload[plen];\n\tmemcpy(copy_payload, payload, plen);\n\n\t/* H.264 targets */\n\tjanus_h264_is_keyframe(payload, plen);\n\n\t/* VP8 targets */\n\tgboolean m = FALSE;\n\tuint16_t picid = 0;\n\tuint8_t tlzi = 0, tid = 0, ybit = 0, keyidx = 0;\n\tjanus_vp8_simulcast_context vp8_context;\n\tmemset(&vp8_context, 0, sizeof(janus_vp8_simulcast_context));\n\tjanus_vp8_is_keyframe(payload, plen);\n\tjanus_vp8_parse_descriptor(payload, plen, &m, &picid, &tlzi, &tid, &ybit, &keyidx);\n\tjanus_vp8_simulcast_descriptor_update(copy_payload, plen, &vp8_context, TRUE);\n\n\t/* VP9 targets */\n\tint found = 0;\n\tjanus_vp9_svc_info info;\n\tjanus_vp9_is_keyframe(payload, plen);\n\tjanus_vp9_parse_svc(payload, plen, &found, &info);\n\n\t/* Free resources */\n\n\treturn 0;\n}\n"
  },
  {
    "path": "fuzzers/run.sh",
    "content": "#!/bin/bash\n\nset -eu\n\nSCRIPTPATH=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\n\nTARGET=${1:-\"rtcp_fuzzer\"}\nCRASH_FILE=${2:-\"\"}\nif [[ ! -z \"$CRASH_FILE\" && \"${CRASH_FILE:0:1}\" != / && \"${CRASH_FILE:0:2}\" != ~[/a-z] ]]; then\n\tCRASH_FILE=\"$SCRIPTPATH\"/\"$CRASH_FILE\"\nfi\nHALF_NCORES=$(expr $(nproc) / 2)\nHALF_NCORES=$(($HALF_NCORES > 0 ? $HALF_NCORES : 1))\nJOBS=${JOBS:-${HALF_NCORES}}\nWORKERS=${WORKERS:-${HALF_NCORES}}\nOUT=${OUT:-\"$SCRIPTPATH/out\"}\nSRC=$(dirname $SCRIPTPATH)\n\necho \"Fuzzer: $TARGET\"\necho \"Crash file/folder: $CRASH_FILE\"\necho \"Output dir: $OUT\"\n\ncd \"$OUT\"\n\n# Extract the corpus dataset from zipfile\nmkdir -p \"$TARGET\"_corpus\nmkdir -p \"$TARGET\"_seed_corpus\nif [ -f \"${TARGET}_seed_corpus.zip\" ]; then\n\techo \"Extracting corpus seed data\"\n\tunzip -oq \"$TARGET\"_seed_corpus.zip -d \"$TARGET\"_seed_corpus\nfi\n\n# Run the target\n# Use -max_len=65535 for network protocols\n# Use -timeout=25 -rss_limit_mb=2048 for time and memory limits\nif [ -z \"$CRASH_FILE\" ]; then\n\t# No crash file supplied, start the fuzzer\n\tASAN_OPTIONS=detect_leaks=1 ./$TARGET -artifact_prefix=\"./$TARGET-\" -print_final_stats=0 -print_corpus_stats=0 -print_coverage=0 -jobs=${JOBS} -workers=${WORKERS} \"$TARGET\"_corpus \"$TARGET\"_seed_corpus\n\t# tail -f fuzz*.log\nelif [ -f \"$CRASH_FILE\" ]; then\n\t# Run without fuzzing to reproduce a bug with a supplied crash file\n\tASAN_OPTIONS=detect_leaks=1 ./$TARGET $CRASH_FILE\n\t# Rerun with GDB to reproduce and debug\n\t#ASAN_OPTIONS=abort_on_error=1 gdb --args ./$TARGET $CRASH_FILE\nelif [ -d \"$CRASH_FILE\" ]; then\n\t# Run without fuzzing, with an user supplied crashes folder\n\tfiles=$(find \"$CRASH_FILE\" -maxdepth 1 -type f)\n\tif [[ -z $files ]]; then\n\t\techo \"Empty crashes folder specified!\"\n\t\texit 1\n\tfi\n\tASAN_OPTIONS=detect_leaks=1 ./$TARGET $files\nelse\n\techo \"Invalid crash file/folder specified!\"\n\texit 1\nfi\n\n# Run without fuzzing, using the extracted corpus dataset (regression testing)\n# Use -max_len=65535 for network protocols\n# Use -timeout=25 -rss_limit_mb=2048 for time and memory limits\n# ASAN_OPTIONS=detect_leaks=1 ./$TARGET \"$TARGET\"_seed_corpus/*\n\n# Run the target for coverage testing\n# NAME=\"$TARGET\".$(date +%s)\n# LLVM_PROFILE_FILE=\"$NAME\".profraw ./$TARGET \"$TARGET\"_seed_corpus/*\n# llvm-profdata merge -sparse \"$NAME\".profraw -o \"$NAME\".profdata\n# llvm-cov show \"$TARGET\" -instr-profile=\"$NAME\".profdata \"$SRC\"/rtcp.c \"$SRC\"/rtp.c \"$SRC\"/utils.c -use-color -format=html > \"$NAME\".html\n\n# dump crashing pattern\n# hexdump -C \"$CRASH_FILE\"\n\n# Convert to pcap\n# od -Ax -tx1 -v \"$CRASH_FILE\" > \"$CRASH_FILE\".hex\n# text2pcap -u1000,2000 \"$CRASH_FILE\".hex \"$CRASH_FILE\".pcap\n\n"
  },
  {
    "path": "fuzzers/sdp_fuzzer.c",
    "content": "#include <stdint.h>\n#include <stddef.h>\n#include <stdlib.h>\n#include <string.h>\n\n#include <glib.h>\n#include \"../src/debug.h\"\n#include \"../src/sdp-utils.h\"\n\nint janus_log_level = LOG_NONE;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = FALSE;\nchar *janus_log_global_prefix = NULL;\nint lock_debug = 0;\nint refcount_debug = 0;\n\n/* This is to avoid linking with openSSL */\nint RAND_bytes(uint8_t *key, int len) {\n\treturn 0;\n}\n\nint LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {\n\t/* Since we're fuzzing SDP, and that in our case SDP always comes\n\t * from a Jansson call, this will need to be a valid string */\n\tif(size <= 0)\n\t\treturn 0;\n\tchar sdp_string[size];\n\tmemcpy(sdp_string, data, size);\n\tsdp_string[size-1] = '\\0';\n\t/* Parse the SDP using the utils */\n\tchar error_str[512];\n\tjanus_sdp *parsed_sdp = janus_sdp_parse((const char *)sdp_string, error_str, sizeof(error_str));\n\tif(parsed_sdp == NULL)\n\t\treturn 0;\n\t/* Regenerate the SDP blog */\n\tchar *generated_sdp = janus_sdp_write(parsed_sdp);\n\n\t/* Free resources */\n\tjanus_sdp_destroy(parsed_sdp);\n\tg_free(generated_sdp);\n\n\treturn 0;\n}\n"
  },
  {
    "path": "html/Makefile.am",
    "content": "demodir = $(datadir)/janus/html\n\nhtml-local:\n\ttrue\n\ninstall-data-local: html-local\n\t$(MKDIR_P) $(DESTDIR)$(demodir)\n\tcp -r * $(DESTDIR)$(demodir)\n\trm -f $(DESTDIR)$(demodir)/Makefile*\n\nuninstall-local:\n\trm -rf $(DESTDIR)$(demodir)\n\nclean-local:\n\ttrue\n"
  },
  {
    "path": "html/citeus.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Cite us!</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top a[href='citeus.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"css/demo.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Cite us!</h1>\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<ul>\n\t\t\t\t\t<li><a href=\"#janus\">IPTComm 2014: \"Janus: a general purpose WebRTC gateway\"</a></li>\n\t\t\t\t\t<li><a href=\"#awes\">AWeS 2015: \"Performance analysis of the Janus WebRTC gateway\"</a></li>\n\t\t\t\t\t<li><a href=\"#jattack\">IPTComm 2016: \"Jattack: a WebRTC load testing tool\"</a></li>\n\t\t\t\t\t<li><a href=\"#ietf\">IEEE Communications Standards Magazine: \"Empowering Remote Participation in IETF Meetings through WebRTC\"</a></li>\n\t\t\t\t\t<li><a href=\"#iceland\">IPTComm 2018: \"Measuring Janus temperature in ICE-land\"</a></li>\n\t\t\t\t\t<li><a href=\"#docker\">IEEE Internet Computing: \"Docker and Janus: friends or foe?\"</a></li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t\t<hr id=\"janus\"/>\n\t\t\t<h4><a href=\"http://dl.acm.org/citation.cfm?id=2670389\">IPTComm 2014: \"Janus: a general purpose WebRTC gateway\"</a></h4>\n\t\t\t<pre class=\"card card-body bg-gray mb-5\">\n@inproceedings{IPTComm2014-Janus,\n\tauthor = {Amirante, A. and Castaldi, T. and Miniero, L. and Romano, S. P.},\n\ttitle = {Janus: A General Purpose WebRTC Gateway},\n\tbooktitle = {Proceedings of the Conference on Principles, Systems and Applications of IP Telecommunications},\n\tseries = {IPTComm '14},\n\tyear = {2014},\n\tisbn = {978-1-4503-2124-2},\n\tlocation = {Chicago, Illinois},\n\tpages = {7:1--7:8},\n\tarticleno = {7},\n\tnumpages = {8},\n\turl = {http://doi.acm.org/10.1145/2670386.2670389},\n\tdoi = {10.1145/2670386.2670389},\n\tacmid = {2670389},\n\tpublisher = {ACM},\n\taddress = {New York, NY, USA},\n\tkeywords = {IETF, JavaScript, Rtcweb, WebRTC, conferencing, gateways, streaming, web communication},\n}\n\t\t\t</pre>\n\t\t\t<hr id=\"awes\"/>\n\t\t\t<h4><a href=\"https://dl.acm.org/citation.cfm?doid=2749215.2749223\">AWeS 2015: \"Performance analysis of the Janus WebRTC gateway\"</a></h4>\n\t\t\t<pre class=\"card card-body bg-gray mb-5\">\n@inproceedings{AWeS2015-Janus,\n\tauthor = {Amirante, Alessandro and Castaldi, Tobia and Miniero, Lorenzo and Romano, Simon Pietro},\n\ttitle = {Performance Analysis of the Janus WebRTC Gateway},\n\tbooktitle = {Proceedings of the 1st Workshop on All-Web Real-Time Systems},\n\tseries = {AWeS '15},\n\tyear = {2015},\n\tisbn = {978-1-4503-3477-8},\n\tlocation = {Bordeaux, France},\n\tpages = {4:1--4:7},\n\tarticleno = {4},\n\tnumpages = {7},\n\turl = {http://doi.acm.org/10.1145/2749215.2749223},\n\tdoi = {10.1145/2749215.2749223},\n\tacmid = {2749223},\n\tpublisher = {ACM},\n\taddress = {New York, NY, USA},\n\tkeywords = {MCU, RTCWEB, SFU, SIP, WebRTC, gateway, performance},\n}\n\t\t\t</pre>\n\t\t\t<hr id=\"jattack\"/>\n\t\t\t<h4><a href=\"https://ieeexplore.ieee.org/document/7780247/\">IPTComm 2016: \"Jattack: a WebRTC load testing tool\"</a></h4>\n\t\t\t<pre class=\"card card-body bg-gray mb-5\">\n@inproceedings{IPTComm2016-Jattack,\n\tauthor = {Amirante, A. and Castaldi, T. and Miniero, L. and Romano, S. P.},\n\ttitle = {Jattack: a WebRTC load testing tool},\n\tbooktitle = {Proceedings of the Conference on Principles, Systems and Applications of IP Telecommunications},\n\tseries = {IPTComm '16},\n\tyear = {2016},\n\tisbn = {978-1-5090-4248-7},\n\tlocation = {Chicago, Illinois},\n\tnumpages = {6},\n\turl = {http://ieeexplore.ieee.org/document/7780247/},\n\tpublisher = {IEEE},\n\tkeywords = {WebRTC, Testing, Media, Scalability, Browsers, Servers, Stress},\n}\n\t\t\t</pre>\n\t\t\t<hr id=\"ietf\"/>\n\t\t\t<h4><a href=\"https://ieeexplore.ieee.org/document/7992930/\">IEEE Communications Standards Magazine: \"Empowering Remote Participation in IETF Meetings through WebRTC\"</a></h4>\n\t\t\t<pre class=\"card card-body bg-gray mb-5\">\n@article{DBLP:journals/csm/AmiranteCMS17,\n\tauthor = {Amirante, A. and Castaldi, T. and Miniero, L. and Saviano, P.},\n\ttitle = {Empowering Remote Participation in {IETF} Meetings through WebRTC},\n\tjournal = {{IEEE} Communications Standards Magazine},\n\tvolume = {1},\n\tnumber = {2},\n\tpages = {60--66},\n\tyear = {2017},\n\turl = {https://doi.org/10.1109/MCOMSTD.2017.1700004},\n\tdoi = {10.1109/MCOMSTD.2017.1700004},\n\ttimestamp = {Mon, 31 Jul 2017 13:11:41 +0200},\n\tbiburl = {http://dblp.dagstuhl.de/rec/bib/journals/csm/AmiranteCMS17},\n\tbibsource = {dblp computer science bibliography, http://dblp.org}\n}\n\t\t\t</pre>\n\t\t\t<hr id=\"iceland\"/>\n\t\t\t<h4><a href=\"https://ieeexplore.ieee.org/document/8567641/\">IPTComm 2018: \"Measuring Janus temperature in ICE-land\"</a></h4>\n\t\t\t<pre class=\"card card-body bg-gray mb-5\">\n@inproceedings{IPTComm2018-libnice,\n\tauthor = {Amirante, A. and Castaldi, T. and Miniero, L. and Romano, S. P. and Toppi, A.},\n\tyear = {2018},\n\tmonth = {10},\n\tpages = {1-7},\n\ttitle = {Measuring Janus temperature in ICE-land},\n\tdoi = {10.1109/IPTCOMM.2018.8567641}\n}\t\t\t</pre>\n\t\t\t<hr id=\"docker\"/>\n\t\t\t<h4><a href=\"https://ieeexplore.ieee.org/abstract/document/8894526\">IEEE Internet Computing: \"Container NATs and Session-Oriented Standards: Friends or Foe?\"</a></h4>\n\t\t\t<pre class=\"card card-body bg-gray mb-5\">\n@ARTICLE{8894526,\n  author={A. {Amirante} and S. P. {Romano}},\n  journal={IEEE Internet Computing},\n  title={Container NATs and Session-Oriented Standards: Friends or Foe?},\n  year={2019},\n  volume={23},\n  number={6},\n  pages={28-37}\n}\n\t\t\t</pre>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "html/css/demo.css",
    "content": "body {\n\tpadding-top: 80px;\n}\n\na {\n\ttext-decoration: none;\n}\n\n.hide {\n\tdisplay: none !important;\n}\n\n.z-2 {\n\tz-index: 2000;\n}\n\n.rounded {\n\tborder-radius: 5px;\n}\n\n.centered {\n\tdisplay: block;\n\tmargin: auto;\n}\n\n.relative {\n\tposition: relative;\n}\n\n.top-left {\n\tposition: absolute;\n\ttop: 0px;\n\tleft: 0px;\n}\n\n.top-right {\n\tposition: absolute;\n\ttop: 0px;\n\tright: 0px;\n}\n\n.bottom-left {\n\tposition: absolute;\n\tbottom: 0px;\n\tleft: 0px;\n}\n\n.bottom-right {\n\tposition: absolute;\n\tbottom: 0px;\n\tright: 0px;\n}\n\n.navbar-brand {\n\tmargin-left: 0px !important;\n}\n\n.navbar {\n\t-webkit-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49);\n\t-moz-box-shadow:    0px 3px 5px rgba(100, 100, 100, 0.49);\n\tbox-shadow:         0px 3px 5px rgba(100, 100, 100, 0.49);\n}\n\n.navbar-header {\n\tpadding-left: 40px;\n}\n\n.btn-group-xs > .btn, .btn-xs {\n\tpadding: 1px 5px;\n\tfont-size: 12px;\n\tline-height: 1.5;\n\tborder-radius: 3px;\n}\n\n.divider {\n\twidth: 100%;\n\ttext-align: center;\n}\n\n.divider hr {\n\tmargin-left: auto;\n\tmargin-right: auto;\n\twidth: 45%;\n}\n\n.fa-2 {\n\tfont-size: 2em !important;\n}\n.fa-3 {\n\tfont-size: 4em !important;\n}\n.fa-4 {\n\tfont-size: 7em !important;\n}\n.fa-xl {\n\tfont-size: 12em !important;\n}\n.fa-6 {\n\tfont-size: 20em !important;\n}\n\ndiv.no-video-container {\n\tposition: relative;\n}\n\n.no-video-icon {\n\twidth: 100%;\n\theight: 240px;\n\ttext-align: center;\n\tpadding-top: 5rem !important;\n}\n\n.no-video-text {\n\ttext-align: center;\n\tposition: absolute;\n\tbottom: 0px;\n\tright: 0px;\n\tleft: 0px;\n\tfont-size: 24px;\n}\n\n.meetecho-logo {\n\tpadding: 12px !important;\n}\n\n.meetecho-logo > img {\n\theight: 26px;\n}\n\npre {\n\twhite-space: pre-wrap;\n\twhite-space: -moz-pre-wrap;\n\twhite-space: -pre-wrap;\n\twhite-space: -o-pre-wrap;\n\tword-wrap: break-word;\n\tbackground-color: #f5f5f5;\n}\n\n.bg-gray {\n\tbackground-color: #f5f5f5;\n}\n\n.januscon {\n\tfont-weight: bold;\n\tanimation: pulsating 1s infinite;\n}\n@keyframes pulsating {\n\t30% {\n\t\tcolor: #FFD700;\n\t}\n}\n"
  },
  {
    "path": "html/demos/admin.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Admin/Monitor</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"admin.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='admin.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Janus WebRTC Server: Admin/Monitor</h1>\n\t\t\t</div>\n\n\t\t\t<div class=\"mt-4\">\n\t\t\t\t<ul id=\"admintabs\" class=\"nav nav-tabs\" role=\"tablist\">\n\t\t\t\t\t<li role=\"presentation\" class=\"nav-item\"><a class=\"nav-link active\" href=\"#home\" aria-controls=\"home\" role=\"tab\" data-bs-toggle=\"tab\">Home</a></li>\n\t\t\t\t\t<li role=\"presentation\" class=\"nav-item\"><a class=\"nav-link disabled\" href=\"#serverinfo\" aria-controls=\"serverinfo\" role=\"tab\" data-bs-toggle=\"tab\">Server Info</a></li>\n\t\t\t\t\t<li role=\"presentation\" class=\"nav-item\"><a class=\"nav-link disabled\" href=\"#settings\" aria-controls=\"settings\" role=\"tab\" data-bs-toggle=\"tab\">Settings</a></li>\n\t\t\t\t\t<li role=\"presentation\" class=\"nav-item\"><a class=\"nav-link disabled\" href=\"#plugins\" aria-controls=\"plugins\" role=\"tab\" data-bs-toggle=\"tab\">Plugins</a></li>\n\t\t\t\t\t<li role=\"presentation\" class=\"nav-item\"><a class=\"nav-link disabled\" href=\"#transports\" aria-controls=\"transports\" role=\"tab\" data-bs-toggle=\"tab\">Transports</a></li>\n\t\t\t\t\t<li role=\"presentation\" class=\"nav-item\"><a class=\"nav-link disabled\" href=\"#handlesinfo\" aria-controls=\"handlesinfo\" role=\"tab\" data-bs-toggle=\"tab\">Handles</a></li>\n\t\t\t\t\t<li role=\"presentation\" class=\"nav-item\"><a class=\"nav-link disabled\" href=\"#tokens\" aria-controls=\"tokens\" role=\"tab\" data-bs-toggle=\"tab\">Stored Tokens</a></li>\n\t\t\t\t</ul>\n\n\t\t\t\t<div class=\"tab-content\" style=\"padding: 20px;\">\n\t\t\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade in active show\" id=\"home\">\n\t\t\t\t\t\t<p>This is just an example of how you can build an UI on top of the\n\t\t\t\t\t\texisting <code>Admin/Monitor</code> interface. This page will only\n\t\t\t\t\t\twork as it is if you enabled the API (which is disabled by default)\n\t\t\t\t\t\tand you're using the default values. Edit the backend settings in\n\t\t\t\t\t\tthe <code>admin.js</code> JavaScript code to adapt it to your\n\t\t\t\t\t\tconfiguration if you changed anything.</p>\n\t\t\t\t\t\t<p>The <code>Server Info</code> tab, as the name suggests, provides\n\t\t\t\t\t\tyou with a view of the information related to the Janus instance\n\t\t\t\t\t\tyou're using, e.g., in terms of the features that have been enabled,\n\t\t\t\t\t\tthe modules that are available and so on. That's the same info you'd\n\t\t\t\t\t\tget contacting the Janus API at the <code>/janus/info</code> backend.</p>\n\t\t\t\t\t\t<p>The <code>Settings</code> tab instead allows you to inspect a\n\t\t\t\t\t\tfew of the current settings in Janus (e.g., debug level and so on)\n\t\t\t\t\t\tand provides you with a way to change them dynamically.</p>\n\t\t\t\t\t\t<p>The <code>Plugins</code> tab presents the list of media plugins\n\t\t\t\t\t\tavailable in this Janus instance, and allows you to interact with\n\t\t\t\t\t\tthem, assuming they implement the <code>handle_admin_message</code> API.\n\t\t\t\t\t\tThe <code>Transports</code> tab does the same for transport plugins,\n\t\t\t\t\t\tand allows you to send requests to tweak the behaviour of the plugin\n\t\t\t\t\t\tor query its internal state, assuming they support this somehow.</p>\n\t\t\t\t\t\t<p>The <code>Handles</code> tab allows you to browse the currently active sessions\n\t\t\t\t\t\tand handles in Janus. Selecting a specific handle will provide you\n\t\t\t\t\t\twith all the current info related to it, including plugin it is\n\t\t\t\t\t\tattached to, any plugin specific information that may be relevant,\n\t\t\t\t\t\tICE/DTLS states, amount of data being exchanged and so on. This\n\t\t\t\t\t\tsection is especially helpful when you want to debug issues with\n\t\t\t\t\t\ta PeerConnection: you can find more details in\n\t\t\t\t\t\t<a href=\"http://www.meetecho.com/blog/understanding-the-janus-admin-api/\" target=\"_blank\">this blog post</a>.</p>\n\t\t\t\t\t\t<p>Finally, the <code>Stored Tokens</code> tab allows you to list\n\t\t\t\t\t\texisting authentication tokens, create new ones, associate plugin\n\t\t\t\t\t\tpermissions and the like. This feature will only be possible if\n\t\t\t\t\t\tyou enabled the stored-token authentication mechanism in Janus, of course.</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade\" id=\"serverinfo\">\n\t\t\t\t\t\t<h5>Server Info</h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped mb-5\" id=\"server-details\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<h5>Dependencies</h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped mb-5\" id=\"server-deps\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<h5>Plugins</h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped mb-5\" id=\"server-plugins\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<h5>Transports</h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped mb-5\" id=\"server-transports\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<h5>Event handlers</h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped mb-5\" id=\"server-handlers\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<h5>Loggers</h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped mb-5\" id=\"server-loggers\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade\" id=\"settings\">\n\t\t\t\t\t\t<h5>Settings <i id=\"update-settings\" class=\"fa-solid fa-rotate\" title=\"Refresh settings\" style=\"cursor: pointer;\"></i></h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped\" id=\"server-settings\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade\" id=\"plugins\">\n\t\t\t\t\t\t<h5>Plugins</h5>\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div class=\"col-md-3\">\n\t\t\t\t\t\t\t\t<ul class=\"list-group\" id=\"plugins-list\">\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div id=\"plugin-message\" class=\"col-md-9 hide\">\n\t\t\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t\t\t<h5>Request</h5>\n\t\t\t\t\t\t\t\t\t<table class=\"table\" id=\"plugin-request\">\n\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t\t\t<h5>Response</h5>\n\t\t\t\t\t\t\t\t\t<pre class=\"card card-body bg-gray w-100\" id=\"plugin-response\"></pre>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade\" id=\"transports\">\n\t\t\t\t\t\t<h5>Transports</h5>\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div class=\"col-md-3\">\n\t\t\t\t\t\t\t\t<ul class=\"list-group\" id=\"transports-list\">\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div id=\"transport-message\" class=\"col-md-9 hide\">\n\t\t\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t\t\t<h5>Tweak/Query</h5>\n\t\t\t\t\t\t\t\t\t<table class=\"table\" id=\"transport-request\">\n\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t\t\t<h5>Response</h5>\n\t\t\t\t\t\t\t\t\t<pre class=\"card card-body bg-gray w-100\" id=\"transport-response\"></pre>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade\" id=\"handlesinfo\">\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div id=\"sessions\" class=\"col-md-2\">\n\t\t\t\t\t\t\t\t<h5>Sessions (<span id=\"sessions-num\">0</span>) <i id=\"update-sessions\" class=\"fa-solid fa-rotate\" title=\"Refresh list of sessions\" style=\"cursor: pointer;\"></i></h5>\n\t\t\t\t\t\t\t\t<div id=\"sessions-list\" class=\"list-group\">\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div id=\"handles\" class=\"col-md-2\">\n\t\t\t\t\t\t\t\t<h5>Handles (<span id=\"handles-num\"></span>) <i id=\"update-handles\" class=\"fa-solid fa-rotate\" title=\"Refresh list of handles\" style=\"cursor: pointer;\"></i></h5>\n\t\t\t\t\t\t\t\t<div id=\"handles-list\" class=\"list-group\">\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div id=\"info\" class=\"col-md-8\">\n\t\t\t\t\t\t\t\t<h5>Handle Info <i id=\"update-handle\" class=\"fa-solid fa-rotate\" title=\"Refresh handle info\" style=\"cursor: pointer;\"></i></h5>\n\t\t\t\t\t\t\t\t<div id=\"options\" class=\"hide\">\n\t\t\t\t\t\t\t\t\t<label class=\"checkbox-inline\" title=\"Autorefresh this info every 5s\">\n\t\t\t\t\t\t\t\t\t\t<input id=\"autorefresh\" type=\"checkbox\" value=\"\" title=\"Autorefresh this info every 5s\">Autorefresh\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<label class=\"checkbox-inline\" title=\"Show information as HTML\">\n\t\t\t\t\t\t\t\t\t\t<input id=\"prettify\" type=\"checkbox\" value=\"\" title=\"Show information as HTML\">Prettify\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<label class=\"checkbox-inline\" title=\"Start of stop capturing traffic to .pcap\">\n\t\t\t\t\t\t\t\t\t\t<input id=\"capture\" type=\"checkbox\" value=\"\" title=\"Start of stop capturing traffic to .pcap\">\n\t\t\t\t\t\t\t\t\t\t<span id=\"capturetext\">Start capture</span>\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div id=\"handle-info\">\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade\" id=\"tokens\">\n\t\t\t\t\t\t<h5>Stored Tokens <i id=\"update-tokens\" class=\"fa-solid fa-rotate\" title=\"Refresh tokens\" style=\"cursor: pointer;\"></i></h5>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<table class=\"table table-striped\" id=\"auth-tokens\">\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/admin.js",
    "content": "//\n// This 'server' variable we use to contact the Admin/Monitor backend is\n// constructed in this example pretty much as we do in all the demos, so\n// refer to the guidelines there with respect to absolute vs. relative\n// paths and the like.\n//\nvar server = null;\nif(window.location.protocol === 'http:')\n\tserver = \"http://\" + window.location.hostname + \":7088/admin\";\nelse\n\tserver = \"https://\" + window.location.hostname + \":7889/admin\";\n\n// If you don't want the page to prompt you for a password, insert it here\nvar secret = \"janusoverlord\";\n\nvar session = null;\t\t// Selected session\nvar handle = null;\t\t// Selected handle\n\nvar plugins = [], pluginsIndex = [], pluginRows = 0;\nvar transports = [], transportsIndex = [], transportRows = 0;\nvar settings = {};\n\nvar currentHandle = null;\nvar localSdp = null, remoteSdp = null;\n\nvar handleInfo;\n\n$(document).ready(function() {\n\t$('#admintabs a').click(function (e) {\n\t\te.preventDefault()\n\t\t$(this).tab('show')\n\t});\n\tif(!server)\n\t\tserver = \"\";\n\tif(!secret)\n\t\tsecret = \"\";\n\tif(server !== \"\" && secret !== \"\") {\n\t\tupdateServerInfo();\n\t} else {\n\t\tpromptAccessDetails();\n\t}\n});\n\nvar prompting = false;\nvar alerted = false;\nfunction promptAccessDetails() {\n\tif(prompting)\n\t\treturn;\n\tprompting = true;\n\tlet serverPlaceholder = \"Insert the address of the Admin API backend\";\n\tlet secretPlaceholder = \"Insert the Admin API secret\";\n\tbootbox.alert({\n\t\tmessage: \"<div class='input-group mt-3 mb-1'>\" +\n\t\t\t\"\t<span class='input-group-text'><i class='fa-solid fa-cloud-arrow-up'></i></span>\" +\n\t\t\t\"\t<input class='form-control' type='text' value='\" + server + \"' placeholder='\" + serverPlaceholder + \"' autocomplete='off' id='server'></input>\" +\n\t\t\t\"</div>\" +\n\t\t\t\"<div class='input-group mt-3 mb-1'>\" +\n\t\t\t\"\t<span class='input-group-text'><i class='fa-solid fa-key'></i></span>\" +\n\t\t\t\"\t<input class='form-control' type='password'  value='\" + secret + \"'placeholder='\" + secretPlaceholder + \"' autocomplete='off' id='secret'></input>\" +\n\t\t\t\"</div>\",\n\t\tcloseButton: false,\n\t\tcallback: function() {\n\t\t\tprompting = false;\n\t\t\tserver = $('#server').val();\n\t\t\tsecret = $('#secret').val();\n\t\t\tupdateServerInfo();\n\t\t}\n\t});\n}\n\n// Helper method to create random identifiers (e.g., transaction)\nfunction randomString(len) {\n\tconst charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\tlet randomString = '';\n\tfor (let i = 0; i < len; i++) {\n\t\tlet randomPoz = Math.floor(Math.random() * charSet.length);\n\t\trandomString += charSet.substring(randomPoz,randomPoz+1);\n\t}\n\treturn randomString;\n}\n\n// Server info\nfunction updateServerInfo() {\n\tplugins = [];\n\tpluginsIndex = [];\n\ttransports = [];\n\ttransportsIndex = [];\n\t$.ajax({\n\t\ttype: 'GET',\n\t\turl: server + \"/info\",\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"server_info\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tif(!prompting && !alerted) {\n\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\talerted = false;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log(\"Got server info:\");\n\t\t\tconsole.log(json);\n\t\t\tlet pluginsJson = json.plugins;\n\t\t\tlet transportsJson = json.transports;\n\t\t\tlet eventsJson = json.events;\n\t\t\tlet loggersJson = json.loggers;\n\t\t\tdelete json.janus;\n\t\t\tdelete json.transaction;\n\t\t\tdelete json.plugins;\n\t\t\tdelete json.transports;\n\t\t\tdelete json.events;\n\t\t\tdelete json.loggers;\n\t\t\t$('#server-details').empty();\n\t\t\tfor(let k in json) {\n\t\t\t\tif(k === \"dependencies\") {\n\t\t\t\t\t$('#server-deps').html(\n\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t'\t<th>Library</th>' +\n\t\t\t\t\t\t'\t<th>Version</th>' +\n\t\t\t\t\t\t'</tr>'\n\t\t\t\t\t);\n\t\t\t\t\tfor(let ln in json[k]) {\n\t\t\t\t\t\t$('#server-deps').append(\n\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t'\t<td>' + ln + '</td>' +\n\t\t\t\t\t\t\t'\t<td>' + json[k][ln] + '</td>' +\n\t\t\t\t\t\t\t'</tr>'\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tlet v = json[k];\n\t\t\t\t$('#server-details').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td><b>' + k + ':</b></td>' +\n\t\t\t\t\t'\t<td>' + v + '</td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t}\n\t\t\t$('#server-plugins').html(\n\t\t\t\t'<tr>' +\n\t\t\t\t'\t<th>Name</th>' +\n\t\t\t\t'\t<th>Author</th>' +\n\t\t\t\t'\t<th>Description</th>' +\n\t\t\t\t'\t<th>Version</th>' +\n\t\t\t\t'</tr>'\n\t\t\t);\n\t\t\t$('#plugins-list').empty();\n\t\t\tfor(let p in pluginsJson) {\n\t\t\t\tplugins.push(p);\n\t\t\t\tlet v = pluginsJson[p];\n\t\t\t\t$('#server-plugins').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td>' + v.name + '</td>' +\n\t\t\t\t\t'\t<td>' + v.author + '</td>' +\n\t\t\t\t\t'\t<td>' + v.description + '</td>' +\n\t\t\t\t\t'\t<td>' + v.version_string + '</td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t\tpluginsIndex.push(p);\n\t\t\t\t$('#plugins-list').append(\n\t\t\t\t\t'<a id=\"plugin-' + (pluginsIndex.length-1) + '\" href=\"#\" class=\"list-group-item list-group-item-action\">' +\n\t\t\t\t\t\t'<small>' + p + '</small>' +\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t\t$('#plugin-'+(pluginsIndex.length-1)).click(function(event) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlet pi = parseInt($(this).attr('id').split('plugin-')[1]);\n\t\t\t\t\tlet plugin = pluginsIndex[pi];\n\t\t\t\t\tconsole.log(\"Selected plugin:\", plugin);\n\t\t\t\t\t$('#plugins-list a').removeClass('active');\n\t\t\t\t\t$('#plugin-'+pi).addClass('active');\n\t\t\t\t\tresetPluginRequest();\n\t\t\t\t});\n\t\t\t}\n\t\t\t$('#server-transports').html(\n\t\t\t\t'<tr>' +\n\t\t\t\t'\t<th>Name</th>' +\n\t\t\t\t'\t<th>Author</th>' +\n\t\t\t\t'\t<th>Description</th>' +\n\t\t\t\t'\t<th>Version</th>' +\n\t\t\t\t'</tr>'\n\t\t\t);\n\t\t\tfor(let t in transportsJson) {\n\t\t\t\ttransports.push(t);\n\t\t\t\tlet v = transportsJson[t];\n\t\t\t\t$('#server-transports').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td>' + v.name + '</td>' +\n\t\t\t\t\t'\t<td>' + v.author + '</td>' +\n\t\t\t\t\t'\t<td>' + v.description + '</td>' +\n\t\t\t\t\t'\t<td>' + v.version_string + '</td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t\ttransportsIndex.push(t);\n\t\t\t\t$('#transports-list').append(\n\t\t\t\t\t'<a id=\"transport-' + (transportsIndex.length-1) + '\" href=\"#\" class=\"list-group-item list-group-item-action\">' +\n\t\t\t\t\t\t'<small>' + t + '</small>' +\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t\t$('#transport-'+(transportsIndex.length-1)).click(function(event) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlet ti = parseInt($(this).attr('id').split('transport-')[1]);\n\t\t\t\t\tlet transport = transportsIndex[ti];\n\t\t\t\t\tconsole.log(\"Selected transport:\", transport);\n\t\t\t\t\t$('#transports-list a').removeClass('active');\n\t\t\t\t\t$('#transport-'+ti).addClass('active');\n\t\t\t\t\tresetTransportRequest();\n\t\t\t\t});\n\t\t\t}\n\t\t\t$('#server-handlers').html(\n\t\t\t\t'<tr>' +\n\t\t\t\t'\t<th>Name</th>' +\n\t\t\t\t'\t<th>Author</th>' +\n\t\t\t\t'\t<th>Description</th>' +\n\t\t\t\t'\t<th>Version</th>' +\n\t\t\t\t'</tr>'\n\t\t\t);\n\t\t\tfor(let e in eventsJson) {\n\t\t\t\tlet v = eventsJson[e];\n\t\t\t\t$('#server-handlers').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td>' + v.name + '</td>' +\n\t\t\t\t\t'\t<td>' + v.author + '</td>' +\n\t\t\t\t\t'\t<td>' + v.description + '</td>' +\n\t\t\t\t\t'\t<td>' + v.version_string + '</td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t}\n\t\t\t$('#server-loggers').html(\n\t\t\t\t'<tr>' +\n\t\t\t\t'\t<th>Name</th>' +\n\t\t\t\t'\t<th>Author</th>' +\n\t\t\t\t'\t<th>Description</th>' +\n\t\t\t\t'\t<th>Version</th>' +\n\t\t\t\t'</tr>'\n\t\t\t);\n\t\t\tfor(let e in loggersJson) {\n\t\t\t\tlet v = loggersJson[e];\n\t\t\t\t$('#server-loggers').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td>' + v.name + '</td>' +\n\t\t\t\t\t'\t<td>' + v.author + '</td>' +\n\t\t\t\t\t'\t<td>' + v.description + '</td>' +\n\t\t\t\t\t'\t<td>' + v.version_string + '</td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t}\n\t\t\t// Unlock tabs\n\t\t\t$('#admintabs li a').removeClass('disabled');\n\t\t\t// Refresh settings now\n\t\t\tupdateSettings();\n\t\t\t// Refresh sessions and handles now\n\t\t\t$('#handles').addClass('hide');\n\t\t\t$('#info').addClass('hide');\n\t\t\t$('#update-sessions').click(updateSessions);\n\t\t\t$('#update-handles').click(updateHandles);\n\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\tupdateSessions();\n\t\t\t$(\"#autorefresh\").change(function() {\n\t\t\t\tif(this.checked) {\n\t\t\t\t\tupdateHandleInfo(true);\n\t\t\t\t}\n\t\t\t});\n\t\t\t$(\"#prettify\").change(function() {\n\t\t\t\tif(this.checked) {\n\t\t\t\t\tprettyHandleInfo();\n\t\t\t\t} else {\n\t\t\t\t\trawHandleInfo();\n\t\t\t\t}\n\t\t\t});\n\t\t\t$(\"#capture\").change(function() {\n\t\t\t\tif(this.checked) {\n\t\t\t\t\t// We're trying to start a new capture, show a dialog\n\t\t\t\t\t$('#capturetext').html('Stop capture');\n\t\t\t\t\tcaptureTrafficPrompt();\n\t\t\t\t} else {\n\t\t\t\t\t// We're trying to stop a capture\n\t\t\t\t\t$('#capturetext').html('Start capture');\n\t\t\t\t\tcaptureTrafficRequest(false, handleInfo[\"dump-to-text2pcap\"] === true);\n\t\t\t\t}\n\t\t\t});\n\t\t\t// Only check tokens if the mechanism is enabled\n\t\t\tif(!json[\"auth_token\"]) {\n\t\t\t\t$(\"a[href='#tokens']\").addClass('disabled');\n\t\t\t\t$(\"a[href='#tokens']\").attr('href', '#').unbind('click').click(function (e) { e.preventDefault(); return false; });\n\t\t\t} else {\n\t\t\t\tupdateTokens();\n\t\t\t}\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\n// Settings\nfunction updateSettings() {\n\t$('#update-settings').unbind('click').addClass('fa-spin');\n\tlet request = { \"janus\": \"get_status\", \"transaction\": randomString(12), \"admin_secret\": secret };\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t$('#update-settings').removeClass('fa-spin').click(updateSettings);\n\t\t\t\t}, 1000);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log(\"Got status:\");\n\t\t\tconsole.log(json);\n\t\t\tsetTimeout(function() {\n\t\t\t\t$('#update-settings').removeClass('fa-spin').click(updateSettings);\n\t\t\t}, 1000);\n\t\t\t$('#server-settings').empty();\n\t\t\tfor(let k in json.status) {\n\t\t\t\tsettings[k] = json.status[k];\n\t\t\t\t$('#server-settings').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td><b>' + k + ':</b></td>' +\n\t\t\t\t\t'\t<td>' + settings[k] + '</td>' +\n\t\t\t\t\t'\t<td id=\"' + k + '\"></td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t\tif(k === 'session_timeout') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs btn-primary\">Edit session timeout value</button>');\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tbootbox.prompt(\"Set the new session timeout value (in seconds, currently \" + settings[\"session_timeout\"] + \")\", function(result) {\n\t\t\t\t\t\t\tif(isNaN(result)) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid session timeout value\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresult = parseInt(result);\n\t\t\t\t\t\t\tif(result < 0) {\n\t\t\t\t\t\t\t\tconsole.log(isNaN(result));\n\t\t\t\t\t\t\t\tconsole.log(result < 0);\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid session timeout value\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsetSessionTimeoutValue(result);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'log_level') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs btn-primary\">Edit log level</button>');\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tbootbox.prompt(\"Set the new desired log level (0-7, currently \" + settings[\"log_level\"] + \")\", function(result) {\n\t\t\t\t\t\t\tif(isNaN(result)) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid log level (should be [0,7])\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresult = parseInt(result);\n\t\t\t\t\t\t\tif(result < 0 || result > 7) {\n\t\t\t\t\t\t\t\tconsole.log(isNaN(result));\n\t\t\t\t\t\t\t\tconsole.log(result < 0);\n\t\t\t\t\t\t\t\tconsole.log(result > 7);\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid log level (should be [0,7])\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsetLogLevel(result);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'min_nack_queue') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs btn-primary\">Edit min NACK queue</button>');\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tbootbox.prompt(\"Set the new desired min NACK queue (a positive integer, currently \" + settings[\"min_nack_queue\"] + \")\", function(result) {\n\t\t\t\t\t\t\tif(isNaN(result)) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid min NACK queue (should be a positive integer)\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresult = parseInt(result);\n\t\t\t\t\t\t\tif(result < 0) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid min NACK queue (should be a positive integer)\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsetMinNackQueue(result);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'no_media_timer') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs btn-primary\">Edit no-media timer value</button>');\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tbootbox.prompt(\"Set the new desired no-media timer value (in seconds, currently \" + settings[\"no_media_timer\"] + \")\", function(result) {\n\t\t\t\t\t\t\tif(isNaN(result)) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid no-media timer (should be a positive integer)\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresult = parseInt(result);\n\t\t\t\t\t\t\tif(result < 0) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid no-media timer (should be a positive integer)\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsetNoMediaTimer(result);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'slowlink_threshold') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs btn-primary\">Edit slowlink-threshold value</button>');\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tbootbox.prompt(\"Set the new desired slowlink-threshold value (in lost packets per seconds, currently \" + settings[\"slowlink_threshold\"] + \")\", function(result) {\n\t\t\t\t\t\t\tif(isNaN(result)) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid slowlink-threshold timer (should be a positive integer)\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresult = parseInt(result);\n\t\t\t\t\t\t\tif(result < 0) {\n\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid slowlink-threshold timer (should be a positive integer)\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsetSlowlinkThreshold(result);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'locking_debug') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs\"></button>');\n\t\t\t\t\t$('#'+k + \"_button\")\n\t\t\t\t\t\t.addClass(!settings[k] ? \"btn-success\" : \"btn-danger\")\n\t\t\t\t\t\t.html(!settings[k] ? \"Enable locking debug\" : \"Disable locking debug\");\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tlet text = (!settings[\"locking_debug\"] ?\n\t\t\t\t\t\t\t\"Are you sure you want to enable the locking debug?<br/>This will print a line on the console any time a mutex is locked/unlocked\"\n\t\t\t\t\t\t\t: \"Are you sure you want to disable the locking debug?\");\n\t\t\t\t\t\tbootbox.confirm(text, function(result) {\n\t\t\t\t\t\t\tif(result)\n\t\t\t\t\t\t\t\tsetLockingDebug(!settings[\"locking_debug\"]);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'refcount_debug') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs\"></button>');\n\t\t\t\t\t$('#'+k + \"_button\")\n\t\t\t\t\t\t.addClass(!settings[k] ? \"btn-success\" : \"btn-danger\")\n\t\t\t\t\t\t.html(!settings[k] ? \"Enable reference counters debug\" : \"Disable reference counters debug\");\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tlet text = (!settings[\"refcount_debug\"] ?\n\t\t\t\t\t\t\t\"Are you sure you want to enable the reference counters debug?<br/>This will print a line on the console any time a reference counter is increased/decreased\"\n\t\t\t\t\t\t\t: \"Are you sure you want to disable the reference counters debug?\");\n\t\t\t\t\t\tbootbox.confirm(text, function(result) {\n\t\t\t\t\t\t\tif(result)\n\t\t\t\t\t\t\t\tsetRefcountDebug(!settings[\"refcount_debug\"]);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'log_timestamps') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs\"></button>');\n\t\t\t\t\t$('#'+k + \"_button\")\n\t\t\t\t\t\t.addClass(!settings[k] ? \"btn-success\" : \"btn-danger\")\n\t\t\t\t\t\t.html(!settings[k] ? \"Enable log timestamps\" : \"Disable log timestamps\");\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tlet text = (!settings[\"log_timestamps\"] ?\n\t\t\t\t\t\t\t\"Are you sure you want to enable the log timestamps?<br/>This will print the current date/time for each new line on the console\"\n\t\t\t\t\t\t\t: \"Are you sure you want to disable the log timestamps?\");\n\t\t\t\t\t\tbootbox.confirm(text, function(result) {\n\t\t\t\t\t\t\tif(result)\n\t\t\t\t\t\t\t\tsetLogTimestamps(!settings[\"log_timestamps\"]);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t} else if(k === 'log_colors') {\n\t\t\t\t\t$('#'+k).append('<button id=\"' + k + '_button\" type=\"button\" class=\"btn btn-xs\"></button>');\n\t\t\t\t\t$('#'+k + \"_button\")\n\t\t\t\t\t\t.addClass(!settings[k] ? \"btn-success\" : \"btn-danger\")\n\t\t\t\t\t\t.html(!settings[k] ? \"Enable log colors\" : \"Disable log colors\");\n\t\t\t\t\t$('#'+k + \"_button\").click(function() {\n\t\t\t\t\t\tlet text = (!settings[\"log_colors\"] ?\n\t\t\t\t\t\t\t\"Are you sure you want to enable the log colors?<br/>This will strip the colors from events like warnings, errors, etc. on the console\"\n\t\t\t\t\t\t\t: \"Are you sure you want to disable the log colors?\");\n\t\t\t\t\t\tbootbox.confirm(text, function(result) {\n\t\t\t\t\t\t\tif(result)\n\t\t\t\t\t\t\t\tsetLogColors(!settings[\"log_colors\"]);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\t$('#update-settings').removeClass('fa-spin').click(updateSettings);\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\nfunction setSessionTimeoutValue(timeout) {\n\tlet request = { \"janus\": \"set_session_timeout\", \"timeout\": timeout, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setLogLevel(level) {\n\tlet request = { \"janus\": \"set_log_level\", \"level\": level, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setLockingDebug(enable) {\n\tlet request = { \"janus\": \"set_locking_debug\", \"debug\": enable, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setRefcountDebug(enable) {\n\tlet request = { \"janus\": \"set_refcount_debug\", \"debug\": enable, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setLogTimestamps(enable) {\n\tlet request = { \"janus\": \"set_log_timestamps\", \"timestamps\": enable, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setLogColors(enable) {\n\tlet request = { \"janus\": \"set_log_colors\", \"colors\": enable, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setMinNackQueue(queue) {\n\tlet request = { \"janus\": \"set_min_nack_queue\", \"min_nack_queue\": queue, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setNoMediaTimer(timer) {\n\tlet request = { \"janus\": \"set_no_media_timer\", \"no_media_timer\": timer, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction setSlowlinkThreshold(packets) {\n\tlet request = { \"janus\": \"set_slowlink_threshold\", \"slowlink_threshold\": packets, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendSettingsRequest(request);\n}\n\nfunction sendSettingsRequest(request) {\n\tconsole.log(request);\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tupdateSettings();\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\n// Plugins\nfunction resetPluginRequest() {\n\tpluginRows = 0;\n\t$('#plugin-request').empty().append(\n\t\t'<tr style=\"background: #f9f9f9;\">' +\n\t\t'\t<th width=\"25%\">Name</th>' +\n\t\t'\t<th width=\"25%\">Value</th>' +\n\t\t'\t<th width=\"25%\">Type</th>' +\n\t\t'\t<th></th>' +\n\t\t'</tr>' +\n\t\t'<tr>' +\n\t\t'\t<td><i id=\"addattr\" class=\"fa-solid fa-plus-circle\" style=\"cursor: pointer;\"></i></td>' +\n\t\t'\t<td></td>' +\n\t\t'\t<td></td>' +\n\t\t'\t<td><button id=\"sendmsg\" type=\"button\" class=\"btn btn-xs btn-success pull-right\">Send message</button></td>' +\n\t\t'</tr>');\n\t$('#addattr').click(addPluginMessageAttribute).click();\n\t$('#sendmsg').click(function() {\n\t\tlet message = {};\n\t\tlet index = 0;\n\t\tfor(let i=0; i<=pluginRows; i++) {\n\t\t\tif($('#attrname'+i).length === 0)\n\t\t\t\tcontinue;\n\t\t\tindex++;\n\t\t\tlet name = $('#attrname'+i).val();\n\t\t\tif(name === '') {\n\t\t\t\tbootbox.alert(\"Missing name in attribute #\" + index);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(message[name] !== null && message[name] !== undefined) {\n\t\t\t\tbootbox.alert(\"Duplicate attribute '\" + name + \"'\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet value = $('#attrvalue'+i).val();\n\t\t\tif(value === '') {\n\t\t\t\tbootbox.alert(\"Missing value in attribute #\" + index);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet type = $('#attrtype'+i).val();\n\t\t\tif(type === \"number\") {\n\t\t\t\tvalue = parseInt(value);\n\t\t\t\tif(isNaN(value)) {\n\t\t\t\t\tbootbox.alert(\"Invalid value in attribute #\" + index + \" (expecting a number)\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if(type === \"boolean\") {\n\t\t\t\tif(value.toLowerCase() === \"true\") {\n\t\t\t\t\tvalue = true;\n\t\t\t\t} else if(value.toLowerCase() === \"false\") {\n\t\t\t\t\tvalue = false;\n\t\t\t\t} else {\n\t\t\t\t\tbootbox.alert(\"Invalid value in attribute #\" + index + \" (expecting a boolean)\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconsole.log(\"Type:\", type);\n\t\t\tmessage[name] = value;\n\t\t}\n\t\tsendPluginMessage($('#plugins-list .active').text(), message);\n\t});\n\t$('#plugin-message').removeClass('hide');\n}\n\nfunction addPluginMessageAttribute() {\n\tlet num = pluginRows;\n\tpluginRows++;\n\t$('#addattr').parent().parent().before(\n\t\t'<tr>' +\n\t\t'\t<td><input type=\"text\" id=\"attrname' + num + '\" placeholder=\"Attribute name\" onkeypress=\"return checkEnter(this, event);\" style=\"width: 100%;\" class=\"pm-property form-control input-sm\"></td>' +\n\t\t'\t<td><input type=\"text\" id=\"attrvalue' + num + '\" placeholder=\"Attribute value\" onkeypress=\"return checkEnter(this, event);\" style=\"width: 100%;\" class=\"form-control input-sm\"></td>' +\n\t\t'\t<td>' +\n\t\t'\t\t<select id=\"attrtype' + num + '\" class=\"form-control input-sm\">' +\n\t\t'\t\t\t<option>string</option>' +\n\t\t'\t\t\t<option>number</option>' +\n\t\t'\t\t\t<option>boolean</option>' +\n\t\t'\t\t</select>' +\n\t\t'\t</td>' +\n\t\t'\t<td><i id=\"rmattr' + num + '\" class=\"fa-solid fa-rectangle-xmark\" style=\"cursor: pointer;\"></i></td>' +\n\t\t'</tr>'\n\t);\n\t$('#rmattr' + num).click(function() {\n\t\t$(this).parent().parent().remove();\n\t});\n}\n\nfunction sendPluginMessage(plugin, message) {\n\tconsole.log(\"Sending message to \" + plugin + \":\", message);\n\tlet request = {\n\t\tjanus: \"message_plugin\",\n\t\ttransaction: randomString(12),\n\t\tadmin_secret: secret,\n\t\tplugin: plugin,\n\t\trequest: message\n\t};\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\t$('#plugin-response').text(JSON.stringify(json, null, 4));\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\n// Transports\nfunction resetTransportRequest() {\n\ttransportRows = 0;\n\t$('#transport-request').empty().append(\n\t\t'<tr style=\"background: #f9f9f9;\">' +\n\t\t'\t<th width=\"25%\">Name</th>' +\n\t\t'\t<th width=\"25%\">Value</th>' +\n\t\t'\t<th width=\"25%\">Type</th>' +\n\t\t'\t<th></th>' +\n\t\t'</tr>' +\n\t\t'<tr>' +\n\t\t'\t<td><i id=\"traddattr\" class=\"fa-solid fa-plus-circle\" style=\"cursor: pointer;\"></i></td>' +\n\t\t'\t<td></td>' +\n\t\t'\t<td></td>' +\n\t\t'\t<td><button id=\"trsendmsg\" type=\"button\" class=\"btn btn-xs btn-success pull-right\">Send message</button></td>' +\n\t\t'</tr>');\n\t$('#traddattr').click(addTransportMessageAttribute).click();\n\t$('#trsendmsg').click(function() {\n\t\tlet message = {};\n\t\tlet index = 0;\n\t\tfor(let i=0; i<=transportRows; i++) {\n\t\t\tif($('#trattrname'+i).length === 0)\n\t\t\t\tcontinue;\n\t\t\tindex++;\n\t\t\tlet name = $('#trattrname'+i).val();\n\t\t\tif(name === '') {\n\t\t\t\tbootbox.alert(\"Missing name in attribute #\" + index);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(message[name] !== null && message[name] !== undefined) {\n\t\t\t\tbootbox.alert(\"Duplicate attribute '\" + name + \"'\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet value = $('#trattrvalue'+i).val();\n\t\t\tif(value === '') {\n\t\t\t\tbootbox.alert(\"Missing value in attribute #\" + index);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet type = $('#trattrtype'+i).val();\n\t\t\tif(type === \"number\") {\n\t\t\t\tvalue = parseInt(value);\n\t\t\t\tif(isNaN(value)) {\n\t\t\t\t\tbootbox.alert(\"Invalid value in attribute #\" + index + \" (expecting a number)\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if(type === \"boolean\") {\n\t\t\t\tif(value.toLowerCase() === \"true\") {\n\t\t\t\t\tvalue = true;\n\t\t\t\t} else if(value.toLowerCase() === \"false\") {\n\t\t\t\t\tvalue = false;\n\t\t\t\t} else {\n\t\t\t\t\tbootbox.alert(\"Invalid value in attribute #\" + index + \" (expecting a boolean)\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconsole.log(\"Type:\", type);\n\t\t\tmessage[name] = value;\n\t\t}\n\t\tsendTransportMessage($('#transports-list .active').text(), message);\n\t});\n\t$('#transport-message').removeClass('hide');\n}\n\nfunction addTransportMessageAttribute() {\n\tlet num = transportRows;\n\ttransportRows++;\n\t$('#traddattr').parent().parent().before(\n\t\t'<tr>' +\n\t\t'\t<td><input type=\"text\" id=\"trattrname' + num + '\" placeholder=\"Attribute name\" onkeypress=\"return checkEnter(this, event);\" style=\"width: 100%;\" class=\"pm-property form-control input-sm\"></td>' +\n\t\t'\t<td><input type=\"text\" id=\"trattrvalue' + num + '\" placeholder=\"Attribute value\" onkeypress=\"return checkEnter(this, event);\" style=\"width: 100%;\" class=\"form-control input-sm\"></td>' +\n\t\t'\t<td>' +\n\t\t'\t\t<select id=\"trattrtype' + num + '\" class=\"form-control input-sm\">' +\n\t\t'\t\t\t<option>string</option>' +\n\t\t'\t\t\t<option>number</option>' +\n\t\t'\t\t\t<option>boolean</option>' +\n\t\t'\t\t</select>' +\n\t\t'\t</td>' +\n\t\t'\t<td><i id=\"rmtrattr' + num + '\" class=\"fa-solid fa-rectangle-xmark\" style=\"cursor: pointer;\"></i></td>' +\n\t\t'</tr>'\n\t);\n\t$('#rmtrattr' + num).click(function() {\n\t\t$(this).parent().parent().remove();\n\t});\n}\n\nfunction sendTransportMessage(transport, message) {\n\tconsole.log(\"Sending message to \" + transport + \":\", message);\n\tlet request = {\n\t\tjanus: \"query_transport\",\n\t\ttransaction: randomString(12),\n\t\tadmin_secret: secret,\n\t\ttransport: transport,\n\t\trequest: message\n\t};\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\t$('#transport-response').text(JSON.stringify(json, null, 4));\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\n\n// Handles\nfunction updateSessions() {\n\t$('#update-sessions').unbind('click').addClass('fa-spin');\n\t$('#update-handles').unbind('click');\n\t$('#update-handle').unbind('click');\n\tlet request = { \"janus\": \"list_sessions\", \"transaction\": randomString(12), \"admin_secret\": secret };\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t$('#update-sessions').removeClass('fa-spin').click(updateSessions);\n\t\t\t\t\t$('#update-handles').click(updateHandles);\n\t\t\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\t\t}, 1000);\n\t\t\t\tsession = null;\n\t\t\t\thandle = null;\n\t\t\t\tcurrentHandle = null;\n\t\t\t\t$('#handles-list').empty();\n\t\t\t\t$('#handles').addClass('hide');\n\t\t\t\t$('#handle-info').empty();\n\t\t\t\t$('#options').addClass('hide');\n\t\t\t\t$('#info').addClass('hide');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log(\"Got sessions:\");\n\t\t\tconsole.log(json);\n\t\t\t$('#sessions-list').empty();\n\t\t\tlet sessions = json[\"sessions\"];\n\t\t\t$('#sessions-num').text(sessions.length);\n\t\t\tfor(let i=0; i<sessions.length; i++) {\n\t\t\t\tlet s = sessions[i];\n\t\t\t\t$('#sessions-list').append(\n\t\t\t\t\t'<a id=\"session-' + s + '\" href=\"#\" class=\"list-group-item list-group-item-action\">' +\n\t\t\t\t\t\t'<small>' + s + '</small>' +\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t\t$('#session-'+s).click(function() {\n\t\t\t\t\tlet sh = $(this).text();\n\t\t\t\t\tconsole.log(\"Getting session \" + sh + \" handles\");\n\t\t\t\t\tsession = sh;\n\t\t\t\t\t$('#sessions-list a').removeClass('active');\n\t\t\t\t\t$('#session-'+sh).addClass('active');\n\t\t\t\t\thandle = null;\n\t\t\t\t\tcurrentHandle = null;\n\t\t\t\t\t$('#handles-list').empty();\n\t\t\t\t\t$('#handles').removeClass('hide');\n\t\t\t\t\t$('#handle-info').empty();\n\t\t\t\t\t$('#options').addClass('hide');\n\t\t\t\t\t$('#info').addClass('hide');\n\t\t\t\t\tupdateHandles();\n\t\t\t\t});\n\t\t\t}\n\t\t\tif(session !== null && session !== undefined) {\n\t\t\t\tif($('#session-'+session).length) {\n\t\t\t\t\t$('#session-'+session).addClass('active');\n\t\t\t\t} else {\n\t\t\t\t\t// The session that was selected has disappeared\n\t\t\t\t\tsession = null;\n\t\t\t\t\thandle = null;\n\t\t\t\t\tcurrentHandle = null;\n\t\t\t\t\t$('#handles-list').empty();\n\t\t\t\t\t$('#handles').addClass('hide');\n\t\t\t\t\t$('#handle-info').empty();\n\t\t\t\t\t$('#options').addClass('hide');\n\t\t\t\t\t$('#info').addClass('hide');\n\t\t\t\t}\n\t\t\t}\n\t\t\tsetTimeout(function() {\n\t\t\t\t$('#update-sessions').removeClass('fa-spin').click(updateSessions);\n\t\t\t\t$('#update-handles').click(updateHandles);\n\t\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\t}, 1000);\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\tsetTimeout(function() {\n\t\t\t\t$('#update-sessions').removeClass('fa-spin').click(updateSessions);\n\t\t\t\t$('#update-handles').click(updateHandles);\n\t\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\t}, 1000);\n\t\t\tsession = null;\n\t\t\thandle = null;\n\t\t\tcurrentHandle = null;\n\t\t\t$('#handles-list').empty();\n\t\t\t$('#handles').addClass('hide');\n\t\t\t$('#handle-info').empty();\n\t\t\t$('#options').addClass('hide');\n\t\t\t$('#info').addClass('hide');\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\nfunction updateHandles() {\n\tif(session === null || session === undefined)\n\t\treturn;\n\t$('#update-sessions').unbind('click');\n\t$('#update-handles').unbind('click').addClass('fa-spin');\n\t$('#update-handle').unbind('click');\n\tlet request = { \"janus\": \"list_handles\", \"transaction\": randomString(12), \"admin_secret\": secret };\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server + \"/\" + session,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t$('#update-handles').removeClass('fa-spin').click(updateHandles);\n\t\t\t\t\t$('#update-sessions').click(updateSessions);\n\t\t\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\t\t}, 1000);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log(\"Got handles:\");\n\t\t\tconsole.log(json);\n\t\t\t$('#handles-list').empty();\n\t\t\tlet handles = json[\"handles\"];\n\t\t\t$('#handles-num').text(handles.length);\n\t\t\tfor(let i=0; i<handles.length; i++) {\n\t\t\t\tlet h = handles[i];\n\t\t\t\t$('#handles-list').append(\n\t\t\t\t\t'<a id=\"handle-' + h + '\" href=\"#\" class=\"list-group-item list-group-item-action\">' +\n\t\t\t\t\t\t'<small>' + h + '</small>' +\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t\t$('#handle-'+h).click(function() {\n\t\t\t\t\tlet hi = $(this).text();\n\t\t\t\t\tconsole.log(\"Getting handle \" + hi + \" info\");\n\t\t\t\t\thandle = hi;\n\t\t\t\t\tif(handle === currentHandle)\n\t\t\t\t\t\treturn;\t// The self-refresh takes care of that\n\t\t\t\t\t$('#handles-list a').removeClass('active');\n\t\t\t\t\t$('#handle-'+hi).addClass('active');\n\t\t\t\t\t$('#handle-info').empty();\n\t\t\t\t\t$('#options').addClass('hide');\n\t\t\t\t\t$('#info').removeClass('hide');\n\t\t\t\t\tupdateHandleInfo();\n\t\t\t\t});\n\t\t\t}\n\t\t\tif(handle !== null && handle !== undefined) {\n\t\t\t\tif($('#handle-'+handle).length) {\n\t\t\t\t\t$('#handle-'+handle).addClass('active');\n\t\t\t\t} else {\n\t\t\t\t\t// The handle that was selected has disappeared\n\t\t\t\t\thandle = null;\n\t\t\t\t\tcurrentHandle = null;\n\t\t\t\t\t$('#handle-info').empty();\n\t\t\t\t\t$('#options').addClass('hide');\n\t\t\t\t\t$('#info').addClass('hide');\n\t\t\t\t}\n\t\t\t}\n\t\t\tsetTimeout(function() {\n\t\t\t\t$('#update-handles').removeClass('fa-spin').click(updateHandles);\n\t\t\t\t$('#update-sessions').click(updateSessions);\n\t\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\t}, 1000);\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\t$('#update-handles').removeClass('fa-spin').click(updateHandles);\n\t\t\t$('#update-sessions').click(updateSessions);\n\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\nfunction updateHandleInfo(refresh) {\n\tif(handle === null || handle === undefined)\n\t\treturn;\n\tif(refresh !== true) {\n\t\tif(handle === currentHandle && $('#autorefresh')[0].checked)\n\t\t\treturn;\t// The self-refresh takes care of that\n\t\tcurrentHandle = handle;\n\t}\n\tlet updateHandle = currentHandle;\n\t$('#update-sessions').unbind('click');\n\t$('#update-handles').unbind('click');\n\t$('#update-handle').unbind('click').addClass('fa-spin');\n\t$('#capture').removeAttr('checked');\n\t$('#capturetext').html('Start capture');\n\tlet request = { \"janus\": \"handle_info\", \"transaction\": randomString(12), \"admin_secret\": secret };\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server + \"/\" + session + \"/\" + handle,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tif(refresh !== true) {\n\t\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\t\talerted = true;\n\t\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t$('#update-sessions').click(updateSessions);\n\t\t\t\t\t$('#update-handles').click(updateHandles);\n\t\t\t\t\t$('#update-handle').removeClass('fa-spin').click(updateHandleInfo);\n\t\t\t\t}, 1000);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log(\"Got info:\");\n\t\t\tconsole.log(json);\n\t\t\thandleInfo = json[\"info\"];\n\t\t\tif($('#prettify')[0].checked) {\n\t\t\t\tprettyHandleInfo();\n\t\t\t} else {\n\t\t\t\trawHandleInfo();\n\t\t\t}\n\t\t\tif(handleInfo[\"dump-to-pcap\"] || handleInfo[\"dump-to-text2pcap\"]) {\n\t\t\t\t$('#capture').attr('checked', true);\n\t\t\t\t$('#capturetext').html('Stop capture');\n\t\t\t}\n\t\t\tsetTimeout(function() {\n\t\t\t\t$('#update-sessions').click(updateSessions);\n\t\t\t\t$('#update-handles').click(updateHandles);\n\t\t\t\t$('#update-handle').removeClass('fa-spin').click(updateHandleInfo);\n\t\t\t}, 1000);\n\t\t\t// Show checkboxes\n\t\t\t$('#options').removeClass('hide');\n\t\t\t// If the related box is checked, autorefresh this handle info every tot seconds\n\t\t\tif($('#autorefresh')[0].checked) {\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\tif(updateHandle !== currentHandle) {\n\t\t\t\t\t\t// The handle changed in the meanwhile, don't autorefresh\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif(!$('#autorefresh')[0].checked) {\n\t\t\t\t\t\t// Unchecked in the meantime\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tupdateHandleInfo(true);\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\t$('#update-handles').removeClass('fa-spin').click(updateHandles);\n\t\t\t$('#update-sessions').click(updateSessions);\n\t\t\t$('#update-handle').click(updateHandleInfo);\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\nfunction rawHandleInfo() {\n\t// Just use <pre> and show the handle info as it is\n\t$('#handle-info').html('<pre class=\"card card-body bg-gray\">' + JSON.stringify(handleInfo, null, 4) + '</pre>');\n}\n\nfunction prettyHandleInfo() {\n\t// Prettify the handle info, processing it and turning it into tables\n\t$('#handle-info').html('<table class=\"table table-striped\" id=\"handle-info-table\"></table>');\n\t$('#options').addClass('hide');\n\tfor(let k in handleInfo) {\n\t\tlet v = handleInfo[k];\n\t\tif(k === \"plugin_specific\") {\n\t\t\t$('#handle-info').append(\n\t\t\t\t'<h4>Plugin specific details</h4>' +\n\t\t\t\t'<table class=\"table table-striped\" id=\"plugin-specific\">' +\n\t\t\t\t'</table>');\n\t\t\tfor(let kk in v) {\n\t\t\t\tlet vv = v[kk];\n\t\t\t\t$('#plugin-specific').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td><b>' + kk + ':</b></td>' +\n\t\t\t\t\t'\t<td>' + vv + '</td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t}\n\t\t} else if(k === \"flags\") {\n\t\t\t$('#handle-info').append(\n\t\t\t\t'<h4>Flags</h4>' +\n\t\t\t\t'<table class=\"table table-striped\" id=\"flags\">' +\n\t\t\t\t'</table>');\n\t\t\tfor(let kk in v) {\n\t\t\t\tlet vv = v[kk];\n\t\t\t\t$('#flags').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td><b>' + kk + ':</b></td>' +\n\t\t\t\t\t'\t<td>' + vv + '</td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t}\n\t\t} else if(k === \"sdps\") {\n\t\t\tlocalSdp = null;\n\t\t\tremoteSdp = null;\n\t\t\t$('#handle-info').append(\n\t\t\t\t'<h4>Session descriptions (SDP)</h4>' +\n\t\t\t\t'<table class=\"table table-striped\" id=\"sdps\">' +\n\t\t\t\t'</table>');\n\t\t\tfor(let kk in v) {\n\t\t\t\tlet vv = v[kk];\n\t\t\t\tif(kk === \"local\") {\n\t\t\t\t\tlocalSdp = vv;\n\t\t\t\t} else if(kk === \"remote\") {\n\t\t\t\t\tremoteSdp = vv;\n\t\t\t\t} else {\n\t\t\t\t\t// What? Skip\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t$('#sdps').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td><b>' + kk + ':</b></td>' +\n\t\t\t\t\t'\t<td><a id=\"' + kk + '\" href=\"#\">' + vv.substring(0, 40) + '...</a></td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t\t$('#' + kk).click(function(event) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlet sdp = $(this).attr('id') === \"local\" ? localSdp : remoteSdp;\n\t\t\t\t\tbootbox.dialog({\n\t\t\t\t\t\ttitle: \"SDP (\" + $(this).attr('id') + \")\",\n\t\t\t\t\t\tmessage: '<div style=\"max-height: ' + ($(window).height()*2/3) + 'px; overflow-y: auto;\">' + sdp.split(\"\\r\\n\").join(\"<br/>\") + '</div>'\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t} else if(k === \"streams\") {\n\t\t\t$('#handle-info').append(\n\t\t\t\t'<h4>ICE streams</h4>' +\n\t\t\t\t'<div id=\"streams\"></table>');\n\t\t\tfor(let kk in v) {\n\t\t\t\t$('#streams').append(\n\t\t\t\t\t'<h5>Stream #' + (parseInt(kk)+1) + '</h5>' +\n\t\t\t\t\t'<table class=\"table table-striped\" id=\"stream' + kk + '\">' +\n\t\t\t\t\t'</table>');\n\t\t\t\tlet vv = v[kk];\n\t\t\t\tconsole.log(vv);\n\t\t\t\tfor(let sk in vv) {\n\t\t\t\t\tlet sv = vv[sk];\n\t\t\t\t\tif(sk === \"ssrc\") {\n\t\t\t\t\t\t$('#stream' + kk).append(\n\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t'<td colspan=\"2\">' +\n\t\t\t\t\t\t\t\t\t'<h6>SSRC</h6>' +\n\t\t\t\t\t\t\t\t\t'<table class=\"table\" id=\"ssrc' + kk + '\">' +\n\t\t\t\t\t\t\t\t\t'</table>' +\n\t\t\t\t\t\t\t\t'</td>' +\n\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t\tfor(let ssk in sv) {\n\t\t\t\t\t\t\tlet ssv = sv[ssk];\n\t\t\t\t\t\t\t$('#ssrc' + kk).append(\n\t\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t'\t<td><b>' + ssk + ':</b></td>' +\n\t\t\t\t\t\t\t\t'\t<td>' + ssv + '</td>' +\n\t\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(sk === \"components\") {\n\t\t\t\t\t\t$('#stream' + kk).append(\n\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t'<td colspan=\"2\">' +\n\t\t\t\t\t\t\t\t\t'<h6>Components of Stream #' + (parseInt(kk)+1) + '</h6>' +\n\t\t\t\t\t\t\t\t\t'<table class=\"table\" id=\"components' + kk + '\">' +\n\t\t\t\t\t\t\t\t\t'</table>' +\n\t\t\t\t\t\t\t\t'</td>' +\n\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t\tfor(let ssk in sv) {\n\t\t\t\t\t\t\tlet ssv = sv[ssk];\n\t\t\t\t\t\t\t$('#components' + kk).append(\n\t\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t\t'<td colspan=\"2\">' +\n\t\t\t\t\t\t\t\t\t\t'<h6>Component #' + (parseInt(ssk)+1) + '</h6>' +\n\t\t\t\t\t\t\t\t\t\t'<table class=\"table\" id=\"stream' + kk + 'component' + ssk + '\">' +\n\t\t\t\t\t\t\t\t\t\t'</table>' +\n\t\t\t\t\t\t\t\t\t'</td>' +\n\t\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t\t\tfor(let cssk in ssv) {\n\t\t\t\t\t\t\t\tlet cssv = ssv[cssk];\n\t\t\t\t\t\t\t\tif(cssk === \"local-candidates\" || cssk === \"remote-candidates\") {\n\t\t\t\t\t\t\t\t\tlet candidates = \"<ul>\";\n\t\t\t\t\t\t\t\t\tfor(let c in cssv)\n\t\t\t\t\t\t\t\t\t\tcandidates += \"<li>\" + cssv[c] + \"</li>\";\n\t\t\t\t\t\t\t\t\tcandidates += \"</ul>\";\n\t\t\t\t\t\t\t\t\t$('#stream' + kk + 'component' + ssk).append(\n\t\t\t\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t\t\t'\t<td><b>' + cssk + ':</b></td>' +\n\t\t\t\t\t\t\t\t\t\t'\t<td>' + candidates + '</td>' +\n\t\t\t\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t\t\t\t} else if(cssk === \"dtls\" || cssk === \"in_stats\" || cssk === \"out_stats\") {\n\t\t\t\t\t\t\t\t\tlet dtls = '<table class=\"table\">';\n\t\t\t\t\t\t\t\t\tfor(let d in cssv) {\n\t\t\t\t\t\t\t\t\t\tdtls +=\n\t\t\t\t\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t\t\t\t\t'<td style=\"width:150px;\"><b>' + d + '</b></td>' +\n\t\t\t\t\t\t\t\t\t\t\t\t'<td>' + cssv[d] + '</td>' +\n\t\t\t\t\t\t\t\t\t\t\t'</tr>';\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdtls += '</table>';\n\t\t\t\t\t\t\t\t\t$('#stream' + kk + 'component' + ssk).append(\n\t\t\t\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t\t\t'\t<td style=\"width:150px;\"><b>' + cssk + ':</b></td>' +\n\t\t\t\t\t\t\t\t\t\t'\t<td>' + dtls + '</td>' +\n\t\t\t\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t$('#stream' + kk + 'component' + ssk).append(\n\t\t\t\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t\t\t\t'\t<td><b>' + cssk + ':</b></td>' +\n\t\t\t\t\t\t\t\t\t\t'\t<td>' + cssv + '</td>' +\n\t\t\t\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$('#stream' + kk).append(\n\t\t\t\t\t\t\t'<tr>' +\n\t\t\t\t\t\t\t'\t<td><b>' + sk + ':</b></td>' +\n\t\t\t\t\t\t\t'\t<td>' + sv + '</td>' +\n\t\t\t\t\t\t\t'</tr>');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t$('#handle-info-table').append(\n\t\t\t\t'<tr>' +\n\t\t\t\t'\t<td><b>' + k + ':</b></td>' +\n\t\t\t\t'\t<td>' + v + '</td>' +\n\t\t\t\t'</tr>');\n\t\t}\n\t}\n\t$('#options').removeClass('hide');\n}\n\n// Tokens\nfunction updateTokens() {\n\t$('#update-tokens').unbind('click').addClass('fa-spin');\n\tlet request = { \"janus\": \"list_tokens\", \"transaction\": randomString(12), \"admin_secret\": secret };\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t$('#update-tokens').removeClass('fa-spin').click(updateTokens);\n\t\t\t\t}, 1000);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.log(\"Got tokens:\");\n\t\t\tconsole.log(json.data.tokens);\n\t\t\tsetTimeout(function() {\n\t\t\t\t$('#update-tokens').removeClass('fa-spin').click(updateTokens);\n\t\t\t}, 1000);\n\t\t\t$('#auth-tokens').html(\n\t\t\t\t'<tr>' +\n\t\t\t\t'\t<th>Token</th>' +\n\t\t\t\t'\t<th>Permissions</th>' +\n\t\t\t\t'\t<th></th>' +\n\t\t\t\t'</tr>');\n\t\t\tfor(let index in json.data.tokens) {\n\t\t\t\tlet t = json.data.tokens[index];\n\t\t\t\tlet tokenPlugins = t.allowed_plugins.toString().replace(/,/g,'<br/>');\n\t\t\t\t$('#auth-tokens').append(\n\t\t\t\t\t'<tr>' +\n\t\t\t\t\t'\t<td>' + t.token + '</td>' +\n\t\t\t\t\t'\t<td>' + tokenPlugins + '</td>' +\n\t\t\t\t\t'\t<td><button  id=\"' + t.token + '\" type=\"button\" class=\"btn btn-xs btn-danger\">Remove token</button></td>' +\n\t\t\t\t\t'</tr>');\n\t\t\t\t$('#'+t.token).click(function() {\n\t\t\t\t\tlet token = $(this).attr('id');\n\t\t\t\t\tbootbox.confirm(\"Are you sure you want to remove token \" + token + \"?\", function(result) {\n\t\t\t\t\t\tif(result)\n\t\t\t\t\t\t\tremoveToken(token);\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t\t$('#auth-tokens').append(\n\t\t\t\t'<tr>' +\n\t\t\t\t'\t<td><input type=\"text\" class=\"form-control\" id=\"token\" placeholder=\"Token to add\" onkeypress=\"return checkEnter(this, event);\" style=\"width: 100%;\"></td>' +\n\t\t\t\t'\t<td><div id=\"permissions\"></div></td>' +\n\t\t\t\t'\t<td><button id=\"addtoken\" type=\"button\" class=\"btn btn-xs btn-success\">Add token</button></td>' +\n\t\t\t\t'</tr>');\n\t\t\tlet pluginsCheckboxes = '';\n\t\t\tfor(let i in plugins) {\n\t\t\t\tlet plugin = plugins[i];\n\t\t\t\tpluginsCheckboxes +=\n\t\t\t\t\t'<div class=\"form-check\">' +\n\t\t\t\t\t'\t<input checked class=\"form-check-input\" type=\"checkbox\" value=\"' + plugin + '\">' +\n\t\t\t\t\t'\t<label class=\"form-check-label\" for=\"' + plugin + '\">' + plugin + '</label>' +\n\t\t\t\t\t'</div>';\n\t\t\t}\n\t\t\t$('#permissions').html(pluginsCheckboxes);\n\t\t\t$('#addtoken').click(function() {\n\t\t\t\tlet token = $(\"#token\").val().replace(/ /g,'');\n\t\t\t\tif(token === \"\") {\n\t\t\t\t\tbootbox.alert(\"Please insert a valid token string\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet checked = $(':checked');\n\t\t\t\tif(checked.length === 0) {\n\t\t\t\t\tbootbox.alert(\"Please allow the token access to at least a plugin\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet pluginPermissions = [];\n\t\t\t\tfor(let i=0; i<checked.length; i++)\n\t\t\t\t\tpluginPermissions.push(checked[i].value);\n\t\t\t\tlet text = \"Are you sure you want to add the new token \" + token + \" with access to the following plugins?\" +\n\t\t\t\t\t\"<br/><ul>\";\n\t\t\t\tfor(let i in pluginPermissions)\n\t\t\t\t\ttext += \"<li>\" + pluginPermissions[i] + \"</li>\";\n\t\t\t\ttext += \"</ul>\";\n\t\t\t\tbootbox.confirm(text, function(result) {\n\t\t\t\t\tif(result)\n\t\t\t\t\t\taddToken(token, pluginPermissions);\n\t\t\t\t});\n\t\t\t});\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\t$('#update-settings').removeClass('fa-spin').click(updateSettings);\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\nfunction addToken(token, permissions) {\n\tlet request = { \"janus\": \"add_token\", \"token\": token, plugins: permissions, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendTokenRequest(request);\n}\n\nfunction removeToken(token) {\n\tlet request = { \"janus\": \"remove_token\", \"token\": token, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tsendTokenRequest(request);\n}\n\nfunction sendTokenRequest(request) {\n\tconsole.log(request);\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tlet authenticate = (json[\"error\"].code === 403);\n\t\t\t\tif(!authenticate || (authenticate && !prompting && !alerted)) {\n\t\t\t\t\tif(authenticate)\n\t\t\t\t\t\talerted = true;\n\t\t\t\t\tbootbox.alert(json[\"error\"].reason, function() {\n\t\t\t\t\t\tif(authenticate) {\n\t\t\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\t\t\talerted = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tupdateTokens();\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\n// text2pcap and pcap requests\nfunction captureTrafficPrompt() {\n\tbootbox.dialog({\n\t\ttitle: \"Start capturing traffic\",\n\t\tmessage:\n\t\t\t'<div class=\"form-content\">' +\n\t\t\t'\t<form class=\"form\" role=\"form\">' +\n\t\t\t'\t\t<div class=\"form-group\">' +\n\t\t\t'\t\t\t<label for=\"type\">Capture Type</label>' +\n\t\t\t'\t\t\t<select class=\"form-control\" id=\"type\" name=\"type\" value=\"pcal\">' +\n\t\t\t'\t\t\t\t<option value=\"pcap\">pcap</option>' +\n\t\t\t'\t\t\t\t<option value=\"text2pcap\">text2pcap</option>' +\n\t\t\t'\t\t\t</select>' +\n\t\t\t'\t\t</div>' +\n\t\t\t'\t\t<div class=\"form-group\">' +\n\t\t\t'\t\t\t<label for=\"extra\">Folder to save in</label>' +\n\t\t\t'\t\t\t<input type=\"text\" class=\"form-control\" id=\"folder\" name=\"folder\" placeholder=\"Insert a path to the target folder\" value=\"\"></input>' +\n\t\t\t'\t\t</div>' +\n\t\t\t'\t\t<div class=\"form-group\">' +\n\t\t\t'\t\t\t<label for=\"extra\">Filename</label>' +\n\t\t\t'\t\t\t<input type=\"text\" class=\"form-control\" id=\"filename\" name=\"filename\" placeholder=\"Insert the target filename\" value=\"\"></input>' +\n\t\t\t'\t\t</div>' +\n\t\t\t'\t\t<div class=\"form-group\">' +\n\t\t\t'\t\t\t<label for=\"extra\">Truncate</label>' +\n\t\t\t'\t\t\t<input type=\"text\" class=\"form-control\" id=\"truncate\" name=\"truncate\" placeholder=\"Bytes to truncate at (0 or omit to save the whole packet)\" value=\"\"></input>' +\n\t\t\t'\t\t</div>' +\n\t\t\t'\t</form>' +\n\t\t\t'</div>',\n\t\tbuttons: [\n\t\t\t{\n\t\t\t\tlabel: \"Start\",\n\t\t\t\tclassName: \"btn btn-primary pull-left\",\n\t\t\t\tcallback: function() {\n\t\t\t\t\tlet text = $('#type').val() === \"text2pcap\";\n\t\t\t\t\tlet folder = $('#folder').val() !== '' ? $('#folder').val() : undefined;\n\t\t\t\t\tlet filename = $('#filename').val() !== '' ? $('#filename').val() : undefined;\n\t\t\t\t\tlet truncate = parseInt($('#truncate').val());\n\t\t\t\t\tif(!truncate || isNaN(truncate))\n\t\t\t\t\t\ttruncate = 0;\n\t\t\t\t\tcaptureTrafficRequest(true, text, folder, filename, truncate);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: \"Close\",\n\t\t\t\tclassName: \"btn btn-secondary pull-left\",\n\t\t\t\tcallback: function() {\n\t\t\t\t\t$('#capture').removeAttr('checked');\n\t\t\t\t\t$('#capturetext').html('Start capture');\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t});\n}\n\nfunction captureTrafficRequest(start, text, folder, filename, truncate) {\n\tlet req = start ? ( text ? \"start_text2pcap\" : \"start_pcap\" ) :\n\t\t( text ? \"stop_text2pcap\" : \"stop_pcap\" )\n\tlet request = { \"janus\": req, \"transaction\": randomString(12), \"admin_secret\": secret };\n\tif(start) {\n\t\trequest[\"folder\"] = folder;\n\t\trequest[\"filename\"] = filename;\n\t\trequest[\"truncate\"] = truncate;\n\t}\n\t$.ajax({\n\t\ttype: 'POST',\n\t\turl: server + \"/\" + session + \"/\" + handle,\n\t\tcache: false,\n\t\tcontentType: \"application/json\",\n\t\tdata: JSON.stringify(request),\n\t\tsuccess: function(json) {\n\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\tconsole.log(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tbootbox.alert(json[\"error\"].reason);\n\t\t\t\tif(start && json[\"error\"].reason.indexOf('already') === -1) {\n\t\t\t\t\t$('#capture').removeAttr('checked');\n\t\t\t\t\t$('#capturetext').html('Start capture');\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t},\n\t\t// eslint-disable-next-line no-unused-vars\n\t\terror: function(XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tconsole.log(textStatus + \": \" + errorThrown);\t// FIXME\n\t\t\tif(!prompting && !alerted) {\n\t\t\t\talerted = true;\n\t\t\t\tbootbox.alert(\"Couldn't contact the backend: is Janus down, or is the Admin/Monitor interface disabled?\", function() {\n\t\t\t\t\tpromptAccessDetails();\n\t\t\t\t\talerted = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tdataType: \"json\"\n\t});\n}\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tif(field.id == 'token')\n\t\t\t$('#addtoken').click();\n\t\telse if(field.id.indexOf('attr') !== -1)\n\t\t\t$('#sendmsg').click();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "html/demos/audiobridge.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Audio Bridge Demo</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/10.6.2/bootstrap-slider.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"audiobridge.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='audiobridge.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/10.6.2/css/bootstrap-slider.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Audio Bridge (mixed)\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>AudioBridge</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/audiobridge\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>The Audio Bridge demo is a simple example of how to implement\n\t\t\t\t\t\tan audio conferencing application through Janus. Since it makes use\n\t\t\t\t\t\tof the AudioBridge plugin, all the audio contributions will be mixed,\n\t\t\t\t\t\twhich means that a single PeerConnection will be created no matter\n\t\t\t\t\t\thow many participants will join the room.</p>\n\t\t\t\t\t\t<p>To try the demo, just insert a username to join the default audio\n\t\t\t\t\t\troom that is configured. This will add you to the mixed audio conference.\n\t\t\t\t\t\tA container on the left will display a list of the other participants\n\t\t\t\t\t\tin the room as they join, with a simple indicator to inform you whether\n\t\t\t\t\t\tthey're muted or not. You can mute/unmute yourself using the icon that\n\t\t\t\t\t\twill appear next to your name.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"audiojoin\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<span class=\"badge bg-info\" id=\"you\"></span>\n\t\t\t\t\t<div class=\"col-md-12\" id=\"controls\">\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-1 hide\" id=\"registernow\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-user\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Choose a display name\" autocomplete=\"off\" id=\"username\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t\t\t\t<button class=\"btn btn-success\" autocomplete=\"off\" id=\"register\">Join the room</button>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"room\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Participants <span class=\"badge bg-info hide\" id=\"participant\"></span>\n\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right\">\n\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger hide\" autocomplete=\"off\" id=\"toggleaudio\">Mute</button>\n\t\t\t\t\t\t\t\t\t<button class=\"btn btn-primary hide\" autocomplete=\"off\" id=\"position\">Position</button>\n\t\t\t\t\t\t\t\t\t<button class=\"btn btn-secondary hide\" autocomplete=\"off\" id=\"togglesuspend\">Suspend</button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\">\n\t\t\t\t\t\t\t\t<ul id=\"list\" class=\"list-group\">\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Mixed Audio</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"mixedaudio\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/audiobridge.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar mixertest = null;\nvar opaqueId = \"audiobridgetest-\"+Janus.randomString(12);\n\nvar remoteStream = null;\n\nvar myroom = 1234;\t// Demo room\nif(getQueryStringValue(\"room\") !== \"\")\n\tmyroom = parseInt(getQueryStringValue(\"room\"));\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar stereo = false;\nif(getQueryStringValue(\"stereo\") !== \"\")\n\tstereo = (getQueryStringValue(\"stereo\") === \"true\");\nvar mygroup = null;\t// Forwarding group, if required by the room\nif(getQueryStringValue(\"group\") !== \"\")\n\tmygroup = getQueryStringValue(\"group\");\nvar myusername = null;\nvar myid = null;\nvar webrtcUp = false;\nvar audioenabled = false;\nvar audiosuspended = (getQueryStringValue(\"suspended\") !== \"\") ? (getQueryStringValue(\"suspended\") === \"true\") : false;\n\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to AudioBridge plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.audiobridge\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tmixertest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + mixertest.getPlugin() + \", id=\" + mixertest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Prepare the username registration\n\t\t\t\t\t\t\t\t\t$('#audiojoin').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#registernow').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#register').click(registerUsername);\n\t\t\t\t\t\t\t\t\t$('#username').focus();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tlet event = msg[\"audiobridge\"];\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Event: \" + event);\n\t\t\t\t\t\t\t\t\tif(event) {\n\t\t\t\t\t\t\t\t\t\tif(event === \"joined\") {\n\t\t\t\t\t\t\t\t\t\t\t// Successfully joined, negotiate WebRTC now\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"id\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tmyid = msg[\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Successfully joined room \" + msg[\"room\"] + \" with ID \" + myid);\n\t\t\t\t\t\t\t\t\t\t\t\tif(!webrtcUp) {\n\t\t\t\t\t\t\t\t\t\t\t\t\twebrtcUp = true;\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Publish our stream\n\t\t\t\t\t\t\t\t\t\t\t\t\tmixertest.createOffer(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We only want bidirectional audio\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcustomizeSdp: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(stereo && jsep.sdp.indexOf(\"stereo=1\") == -1) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Make sure that our offer contains stereo too\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tjsep.sdp = jsep.sdp.replace(\"useinbandfec=1\", \"useinbandfec=1;stereo=1\");\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#mixedaudio').html(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet publish = { request: \"configure\", muted: false };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmixertest.send({ message: publish, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// Any room participant?\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"participants\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"participants\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of participants:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = escapeXmlTags(list[f][\"display\"]);\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet setup = list[f][\"setup\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet muted = list[f][\"muted\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet suspended = list[f][\"suspended\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet spatial = list[f][\"spatial_position\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display + \" (setup=\" + setup + \", muted=\" + muted + \")\");\n\t\t\t\t\t\t\t\t\t\t\t\t\tif($('#rp' + id).length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Add to the participants list\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet slider = '';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tslider = '<span>[L <input id=\"sp' + id + '\" type=\"text\" style=\"width: 10%;\"/> R] </span>';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#list').append('<li id=\"rp' + id +'\" class=\"list-group-item\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tslider +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"absetup fa-solid fa-link-slash\" title=\"No PeerConnection\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"absusp fa-solid fa-eye-slash\" title=\"Suspended\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"abmuted fa-solid fa-microphone-slash\" title=\"Muted\"></i></li>');\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#sp' + id).slider({ min: 0, max: 100, step: 1, value: 50, handle: 'triangle', enabled: false });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#position').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(muted === true || muted === \"true\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.abmuted').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.abmuted').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(setup === true || setup === \"true\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absetup').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absetup').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(suspended === true)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#sp' + id).slider('setValue', spatial);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"roomchanged\") {\n\t\t\t\t\t\t\t\t\t\t\t// The user switched to a different room\n\t\t\t\t\t\t\t\t\t\t\tmyid = msg[\"id\"];\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Moved to room \" + msg[\"room\"] + \", new ID: \" + myid);\n\t\t\t\t\t\t\t\t\t\t\t// Any room participant?\n\t\t\t\t\t\t\t\t\t\t\t$('#list').empty();\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"participants\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"participants\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of participants:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = escapeXmlTags(list[f][\"display\"]);\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet setup = list[f][\"setup\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet muted = list[f][\"muted\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet suspended = list[f][\"suspended\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet spatial = list[f][\"spatial_position\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display + \" (setup=\" + setup + \", muted=\" + muted + \")\");\n\t\t\t\t\t\t\t\t\t\t\t\t\tif($('#rp' + id).length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Add to the participants list\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet slider = '';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tslider = '<span>[L <input id=\"sp' + id + '\" type=\"text\" style=\"width: 10%;\"/> R] </span>';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#list').append('<li id=\"rp' + id +'\" class=\"list-group-item\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tslider +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"absetup fa-solid fa-link-slash\" title=\"No PeerConnection\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"absusp fa-solid fa-eye-slash\" title=\"Suspended\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"abmuted fa-solid fa-microphone-slash\" title=\"Muted\"></i></li>');\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#sp' + id).slider({ min: 0, max: 100, step: 1, value: 50, handle: 'triangle', enabled: false });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#position').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(muted === true || muted === \"true\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.abmuted').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.abmuted').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(setup === true || setup === \"true\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absetup').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absetup').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(suspended === true)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#sp' + id).slider('setValue', spatial);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"destroyed\") {\n\t\t\t\t\t\t\t\t\t\t\t// The room has been destroyed\n\t\t\t\t\t\t\t\t\t\t\tJanus.warn(\"The room has been destroyed!\");\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The room has been destroyed\", function() {\n\t\t\t\t\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"event\") {\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"participants\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(msg[\"resumed\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// This is a full recap after a suspend: clear the list of participants\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#list').empty();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"participants\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of participants:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = escapeXmlTags(list[f][\"display\"]);\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet setup = list[f][\"setup\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet muted = list[f][\"muted\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet suspended = list[f][\"suspended\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet spatial = list[f][\"spatial_position\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display + \" (setup=\" + setup + \", muted=\" + muted + \")\");\n\t\t\t\t\t\t\t\t\t\t\t\t\tif($('#rp' + id).length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Add to the participants list\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet slider = '';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tslider = '<span>[L <input id=\"sp' + id + '\" type=\"text\" style=\"width: 10%;\"/> R] </span>';\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#list').append('<li id=\"rp' + id +'\" class=\"list-group-item\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tslider +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"absetup fa-solid fa-link-slash\" title=\"No PeerConnection\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"absusp fa-solid fa-eye-slash\" title=\"Suspended\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t' <i class=\"abmuted fa-solid fa-microphone-slash\" title=\"Muted\"></i></li>');\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#sp' + id).slider({ min: 0, max: 100, step: 1, value: 50, handle: 'triangle', enabled: false });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#position').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(muted === true || muted === \"true\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.abmuted').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.abmuted').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(setup === true || setup === \"true\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absetup').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absetup').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(suspended === true)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(spatial !== null && spatial !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#sp' + id).slider('setValue', spatial);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"suspended\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet id = msg[\"suspended\"];\n\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"resumed\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet id = msg[\"resumed\"];\n\t\t\t\t\t\t\t\t\t\t\t\t$('#rp' + id + ' > i.absusp').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"error\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(msg[\"error_code\"] === 485) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// This is a \"no such room\" error: give a more meaningful description\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"<p>Apparently room <code>\" + myroom + \"</code> (the one this demo uses as a test room) \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"does not exist...</p><p>Do you have an updated <code>janus.plugin.audiobridge.jcfg</code> \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"configuration file? If not, make sure you copy the details of room <code>\" + myroom + \"</code> \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"from that sample in your current configuration file, then restart Janus and try again.\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// Any new feed to attach to?\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"leaving\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t// One of the participants has gone away?\n\t\t\t\t\t\t\t\t\t\t\t\tlet leaving = msg[\"leaving\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Participant left: \" + leaving + \" (we have \" + $('#rp'+leaving).length + \" elements with ID #rp\" +leaving + \")\");\n\t\t\t\t\t\t\t\t\t\t\t\t$('#rp'+leaving).remove();\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\tmixertest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We're not going to attach the local audio stream\n\t\t\t\t\t\t\t\t\t$('#audiojoin').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#room').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#participant').removeClass('hide').html(myusername).removeClass('hide');\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata ? \" (\" + metadata.reason + \") \" : \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tif(remoteStream || track.kind !== \"audio\")\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tremoteStream = null;\n\t\t\t\t\t\t\t\t\t\t$('#roomaudio').remove();\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tremoteStream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t$('#room').removeClass('hide');\n\t\t\t\t\t\t\t\t\tif($('#roomaudio').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#mixedaudio').append('<audio class=\"rounded centered w-100\" id=\"roomaudio\" controls autoplay/>');\n\t\t\t\t\t\t\t\t\t\t$('#roomaudio').get(0).volume = 0;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#roomaudio').get(0), remoteStream);\n\t\t\t\t\t\t\t\t\t$('#roomaudio').get(0).play();\n\t\t\t\t\t\t\t\t\t$('#roomaudio').get(0).volume = 1;\n\t\t\t\t\t\t\t\t\t// Mute button\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = !audioenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(audioenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Mute\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Unmute\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\tmixertest.send({ message: { request: \"configure\", muted: !audioenabled }});\n\t\t\t\t\t\t\t\t\t\t}).removeClass('hide');\n\t\t\t\t\t\t\t\t\t// Suspend button\n\t\t\t\t\t\t\t\t\tif(!audiosuspended)\n\t\t\t\t\t\t\t\t\t\t$('#togglesuspend').html(\"Suspend\").removeClass(\"btn-info\").addClass(\"btn-secondary\");\n\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t$('#togglesuspend').html(\"Resume\").removeClass(\"btn-secondary\").addClass(\"btn-info\");\n\t\t\t\t\t\t\t\t\t$('#togglesuspend').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudiosuspended = !audiosuspended;\n\t\t\t\t\t\t\t\t\t\t\tif(!audiosuspended)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglesuspend').html(\"Suspend\").removeClass(\"btn-info\").addClass(\"btn-secondary\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglesuspend').html(\"Resume\").removeClass(\"btn-secondary\").addClass(\"btn-info\");\n\t\t\t\t\t\t\t\t\t\t\tmixertest.send({ message: {\n\t\t\t\t\t\t\t\t\t\t\t\trequest: (audiosuspended ? \"suspend\" : \"resume\"),\n\t\t\t\t\t\t\t\t\t\t\t\troom: myroom,\n\t\t\t\t\t\t\t\t\t\t\t\tid: myid\n\t\t\t\t\t\t\t\t\t\t\t}});\n\t\t\t\t\t\t\t\t\t\t}).removeClass('hide');\n\t\t\t\t\t\t\t\t\t// Spatial position, if enabled\n\t\t\t\t\t\t\t\t\t$('#position').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\tbootbox.prompt(\"Insert new spatial position: [0-100] (0=left, 50=center, 100=right)\", function(result) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet spatial = parseInt(result);\n\t\t\t\t\t\t\t\t\t\t\t\tif(isNaN(spatial) || spatial < 0 || spatial > 100) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Invalid value\");\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tmixertest.send({ message: { request: \"configure\", spatial_position: spatial }});\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\twebrtcUp = false;\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$('#participant').empty().addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#list').empty();\n\t\t\t\t\t\t\t\t\t$('#mixedaudio').empty();\n\t\t\t\t\t\t\t\t\t$('#room').addClass('hide');\n\t\t\t\t\t\t\t\t\tremoteStream = null;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tregisterUsername();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction registerUsername() {\n\tif($('#username').length === 0) {\n\t\t// Create fields to register\n\t\t$('#register').click(registerUsername);\n\t\t$('#username').focus();\n\t} else {\n\t\t// Try a registration\n\t\t$('#username').attr('disabled', true);\n\t\t$('#register').attr('disabled', true).unbind('click');\n\t\tlet username = $('#username').val();\n\t\tif(username === \"\") {\n\t\t\t$('#you')\n\t\t\t\t.removeClass().addClass('badge bg-warning')\n\t\t\t\t.html(\"Insert your display name (e.g., pippo)\");\n\t\t\t$('#username').removeAttr('disabled');\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\treturn;\n\t\t}\n\t\tif(/[^a-zA-Z0-9]/.test(username)) {\n\t\t\t$('#you')\n\t\t\t\t.removeClass().addClass('badge bg-warning')\n\t\t\t\t.html('Input is not alphanumeric');\n\t\t\t$('#username').removeAttr('disabled').val(\"\");\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\treturn;\n\t\t}\n\t\tlet register = { request: \"join\", room: myroom, display: username, suspended: audiosuspended };\n\t\tmyusername = escapeXmlTags(username);\n\t\t// Check if we need to join using G.711 instead of (default) Opus\n\t\tif(acodec === 'opus' || acodec === 'pcmu' || acodec === 'pcma')\n\t\t\tregister.codec = acodec;\n\t\t// If the room uses forwarding groups, this is how we state ours\n\t\tif(mygroup)\n\t\t\tregister[\"group\"] = mygroup;\n\t\t// Send the message\n\t\tmixertest.send({ message: register });\n\t}\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n"
  },
  {
    "path": "html/demos/canvas.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Canvas Capture</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"canvas.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='canvas.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Canvas Capture\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>EchoTest</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/echotest\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a variant of the Echo Test demo meant to showcase how\n\t\t\t\t\t\tyou can use an HTML5 <code>canvas</code> element as a WebRTC media\n\t\t\t\t\t\tsource: everything is exactly the same in term of available controls,\n\t\t\t\t\t\tfeatures, and the like, with the substantial difference that we'll\n\t\t\t\t\t\tplay a bit with what we'll send on the video stream.</p>\n\t\t\t\t\t\t<p>More precisely, the demo captures the webcam feed via a\n\t\t\t\t\t\t<code>getUserMedia</code> call to use as a background in a\n\t\t\t\t\t\t<code>canvas</code> element, and then presents some basic controls\n\t\t\t\t\t\tto add some text dynamically; an image is also statically added\n\t\t\t\t\t\tto the element as well. The <code>canvas</code> element is then used\n\t\t\t\t\t\tas the actual source of media for our PeerConnection, which means the\n\t\t\t\t\t\tvideo we get back from the EchoTest plugin should reflect the\n\t\t\t\t\t\ttweaks we've made on the stream.</p>\n\t\t\t\t\t\t<p>Notice that this is a very naive implementation, with many\n\t\t\t\t\t\thardcoded assumptions about video resolution and other things, and may not\n\t\t\t\t\t\tperform very well either: it's only meant to showcase an interesting\n\t\t\t\t\t\tapproach that can be used for WebRTC, so you're encouraged to dig\n\t\t\t\t\t\tdeeper yourself to see how to make the <code>canvas</code>\n\t\t\t\t\t\tprocessing more efficient and cooler. The code for this demo comes from a\n\t\t\t\t\t\t<a href=\"https://youtu.be/zwYUojfm0hY?t=2140\" target=\"blank\">Dangerous Demo</a>\n\t\t\t\t\t\twe showed during a past edition of Kamailio World; you can read more details in a\n\t\t\t\t\t\t<a href=\"https://www.meetecho.com/blog/firefox-webrtc-youtube-kinda/\" target=\"blank\">blog post</a>\n\t\t\t\t\t\twe wrote on the Meetecho blog after the fact.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"toggleaudio\">Disable audio</button>\n\t\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"togglevideo\">Disable video</button>\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t<button id=\"bitrateset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\t\tBandwidth\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\">\n\t\t\t\t\t\t\t\t\t<video class=\"rounded centered hide\" id=\"myvideo\" width=\"100%\" height=\"100%\" muted=\"muted\"></video>\n\t\t\t\t\t\t\t\t\t<video class=\"rounded centered hide\" id=\"canvasvideo\" width=\"640\" height=\"480\" muted=\"muted\"></video>\n\t\t\t\t\t\t\t\t\t<canvas id=\"canvas\" class=\"hide\" width=\"640\" height=\"480\" style=\"display: block; margin: auto; padding: 0\"></canvas>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"row mt-3 mb-3\">\n\t\t\t\t\t\t\t<div class=\"card\" style=\"width: 100%;\">\n\t\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t\t<span class=\"card-title\">Tweaks</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"card-body\" id=\"tweaks\">\n\t\t\t\t\t\t\t\t\t<div class=\"input-group mb-2\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-font\"></i></span>\n\t\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" autocomplete=\"off\" id=\"font\" onkeypress=\"return checkEnter(event);\"></input>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"input-group mt-2 mb-2\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-eraser\"></i></span>\n\t\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" autocomplete=\"off\" id=\"color\" onkeypress=\"return checkEnter(event);\"></input>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"input-group mt-2 mb-2\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-text-width\"></i></span>\n\t\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" autocomplete=\"off\" id=\"posX\" onkeypress=\"return checkEnter(event);\"></input>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"input-group mt-2 mb-2\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-text-height\"></i></span>\n\t\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" autocomplete=\"off\" id=\"posY\" onkeypress=\"return checkEnter(event);\"></input>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"input-group mt-2\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-pen-to-square\"></i></span>\n\t\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" autocomplete=\"off\" id=\"text\" onkeypress=\"return checkEnter(event);\"></input>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream <span class=\"badge bg-primary hide\" id=\"curres\"></span> <span class=\"badge bg-info hide\" id=\"curbitrate\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/canvas.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar echotest = null;\nvar opaqueId = \"canvas-\"+Janus.randomString(12);\n\nvar remoteTracks = {}, remoteVideos = 0;\nvar bitrateTimer = null;\n\nvar audioenabled = false;\nvar videoenabled = false;\n\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar vprofile = (getQueryStringValue(\"vprofile\") !== \"\" ? getQueryStringValue(\"vprofile\") : null);\nvar simulcastStarted = false;\n\nvar stream = null;\nvar canvasStream = null;\n\n// We'll try to do 15 frames per second: should be relatively fluid, and\n// most important should be doable in JavaScript on lower end machines too\nvar fps = 15;\n// Let's add some placeholders for the tweaks we can configure\nvar myText = \"Hi there!\";\nvar myColor = \"white\";\nvar myFont = \"20pt Calibri\";\nvar myX = 15, myY = 460;\n// As the \"watermark\", we'll use a smaller version of the Janus logo\nvar logoUrl = \"../janus-logo-small.png\";\nvar logoW = 340, logoH = 110;\nvar logoS = 0.4;\nvar logoX = 640 - logoW*logoS - 15, logoY = 15;\n\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to EchoTest plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.echotest\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\techotest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + echotest.getPlugin() + \", id=\" + echotest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// We're connected to the plugin, create and populate the canvas element\n\t\t\t\t\t\t\t\t\tcreateCanvas();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\techotest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result === \"done\") {\n\t\t\t\t\t\t\t\t\t\t\t// The plugin closed the echo test\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The Echo Test is over\");\n\t\t\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#peervideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// Any loss?\n\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\tif(status === \"slow_link\") {\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Janus apparently missed many packets we sent, maybe we should reduce the bitrate\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted) {\n\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted = true;\n\t\t\t\t\t\t\t\t\t\t\taddSimulcastButtons(msg[\"videocodec\"] === \"vp8\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\tupdateSimulcastButtons(substream, temporal);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\t// We ignore the stream we got here, we're using the canvas to render it\n\t\t\t\t\t\t\t\t\tif(echotest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\techotest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata? \" (\" + metadata.reason + \") \" : \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tlet addButtons = false;\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\taddButtons = true;\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// FIXME we'll need this for additional videos too\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#peervideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = echotest.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t//~ Janus.debug(\"Current bitrate is \" + echotest.getBitrate());\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#peervideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#peervideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(!addButtons)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// Enable audio/video buttons and bitrate limiter\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = !audioenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(audioenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Enable audio\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { audio: audioenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#togglevideo').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\tvideoenabled = !videoenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(videoenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Enable video\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { video: videoenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\techotest.send({ message: { bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\tsimulcastStarted = false;\n\t\t\t\t\t\t\t\t\t$('#simulcast').remove();\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tupdateCanvas();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\n// Helper function to create (and populate) our canvas element\nfunction createCanvas() {\n\t// Predefined tweaks\n\t$('#text').val(myText);\n\t$('#color').val(myColor);\n\t$('#font').val(myFont);\n\t$('#posX').val(\"\"+myX);\n\t$('#posY').val(\"\"+myY);\n\t// Capture the local webcam\n\tnavigator.mediaDevices.getUserMedia(\n\t\t{\n\t\t\taudio: true,\n\t\t\tvideo: {\n\t\t\t\twidth: { ideal: 640 },\n\t\t\t\theight: { ideal: 480 }\n\t\t\t}\n\t\t})\n\t\t.then(function(stream) {\n\t\t\t// We have our video\n\t\t\tJanus.debug(stream);\n\t\t\tJanus.attachMediaStream($('#canvasvideo').get(0), stream);\n\t\t\t$('#canvasvideo').get(0).muted = \"muted\";\n\t\t\t$('#canvasvideo').get(0).play();\n\t\t\t// Let's setup the canvas, now\n\t\t\t$('#canvasvideo').get(0).addEventListener('play', function () {\n\t\t\t\tlet myvideo = this;\n\t\t\t\tlet canvas = document.getElementById('canvas');\n\t\t\t\tlet context = canvas.getContext('2d');\n\t\t\t\tlet logo = new Image();\n\t\t\t\tlogo.onload = function() {\n\t\t\t\t\t(function loop() {\n\t\t\t\t\t\tif(!myvideo.paused && !myvideo.ended) {\n\t\t\t\t\t\t\t// Copy video to image\n\t\t\t\t\t\t\tcontext.drawImage(myvideo, 0, 0);\n\t\t\t\t\t\t\t// Add logo\n\t\t\t\t\t\t\tcontext.drawImage(logo,\n\t\t\t\t\t\t\t\t0, 0, logoW, logoH,\n\t\t\t\t\t\t\t\tlogoX, logoY, logoW*logoS, logoH*logoS);\n\t\t\t\t\t\t\t// Add some text\n\t\t\t\t\t\t\tcontext.fillStyle = 'rgba(0,0,0,0.5)';\n\t\t\t\t\t\t\tcontext.fillRect(0, 420, 640, 480);\n\t\t\t\t\t\t\tcontext.font = myFont;\n\t\t\t\t\t\t\tcontext.fillStyle = myColor;\n\t\t\t\t\t\t\tcontext.fillText(myText, myX, myY);\n\t\t\t\t\t\t\t// We're drawing at the specified fps\n\t\t\t\t\t\t\tsetTimeout(loop, 1000 / fps);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t};\n\t\t\t\tlogo.src = logoUrl;\n\t\t\t\t// Capture the canvas as a local MediaStream\n\t\t\t\tcanvasStream = canvas.captureStream();\n\t\t\t\tcanvasStream.addTrack(stream.getAudioTracks()[0]);\n\t\t\t\tJanus.attachMediaStream($('#myvideo').get(0), canvasStream);\n\t\t\t\t$('#myvideo').get(0).muted = \"muted\";\n\t\t\t\t$('#myvideo').get(0).play();\n\t\t\t\t$('#myvideo').removeClass('hide');\n\t\t\t\t// Now that the stream is ready, we can create the PeerConnection\n\t\t\t\tlet body = { audio: true, video: true };\n\t\t\t\t// We can try and force a specific codec, by telling the plugin what we'd prefer\n\t\t\t\t// For simplicity, you can set it via a query string (e.g., ?vcodec=vp9)\n\t\t\t\tif(acodec)\n\t\t\t\t\tbody[\"audiocodec\"] = acodec;\n\t\t\t\tif(vcodec)\n\t\t\t\t\tbody[\"videocodec\"] = vcodec;\n\t\t\t\t// For the codecs that support them (VP9 and H.264) you can specify a codec\n\t\t\t\t// profile as well (e.g., ?vprofile=2 for VP9, or ?vprofile=42e01f for H.264)\n\t\t\t\tif(vprofile)\n\t\t\t\t\tbody[\"videoprofile\"] = vprofile;\n\t\t\t\tJanus.debug(\"Sending message:\", body);\n\t\t\t\techotest.send({ message: body });\n\t\t\t\tJanus.debug(\"Trying a createOffer too (audio/video sendrecv)\");\n\t\t\t\t// We need to pass the canvas MediaStream tracks we\n\t\t\t\t// captured here, so we tell janus.js to use those\n\t\t\t\tlet canvasTracks = [];\n\t\t\t\tif(canvasStream.getAudioTracks().length > 0)\n\t\t\t\t\tcanvasTracks.push({ type: 'audio', capture: canvasStream.getAudioTracks()[0], recv: true });\n\t\t\t\tif(canvasStream.getVideoTracks().length > 0)\n\t\t\t\t\tcanvasTracks.push({ type: 'video', capture: canvasStream.getVideoTracks()[0], recv: true });\n\t\t\t\tJanus.warn(canvasTracks);\n\t\t\t\techotest.createOffer(\n\t\t\t\t\t{\n\t\t\t\t\t\ttracks: canvasTracks,\n\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\techotest.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t\t\t$('#videoright').html(\n\t\t\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t},\n\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t}, 0);\n\t\t})\n\t\t.catch(function(error) {\n\t\t\tJanus.error(error);\n\t\t\tbootbox.alert(error);\n\t\t});\n}\n// Helper function to update a canvas when the tweaks are used\nfunction updateCanvas() {\n\tmyText = $('#text').val();\n\tmyColor = $('#color').val();\n\tmyFont = $('#font').val();\n\tmyX = parseInt($('#posX').val());\n\tmyY = parseInt($('#posY').val());\n}\n\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helpers to create Simulcast-related UI, if enabled\nfunction addSimulcastButtons(temporal) {\n\t$('#curres').parent().append(\n\t\t'<div id=\"simulcast\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>');\n\tif(Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers\n\t\t$('#tl-2').remove();\n\t}\n\t// Enable the simulcast selection buttons\n\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { substream: 0 }});\n\t\t});\n\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 1 }});\n\t\t});\n\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl-0').parent().removeClass('hide');\n\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { temporal: 0 }});\n\t\t});\n\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 1 }});\n\t\t});\n\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastButtons(substream, temporal) {\n\t// Check the substream\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched simulcast substream! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched simulcast substream! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched simulcast substream! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/devices.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Device Selection Test</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"devices.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='devices.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Device Selection\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>EchoTest</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/echotest\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a variant of the Echo Test demo: everything is exactly\n\t\t\t\t\t\tthe same in term of available controls, features, and the like, with\n\t\t\t\t\t\tthe substantial difference that you can select which of the available\n\t\t\t\t\t\tdevices (microphones, webcams) you want to use for the media setup.</p>\n\t\t\t\t\t\t<p>The demo will start by automatically selecting some default devices,\n\t\t\t\t\t\tpretty much as the regular Echo Test demo already does. Once done,\n\t\t\t\t\t\tyou'll be able to individually change the capture audio and/or video\n\t\t\t\t\t\tdevice: this will result in the Echo Test channel being reset and\n\t\t\t\t\t\trecreated, in order to use the device(s) you selected instead. Just\n\t\t\t\t\t\tas with the regular Echo Test demo, the <code>?simulcast=true</code>\n\t\t\t\t\t\tquery string will allow you to test simulcasting as well.</p>\n\t\t\t\t\t\t<p>The demo exploits a functionality made available in the\n\t\t\t\t\t\t<code>janus.js</code> library, meaning you should be able to\n\t\t\t\t\t\teasily adapt all the other demos to follow the same approach as\n\t\t\t\t\t\twell, and that you'll be able to do the same in your Janus-based\n\t\t\t\t\t\tweb application too.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4\">\n\t\t\t\t<div class=\"row mb-4 hide\" id=\"devices\">\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t\t<label for=\"audio-device\">Audio device (input):</label>\n\t\t\t\t\t\t\t\t<select id=\"audio-device\" class=\"form-control\"></select>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t\t<label for=\"video-device\">Video device (input):</label>\n\t\t\t\t\t\t\t\t<select id=\"video-device\" class=\"form-control\"></select>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"col-md-4 mt-auto\">\n\t\t\t\t\t\t\t<div class=\"form-group\">\n\t\t\t\t\t\t\t\t<div id=\"change-devices\" class=\"form-control btn btn-primary\">Change devices</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<hr/>\n \t\t\t\t<div class=\"row hide\" id=\"videos\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"toggleaudio\">Disable audio</button>\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"togglevideo\">Disable video</button>\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<button id=\"bitrateset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\tBandwidth</span>\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-up\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" placeholder=\"Write a DataChannel message\" autocomplete=\"off\" id=\"datasend\" onkeypress=\"return checkEnter(event);\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream\n\t\t\t\t\t\t\t\t\t<span class=\"badge bg-primary hide\" id=\"curres\"></span>\n\t\t\t\t\t\t\t\t\t<span class=\"badge bg-info hide\" id=\"curbitrate\"></span>\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm bottom-right hide\" id=\"output-devices\">\n\t\t\t\t\t\t\t\t\t\t<button id=\"outputdeviceset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\tOutput device\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t<ul id=\"audiooutput\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-down\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"datarecv\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/devices.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar echotest = null;\nvar opaqueId = \"devicetest-\"+Janus.randomString(12);\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\nvar bitrateTimer = null;\n\nvar audioDeviceId = null;\nvar videoDeviceId = null;\n\nvar audioenabled = false;\nvar videoenabled = false;\n\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar vprofile = (getQueryStringValue(\"vprofile\") !== \"\" ? getQueryStringValue(\"vprofile\") : null);\nvar simulcastStarted = false;\n\n// Helper method to prepare a UI selection of the available devices\nfunction initDevices(devices) {\n\t$('#devices').removeClass('hide');\n\t$('#devices').parent().removeClass('hide');\n\t$('#choose-device').click(restartCapture);\n\t$('#audio-device, #video-device').find('option').remove();\n\n\tdevices.forEach(function(device) {\n\t\tlet label = device.label;\n\t\tif(!label || label === \"\")\n\t\t\tlabel = device.deviceId;\n\t\tlet option = $('<option value=\"' + device.deviceId + '\">' + label + '</option>');\n\t\tif(device.kind === 'audioinput') {\n\t\t\t$('#audio-device').append(option);\n\t\t} else if(device.kind === 'videoinput') {\n\t\t\t$('#video-device').append(option);\n\t\t} else if(device.kind === 'audiooutput') {\n\t\t\t// Apparently only available from Chrome 49 on?\n\t\t\t// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId\n\t\t\t// Definitely missing in Safari at the moment: https://bugs.webkit.org/show_bug.cgi?id=179415\n\t\t\t$('#output-devices').removeClass('hide');\n\t\t\t$('#audiooutput').append('<a class=\"dropdown-item\" href=\"#\" id=\"' + device.deviceId + '\">' + label + '</a>');\n\t\t\t$('#audiooutput a').unbind('click')\n\t\t\t\t.click(function() {\n\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\tlet deviceId = $(this).attr(\"id\");\n\t\t\t\t\tlet label = $(this).text();\n\t\t\t\t\tJanus.log(\"Trying to set device \" + deviceId + \" (\" + label + \") as sink for the output\");\n\t\t\t\t\tif($('#peervideo1').length === 0) {\n\t\t\t\t\t\tJanus.error(\"No remote video element available\");\n\t\t\t\t\t\tbootbox.alert(\"No remote video element available\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tif(!$('#peervideo1').get(0).setSinkId) {\n\t\t\t\t\t\tJanus.error(\"SetSinkId not supported\");\n\t\t\t\t\t\tbootbox.warn(\"SetSinkId not supported\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\t$('#peervideo1').get(0).setSinkId(deviceId)\n\t\t\t\t\t\t.then(function() {\n\t\t\t\t\t\t\tJanus.log('Audio output device attached:', deviceId);\n\t\t\t\t\t\t\t$('#outputdeviceset').text(label).parent().removeClass('open');\n\t\t\t\t\t\t}).catch(function(error) {\n\t\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\t\tbootbox.alert(error);\n\t\t\t\t\t\t});\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\t\t}\n\t});\n\n\t$('#change-devices').click(function() {\n\t\t// A different device has been selected: hangup the session, and set it up again\n\t\t$('#audio-device, #video-device').attr('disabled', true);\n\t\t$('#change-devices').attr('disabled', true);\n\t\trestartCapture();\n\t});\n}\n\nvar firstOffer = true;\nfunction restartCapture() {\n\tlet replaceAudio = $('#audio-device').val() !== audioDeviceId;\n\taudioDeviceId = $('#audio-device').val();\n\tlet replaceVideo = $('#video-device').val() !== videoDeviceId;\n\tvideoDeviceId = $('#video-device').val();\n\tJanus.warn(videoDeviceId);\n\tlet width = doSimulcast ? 1280 : 640;\n\tlet height = doSimulcast ? 720 : 480;\n\tif(!firstOffer) {\n\t\tif(!replaceAudio && !replaceVideo) {\n\t\t\t// Nothing to do, reset devices controls\n\t\t\t$('#audio-device, #video-device').removeAttr('disabled');\n\t\t\t$('#change-devices').removeAttr('disabled');\n\t\t\treturn;\n\t\t}\n\t\t// Just replacing tracks, no need for a renegotiation\n\t\tlet tracks = [];\n\t\tif(replaceAudio) {\n\t\t\ttracks.push({\n\t\t\t\ttype: 'audio',\n\t\t\t\tmid: '0',\t// We assume mid 0 is audio\n\t\t\t\tcapture: { deviceId: { exact: audioDeviceId } }\n\t\t\t});\n\t\t}\n\t\tif(replaceVideo) {\n\t\t\ttracks.push({\n\t\t\t\ttype: 'video',\n\t\t\t\tmid: '1',\t// We assume mid 1 is video\n\t\t\t\tcapture: {\n\t\t\t\t\tdeviceId: { exact: videoDeviceId },\n\t\t\t\t\twidth: { ideal: width },\n\t\t\t\t\theight: { ideal: height }\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\t// We use the replaceTracks helper function, that will in turn\n\t\t// call the WebRTC replaceTrack API with the info we requested,\n\t\t// without the need to do any renegotiation on the PeerConnection\n\t\techotest.replaceTracks({\n\t\t\ttracks: tracks,\n\t\t\terror: function(err) {\n\t\t\t\tbootbox.alert(err.message);\n\t\t\t}\n\t\t});\n\t\t// Reset devices controls\n\t\t$('#audio-device, #video-device').removeAttr('disabled');\n\t\t$('#change-devices').removeAttr('disabled');\n\t\treturn;\n\t}\n\t// We're only now starting, create a new PeerConnection\n\tfirstOffer = false;\n\tlet body = { audio: true, video: true };\n\t// We can try and force a specific codec, by telling the plugin what we'd prefer\n\t// For simplicity, you can set it via a query string (e.g., ?vcodec=vp9)\n\tif(acodec)\n\t\tbody[\"audiocodec\"] = acodec;\n\tif(vcodec)\n\t\tbody[\"videocodec\"] = vcodec;\n\t// For the codecs that support them (VP9 and H.264) you can specify a codec\n\t// profile as well (e.g., ?vprofile=2 for VP9, or ?vprofile=42e01f for H.264)\n\tif(vprofile)\n\t\tbody[\"videoprofile\"] = vprofile;\n\tJanus.debug(\"Trying a createOffer too (audio/video sendrecv)\");\n\techotest.createOffer(\n\t\t{\n\t\t\t// We provide a specific device ID for both audio and video\n\t\t\ttracks: [\n\t\t\t\t{ type: 'audio', capture: { deviceId: { exact: audioDeviceId }}, recv: true },\n\t\t\t\t{ type: 'video', capture: { deviceId: { exact: videoDeviceId },\n\t\t\t\t\twidth: { ideal: width }, height: { ideal: height }}, recv: true, simulcast: doSimulcast },\n\t\t\t\t{ type: 'data' }\t// Let's negotiate data channels as well\n\t\t\t],\n\t\t\tsuccess: function(jsep) {\n\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\techotest.send({ message: body, jsep: jsep });\n\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t$('#videoright').html(\n\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t'</div>');\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t}\n\t\t});\n}\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to EchoTest plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.echotest\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\techotest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + echotest.getPlugin() + \", id=\" + echotest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Enumerate devices: that's what we're here for\n\t\t\t\t\t\t\t\t\tJanus.listDevices(initDevices);\n\t\t\t\t\t\t\t\t\t// We wait for the user to select the first device before making a move\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\techotest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result === \"done\") {\n\t\t\t\t\t\t\t\t\t\t\t// The plugin closed the echo test\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The Echo Test is over\");\n\t\t\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true).html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true).html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text('Bandwidth');\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#datasend').val('').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#datarecv').val('');\n\t\t\t\t\t\t\t\t\t\t\t$('#outputdeviceset').text('Output device');\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// Any loss?\n\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\tif(status === \"slow_link\") {\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Janus apparently missed many packets we sent, maybe we should reduce the bitrate\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted) {\n\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted = true;\n\t\t\t\t\t\t\t\t\t\t\taddSimulcastButtons(msg[\"videocodec\"] === \"vp8\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\tupdateSimulcastButtons(substream, temporal);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#videoleft video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoleft .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoleft').append('<video class=\"rounded centered\" id=\"myvideo' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(echotest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\techotest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Reset devices controls\n\t\t\t\t\t\t\t\t\t$('#audio-device, #video-device').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t$('#change-devices').removeAttr('disabled');\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Remote track (mid=\" + mid + \") \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tlet addButtons = false;\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\taddButtons = true;\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// FIXME we'll need this for additional videos too\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#peervideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = echotest.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t//~ Janus.debug(\"Current bitrate is \" + echotest.getBitrate());\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#peervideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#peervideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(!addButtons)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// Enable audio/video buttons and bitrate limiter\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = !audioenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(audioenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Enable audio\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { audio: audioenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#togglevideo').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\tvideoenabled = !videoenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(videoenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Enable video\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { video: videoenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\techotest.send({ message: { bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').removeAttr('disabled');\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\t$('#datarecv').val(data);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t$('.no-video-container').remove();\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true).html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true).html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#bitrateset').html('Bandwidth');\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').val('').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#datarecv').val('');\n\t\t\t\t\t\t\t\t\t$('#outputdeviceset').html('Output device');\n\t\t\t\t\t\t\t\t\tsimulcastStarted = false;\n\t\t\t\t\t\t\t\t\t$('#simulcast').remove();\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tsendData();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction sendData() {\n\tlet data = $('#datasend').val();\n\tif(data === \"\") {\n\t\tbootbox.alert('Insert a message to send on the DataChannel');\n\t\treturn;\n\t}\n\techotest.data({\n\t\ttext: data,\n\t\terror: function(reason) { bootbox.alert(reason); },\n\t\tsuccess: function() { $('#datasend').val(''); },\n\t});\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helpers to create Simulcast-related UI, if enabled\nfunction addSimulcastButtons(temporal) {\n\t$(\t'<div id=\"simulcast\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>').insertBefore('#output-devices');\n\tif(Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers\n\t\t$('#tl-2').remove();\n\t}\n\t// Enable the simulcast selection buttons\n\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { substream: 0 }});\n\t\t});\n\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 1 }});\n\t\t});\n\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl-0').parent().removeClass('hide');\n\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { temporal: 0 }});\n\t\t});\n\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 1 }});\n\t\t});\n\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastButtons(substream, temporal) {\n\t// Check the substream\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched simulcast substream! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched simulcast substream! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched simulcast substream! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/e2e.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): End-to-end Encryption Test</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"e2e.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='e2e.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: End-to-end Encryption\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>EchoTest</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/echotest\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a variant of the Echo Test demo: everything is exactly\n\t\t\t\t\t\tthe same in term of available controls, features, and the like, with\n\t\t\t\t\t\tthe substantial difference that it shows how you can configure an\n\t\t\t\t\t\tend-to-end encryption context using the recently introduced\n\t\t\t\t\t\t<a target=\"_blank\" href=\"https://www.chromestatus.com/feature/6321945865879552\">Insertable Streams</a>.</p>\n\t\t\t\t\t\t<p>Specifically, this demo will prompt for a secret, and then use the same transform functions as\n\t\t\t\t\t\t<a target=\"_blank\" href=\"https://webrtc.github.io/samples/src/content/insertable-streams/endtoend-encryption/\">this official demo</a>\n\t\t\t\t\t\tto encrypt the media: this means Janus will NOT have access to the media, but\n\t\t\t\t\t\twill still be able to pass the packets to the EchoTest plugin and send them\n\t\t\t\t\t\tback, thus ensuring an end-to-end encryption of the content. In this context,\n\t\t\t\t\t\tthe same page that encrypted the media will also decrypt it, which means\n\t\t\t\t\t\tthat if you see both local and remote video, then it's working as expected.\n\t\t\t\t\t\tYou'll probably want to then experiment with this feature by playing with\n\t\t\t\t\t\tother plugins as well, e.g., the VideoRoom, for E2E-encrypted conferences.</p>\n\t\t\t\t\t\t<p>Notice that this demo only works on Chrome 86+ and makes use of\n\t\t\t\t\t\tthe Insertable Streams API running on the main thread. For using e2ee with\n\t\t\t\t\t\tJavaScript workers or with non-Chromium browsers, please refer to\n\t\t\t\t\t\t<a target=\"_blank\" href=\"https://github.com/webrtc/samples/tree/gh-pages/src/content/insertable-streams/endtoend-encryption\"> the official demo code</a>\n\t\t\t\t\t\tor contact us.\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"toggleaudio\">Disable audio</button>\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"togglevideo\">Disable video</button>\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<button id=\"bitrateset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\tBandwidth\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-up\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" placeholder=\"Write a DataChannel message\" autocomplete=\"off\" id=\"datasend\" onkeypress=\"return checkEnter(event);\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream <span class=\"badge bg-primary hide\" id=\"curres\"></span> <span class=\"badge bg-info hide\" id=\"curbitrate\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-down\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"datarecv\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/e2e.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar echotest = null;\nvar opaqueId = \"echotest-\"+Janus.randomString(12);\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\nvar bitrateTimer = null;\n\nvar audioenabled = false;\nvar videoenabled = false;\n\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar simulcastStarted = false;\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Make sure Insertable Streams are supported too\n\t\t\tif(!RTCRtpSender.prototype.createEncodedStreams &&\n\t\t\t\t\t!RTCRtpSender.prototype.createEncodedAudioStreams &&\n\t\t\t\t\t!RTCRtpSender.prototype.createEncodedVideoStreams) {\n\t\t\t\tbootbox.alert(\"Insertable Streams not supported by this browser... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to EchoTest plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.echotest\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\techotest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + echotest.getPlugin() + \", id=\" + echotest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t// Before starting, we prompt for a secret\n\t\t\t\t\t\t\t\t\tpromptCryptoKey();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(jsep !== undefined && jsep !== null) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\techotest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result === \"done\") {\n\t\t\t\t\t\t\t\t\t\t\t// The plugin closed the echo test\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The Echo Test is over\");\n\t\t\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// Any loss?\n\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\tif(status === \"slow_link\") {\n\t\t\t\t\t\t\t\t\t\t\t//~ let bitrate = result[\"bitrate\"];\n\t\t\t\t\t\t\t\t\t\t\t//~ toastr.warning(\"The bitrate has been cut to \" + (bitrate/1000) + \"kbps\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Janus apparently missed many packets we sent, maybe we should reduce the bitrate\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted) {\n\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted = true;\n\t\t\t\t\t\t\t\t\t\t\taddSimulcastButtons(msg[\"videocodec\"] === \"vp8\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\tupdateSimulcastButtons(substream, temporal);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#videoleft video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoleft .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoleft').append('<video class=\"rounded centered\" id=\"myvideo' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(echotest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\techotest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Remote track (mid=\" + mid + \") \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tlet addButtons = false;\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\taddButtons = true;\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// FIXME we'll need this for additional videos too\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#peervideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = echotest.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t//~ Janus.debug(\"Current bitrate is \" + echotest.getBitrate());\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#peervideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#peervideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(!addButtons)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// Enable audio/video buttons and bitrate limiter\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = !audioenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(audioenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Enable audio\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { audio: audioenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#togglevideo').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\tvideoenabled = !videoenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(videoenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Enable video\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { video: videoenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\techotest.send({ message: { bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').removeAttr('disabled');\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\t$('#datarecv').val(data);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').attr('disabled', true);\n\t\t\t\t\t\t\t\t\tsimulcastStarted = false;\n\t\t\t\t\t\t\t\t\t$('#simulcast').remove();\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tsendData();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction sendData() {\n\tlet data = $('#datasend').val();\n\tif(data === \"\") {\n\t\tbootbox.alert('Insert a message to send on the DataChannel');\n\t\treturn;\n\t}\n\techotest.data({\n\t\ttext: data,\n\t\terror: function(reason) { bootbox.alert(reason); },\n\t\tsuccess: function() { $('#datasend').val(''); },\n\t});\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helpers to create Simulcast-related UI, if enabled\nfunction addSimulcastButtons(temporal) {\n\t$('#curres').parent().append(\n\t\t'<div id=\"simulcast\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>');\n\tif(Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers\n\t\t$('#tl-2').remove();\n\t}\n\t// Enable the simulcast selection buttons\n\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { substream: 0 }});\n\t\t});\n\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 1 }});\n\t\t});\n\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl-0').parent().removeClass('hide');\n\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { temporal: 0 }});\n\t\t});\n\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 1 }});\n\t\t});\n\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastButtons(substream, temporal) {\n\t// Check the substream\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched simulcast substream! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched simulcast substream! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched simulcast substream! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n\n// Custom functions to use with Insertable streams: the senderTransform\n// function is what we use to \"encrypt\" the media we send, while the\n// receiverTransform is what we use to \"decrypt\" the media we receive.\n// Both functions will use a secret that we'll prompt for when opening\n// the page, meaning that, ideally, we'll be the only one able to decrypt\n// it: Janus will not have access to the media. In a scenario involving\n// multiple participants (e.g., a video call or conference), to use these\n// same functions you'll want to distribute the secret among all participants\n// using an out of band mechanism, so that they can encrypt and decrypt\n// the media and keep Janus out of the loop. Notice that these functions\n// are just an example, and are basically exact copies from this demo:\n// \t\thttps://github.com/webrtc/samples/blob/gh-pages/src/content/peerconnection/endtoend-encryption/js/main.js\n// That said, you're free (or actually, encouraged) to try and use other\n// custom, and more advanced, transform functions as well (e.g., SFrames).\nvar currentCryptoKey = null;\nvar currentKeyIdentifier = 0;\nfunction promptCryptoKey() {\n\tbootbox.prompt(\"Insert crypto key (e.g., 'mysecret') to use\", function(result) {\n\t\tif(!result || result === \"\") {\n\t\t\tpromptCryptoKey();\n\t\t\treturn;\n\t\t}\n\t\t// Take note of the secret\n\t\tcurrentCryptoKey = result;\n\t\tcurrentKeyIdentifier++;\n\t\t// Negotiate the WebRTC PeerConnection, now (clone of EchoTest demo code)\n\t\tJanus.debug(\"Trying a createOffer too (audio/video sendrecv)\");\n\t\techotest.createOffer(\n\t\t\t{\n\t\t\t\t// We want bidirectional audio and video, plus data channels,\n\t\t\t\t// and since we want to use Insertable Streams as well, we\n\t\t\t\t// specify the transform functions to use for audio and video\n\t\t\t\ttracks: [\n\t\t\t\t\t{ type: 'audio', capture: true, recv: true,\n\t\t\t\t\t\ttransforms: { sender: senderTransforms['audio'], receiver: receiverTransforms['audio']} },\n\t\t\t\t\t{ type: 'video', capture: true, recv: true, simulcast: doSimulcast,\n\t\t\t\t\t\ttransforms: { sender: senderTransforms['video'], receiver: receiverTransforms['video']} },\n\t\t\t\t\t{ type: 'data' },\n\t\t\t\t],\n\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\tlet body = { audio: true, video: true, data: true };\n\t\t\t\t\t// We can try and force a specific codec, by telling the plugin what we'd prefer\n\t\t\t\t\t// For simplicity, you can set it via a query string (e.g., ?vcodec=vp9)\n\t\t\t\t\tif(acodec)\n\t\t\t\t\t\tbody[\"audiocodec\"] = acodec;\n\t\t\t\t\tif(vcodec)\n\t\t\t\t\t\tbody[\"videocodec\"] = vcodec;\n\t\t\t\t\techotest.send({ message: body, jsep: jsep });\n\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t$('#videoright').html(\n\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t'</div>');\n\t\t\t\t},\n\t\t\t\terror: function(error) {\n\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t}\n\t\t\t});\n\t});\n}\nvar senderTransforms = {}, receiverTransforms = {};\nfor(var m of [\"audio\", \"video\"]) {\n\tsenderTransforms[m] = new TransformStream({\n\t\tstart() {\n\t\t\t// Called on startup.\n\t\t\tconsole.log(\"[Sender transform)] Startup\");\n\t\t},\n\t\ttransform(chunk, controller) {\n\t\t\t// Copy of the above mentioned demo's encodeFunction()\n\t\t\tconst view = new DataView(chunk.data);\n\t\t\tconst newData = new ArrayBuffer(chunk.data.byteLength + 5);\n\t\t\tconst newView = new DataView(newData);\n\t\t\tfor(let i=0; i<10; ++i) {\n\t\t\t\tnewView.setInt8(i, view.getInt8(i));\n\t\t\t}\n\t\t\t// This is a bitwise xor of the key with the payload. This is not strong encryption, just a demo.\n\t\t\tfor(let i=10; i<chunk.data.byteLength; ++i) {\n\t\t\t\tconst keyByte = currentCryptoKey.charCodeAt(i % currentCryptoKey.length);\n\t\t\t\tnewView.setInt8(i, view.getInt8(i) ^ keyByte);\n\t\t\t}\n\t\t\tnewView.setUint8(chunk.data.byteLength, currentKeyIdentifier % 0xff);\n\t\t\tnewView.setUint32(chunk.data.byteLength + 1, 0xDEADBEEF);\n\t\t\tchunk.data = newData;\n\t\t\tcontroller.enqueue(chunk);\n\t\t},\n\t\tflush() {\n\t\t\t// Called when the stream is about to be closed\n\t\t\tconsole.log(\"[Sender transform] Closing\");\n\t\t}\n\t});\n\treceiverTransforms[m] = new TransformStream({\n\t\tstart() {\n\t\t\t// Called on startup.\n\t\t\tconsole.log(\"[Receiver transform] Startup\");\n\t\t},\n\t\ttransform(chunk, controller) {\n\t\t\t// Copy of the above mentioned demo's decodeFunction()\n\t\t\tconst view = new DataView(chunk.data);\n\t\t\tconst checksum = view.getUint32(chunk.data.byteLength - 4);\n\t\t\tif(checksum !== 0xDEADBEEF) {\n\t\t\t\tconsole.log('Corrupted frame received');\n\t\t\t\tconsole.log(checksum.toString(16));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst keyIdentifier = view.getUint8(chunk.data.byteLength - 5);\n\t\t\tif(keyIdentifier !== currentKeyIdentifier) {\n\t\t\t\tconsole.log(`Key identifier mismatch, got ${keyIdentifier} expected ${currentKeyIdentifier}.`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst newData = new ArrayBuffer(chunk.data.byteLength - 5);\n\t\t\tconst newView = new DataView(newData);\n\t\t\tfor(let i=0; i<10; ++i) {\n\t\t\t\tnewView.setInt8(i, view.getInt8(i));\n\t\t\t}\n\t\t\tfor(let i=10; i<chunk.data.byteLength - 5; ++i) {\n\t\t\t\tconst keyByte = currentCryptoKey.charCodeAt(i % currentCryptoKey.length);\n\t\t\t\tnewView.setInt8(i, view.getInt8(i) ^ keyByte);\n\t\t\t}\n\t\t\tchunk.data = newData;\n\t\t\tcontroller.enqueue(chunk);\n\t\t},\n\t\tflush() {\n\t\t\t// Called when the stream is about to be closed\n\t\t\tconsole.log(\"[Receiver transform] Closing\");\n\t\t}\n\t});\n}\n"
  },
  {
    "path": "html/demos/echotest.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Echo Test</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"echotest.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='echotest.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Echo Test\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>EchoTest</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/echotest\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This Echo Test demo just blindly sends you back whatever you\n\t\t\t\t\t\tsend to it. You're basically attached to yourself, and so your audio\n\t\t\t\t\t\tand video you send to Janus are echoed back to you. The same\n\t\t\t\t\t\tis done for RTCP packets as well, with the information properly updated.</p>\n\t\t\t\t\t\t<p>The demo also provides a few controls to manipulate the media\n\t\t\t\t\t\tbefore you send them. You can mute audio and video, for instance,\n\t\t\t\t\t\twhich will tell the server to drop the frames and not echo them\n\t\t\t\t\t\tback to you. You can also try and cap the bitrate: such control\n\t\t\t\t\t\twill tell the server to manipulate the RTCP REMB packets passing\n\t\t\t\t\t\tthrough, in order to simulate a bandwidth limitation. In case\n\t\t\t\t\t\tyou're interested in testing simulcasting or SVC, add a\n\t\t\t\t\t\t<code>?simulcast=true</code> (for simulcast) or <code>?svc=&lt;mode&gt;</code> (for SVC)\n\t\t\t\t\t\tquery string to the url of this page and reload it: buttons will appear\n\t\t\t\t\t\tto allow you to try and switch between lower and higher quality\n\t\t\t\t\t\tversions of the video you're capturing. Notice that you may have\n\t\t\t\t\t\tto increase the bandwidth indicator to have the higher quality\n\t\t\t\t\t\tversions appear, as the browser will not encode them otherwise.\n\t\t\t\t\t\tBesides, notice that simulcast will only work when using VP8 or H.264\n\t\t\t\t\t\t(or, if you're using a recent version of Chrome, VP9 and AV1 too),\n\t\t\t\t\t\twhile SVC will only work if you're using VP9 or AV1 on a browser that\n\t\t\t\t\t\tsupports setting the <code>scalabilityMode</code>.</p>\n\t\t\t\t\t\t<p>Finally, this demo also includes Data Channels: whatever you\n\t\t\t\t\t\twrite in the text box under your local video, will be sent via\n\t\t\t\t\t\tData Channels to the plugins, modified by adding a fixed prefix,\n\t\t\t\t\t\tand sent back to you in the text area below the remote video.</p>\n\t\t\t\t\t\t<div class=\"alert alert-info\">This demo, as all the others, for the\n\t\t\t\t\t\tsake of simplicity accesses the default devices you have available on\n\t\t\t\t\t\tyour machine. If you're interested in a demo that shows you how you\n\t\t\t\t\t\tcan select specific devices with <code>janus.js</code>, open the\n\t\t\t\t\t\t<a class=\"alert-link\" href=\"devicetest.html\">Devices</a> demo instead.</div>\n\t\t\t\t\t\t<div class=\"alert alert-info\">The <b>Lua</b> and <b>Duktape</b> plugins, when\n\t\t\t\t\t\tinstalled out of the box, come with a script that mimics the default EchoTest\n\t\t\t\t\t\t<b>C</b> plugin: this means that you can use this demo page to talk to them\n\t\t\t\t\t\tas well, so long as you tell the page code to connect to the respective plugin.\n\t\t\t\t\t\tTo talk to the <b>Lua</b> plugin instead of the stock EchoTest plugin,\n\t\t\t\t\t\tadd the <code>?plugin=lua</code> query string to the url of this page;\n\t\t\t\t\t\tto talk to the <b>Duktape</b> plugin instead, use <code>?plugin=duktape</code>.\n\t\t\t\t\t\tSince they both share pretty much the same syntax as the stock\n\t\t\t\t\t\tEchoTest plugin, most if not all features should work just the same way.</div>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-sm btn-danger\" autocomplete=\"off\" id=\"toggleaudio\">Disable audio</button>\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-sm btn-danger\" autocomplete=\"off\" id=\"togglevideo\">Disable video</button>\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<button id=\"bitrateset\" autocomplete=\"off\" class=\"btn btn-sm btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\tBandwidth\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<li><a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a></li>\n\t\t\t\t\t\t\t\t\t\t\t\t<li><a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a></li>\n\t\t\t\t\t\t\t\t\t\t\t\t<li><a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a></li>\n\t\t\t\t\t\t\t\t\t\t\t\t<li><a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a></li>\n\t\t\t\t\t\t\t\t\t\t\t\t<li><a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a></li>\n\t\t\t\t\t\t\t\t\t\t\t\t<li><a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a></li>\n\t\t\t\t\t\t\t\t\t\t\t\t<li><a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a></li>\n\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-up\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" placeholder=\"Write a DataChannel message\" autocomplete=\"off\" id=\"datasend\" onkeypress=\"return checkEnter(event);\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream <span class=\"badge bg-primary hide\" id=\"curres\"></span> <span class=\"badge bg-info hide\" id=\"curbitrate\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-down\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"datarecv\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/echotest.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar echotest = null;\nvar opaqueId = \"echotest-\"+Janus.randomString(12);\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\nvar bitrateTimer = null;\n\nvar audioenabled = false;\nvar videoenabled = false;\n\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar doSvc = getQueryStringValue(\"svc\");\nif(doSvc === \"\")\n\tdoSvc = null;\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar vprofile = (getQueryStringValue(\"vprofile\") !== \"\" ? getQueryStringValue(\"vprofile\") : null);\nvar stereo = false;\nif(getQueryStringValue(\"stereo\") !== \"\")\n\tstereo = (getQueryStringValue(\"stereo\") === \"true\");\nvar doDtx = (getQueryStringValue(\"dtx\") === \"yes\" || getQueryStringValue(\"dtx\") === \"true\");\nvar doOpusred = (getQueryStringValue(\"opusred\") === \"yes\" || getQueryStringValue(\"opusred\") === \"true\");\nvar simulcastStarted = false, svcStarted = false;\n\n// By default we talk to the \"regular\" EchoTest plugin\nvar echotestPluginBackend = \"janus.plugin.echotest\";\n// We can use query string arguments to talk to the Lua or Duktape EchoTest\n// demo scripts instead. Notice that this assumes that the Lua or Duktape\n// plugins are configured to run the sample scripts that comes with the repo\nif(getQueryStringValue(\"plugin\") === \"lua\")\n\techotestPluginBackend = \"janus.plugin.echolua\";\nelse if(getQueryStringValue(\"plugin\") === \"duktape\")\n\techotestPluginBackend = \"janus.plugin.echojs\";\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to EchoTest plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: echotestPluginBackend,\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\techotest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + echotest.getPlugin() + \", id=\" + echotest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Negotiate WebRTC\n\t\t\t\t\t\t\t\t\tlet body = { audio: true, video: true };\n\t\t\t\t\t\t\t\t\t// We can try and force a specific codec, by telling the plugin what we'd prefer\n\t\t\t\t\t\t\t\t\t// For simplicity, you can set it via a query string (e.g., ?vcodec=vp9)\n\t\t\t\t\t\t\t\t\tif(acodec)\n\t\t\t\t\t\t\t\t\t\tbody[\"audiocodec\"] = acodec;\n\t\t\t\t\t\t\t\t\tif(vcodec)\n\t\t\t\t\t\t\t\t\t\tbody[\"videocodec\"] = vcodec;\n\t\t\t\t\t\t\t\t\t// For the codecs that support them (VP9 and H.264) you can specify a codec\n\t\t\t\t\t\t\t\t\t// profile as well (e.g., ?vprofile=2 for VP9, or ?vprofile=42e01f for H.264)\n\t\t\t\t\t\t\t\t\tif(vprofile)\n\t\t\t\t\t\t\t\t\t\tbody[\"videoprofile\"] = vprofile;\n\t\t\t\t\t\t\t\t\t// We can force RED for audio too, if supported by the browser\n\t\t\t\t\t\t\t\t\tif(doOpusred)\n\t\t\t\t\t\t\t\t\t\tbody[\"opusred\"] = true;\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Sending message:\", body);\n\t\t\t\t\t\t\t\t\techotest.send({ message: body });\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Trying a createOffer too (audio/video sendrecv)\");\n\t\t\t\t\t\t\t\t\techotest.createOffer(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and video, plus data channels\n\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'video', capture: true, recv: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t// We may need to enable simulcast or SVC on the video track\n\t\t\t\t\t\t\t\t\t\t\t\t\tsimulcast: doSimulcast,\n\t\t\t\t\t\t\t\t\t\t\t\t\t// We only support SVC for VP9 and (still WIP) AV1\n\t\t\t\t\t\t\t\t\t\t\t\t\tsvc: ((vcodec === 'vp9' || vcodec === 'av1') && doSvc) ? doSvc : null\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'data' },\n\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\tcustomizeSdp: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t// If DTX is enabled, munge the SDP\n\t\t\t\t\t\t\t\t\t\t\t\tif(doDtx) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tjsep.sdp = jsep.sdp\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t.replace(\"useinbandfec=1\", \"useinbandfec=1;usedtx=1\")\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tif(stereo && jsep.sdp.indexOf(\"stereo=1\") == -1) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Make sure that our offer contains stereo too\n\t\t\t\t\t\t\t\t\t\t\t\t\tjsep.sdp = jsep.sdp.replace(\"useinbandfec=1\", \"useinbandfec=1;stereo=1\");\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').html(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\techotest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result === \"done\") {\n\t\t\t\t\t\t\t\t\t\t\t// The plugin closed the echo test\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The Echo Test is over\");\n\t\t\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// Any loss?\n\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\tif(status === \"slow_link\") {\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Janus apparently missed many packets we sent, maybe we should reduce the bitrate\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted) {\n\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted = true;\n\t\t\t\t\t\t\t\t\t\t\taddSimulcastSvcButtons(msg[\"videocodec\"] === \"vp8\" || msg[\"videocodec\"] === \"vp9\" || msg[\"videocodec\"] === \"av1\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\tupdateSimulcastSvcButtons(substream, temporal);\n\t\t\t\t\t\t\t\t\t\t// See if we need to update the UI\n\t\t\t\t\t\t\t\t\t\tlet totTl = msg[\"tot_temporal_layers\"];\n\t\t\t\t\t\t\t\t\t\tif(totTl !== null && totTl !== undefined) {\n\t\t\t\t\t\t\t\t\t\t\tif(totTl < 3 && !$('#tl-2').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-2').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totTl >= 3)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-2').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tif(totTl < 2 && !$('#tl-1').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-1').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totTl >= 2)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-1').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tif(totTl < 1 && !$('#tl-0').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-0').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totTl >= 1)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-0').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Or maybe SVC?\n\t\t\t\t\t\t\t\t\tlet spatial = msg[\"spatial_layer\"];\n\t\t\t\t\t\t\t\t\ttemporal = msg[\"temporal_layer\"];\n\t\t\t\t\t\t\t\t\tif((spatial !== null && spatial !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\tif(!svcStarted) {\n\t\t\t\t\t\t\t\t\t\t\tsvcStarted = true;\n\t\t\t\t\t\t\t\t\t\t\taddSimulcastSvcButtons(true);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\tupdateSimulcastSvcButtons(spatial, temporal);\n\t\t\t\t\t\t\t\t\t\tlet totSl = msg[\"tot_spatial_layers\"];\n\t\t\t\t\t\t\t\t\t\tif(totSl !== null && totSl !== undefined) {\n\t\t\t\t\t\t\t\t\t\t\tif(totSl < 3 && !$('#sl-2').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#sl-2').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totSl >= 3)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#sl-2').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tif(totSl < 2 && !$('#sl-1').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#sl-1').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totSl >= 2)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#sl-1').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tif(totSl < 1 && !$('#sl-0').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#sl-0').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totSl >= 1)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#sl-0').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tlet totTl = msg[\"tot_temporal_layers\"];\n\t\t\t\t\t\t\t\t\t\tif(totTl !== null && totTl !== undefined) {\n\t\t\t\t\t\t\t\t\t\t\tif(totTl < 3 && !$('#tl-2').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-2').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totTl >= 3)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-2').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tif(totTl < 2 && !$('#tl-1').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-1').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totTl >= 2)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-1').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tif(totTl < 1 && !$('#tl-0').hasClass('hide'))\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-0').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(totTl >= 1)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#tl-0').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#videoleft video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoleft .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoleft').append('<video class=\"rounded centered\" id=\"myvideo' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(echotest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\techotest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata ? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tlet addButtons = false;\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\taddButtons = true;\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// FIXME we'll need this for additional videos too\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#peervideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = echotest.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t//~ Janus.debug(\"Current bitrate is \" + echotest.getBitrate());\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#peervideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#peervideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(!addButtons)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// Enable audio/video buttons and bitrate limiter\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = !audioenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(audioenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Enable audio\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { audio: audioenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#togglevideo').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\tvideoenabled = !videoenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(videoenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Enable video\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { video: videoenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\techotest.send({ message: { bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').removeAttr('disabled');\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\t$('#datarecv').val(data);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').attr('disabled', true);\n\t\t\t\t\t\t\t\t\tsimulcastStarted = false;\n\t\t\t\t\t\t\t\t\t$('#simulcast').remove();\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tsendData();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction sendData() {\n\tlet data = $('#datasend').val();\n\tif(data === \"\") {\n\t\tbootbox.alert('Insert a message to send on the DataChannel');\n\t\treturn;\n\t}\n\techotest.data({\n\t\ttext: data,\n\t\terror: function(reason) { bootbox.alert(reason); },\n\t\tsuccess: function() { $('#datasend').val(''); },\n\t});\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helpers to create Simulcast- or SVC-related UI, if enabled\nfunction addSimulcastSvcButtons(temporal) {\n\tlet what = (simulcastStarted ? 'simulcast' : 'SVC');\n\tlet layer = (simulcastStarted ? 'substream' : 'layer');\n\t$('#curres').parent().append(\n\t\t'<div id=\"simulcast\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>');\n\t// Enable the simulcast selection buttons\n\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \", wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(simulcastStarted)\n\t\t\t\techotest.send({ message: { substream: 0 }});\n\t\t\telse\n\t\t\t\techotest.send({ message: { spatial_layer: 0 }});\n\t\t});\n\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \", wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted)\n\t\t\t\techotest.send({ message: { substream: 1 }});\n\t\t\telse\n\t\t\t\techotest.send({ message: { spatial_layer: 1 }});\n\t\t});\n\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \", wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted)\n\t\t\t\techotest.send({ message: { substream: 2 }});\n\t\t\telse\n\t\t\t\techotest.send({ message: { spatial_layer: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl-0').parent().removeClass('hide');\n\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(simulcastStarted)\n\t\t\t\techotest.send({ message: { temporal: 0 }});\n\t\t\telse\n\t\t\t\techotest.send({ message: { temporal_layer: 0 }});\n\t\t});\n\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted)\n\t\t\t\techotest.send({ message: { temporal: 1 }});\n\t\t\telse\n\t\t\t\techotest.send({ message: { temporal_layer: 1 }});\n\t\t});\n\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted)\n\t\t\t\techotest.send({ message: { temporal: 2 }});\n\t\t\telse\n\t\t\t\techotest.send({ message: { temporal_layer: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastSvcButtons(substream, temporal) {\n\t// Check the substream\n\tlet what = (simulcastStarted ? 'simulcast' : 'SVC');\n\tlet layer = (simulcastStarted ? 'substream' : 'layer');\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/index.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Demo Tests</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='index.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Janus WebRTC Server: Demo Tests</h1>\n\t\t\t</div>\n\t\t\t<table class=\"table table-striped mt-5\">\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"table-info\" colspan=2><h3>Plugin demos</h3></td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"echotest.html\">Echo Test</a></td>\n\t\t\t\t\t<td>A simple Echo Test demo, with knobs to control the bitrate.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"streaming.html\">Streaming</a></td>\n\t\t\t\t\t<td>A media Streaming demo, with sample live and on-demand streams.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"videocall.html\">Video Call</a></td>\n\t\t\t\t\t<td>A Video Call demo, a bit like AppRTC but with media passing through Janus.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"sip.html\">SIP Gateway</a></td>\n\t\t\t\t\t<td>A SIP Gateway demo, allowing you to register at a SIP server and start/receive calls.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"videoroom.html\">Video Room</a></td>\n\t\t\t\t\t<td>A videoconferencing demo, allowing you to join a video room with up to six users.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"mvideoroom.html\">Video Room (multistream)</a></td>\n\t\t\t\t\t<td>The same videoconferencing demo, but using one PeerConnection to receive multiple streams.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"audiobridge.html\">Audio Room</a></td>\n\t\t\t\t\t<td>An audio mixing/bridge demo, allowing you join an Audio Room.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"textroom.html\">Text Room</a></td>\n\t\t\t\t\t<td>A text room demo, using DataChannels only.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"recordplay.html\">Recorder/Playout</a></td>\n\t\t\t\t\t<td>A demo to record audio/video messages, and subsequently replay them through WebRTC.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"screensharing.html\">Screen Sharing</a></td>\n\t\t\t\t\t<td>A webinar-like screen sharing session, based on the Video Room plugin.</td>\n\t\t\t\t</tr>\n\t\t\t</table>\n\t\t\t<table class=\"table table-striped mt-5\">\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"table-info\" colspan=2><h3>Other legacy demos</h3></td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"nosip.html\">NoSIP (SDP/RTP)</a></td>\n\t\t\t\t\t<td>A legacy interop demo (e.g., with a SIP peer) where signalling is up to the application.</td>\n\t\t\t\t</tr>\n\t\t\t</table>\n\t\t\t<table class=\"table table-striped mt-5\">\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"table-info\" colspan=2><h3>Advanced demos</h3></td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"devices.html\">Device Selection</a></td>\n\t\t\t\t\t<td>A variant of the Echo Test demo, that allows you to choose a specific capture device.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"e2e.html\">End-to-end Encryption</a></td>\n\t\t\t\t\t<td>A variant of the Echo Test demo, that allows you to encrypt the video in a way that Janus can't access it, but can still route it.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"multiopus.html\">Multichannel Opus (surround)</a></td>\n\t\t\t\t\t<td>A variant of the Echo Test demo, that shows multichannel/surround Opus support.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"canvas.html\">Canvas Capture</a></td>\n\t\t\t\t\t<td>A variant of the Echo Test demo, that shows how to use a canvas element as a WebRTC media source.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"webaudio.html\">Web Audio Processing</a></td>\n\t\t\t\t\t<td>A variant of the Echo Test demo, that shows how to use Web Audio to process audio before sending it to Janus.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"virtualbg.html\">Virtual Background</a></td>\n\t\t\t\t\t<td>A variant of the Echo Test demo, that shows how to use something like MediaPipe to add a virtual background before sending video to Janus.</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"admin.html\">Admin/Monitor</a></td>\n\t\t\t\t\t<td>A simple page showcasing how you can use the Janus Admin/Monitor API.</td>\n\t\t\t\t</tr>\n\t\t\t</table>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/janus.js",
    "content": "\"use strict\";\n\n/*\n\tThe MIT License (MIT)\n\n\tCopyright (c) 2016 Meetecho\n\n\tPermission is hereby granted, free of charge, to any person obtaining\n\ta copy of this software and associated documentation files (the \"Software\"),\n\tto deal in the Software without restriction, including without limitation\n\tthe rights to use, copy, modify, merge, publish, distribute, sublicense,\n\tand/or sell copies of the Software, and to permit persons to whom the\n\tSoftware is furnished to do so, subject to the following conditions:\n\n\tThe above copyright notice and this permission notice shall be included\n\tin all copies or substantial portions of the Software.\n\n\tTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n\tOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n\tFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL\n\tTHE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR\n\tOTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\n\tARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n\tOTHER DEALINGS IN THE SOFTWARE.\n */\n\n// eslint-disable-next-line no-unused-vars\nvar Janus = (function (factory) {\n\tif (typeof define === 'function' && define.amd) {\n\t\tdefine(factory);\n\t} else if (typeof module === 'object' && module.exports) {\n\t\tmodule.exports = factory();\n\t} else if (typeof window === 'object') {\n\t\treturn factory();\n\t}\n}(function () {\n\n\t// List of sessions\n\tJanus.sessions = new Map();\n\n\tJanus.isExtensionEnabled = function() {\n\t\tif(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {\n\t\t\t// No need for the extension, getDisplayMedia is supported\n\t\t\treturn true;\n\t\t}\n\t\tif(window.navigator.userAgent.match('Chrome')) {\n\t\t\tlet chromever = parseInt(window.navigator.userAgent.match(/Chrome\\/(.*) /)[1], 10);\n\t\t\tlet maxver = 33;\n\t\t\tif(window.navigator.userAgent.match('Linux'))\n\t\t\t\tmaxver = 35;\t// \"known\" crash in chrome 34 and 35 on linux\n\t\t\tif(chromever >= 26 && chromever <= maxver) {\n\t\t\t\t// Older versions of Chrome don't support this extension-based approach, so lie\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn Janus.extension.isInstalled();\n\t\t} else {\n\t\t\t// Firefox and others, no need for the extension (but this doesn't mean it will work)\n\t\t\treturn true;\n\t\t}\n\t};\n\n\tvar defaultExtension = {\n\t\t// Screensharing Chrome Extension ID\n\t\textensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj',\n\t\tisInstalled: function() { return document.querySelector('#janus-extension-installed') !== null; },\n\t\tgetScreen: function (callback) {\n\t\t\tlet pending = window.setTimeout(function () {\n\t\t\t\tlet error = new Error('NavigatorUserMediaError');\n\t\t\t\terror.name = 'The required Chrome extension is not installed: click <a href=\"#\">here</a> to install it. (NOTE: this will need you to refresh the page)';\n\t\t\t\treturn callback(error);\n\t\t\t}, 1000);\n\t\t\tthis.cache[pending] = callback;\n\t\t\twindow.postMessage({ type: 'janusGetScreen', id: pending }, '*');\n\t\t},\n\t\tinit: function () {\n\t\t\tlet cache = {};\n\t\t\tthis.cache = cache;\n\t\t\t// Wait for events from the Chrome Extension\n\t\t\twindow.addEventListener('message', function (event) {\n\t\t\t\tif(event.origin != window.location.origin)\n\t\t\t\t\treturn;\n\t\t\t\tif(event.data.type == 'janusGotScreen' && cache[event.data.id]) {\n\t\t\t\t\tlet callback = cache[event.data.id];\n\t\t\t\t\tdelete cache[event.data.id];\n\t\t\t\t\tif(event.data.sourceId === '') {\n\t\t\t\t\t\t// user canceled\n\t\t\t\t\t\tlet error = new Error('NavigatorUserMediaError');\n\t\t\t\t\t\terror.name = 'You cancelled the request for permission, giving up...';\n\t\t\t\t\t\tcallback(error);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcallback(null, event.data.sourceId);\n\t\t\t\t\t}\n\t\t\t\t} else if(event.data.type == 'janusGetScreenPending') {\n\t\t\t\t\tconsole.log('clearing ', event.data.id);\n\t\t\t\t\twindow.clearTimeout(event.data.id);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n\n\tJanus.useDefaultDependencies = function (deps) {\n\t\tlet f = (deps && deps.fetch) || fetch;\n\t\tlet p = (deps && deps.Promise) || Promise;\n\t\tlet socketCls = (deps && deps.WebSocket) || WebSocket;\n\n\t\treturn {\n\t\t\tnewWebSocket: function(server, proto) { return new socketCls(server, proto); },\n\t\t\textension: (deps && deps.extension) || defaultExtension,\n\t\t\tisArray: function(arr) { return Array.isArray(arr); },\n\t\t\twebRTCAdapter: (deps && deps.adapter) || adapter,\n\t\t\thttpAPICall: function(url, options) {\n\t\t\t\tlet fetchOptions = {\n\t\t\t\t\tmethod: options.verb,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Accept': 'application/json, text/plain, */*'\n\t\t\t\t\t},\n\t\t\t\t\tcache: 'no-cache'\n\t\t\t\t};\n\t\t\t\tif(options.verb === \"POST\") {\n\t\t\t\t\tfetchOptions.headers['Content-Type'] = 'application/json';\n\t\t\t\t}\n\t\t\t\tif(typeof options.withCredentials !== 'undefined') {\n\t\t\t\t\tfetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit');\n\t\t\t\t}\n\t\t\t\tif(options.body) {\n\t\t\t\t\tfetchOptions.body = JSON.stringify(options.body);\n\t\t\t\t}\n\n\t\t\t\tlet fetching = f(url, fetchOptions).catch(function(error) {\n\t\t\t\t\treturn p.reject({message: 'Probably a network error, is the server down?', error: error});\n\t\t\t\t});\n\n\t\t\t\t/*\n\t\t\t * fetch() does not natively support timeouts.\n\t\t\t * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first.\n\t\t\t */\n\n\t\t\t\tif(options.timeout) {\n\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\tlet timeout = new p(function(resolve, reject) {\n\t\t\t\t\t\tlet timerId = setTimeout(function() {\n\t\t\t\t\t\t\tclearTimeout(timerId);\n\t\t\t\t\t\t\treturn reject({message: 'Request timed out', timeout: options.timeout});\n\t\t\t\t\t\t}, options.timeout);\n\t\t\t\t\t});\n\t\t\t\t\tfetching = p.race([fetching, timeout]);\n\t\t\t\t}\n\n\t\t\t\tfetching.then(function(response) {\n\t\t\t\t\tif(response.ok) {\n\t\t\t\t\t\tif(typeof(options.success) === typeof(Janus.noop)) {\n\t\t\t\t\t\t\treturn response.json().then(function(parsed) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\toptions.success(parsed);\n\t\t\t\t\t\t\t\t} catch(error) {\n\t\t\t\t\t\t\t\t\tJanus.error('Unhandled httpAPICall success callback error', error);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}, function(error) {\n\t\t\t\t\t\t\t\treturn p.reject({message: 'Failed to parse response body', error: error, response: response});\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\treturn p.reject({message: 'API call failed', response: response});\n\t\t\t\t\t}\n\t\t\t\t}).catch(function(error) {\n\t\t\t\t\tif(typeof(options.error) === typeof(Janus.noop)) {\n\t\t\t\t\t\toptions.error(error.message || '<< internal error >>', error);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\treturn fetching;\n\t\t\t}\n\t\t}\n\t};\n\n\tJanus.useOldDependencies = function (deps) {\n\t\tlet jq = (deps && deps.jQuery) || jQuery;\n\t\tlet socketCls = (deps && deps.WebSocket) || WebSocket;\n\t\treturn {\n\t\t\tnewWebSocket: function(server, proto) { return new socketCls(server, proto); },\n\t\t\tisArray: function(arr) { return jq.isArray(arr); },\n\t\t\textension: (deps && deps.extension) || defaultExtension,\n\t\t\twebRTCAdapter: (deps && deps.adapter) || adapter,\n\t\t\thttpAPICall: function(url, options) {\n\t\t\t\tlet payload = (typeof options.body !== 'undefined') ? {\n\t\t\t\t\tcontentType: 'application/json',\n\t\t\t\t\tdata: JSON.stringify(options.body)\n\t\t\t\t} : {};\n\t\t\t\tlet credentials = (typeof options.withCredentials !== 'undefined') ? {xhrFields: {withCredentials: options.withCredentials}} : {};\n\n\t\t\t\treturn jq.ajax(jq.extend(payload, credentials, {\n\t\t\t\t\turl: url,\n\t\t\t\t\ttype: options.verb,\n\t\t\t\t\tcache: false,\n\t\t\t\t\tdataType: 'json',\n\t\t\t\t\tasync: options.async,\n\t\t\t\t\ttimeout: options.timeout,\n\t\t\t\t\tsuccess: function(result) {\n\t\t\t\t\t\tif(typeof(options.success) === typeof(Janus.noop)) {\n\t\t\t\t\t\t\toptions.success(result);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\terror: function(xhr, status, err) {\n\t\t\t\t\t\tif(typeof(options.error) === typeof(Janus.noop)) {\n\t\t\t\t\t\t\toptions.error(status, err);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\t}\n\t\t};\n\t};\n\n\t// Helper function to convert a deprecated media object to a tracks array\n\tJanus.mediaToTracks = function(media) {\n\t\tlet tracks = [];\n\t\tif(!media) {\n\t\t\t// Default is bidirectional audio and video, using default devices\n\t\t\ttracks.push({ type: 'audio', capture: true, recv: true });\n\t\t\ttracks.push({ type: 'video', capture: true, recv: true });\n\t\t} else {\n\t\t\tif(!media.keepAudio && media.audio !== false && ((typeof media.audio === 'undefined') || media.audio || media.audioSend || media.audioRecv ||\n\t\t\t\t\tmedia.addAudio || media.replaceAudio || media.removeAudio)) {\n\t\t\t\t// We may need an audio track\n\t\t\t\tlet track = { type: 'audio' };\n\t\t\t\tif(media.removeAudio) {\n\t\t\t\t\ttrack.remove = true;\n\t\t\t\t} else {\n\t\t\t\t\tif(media.addAudio)\n\t\t\t\t\t\ttrack.add = true;\n\t\t\t\t\telse if(media.replaceAudio)\n\t\t\t\t\t\ttrack.replace = true;\n\t\t\t\t\t// Check if we need to capture an audio device\n\t\t\t\t\tif(media.audioSend !== false)\n\t\t\t\t\t\ttrack.capture = media.audio || true;\n\t\t\t\t\t// Check if we need to receive audio\n\t\t\t\t\tif(media.audioRecv !== false)\n\t\t\t\t\t\ttrack.recv = true;\n\t\t\t\t}\n\t\t\t\t// Add an audio track if needed\n\t\t\t\tif(track.remove || track.capture || track.recv)\n\t\t\t\t\ttracks.push(track);\n\t\t\t}\n\t\t\tif(!media.keepVideo && media.video !== false && ((typeof media.video === 'undefined') || media.video || media.videoSend || media.videoRecv ||\n\t\t\t\t\tmedia.addVideo || media.replaceVideo || media.removeVideo)) {\n\t\t\t\t// We may need a video track\n\t\t\t\tlet track = { type: 'video' };\n\t\t\t\tif(media.removeVideo) {\n\t\t\t\t\ttrack.remove = true;\n\t\t\t\t} else {\n\t\t\t\t\tif(media.addVideo)\n\t\t\t\t\t\ttrack.add = true;\n\t\t\t\t\telse if(media.replaceVideo)\n\t\t\t\t\t\ttrack.replace = true;\n\t\t\t\t\t// Check if we need to capture a video device\n\t\t\t\t\tif(media.videoSend !== false) {\n\t\t\t\t\t\ttrack.capture = media.video || true;\n\t\t\t\t\t\tif(['screen', 'window', 'desktop'].includes(track.capture)) {\n\t\t\t\t\t\t\t// Change the type to 'screen'\n\t\t\t\t\t\t\ttrack.type = 'screen';\n\t\t\t\t\t\t\ttrack.capture = { video: {} };\n\t\t\t\t\t\t\t// Check if there's constraints\n\t\t\t\t\t\t\tif(media.screenshareFrameRate)\n\t\t\t\t\t\t\t\ttrack.capture.frameRate = media.screenshareFrameRate;\n\t\t\t\t\t\t\tif(media.screenshareHeight)\n\t\t\t\t\t\t\t\ttrack.capture.height = media.screenshareHeight;\n\t\t\t\t\t\t\tif(media.screenshareWidth)\n\t\t\t\t\t\t\t\ttrack.capture.width = media.screenshareWidth;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Check if we need to receive video\n\t\t\t\t\tif(media.videoRecv !== false)\n\t\t\t\t\t\ttrack.recv = true;\n\t\t\t\t}\n\t\t\t\t// Add a video track if needed\n\t\t\t\tif(track.remove || track.capture || track.recv)\n\t\t\t\t\ttracks.push(track);\n\t\t\t}\n\t\t\tif(media.data) {\n\t\t\t\t// We need a data channel\n\t\t\t\ttracks.push({ type: 'data' });\n\t\t\t}\n\t\t}\n\t\t// Done\n\t\treturn tracks;\n\t};\n\n\t// Helper function to convert a track object to a set of constraints\n\tJanus.trackConstraints = function(track) {\n\t\tlet constraints = {};\n\t\tif(!track || !track.capture)\n\t\t\treturn constraints;\n\t\tif(track.type === 'audio') {\n\t\t\t// Just put the capture part in the constraints\n\t\t\tconstraints.audio = track.capture;\n\t\t} else if(track.type === 'video') {\n\t\t\t// Check if one of the keywords was passed\n\t\t\tif((track.simulcast || track.svc) && track.capture === true)\n\t\t\t\ttrack.capture = 'hires';\n\t\t\tif(track.capture === true || typeof track.capture === 'object') {\n\t\t\t\t// Use the provided capture object as video constraint\n\t\t\t\tconstraints.video = track.capture;\n\t\t\t} else {\n\t\t\t\tlet width = 0;\n\t\t\t\tlet height = 0;\n\t\t\t\tif(track.capture === 'lowres') {\n\t\t\t\t\t// Small resolution, 4:3\n\t\t\t\t\twidth = 320;\n\t\t\t\t\theight = 240;\n\t\t\t\t} else if(track.capture === 'lowres-16:9') {\n\t\t\t\t\t// Small resolution, 16:9\n\t\t\t\t\twidth = 320;\n\t\t\t\t\theight = 180;\n\t\t\t\t} else if(track.capture === 'hires' || track.capture === 'hires-16:9' || track.capture === 'hdres') {\n\t\t\t\t\t// High(HD) resolution is only 16:9\n\t\t\t\t\twidth = 1280;\n\t\t\t\t\theight = 720;\n\t\t\t\t} else if(track.capture === 'fhdres') {\n\t\t\t\t\t// Full HD resolution is only 16:9\n\t\t\t\t\twidth = 1920;\n\t\t\t\t\theight = 1080;\n\t\t\t\t} else if(track.capture === '4kres') {\n\t\t\t\t\t// 4K resolution is only 16:9\n\t\t\t\t\twidth = 3840;\n\t\t\t\t\theight = 2160;\n\t\t\t\t} else if(track.capture === 'stdres') {\n\t\t\t\t\t// Normal resolution, 4:3\n\t\t\t\t\twidth = 640;\n\t\t\t\t\theight = 480;\n\t\t\t\t} else if(track.capture === 'stdres-16:9') {\n\t\t\t\t\t// Normal resolution, 16:9\n\t\t\t\t\twidth = 640;\n\t\t\t\t\theight = 360;\n\t\t\t\t} else {\n\t\t\t\t\tJanus.log('Default video setting is stdres 4:3');\n\t\t\t\t\twidth = 640;\n\t\t\t\t\theight = 480;\n\t\t\t\t}\n\t\t\t\tconstraints.video = {\n\t\t\t\t\twidth: { ideal: width },\n\t\t\t\t\theight: { ideal: height }\n\t\t\t\t};\n\t\t\t}\n\t\t} else if(track.type === 'screen') {\n\t\t\t// Use the provided capture object as video constraint\n\t\t\tconstraints.video = track.capture;\n\t\t}\n\t\treturn constraints;\n\t};\n\n\tJanus.noop = function() {};\n\n\tJanus.dataChanDefaultLabel = \"JanusDataChannel\";\n\n\t// Note: in the future we may want to change this, e.g., as was\n\t// attempted in https://github.com/meetecho/janus-gateway/issues/1670\n\tJanus.endOfCandidates = null;\n\n\t// Stop all tracks from a given stream\n\tJanus.stopAllTracks = function(stream) {\n\t\ttry {\n\t\t\t// Try a MediaStreamTrack.stop() for each track\n\t\t\tlet tracks = stream.getTracks();\n\t\t\tfor(let mst of tracks) {\n\t\t\t\tJanus.log(mst);\n\t\t\t\tif(mst && mst.dontStop !== true) {\n\t\t\t\t\tmst.stop();\n\t\t\t\t}\n\t\t\t}\n\t\t// eslint-disable-next-line no-unused-vars\n\t\t} catch(e) {\n\t\t\t// Do nothing if this fails\n\t\t}\n\t}\n\n\t// Initialization\n\tJanus.init = function(options) {\n\t\toptions = options || {};\n\t\toptions.callback = (typeof options.callback == \"function\") ? options.callback : Janus.noop;\n\t\tif(Janus.initDone) {\n\t\t\t// Already initialized\n\t\t\toptions.callback();\n\t\t} else {\n\t\t\tif(typeof console.log == \"undefined\") {\n\t\t\t\tconsole.log = function() {};\n\t\t\t}\n\t\t\t// Console logging (all debugging disabled by default)\n\t\t\tJanus.trace = Janus.noop;\n\t\t\tJanus.debug = Janus.noop;\n\t\t\tJanus.vdebug = Janus.noop;\n\t\t\tJanus.log = Janus.noop;\n\t\t\tJanus.warn = Janus.noop;\n\t\t\tJanus.error = Janus.noop;\n\t\t\tif(options.debug === true || options.debug === \"all\") {\n\t\t\t\t// Enable all debugging levels\n\t\t\t\tJanus.trace = console.trace.bind(console);\n\t\t\t\tJanus.debug = console.debug.bind(console);\n\t\t\t\tJanus.vdebug = console.debug.bind(console);\n\t\t\t\tJanus.log = console.log.bind(console);\n\t\t\t\tJanus.warn = console.warn.bind(console);\n\t\t\t\tJanus.error = console.error.bind(console);\n\t\t\t} else if(Array.isArray(options.debug)) {\n\t\t\t\tfor(let d of options.debug) {\n\t\t\t\t\tswitch(d) {\n\t\t\t\t\t\tcase \"trace\":\n\t\t\t\t\t\t\tJanus.trace = console.trace.bind(console);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"debug\":\n\t\t\t\t\t\t\tJanus.debug = console.debug.bind(console);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"vdebug\":\n\t\t\t\t\t\t\tJanus.vdebug = console.debug.bind(console);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"log\":\n\t\t\t\t\t\t\tJanus.log = console.log.bind(console);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"warn\":\n\t\t\t\t\t\t\tJanus.warn = console.warn.bind(console);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase \"error\":\n\t\t\t\t\t\t\tJanus.error = console.error.bind(console);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tconsole.error(\"Unknown debugging option '\" + d + \"' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')\");\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tJanus.log(\"Initializing library\");\n\n\t\t\tlet usedDependencies = options.dependencies || Janus.useDefaultDependencies();\n\t\t\tJanus.isArray = usedDependencies.isArray;\n\t\t\tJanus.webRTCAdapter = usedDependencies.webRTCAdapter;\n\t\t\tJanus.httpAPICall = usedDependencies.httpAPICall;\n\t\t\tJanus.newWebSocket = usedDependencies.newWebSocket;\n\t\t\tJanus.extension = usedDependencies.extension;\n\t\t\tJanus.extension.init();\n\n\t\t\t// Helper method to enumerate devices\n\t\t\tJanus.listDevices = function(callback, config) {\n\t\t\t\tcallback = (typeof callback == \"function\") ? callback : Janus.noop;\n\t\t\t\tif(!config)\n\t\t\t\t\tconfig = { audio: true, video: true };\n\t\t\t\tif(Janus.isGetUserMediaAvailable()) {\n\t\t\t\t\tnavigator.mediaDevices.getUserMedia(config)\n\t\t\t\t\t\t.then(function(stream) {\n\t\t\t\t\t\t\tnavigator.mediaDevices.enumerateDevices().then(function(devices) {\n\t\t\t\t\t\t\t\tJanus.debug(devices);\n\t\t\t\t\t\t\t\tcallback(devices);\n\t\t\t\t\t\t\t\t// Get rid of the now useless stream\n\t\t\t\t\t\t\t\tJanus.stopAllTracks(stream)\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(function(err) {\n\t\t\t\t\t\t\tJanus.error(err);\n\t\t\t\t\t\t\tcallback([]);\n\t\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tJanus.warn(\"navigator.mediaDevices unavailable\");\n\t\t\t\t\tcallback([]);\n\t\t\t\t}\n\t\t\t};\n\t\t\t// Helper methods to attach/reattach a stream to a video element (previously part of adapter.js)\n\t\t\tJanus.attachMediaStream = function(element, stream) {\n\t\t\t\ttry {\n\t\t\t\t\telement.srcObject = stream;\n\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t} catch (e) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\telement.src = URL.createObjectURL(stream);\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tJanus.error(\"Error attaching stream to element\", e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tJanus.reattachMediaStream = function(to, from) {\n\t\t\t\ttry {\n\t\t\t\t\tto.srcObject = from.srcObject;\n\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t} catch (e) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tto.src = from.src;\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tJanus.error(\"Error reattaching stream to element\", e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\t// Detect tab close: make sure we don't loose existing onbeforeunload handlers\n\t\t\t// (note: for iOS we need to subscribe to a different event, 'pagehide', see\n\t\t\t// https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe)\n\t\t\tlet iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;\n\t\t\tlet eventName = iOS ? 'pagehide' : 'beforeunload';\n\t\t\tlet oldOBF = window[\"on\" + eventName];\n\t\t\twindow.addEventListener(eventName, function() {\n\t\t\t\tJanus.log(\"Closing window\");\n\t\t\t\tfor(const [sessionId, session] of Janus.sessions) {\n\t\t\t\t\tif(session && session.destroyOnUnload) {\n\t\t\t\t\t\tJanus.log(\"Destroying session \" + sessionId);\n\t\t\t\t\t\tsession.destroy({unload: true, notifyDestroyed: false});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(oldOBF && typeof oldOBF == \"function\") {\n\t\t\t\t\toldOBF();\n\t\t\t\t}\n\t\t\t});\n\t\t\t// If this is a Safari, check if VP8 or VP9 are supported\n\t\t\tJanus.safariVp8 = false;\n\t\t\tJanus.safariVp9 = false;\n\t\t\tif(Janus.webRTCAdapter.browserDetails.browser === 'safari' &&\n\t\t\t\tJanus.webRTCAdapter.browserDetails.version >= 605) {\n\t\t\t\t// Let's see if RTCRtpSender.getCapabilities() is there\n\t\t\t\tif(RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities(\"video\") &&\n\t\t\t\t\tRTCRtpSender.getCapabilities(\"video\").codecs && RTCRtpSender.getCapabilities(\"video\").codecs.length) {\n\t\t\t\t\tfor(let codec of RTCRtpSender.getCapabilities(\"video\").codecs) {\n\t\t\t\t\t\tif(codec && codec.mimeType && codec.mimeType.toLowerCase() === \"video/vp8\") {\n\t\t\t\t\t\t\tJanus.safariVp8 = true;\n\t\t\t\t\t\t} else if(codec && codec.mimeType && codec.mimeType.toLowerCase() === \"video/vp9\") {\n\t\t\t\t\t\t\tJanus.safariVp9 = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(Janus.safariVp8) {\n\t\t\t\t\t\tJanus.log(\"This version of Safari supports VP8\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJanus.warn(\"This version of Safari does NOT support VP8: if you're using a Technology Preview, \" +\n\t\t\t\t\t\t\"try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu\");\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// We do it in a very ugly way, as there's no alternative...\n\t\t\t\t\t// We create a PeerConnection to see if VP8 is in an offer\n\t\t\t\t\tlet testpc = new RTCPeerConnection({});\n\t\t\t\t\ttestpc.createOffer({offerToReceiveVideo: true}).then(function(offer) {\n\t\t\t\t\t\tJanus.safariVp8 = offer.sdp.indexOf(\"VP8\") !== -1;\n\t\t\t\t\t\tJanus.safariVp9 = offer.sdp.indexOf(\"VP9\") !== -1;\n\t\t\t\t\t\tif(Janus.safariVp8) {\n\t\t\t\t\t\t\tJanus.log(\"This version of Safari supports VP8\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJanus.warn(\"This version of Safari does NOT support VP8: if you're using a Technology Preview, \" +\n\t\t\t\t\t\t\t\"try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttestpc.close();\n\t\t\t\t\t\ttestpc = null;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tJanus.initDone = true;\n\t\t\toptions.callback();\n\t\t}\n\t};\n\n\t// Helper method to check whether WebRTC is supported by this browser\n\tJanus.isWebrtcSupported = function() {\n\t\treturn !!window.RTCPeerConnection;\n\t};\n\t// Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP)\n\tJanus.isGetUserMediaAvailable = function() {\n\t\treturn navigator.mediaDevices && navigator.mediaDevices.getUserMedia;\n\t};\n\n\t// Helper method to create random identifiers (e.g., transaction)\n\tJanus.randomString = function(len) {\n\t\tlet charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\t\tlet randomString = '';\n\t\tfor(let i=0; i<len; i++) {\n\t\t\tlet randomPoz = Math.floor(Math.random() * charSet.length);\n\t\t\trandomString += charSet.charAt(randomPoz);\n\t\t}\n\t\treturn randomString;\n\t};\n\n\tfunction Janus(gatewayCallbacks) {\n\t\tgatewayCallbacks = gatewayCallbacks || {};\n\t\tgatewayCallbacks.success = (typeof gatewayCallbacks.success == \"function\") ? gatewayCallbacks.success : Janus.noop;\n\t\tgatewayCallbacks.error = (typeof gatewayCallbacks.error == \"function\") ? gatewayCallbacks.error : Janus.noop;\n\t\tgatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == \"function\") ? gatewayCallbacks.destroyed : Janus.noop;\n\t\tif(!Janus.initDone) {\n\t\t\tgatewayCallbacks.error(\"Library not initialized\");\n\t\t\treturn {};\n\t\t}\n\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\tgatewayCallbacks.error(\"WebRTC not supported by this browser\");\n\t\t\treturn {};\n\t\t}\n\t\tJanus.log(\"Library initialized: \" + Janus.initDone);\n\t\tif(!gatewayCallbacks.server) {\n\t\t\tgatewayCallbacks.error(\"Invalid server url\");\n\t\t\treturn {};\n\t\t}\n\t\tlet websockets = false;\n\t\tlet ws = null;\n\t\tlet wsHandlers = {};\n\t\tlet wsKeepaliveTimeoutId = null;\n\t\tlet servers = null;\n\t\tlet serversIndex = 0;\n\t\tlet server = gatewayCallbacks.server;\n\t\tif(Janus.isArray(server)) {\n\t\t\tJanus.log(\"Multiple servers provided (\" + server.length + \"), will use the first that works\");\n\t\t\tserver = null;\n\t\t\tservers = gatewayCallbacks.server;\n\t\t\tJanus.debug(servers);\n\t\t} else {\n\t\t\tif(server.indexOf(\"ws\") === 0) {\n\t\t\t\twebsockets = true;\n\t\t\t\tJanus.log(\"Using WebSockets to contact Janus: \" + server);\n\t\t\t} else {\n\t\t\t\twebsockets = false;\n\t\t\t\tJanus.log(\"Using REST API to contact Janus: \" + server);\n\t\t\t}\n\t\t}\n\t\tlet iceServers = gatewayCallbacks.iceServers || [{urls: \"stun:stun.l.google.com:19302\"}];\n\t\tlet iceTransportPolicy = gatewayCallbacks.iceTransportPolicy;\n\t\tlet bundlePolicy = gatewayCallbacks.bundlePolicy;\n\t\t// Whether we should enable the withCredentials flag for XHR requests\n\t\tlet withCredentials = false;\n\t\tif(typeof gatewayCallbacks.withCredentials !== 'undefined' && gatewayCallbacks.withCredentials !== null)\n\t\t\twithCredentials = gatewayCallbacks.withCredentials === true;\n\t\t// Optional max events\n\t\tlet maxev = 10;\n\t\tif(typeof gatewayCallbacks.max_poll_events !== 'undefined' && gatewayCallbacks.max_poll_events !== null)\n\t\t\tmaxev = gatewayCallbacks.max_poll_events;\n\t\tif(maxev < 1)\n\t\t\tmaxev = 1;\n\t\t// Token to use (only if the token based authentication mechanism is enabled)\n\t\tlet token = null;\n\t\tif(typeof gatewayCallbacks.token !== 'undefined' && gatewayCallbacks.token !== null)\n\t\t\ttoken = gatewayCallbacks.token;\n\t\t// API secret to use (only if the shared API secret is enabled)\n\t\tlet apisecret = null;\n\t\tif(typeof gatewayCallbacks.apisecret !== 'undefined' && gatewayCallbacks.apisecret !== null)\n\t\t\tapisecret = gatewayCallbacks.apisecret;\n\t\t// Whether we should destroy this session when onbeforeunload is called\n\t\tthis.destroyOnUnload = true;\n\t\tif(typeof gatewayCallbacks.destroyOnUnload !== 'undefined' && gatewayCallbacks.destroyOnUnload !== null)\n\t\t\tthis.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true);\n\t\t// Some timeout-related values\n\t\tlet keepAlivePeriod = 25000;\n\t\tif(typeof gatewayCallbacks.keepAlivePeriod !== 'undefined' && gatewayCallbacks.keepAlivePeriod !== null)\n\t\t\tkeepAlivePeriod = gatewayCallbacks.keepAlivePeriod;\n\t\tif(isNaN(keepAlivePeriod))\n\t\t\tkeepAlivePeriod = 25000;\n\t\tlet longPollTimeout = 60000;\n\t\tif(typeof gatewayCallbacks.longPollTimeout !== 'undefined' && gatewayCallbacks.longPollTimeout !== null)\n\t\t\tlongPollTimeout = gatewayCallbacks.longPollTimeout;\n\t\tif(isNaN(longPollTimeout))\n\t\t\tlongPollTimeout = 60000;\n\n\t\t// overrides for default maxBitrate values for simulcasting\n\t\tfunction getMaxBitrates(simulcastMaxBitrates) {\n\t\t\tlet maxBitrates = {\n\t\t\t\thigh: 900000,\n\t\t\t\tmedium: 300000,\n\t\t\t\tlow: 100000,\n\t\t\t};\n\n\t\t\tif(typeof simulcastMaxBitrates !== 'undefined' && simulcastMaxBitrates !== null) {\n\t\t\t\tif(simulcastMaxBitrates.high)\n\t\t\t\t\tmaxBitrates.high = simulcastMaxBitrates.high;\n\t\t\t\tif(simulcastMaxBitrates.medium)\n\t\t\t\t\tmaxBitrates.medium = simulcastMaxBitrates.medium;\n\t\t\t\tif(simulcastMaxBitrates.low)\n\t\t\t\t\tmaxBitrates.low = simulcastMaxBitrates.low;\n\t\t\t}\n\n\t\t\treturn maxBitrates;\n\t\t}\n\n\t\tlet connected = false;\n\t\tlet sessionId = null;\n\t\tlet pluginHandles = new Map();\n\t\tlet that = this;\n\t\tlet retries = 0;\n\t\tlet transactions = new Map();\n\t\tcreateSession(gatewayCallbacks);\n\n\t\t// Public methods\n\t\tthis.getServer = function() { return server; };\n\t\tthis.isConnected = function() { return connected; };\n\t\tthis.reconnect = function(callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tcallbacks[\"reconnect\"] = true;\n\t\t\tcreateSession(callbacks);\n\t\t};\n\t\tthis.getSessionId = function() { return sessionId; };\n\t\tthis.getInfo = function(callbacks) { getInfo(callbacks); };\n\t\tthis.destroy = function(callbacks) { destroySession(callbacks); };\n\t\tthis.attach = function(callbacks) { createHandle(callbacks); };\n\n\t\tfunction eventHandler() {\n\t\t\tif(sessionId == null)\n\t\t\t\treturn;\n\t\t\tJanus.debug('Long poll...');\n\t\t\tif(!connected) {\n\t\t\t\tJanus.warn(\"Is the server down? (connected=false)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet longpoll = server + \"/\" + sessionId + \"?rid=\" + new Date().getTime();\n\t\t\tif(maxev)\n\t\t\t\tlongpoll = longpoll + \"&maxev=\" + maxev;\n\t\t\tif(token)\n\t\t\t\tlongpoll = longpoll + \"&token=\" + encodeURIComponent(token);\n\t\t\tif(apisecret)\n\t\t\t\tlongpoll = longpoll + \"&apisecret=\" + encodeURIComponent(apisecret);\n\t\t\tJanus.httpAPICall(longpoll, {\n\t\t\t\tverb: 'GET',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tsuccess: handleEvent,\n\t\t\t\ttimeout: longPollTimeout,\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\n\t\t\t\t\tretries++;\n\t\t\t\t\tif(retries > 3) {\n\t\t\t\t\t\t// Did we just lose the server? :-(\n\t\t\t\t\t\tconnected = false;\n\t\t\t\t\t\tgatewayCallbacks.error(\"Lost connection to the server (is it down?)\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\teventHandler();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Private event handler: this will trigger plugin callbacks, if set\n\t\tfunction handleEvent(json, skipTimeout) {\n\t\t\tretries = 0;\n\t\t\tif(!websockets && typeof sessionId !== 'undefined' && sessionId !== null && skipTimeout !== true)\n\t\t\t\teventHandler();\n\t\t\tif(!websockets && Janus.isArray(json)) {\n\t\t\t\t// We got an array: it means we passed a maxev > 1, iterate on all objects\n\t\t\t\tfor(let i=0; i<json.length; i++) {\n\t\t\t\t\thandleEvent(json[i], true);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(json[\"janus\"] === \"keepalive\") {\n\t\t\t\t// Nothing happened\n\t\t\t\tJanus.vdebug(\"Got a keepalive on session \" + sessionId);\n\t\t\t\treturn;\n\t\t\t} else if(json[\"janus\"] === \"server_info\") {\n\t\t\t\t// Just info on the Janus instance\n\t\t\t\tJanus.debug(\"Got info on the Janus instance\");\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst transaction = json[\"transaction\"];\n\t\t\t\tif(transaction) {\n\t\t\t\t\tconst reportSuccess = transactions.get(transaction);\n\t\t\t\t\tif(reportSuccess)\n\t\t\t\t\t\treportSuccess(json);\n\t\t\t\t\ttransactions.delete(transaction);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t} else if(json[\"janus\"] === \"ack\") {\n\t\t\t\t// Just an ack, we can probably ignore\n\t\t\t\tJanus.debug(\"Got an ack on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst transaction = json[\"transaction\"];\n\t\t\t\tif(transaction) {\n\t\t\t\t\tconst reportSuccess = transactions.get(transaction);\n\t\t\t\t\tif(reportSuccess)\n\t\t\t\t\t\treportSuccess(json);\n\t\t\t\t\ttransactions.delete(transaction);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t} else if(json[\"janus\"] === \"success\") {\n\t\t\t\t// Success!\n\t\t\t\tJanus.debug(\"Got a success on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst transaction = json[\"transaction\"];\n\t\t\t\tif(transaction) {\n\t\t\t\t\tconst reportSuccess = transactions.get(transaction);\n\t\t\t\t\tif(reportSuccess)\n\t\t\t\t\t\treportSuccess(json);\n\t\t\t\t\ttransactions.delete(transaction);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t} else if(json[\"janus\"] === \"trickle\") {\n\t\t\t\t// We got a trickle candidate from Janus\n\t\t\t\tconst sender = json[\"sender\"];\n\t\t\t\tif(!sender) {\n\t\t\t\t\tJanus.warn(\"Missing sender...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst pluginHandle = pluginHandles.get(sender);\n\t\t\t\tif(!pluginHandle) {\n\t\t\t\t\tJanus.debug(\"This handle is not attached to this session\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet candidate = json[\"candidate\"];\n\t\t\t\tJanus.debug(\"Got a trickled candidate on session \" + sessionId);\n\t\t\t\tJanus.debug(candidate);\n\t\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\t\tif(config.pc && config.remoteSdp) {\n\t\t\t\t\t// Add candidate right now\n\t\t\t\t\tJanus.debug(\"Adding remote candidate:\", candidate);\n\t\t\t\t\tif(!candidate || candidate.completed === true) {\n\t\t\t\t\t\t// end-of-candidates\n\t\t\t\t\t\tconfig.pc.addIceCandidate(Janus.endOfCandidates);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// New candidate\n\t\t\t\t\t\tconfig.pc.addIceCandidate(candidate);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// We didn't do setRemoteDescription (trickle got here before the offer?)\n\t\t\t\t\tJanus.debug(\"We didn't do setRemoteDescription (trickle got here before the offer?), caching candidate\");\n\t\t\t\t\tif(!config.candidates)\n\t\t\t\t\t\tconfig.candidates = [];\n\t\t\t\t\tconfig.candidates.push(candidate);\n\t\t\t\t\tJanus.debug(config.candidates);\n\t\t\t\t}\n\t\t\t} else if(json[\"janus\"] === \"webrtcup\") {\n\t\t\t\t// The PeerConnection with the server is up! Notify this\n\t\t\t\tJanus.debug(\"Got a webrtcup event on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst sender = json[\"sender\"];\n\t\t\t\tif(!sender) {\n\t\t\t\t\tJanus.warn(\"Missing sender...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst pluginHandle = pluginHandles.get(sender);\n\t\t\t\tif(!pluginHandle) {\n\t\t\t\t\tJanus.debug(\"This handle is not attached to this session\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tpluginHandle.webrtcState(true);\n\t\t\t\treturn;\n\t\t\t} else if(json[\"janus\"] === \"hangup\") {\n\t\t\t\t// A plugin asked the core to hangup a PeerConnection on one of our handles\n\t\t\t\tJanus.debug(\"Got a hangup event on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst sender = json[\"sender\"];\n\t\t\t\tif(!sender) {\n\t\t\t\t\tJanus.warn(\"Missing sender...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst pluginHandle = pluginHandles.get(sender);\n\t\t\t\tif(!pluginHandle) {\n\t\t\t\t\tJanus.debug(\"This handle is not attached to this session\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tpluginHandle.webrtcState(false, json[\"reason\"]);\n\t\t\t\tpluginHandle.hangup();\n\t\t\t} else if(json[\"janus\"] === \"detached\") {\n\t\t\t\t// A plugin asked the core to detach one of our handles\n\t\t\t\tJanus.debug(\"Got a detached event on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst sender = json[\"sender\"];\n\t\t\t\tif(!sender) {\n\t\t\t\t\tJanus.warn(\"Missing sender...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst pluginHandle = pluginHandles.get(sender);\n\t\t\t\tif(!pluginHandle) {\n\t\t\t\t\t// Don't warn here because destroyHandle causes this situation.\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tpluginHandle.ondetached();\n\t\t\t\tpluginHandle.detach();\n\t\t\t} else if(json[\"janus\"] === \"media\") {\n\t\t\t\t// Media started/stopped flowing\n\t\t\t\tJanus.debug(\"Got a media event on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst sender = json[\"sender\"];\n\t\t\t\tif(!sender) {\n\t\t\t\t\tJanus.warn(\"Missing sender...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst pluginHandle = pluginHandles.get(sender);\n\t\t\t\tif(!pluginHandle) {\n\t\t\t\t\tJanus.debug(\"This handle is not attached to this session\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tpluginHandle.mediaState(json[\"type\"], json[\"receiving\"], json[\"mid\"]);\n\t\t\t} else if(json[\"janus\"] === \"slowlink\") {\n\t\t\t\tJanus.debug(\"Got a slowlink event on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\t// Trouble uplink or downlink\n\t\t\t\tconst sender = json[\"sender\"];\n\t\t\t\tif(!sender) {\n\t\t\t\t\tJanus.warn(\"Missing sender...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst pluginHandle = pluginHandles.get(sender);\n\t\t\t\tif(!pluginHandle) {\n\t\t\t\t\tJanus.debug(\"This handle is not attached to this session\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tpluginHandle.slowLink(json[\"uplink\"], json[\"lost\"], json[\"mid\"]);\n\t\t\t} else if(json[\"janus\"] === \"error\") {\n\t\t\t\t// Oops, something wrong happened\n\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\tJanus.debug(json);\n\t\t\t\tlet transaction = json[\"transaction\"];\n\t\t\t\tif(transaction) {\n\t\t\t\t\tlet reportSuccess = transactions.get(transaction);\n\t\t\t\t\tif(reportSuccess) {\n\t\t\t\t\t\treportSuccess(json);\n\t\t\t\t\t}\n\t\t\t\t\ttransactions.delete(transaction);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t} else if(json[\"janus\"] === \"event\") {\n\t\t\t\tJanus.debug(\"Got a plugin event on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tconst sender = json[\"sender\"];\n\t\t\t\tif(!sender) {\n\t\t\t\t\tJanus.warn(\"Missing sender...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet plugindata = json[\"plugindata\"];\n\t\t\t\tif(!plugindata) {\n\t\t\t\t\tJanus.warn(\"Missing plugindata...\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tJanus.debug(\"  -- Event is coming from \" + sender + \" (\" + plugindata[\"plugin\"] + \")\");\n\t\t\t\tlet data = plugindata[\"data\"];\n\t\t\t\tJanus.debug(data);\n\t\t\t\tconst pluginHandle = pluginHandles.get(sender);\n\t\t\t\tif(!pluginHandle) {\n\t\t\t\t\tJanus.warn(\"This handle is not attached to this session\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet jsep = json[\"jsep\"];\n\t\t\t\tif(jsep) {\n\t\t\t\t\tJanus.debug(\"Handling SDP as well...\");\n\t\t\t\t\tJanus.debug(jsep);\n\t\t\t\t}\n\t\t\t\tlet callback = pluginHandle.onmessage;\n\t\t\t\tif(callback) {\n\t\t\t\t\tJanus.debug(\"Notifying application...\");\n\t\t\t\t\t// Send to callback specified when attaching plugin handle\n\t\t\t\t\tcallback(data, jsep);\n\t\t\t\t} else {\n\t\t\t\t\t// Send to generic callback (?)\n\t\t\t\t\tJanus.debug(\"No provided notification callback\");\n\t\t\t\t}\n\t\t\t} else if(json[\"janus\"] === \"timeout\") {\n\t\t\t\tJanus.error(\"Timeout on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t\tif(websockets) {\n\t\t\t\t\tws.close(3504, \"Gateway timeout\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tJanus.warn(\"Unknown message/event  '\" + json[\"janus\"] + \"' on session \" + sessionId);\n\t\t\t\tJanus.debug(json);\n\t\t\t}\n\t\t}\n\n\t\t// Private helper to send keep-alive messages on WebSockets\n\t\tfunction keepAlive() {\n\t\t\tif(!server || !websockets || !connected)\n\t\t\t\treturn;\n\t\t\twsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);\n\t\t\tlet request = { \"janus\": \"keepalive\", \"session_id\": sessionId, \"transaction\": Janus.randomString(12) };\n\t\t\tif(token)\n\t\t\t\trequest[\"token\"] = token;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tws.send(JSON.stringify(request));\n\t\t}\n\n\t\t// Private method to create a session\n\t\tfunction createSession(callbacks) {\n\t\t\tlet transaction = Janus.randomString(12);\n\t\t\tlet request = { \"janus\": \"create\", \"transaction\": transaction };\n\t\t\tif(callbacks[\"reconnect\"]) {\n\t\t\t\t// We're reconnecting, claim the session\n\t\t\t\tconnected = false;\n\t\t\t\trequest[\"janus\"] = \"claim\";\n\t\t\t\trequest[\"session_id\"] = sessionId;\n\t\t\t\t// If we were using websockets, ignore the old connection\n\t\t\t\tif(ws) {\n\t\t\t\t\tws.onopen = null;\n\t\t\t\t\tws.onerror = null;\n\t\t\t\t\tws.onclose = null;\n\t\t\t\t\tif(wsKeepaliveTimeoutId) {\n\t\t\t\t\t\tclearTimeout(wsKeepaliveTimeoutId);\n\t\t\t\t\t\twsKeepaliveTimeoutId = null;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(token)\n\t\t\t\trequest[\"token\"] = token;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tif(!server && Janus.isArray(servers)) {\n\t\t\t\t// We still need to find a working server from the list we were given\n\t\t\t\tserver = servers[serversIndex];\n\t\t\t\tif(server.indexOf(\"ws\") === 0) {\n\t\t\t\t\twebsockets = true;\n\t\t\t\t\tJanus.log(\"Server #\" + (serversIndex+1) + \": trying WebSockets to contact Janus (\" + server + \")\");\n\t\t\t\t} else {\n\t\t\t\t\twebsockets = false;\n\t\t\t\t\tJanus.log(\"Server #\" + (serversIndex+1) + \": trying REST API to contact Janus (\" + server + \")\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(websockets) {\n\t\t\t\tws = Janus.newWebSocket(server, 'janus-protocol');\n\t\t\t\twsHandlers = {\n\t\t\t\t\t'error': function() {\n\t\t\t\t\t\tJanus.error(\"Error connecting to the Janus WebSockets server... \" + server);\n\t\t\t\t\t\tif(Janus.isArray(servers) && !callbacks[\"reconnect\"]) {\n\t\t\t\t\t\t\tserversIndex++;\n\t\t\t\t\t\t\tif(serversIndex === servers.length) {\n\t\t\t\t\t\t\t\t// We tried all the servers the user gave us and they all failed\n\t\t\t\t\t\t\t\tcallbacks.error(\"Error connecting to any of the provided Janus servers: Is the server down?\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Let's try the next server\n\t\t\t\t\t\t\tserver = null;\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tcreateSession(callbacks);\n\t\t\t\t\t\t\t}, 200);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcallbacks.error(\"Error connecting to the Janus WebSockets server: Is the server down?\");\n\t\t\t\t\t},\n\n\t\t\t\t\t'open': function() {\n\t\t\t\t\t\t// We need to be notified about the success\n\t\t\t\t\t\ttransactions.set(transaction, function(json) {\n\t\t\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t\t\t\tcallbacks.error(json[\"error\"].reason);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\twsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);\n\t\t\t\t\t\t\tconnected = true;\n\t\t\t\t\t\t\tsessionId = json[\"session_id\"] ? json[\"session_id\"] : json.data[\"id\"];\n\t\t\t\t\t\t\tif(callbacks[\"reconnect\"]) {\n\t\t\t\t\t\t\t\tJanus.log(\"Claimed session: \" + sessionId);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tJanus.log(\"Created session: \" + sessionId);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tJanus.sessions.set(sessionId, that);\n\t\t\t\t\t\t\tcallbacks.success();\n\t\t\t\t\t\t});\n\t\t\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\t\t},\n\n\t\t\t\t\t'message': function(event) {\n\t\t\t\t\t\thandleEvent(JSON.parse(event.data));\n\t\t\t\t\t},\n\n\t\t\t\t\t'close': function() {\n\t\t\t\t\t\tif(!server || !connected) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconnected = false;\n\t\t\t\t\t\t// FIXME What if this is called when the page is closed?\n\t\t\t\t\t\tgatewayCallbacks.error(\"Lost connection to the server (is it down?)\");\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tfor(let eventName in wsHandlers) {\n\t\t\t\t\tws.addEventListener(eventName, wsHandlers[eventName]);\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.httpAPICall(server, {\n\t\t\t\tverb: 'POST',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tbody: request,\n\t\t\t\tsuccess: function(json) {\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t\tcallbacks.error(json[\"error\"].reason);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconnected = true;\n\t\t\t\t\tsessionId = json[\"session_id\"] ? json[\"session_id\"] : json.data[\"id\"];\n\t\t\t\t\tif(callbacks[\"reconnect\"]) {\n\t\t\t\t\t\tJanus.log(\"Claimed session: \" + sessionId);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJanus.log(\"Created session: \" + sessionId);\n\t\t\t\t\t}\n\t\t\t\t\tJanus.sessions.set(sessionId, that);\n\t\t\t\t\teventHandler();\n\t\t\t\t\tcallbacks.success();\n\t\t\t\t},\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\t// FIXME\n\t\t\t\t\tif(Janus.isArray(servers) && !callbacks[\"reconnect\"]) {\n\t\t\t\t\t\tserversIndex++;\n\t\t\t\t\t\tif(serversIndex === servers.length) {\n\t\t\t\t\t\t\t// We tried all the servers the user gave us and they all failed\n\t\t\t\t\t\t\tcallbacks.error(\"Error connecting to any of the provided Janus servers: Is the server down?\");\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Let's try the next server\n\t\t\t\t\t\tserver = null;\n\t\t\t\t\t\tsetTimeout(function() { createSession(callbacks); }, 200);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif(errorThrown === \"\")\n\t\t\t\t\t\tcallbacks.error(textStatus + \": Is the server down?\");\n\t\t\t\t\telse if(errorThrown && errorThrown.error)\n\t\t\t\t\t\tcallbacks.error(textStatus + \": \" + errorThrown.error.message);\n\t\t\t\t\telse\n\t\t\t\t\t\tcallbacks.error(textStatus + \": \" + errorThrown);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Private method to get info on the server\n\t\tfunction getInfo(callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\t// FIXME This method triggers a success even when we fail\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tJanus.log(\"Getting info on Janus instance\");\n\t\t\tif(!connected) {\n\t\t\t\tJanus.warn(\"Is the server down? (connected=false)\");\n\t\t\t\tcallbacks.error(\"Is the server down? (connected=false)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// We just need to send an \"info\" request\n\t\t\tlet transaction = Janus.randomString(12);\n\t\t\tlet request = { \"janus\": \"info\", \"transaction\": transaction };\n\t\t\tif(token)\n\t\t\t\trequest[\"token\"] = token;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tif(websockets) {\n\t\t\t\ttransactions.set(transaction, function(json) {\n\t\t\t\t\tJanus.log(\"Server info:\");\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] !== \"server_info\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t}\n\t\t\t\t\tcallbacks.success(json);\n\t\t\t\t});\n\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.httpAPICall(server, {\n\t\t\t\tverb: 'POST',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tbody: request,\n\t\t\t\tsuccess: function(json) {\n\t\t\t\t\tJanus.log(\"Server info:\");\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] !== \"server_info\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t}\n\t\t\t\t\tcallbacks.success(json);\n\t\t\t\t},\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\t// FIXME\n\t\t\t\t\tif(errorThrown === \"\")\n\t\t\t\t\t\tcallbacks.error(textStatus + \": Is the server down?\");\n\t\t\t\t\telse\n\t\t\t\t\t\tcallbacks.error(textStatus + \": \" + errorThrown);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Private method to destroy a session\n\t\tfunction destroySession(callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\t// FIXME This method triggers a success even when we fail\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tlet unload = (callbacks.unload === true);\n\t\t\tlet notifyDestroyed = true;\n\t\t\tif(typeof callbacks.notifyDestroyed !== 'undefined' && callbacks.notifyDestroyed !== null)\n\t\t\t\tnotifyDestroyed = (callbacks.notifyDestroyed === true);\n\t\t\tlet cleanupHandles = (callbacks.cleanupHandles === true);\n\t\t\tJanus.log(\"Destroying session \" + sessionId + \" (unload=\" + unload + \")\");\n\t\t\tif(!sessionId) {\n\t\t\t\tJanus.warn(\"No session to destroy\");\n\t\t\t\tcallbacks.success();\n\t\t\t\tif(notifyDestroyed)\n\t\t\t\t\tgatewayCallbacks.destroyed();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(cleanupHandles) {\n\t\t\t\tfor(const handleId of pluginHandles.keys())\n\t\t\t\t\tdestroyHandle(handleId, { noRequest: true });\n\t\t\t}\n\t\t\tif(!connected) {\n\t\t\t\tJanus.warn(\"Is the server down? (connected=false)\");\n\t\t\t\tsessionId = null;\n\t\t\t\tcallbacks.success();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// No need to destroy all handles first, Janus will do that itself\n\t\t\tlet request = { \"janus\": \"destroy\", \"transaction\": Janus.randomString(12) };\n\t\t\tif(token)\n\t\t\t\trequest[\"token\"] = token;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tif(unload) {\n\t\t\t\t// We're unloading the page: use sendBeacon for HTTP instead,\n\t\t\t\t// or just close the WebSocket connection if we're using that\n\t\t\t\tif(websockets) {\n\t\t\t\t\tws.onclose = null;\n\t\t\t\t\tws.close();\n\t\t\t\t\tws = null;\n\t\t\t\t} else {\n\t\t\t\t\tnavigator.sendBeacon(server + \"/\" + sessionId, JSON.stringify(request));\n\t\t\t\t}\n\t\t\t\tJanus.log(\"Destroyed session:\");\n\t\t\t\tsessionId = null;\n\t\t\t\tconnected = false;\n\t\t\t\tcallbacks.success();\n\t\t\t\tif(notifyDestroyed)\n\t\t\t\t\tgatewayCallbacks.destroyed();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(websockets) {\n\t\t\t\trequest[\"session_id\"] = sessionId;\n\n\t\t\t\tlet unbindWebSocket = function() {\n\t\t\t\t\tfor(let eventName in wsHandlers) {\n\t\t\t\t\t\tws.removeEventListener(eventName, wsHandlers[eventName]);\n\t\t\t\t\t}\n\t\t\t\t\tws.removeEventListener('message', onUnbindMessage);\n\t\t\t\t\tws.removeEventListener('error', onUnbindError);\n\t\t\t\t\tif(wsKeepaliveTimeoutId) {\n\t\t\t\t\t\tclearTimeout(wsKeepaliveTimeoutId);\n\t\t\t\t\t}\n\t\t\t\t\tws.close();\n\t\t\t\t};\n\n\t\t\t\tlet onUnbindMessage = function(event){\n\t\t\t\t\tlet data = JSON.parse(event.data);\n\t\t\t\t\tif(data.session_id == request.session_id && data.transaction == request.transaction) {\n\t\t\t\t\t\tunbindWebSocket();\n\t\t\t\t\t\tcallbacks.success();\n\t\t\t\t\t\tif(notifyDestroyed)\n\t\t\t\t\t\t\tgatewayCallbacks.destroyed();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tlet onUnbindError = function() {\n\t\t\t\t\tunbindWebSocket();\n\t\t\t\t\tcallbacks.error(\"Failed to destroy the server: Is the server down?\");\n\t\t\t\t\tif(notifyDestroyed)\n\t\t\t\t\t\tgatewayCallbacks.destroyed();\n\t\t\t\t};\n\n\t\t\t\tws.addEventListener('message', onUnbindMessage);\n\t\t\t\tws.addEventListener('error', onUnbindError);\n\n\t\t\t\tif(ws.readyState === 1) {\n\t\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\t} else {\n\t\t\t\t\tonUnbindError();\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.httpAPICall(server + \"/\" + sessionId, {\n\t\t\t\tverb: 'POST',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tbody: request,\n\t\t\t\tsuccess: function(json) {\n\t\t\t\t\tJanus.log(\"Destroyed session:\");\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tsessionId = null;\n\t\t\t\t\tconnected = false;\n\t\t\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t}\n\t\t\t\t\tcallbacks.success();\n\t\t\t\t\tif(notifyDestroyed)\n\t\t\t\t\t\tgatewayCallbacks.destroyed();\n\t\t\t\t},\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\t// FIXME\n\t\t\t\t\t// Reset everything anyway\n\t\t\t\t\tsessionId = null;\n\t\t\t\t\tconnected = false;\n\t\t\t\t\tcallbacks.success();\n\t\t\t\t\tif(notifyDestroyed)\n\t\t\t\t\t\tgatewayCallbacks.destroyed();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Private method to create a plugin handle\n\t\tfunction createHandle(callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tcallbacks.dataChannelOptions = callbacks.dataChannelOptions || { ordered: true };\n\t\t\tcallbacks.consentDialog = (typeof callbacks.consentDialog == \"function\") ? callbacks.consentDialog : Janus.noop;\n\t\t\tcallbacks.connectionState = (typeof callbacks.connectionState == \"function\") ? callbacks.connectionState : Janus.noop;\n\t\t\tcallbacks.iceState = (typeof callbacks.iceState == \"function\") ? callbacks.iceState : Janus.noop;\n\t\t\tcallbacks.mediaState = (typeof callbacks.mediaState == \"function\") ? callbacks.mediaState : Janus.noop;\n\t\t\tcallbacks.webrtcState = (typeof callbacks.webrtcState == \"function\") ? callbacks.webrtcState : Janus.noop;\n\t\t\tcallbacks.slowLink = (typeof callbacks.slowLink == \"function\") ? callbacks.slowLink : Janus.noop;\n\t\t\tcallbacks.onmessage = (typeof callbacks.onmessage == \"function\") ? callbacks.onmessage : Janus.noop;\n\t\t\tcallbacks.onlocaltrack = (typeof callbacks.onlocaltrack == \"function\") ? callbacks.onlocaltrack : Janus.noop;\n\t\t\tcallbacks.onremotetrack = (typeof callbacks.onremotetrack == \"function\") ? callbacks.onremotetrack : Janus.noop;\n\t\t\tcallbacks.ondata = (typeof callbacks.ondata == \"function\") ? callbacks.ondata : Janus.noop;\n\t\t\tcallbacks.ondataopen = (typeof callbacks.ondataopen == \"function\") ? callbacks.ondataopen : Janus.noop;\n\t\t\tcallbacks.oncleanup = (typeof callbacks.oncleanup == \"function\") ? callbacks.oncleanup : Janus.noop;\n\t\t\tcallbacks.ondetached = (typeof callbacks.ondetached == \"function\") ? callbacks.ondetached : Janus.noop;\n\t\t\tif(!connected) {\n\t\t\t\tJanus.warn(\"Is the server down? (connected=false)\");\n\t\t\t\tcallbacks.error(\"Is the server down? (connected=false)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet plugin = callbacks.plugin;\n\t\t\tif(!plugin) {\n\t\t\t\tJanus.error(\"Invalid plugin\");\n\t\t\t\tcallbacks.error(\"Invalid plugin\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet opaqueId = callbacks.opaqueId;\n\t\t\tlet loopIndex = callbacks.loopIndex;\n\t\t\tlet handleToken = callbacks.token ? callbacks.token : token;\n\t\t\tlet transaction = Janus.randomString(12);\n\t\t\tlet request = { \"janus\": \"attach\", \"plugin\": plugin, \"opaque_id\": opaqueId, \"loop_index\": loopIndex, \"transaction\": transaction };\n\t\t\tif(handleToken)\n\t\t\t\trequest[\"token\"] = handleToken;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tif(websockets) {\n\t\t\t\ttransactions.set(transaction, function(json) {\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t\tcallbacks.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet handleId = json.data[\"id\"];\n\t\t\t\t\tJanus.log(\"Created handle: \" + handleId);\n\t\t\t\t\tlet pluginHandle =\n\t\t\t\t\t{\n\t\t\t\t\t\tsession : that,\n\t\t\t\t\t\tplugin : plugin,\n\t\t\t\t\t\tid : handleId,\n\t\t\t\t\t\ttoken : handleToken,\n\t\t\t\t\t\tdetached : false,\n\t\t\t\t\t\twebrtcStuff : {\n\t\t\t\t\t\t\tstarted : false,\n\t\t\t\t\t\t\tmyStream : null,\n\t\t\t\t\t\t\tstreamExternal : false,\n\t\t\t\t\t\t\tmySdp : null,\n\t\t\t\t\t\t\tmediaConstraints : null,\n\t\t\t\t\t\t\tpc : null,\n\t\t\t\t\t\t\tdataChannelOptions: callbacks.dataChannelOptions,\n\t\t\t\t\t\t\tdataChannel : {},\n\t\t\t\t\t\t\tdtmfSender : null,\n\t\t\t\t\t\t\ttrickle : true,\n\t\t\t\t\t\t\ticeDone : false,\n\t\t\t\t\t\t\tbitrate : {}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tgetId : function() { return handleId; },\n\t\t\t\t\t\tgetPlugin : function() { return plugin; },\n\t\t\t\t\t\tgetVolume : function(mid, result) { return getVolume(handleId, mid, true, result); },\n\t\t\t\t\t\tgetRemoteVolume : function(mid, result) { return getVolume(handleId, mid, true, result); },\n\t\t\t\t\t\tgetLocalVolume : function(mid, result) { return getVolume(handleId, mid, false, result); },\n\t\t\t\t\t\tisAudioMuted : function(mid) { return isMuted(handleId, mid, false); },\n\t\t\t\t\t\tmuteAudio : function(mid) { return mute(handleId, mid, false, true); },\n\t\t\t\t\t\tunmuteAudio : function(mid) { return mute(handleId, mid, false, false); },\n\t\t\t\t\t\tisVideoMuted : function(mid) { return isMuted(handleId, mid, true); },\n\t\t\t\t\t\tmuteVideo : function(mid) { return mute(handleId, mid, true, true); },\n\t\t\t\t\t\tunmuteVideo : function(mid) { return mute(handleId, mid, true, false); },\n\t\t\t\t\t\tgetBitrate : function(mid) { return getBitrate(handleId, mid); },\n\t\t\t\t\t\tsetMaxBitrate : function(mid, bitrate) { return setBitrate(handleId, mid, bitrate); },\n\t\t\t\t\t\tsend : function(callbacks) { sendMessage(handleId, callbacks); },\n\t\t\t\t\t\tdata : function(callbacks) { sendData(handleId, callbacks); },\n\t\t\t\t\t\tdtmf : function(callbacks) { sendDtmf(handleId, callbacks); },\n\t\t\t\t\t\tconsentDialog : callbacks.consentDialog,\n\t\t\t\t\t\tconnectionState : callbacks.connectionState,\n\t\t\t\t\t\ticeState : callbacks.iceState,\n\t\t\t\t\t\tmediaState : callbacks.mediaState,\n\t\t\t\t\t\twebrtcState : callbacks.webrtcState,\n\t\t\t\t\t\tslowLink : callbacks.slowLink,\n\t\t\t\t\t\tonmessage : callbacks.onmessage,\n\t\t\t\t\t\tcreateOffer : function(callbacks) { prepareWebrtc(handleId, true, callbacks); },\n\t\t\t\t\t\tcreateAnswer : function(callbacks) { prepareWebrtc(handleId, false, callbacks); },\n\t\t\t\t\t\thandleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); },\n\t\t\t\t\t\treplaceTracks : function(callbacks) { replaceTracks(handleId, callbacks); },\n\t\t\t\t\t\tgetLocalTracks : function() { return getLocalTracks(handleId); },\n\t\t\t\t\t\tgetRemoteTracks : function() { return getRemoteTracks(handleId); },\n\t\t\t\t\t\tonlocaltrack : callbacks.onlocaltrack,\n\t\t\t\t\t\tonremotetrack : callbacks.onremotetrack,\n\t\t\t\t\t\tondata : callbacks.ondata,\n\t\t\t\t\t\tondataopen : callbacks.ondataopen,\n\t\t\t\t\t\toncleanup : callbacks.oncleanup,\n\t\t\t\t\t\tondetached : callbacks.ondetached,\n\t\t\t\t\t\thangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); },\n\t\t\t\t\t\tdetach : function(callbacks) { destroyHandle(handleId, callbacks); }\n\t\t\t\t\t};\n\t\t\t\t\tpluginHandles.set(handleId, pluginHandle);\n\t\t\t\t\tcallbacks.success(pluginHandle);\n\t\t\t\t});\n\t\t\t\trequest[\"session_id\"] = sessionId;\n\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.httpAPICall(server + \"/\" + sessionId, {\n\t\t\t\tverb: 'POST',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tbody: request,\n\t\t\t\tsuccess: function(json) {\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t\tcallbacks.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet handleId = json.data[\"id\"];\n\t\t\t\t\tJanus.log(\"Created handle: \" + handleId);\n\t\t\t\t\tlet pluginHandle =\n\t\t\t\t\t{\n\t\t\t\t\t\tsession : that,\n\t\t\t\t\t\tplugin : plugin,\n\t\t\t\t\t\tid : handleId,\n\t\t\t\t\t\ttoken : handleToken,\n\t\t\t\t\t\tdetached : false,\n\t\t\t\t\t\twebrtcStuff : {\n\t\t\t\t\t\t\tstarted : false,\n\t\t\t\t\t\t\tmyStream : null,\n\t\t\t\t\t\t\tstreamExternal : false,\n\t\t\t\t\t\t\tmySdp : null,\n\t\t\t\t\t\t\tmediaConstraints : null,\n\t\t\t\t\t\t\tpc : null,\n\t\t\t\t\t\t\tdataChannelOptions: callbacks.dataChannelOptions,\n\t\t\t\t\t\t\tdataChannel : {},\n\t\t\t\t\t\t\tdtmfSender : null,\n\t\t\t\t\t\t\ttrickle : true,\n\t\t\t\t\t\t\ticeDone : false,\n\t\t\t\t\t\t\tbitrate: {}\n\t\t\t\t\t\t},\n\t\t\t\t\t\tgetId : function() { return handleId; },\n\t\t\t\t\t\tgetPlugin : function() { return plugin; },\n\t\t\t\t\t\tgetVolume : function(mid, result) { return getVolume(handleId, mid, true, result); },\n\t\t\t\t\t\tgetRemoteVolume : function(mid, result) { return getVolume(handleId, mid, true, result); },\n\t\t\t\t\t\tgetLocalVolume : function(mid, result) { return getVolume(handleId, mid, false, result); },\n\t\t\t\t\t\tisAudioMuted : function(mid) { return isMuted(handleId, mid, false); },\n\t\t\t\t\t\tmuteAudio : function(mid) { return mute(handleId, mid, false, true); },\n\t\t\t\t\t\tunmuteAudio : function(mid) { return mute(handleId, mid, false, false); },\n\t\t\t\t\t\tisVideoMuted : function(mid) { return isMuted(handleId, mid, true); },\n\t\t\t\t\t\tmuteVideo : function(mid) { return mute(handleId, mid, true, true); },\n\t\t\t\t\t\tunmuteVideo : function(mid) { return mute(handleId, mid, true, false); },\n\t\t\t\t\t\tgetBitrate : function(mid) { return getBitrate(handleId, mid); },\n\t\t\t\t\t\tsetMaxBitrate : function(mid, bitrate) { return setBitrate(handleId, mid, bitrate); },\n\t\t\t\t\t\tsend : function(callbacks) { sendMessage(handleId, callbacks); },\n\t\t\t\t\t\tdata : function(callbacks) { sendData(handleId, callbacks); },\n\t\t\t\t\t\tdtmf : function(callbacks) { sendDtmf(handleId, callbacks); },\n\t\t\t\t\t\tconsentDialog : callbacks.consentDialog,\n\t\t\t\t\t\tconnectionState : callbacks.connectionState,\n\t\t\t\t\t\ticeState : callbacks.iceState,\n\t\t\t\t\t\tmediaState : callbacks.mediaState,\n\t\t\t\t\t\twebrtcState : callbacks.webrtcState,\n\t\t\t\t\t\tslowLink : callbacks.slowLink,\n\t\t\t\t\t\tonmessage : callbacks.onmessage,\n\t\t\t\t\t\tcreateOffer : function(callbacks) { prepareWebrtc(handleId, true, callbacks); },\n\t\t\t\t\t\tcreateAnswer : function(callbacks) { prepareWebrtc(handleId, false, callbacks); },\n\t\t\t\t\t\thandleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); },\n\t\t\t\t\t\treplaceTracks : function(callbacks) { replaceTracks(handleId, callbacks); },\n\t\t\t\t\t\tgetLocalTracks : function() { return getLocalTracks(handleId); },\n\t\t\t\t\t\tgetRemoteTracks : function() { return getRemoteTracks(handleId); },\n\t\t\t\t\t\tonlocaltrack : callbacks.onlocaltrack,\n\t\t\t\t\t\tonremotetrack : callbacks.onremotetrack,\n\t\t\t\t\t\tondata : callbacks.ondata,\n\t\t\t\t\t\tondataopen : callbacks.ondataopen,\n\t\t\t\t\t\toncleanup : callbacks.oncleanup,\n\t\t\t\t\t\tondetached : callbacks.ondetached,\n\t\t\t\t\t\thangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); },\n\t\t\t\t\t\tdetach : function(callbacks) { destroyHandle(handleId, callbacks); }\n\t\t\t\t\t}\n\t\t\t\t\tpluginHandles.set(handleId, pluginHandle);\n\t\t\t\t\tcallbacks.success(pluginHandle);\n\t\t\t\t},\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\t// FIXME\n\t\t\t\t\tif(errorThrown === \"\")\n\t\t\t\t\t\tcallbacks.error(textStatus + \": Is the server down?\");\n\t\t\t\t\telse\n\t\t\t\t\t\tcallbacks.error(textStatus + \": \" + errorThrown);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Private method to send a message\n\t\tfunction sendMessage(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tif(!connected) {\n\t\t\t\tJanus.warn(\"Is the server down? (connected=false)\");\n\t\t\t\tcallbacks.error(\"Is the server down? (connected=false)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tcallbacks.error(\"Invalid handle\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet message = callbacks.message;\n\t\t\tlet jsep = callbacks.jsep;\n\t\t\tlet transaction = Janus.randomString(12);\n\t\t\tlet request = { \"janus\": \"message\", \"body\": message, \"transaction\": transaction };\n\t\t\tif(pluginHandle.token)\n\t\t\t\trequest[\"token\"] = pluginHandle.token;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tif(jsep) {\n\t\t\t\trequest.jsep = {\n\t\t\t\t\ttype: jsep.type,\n\t\t\t\t\tsdp: jsep.sdp\n\t\t\t\t};\n\t\t\t\tif(jsep.e2ee)\n\t\t\t\t\trequest.jsep.e2ee = true;\n\t\t\t\tif(jsep.rid_order === \"hml\" || jsep.rid_order === \"lmh\")\n\t\t\t\t\trequest.jsep.rid_order = jsep.rid_order;\n\t\t\t\tif(jsep.force_relay)\n\t\t\t\t\trequest.jsep.force_relay = true;\n\t\t\t\t// Check if there's SVC video streams to tell Janus about\n\t\t\t\tlet svc = null;\n\t\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\t\tif(config.pc) {\n\t\t\t\t\tlet transceivers = config.pc.getTransceivers();\n\t\t\t\t\tif(transceivers && transceivers.length > 0) {\n\t\t\t\t\t\tfor(let mindex in transceivers) {\n\t\t\t\t\t\t\tlet tr = transceivers[mindex];\n\t\t\t\t\t\t\tif(tr && tr.sender && tr.sender.track && tr.sender.track.kind === 'video') {\n\t\t\t\t\t\t\t\tlet params = tr.sender.getParameters();\n\t\t\t\t\t\t\t\tif(params && params.encodings && params.encodings.length === 1 &&\n\t\t\t\t\t\t\t\t\t\tparams.encodings[0] && params.encodings[0].scalabilityMode) {\n\t\t\t\t\t\t\t\t\t// This video stream uses SVC\n\t\t\t\t\t\t\t\t\tif(!svc)\n\t\t\t\t\t\t\t\t\t\tsvc = [];\n\t\t\t\t\t\t\t\t\tsvc.push({\n\t\t\t\t\t\t\t\t\t\tmindex: parseInt(mindex),\n\t\t\t\t\t\t\t\t\t\tmid: tr.mid,\n\t\t\t\t\t\t\t\t\t\tsvc: params.encodings[0].scalabilityMode\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(svc)\n\t\t\t\t\trequest.jsep.svc = svc;\n\t\t\t}\n\t\t\tJanus.debug(\"Sending message to plugin (handle=\" + handleId + \"):\");\n\t\t\tJanus.debug(request);\n\t\t\tif(websockets) {\n\t\t\t\trequest[\"session_id\"] = sessionId;\n\t\t\t\trequest[\"handle_id\"] = handleId;\n\t\t\t\ttransactions.set(transaction, function(json) {\n\t\t\t\t\tJanus.debug(\"Message sent!\");\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] === \"success\") {\n\t\t\t\t\t\t// We got a success, must have been a synchronous transaction\n\t\t\t\t\t\tlet plugindata = json[\"plugindata\"];\n\t\t\t\t\t\tif(!plugindata) {\n\t\t\t\t\t\t\tJanus.warn(\"Request succeeded, but missing plugindata...\");\n\t\t\t\t\t\t\tcallbacks.success();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJanus.log(\"Synchronous transaction successful (\" + plugindata[\"plugin\"] + \")\");\n\t\t\t\t\t\tlet data = plugindata[\"data\"];\n\t\t\t\t\t\tJanus.debug(data);\n\t\t\t\t\t\tcallbacks.success(data);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if(json[\"janus\"] !== \"ack\") {\n\t\t\t\t\t\t// Not a success and not an ack, must be an error\n\t\t\t\t\t\tif(json[\"error\"]) {\n\t\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t\t\tcallbacks.error(json[\"error\"].code + \" \" + json[\"error\"].reason);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJanus.error(\"Unknown error\");\t// FIXME\n\t\t\t\t\t\t\tcallbacks.error(\"Unknown error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// If we got here, the plugin decided to handle the request asynchronously\n\t\t\t\t\tcallbacks.success();\n\t\t\t\t});\n\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.httpAPICall(server + \"/\" + sessionId + \"/\" + handleId, {\n\t\t\t\tverb: 'POST',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tbody: request,\n\t\t\t\tsuccess: function(json) {\n\t\t\t\t\tJanus.debug(\"Message sent!\");\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] === \"success\") {\n\t\t\t\t\t\t// We got a success, must have been a synchronous transaction\n\t\t\t\t\t\tlet plugindata = json[\"plugindata\"];\n\t\t\t\t\t\tif(!plugindata) {\n\t\t\t\t\t\t\tJanus.warn(\"Request succeeded, but missing plugindata...\");\n\t\t\t\t\t\t\tcallbacks.success();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJanus.log(\"Synchronous transaction successful (\" + plugindata[\"plugin\"] + \")\");\n\t\t\t\t\t\tlet data = plugindata[\"data\"];\n\t\t\t\t\t\tJanus.debug(data);\n\t\t\t\t\t\tcallbacks.success(data);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if(json[\"janus\"] !== \"ack\") {\n\t\t\t\t\t\t// Not a success and not an ack, must be an error\n\t\t\t\t\t\tif(json[\"error\"]) {\n\t\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t\t\tcallbacks.error(json[\"error\"].code + \" \" + json[\"error\"].reason);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJanus.error(\"Unknown error\");\t// FIXME\n\t\t\t\t\t\t\tcallbacks.error(\"Unknown error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// If we got here, the plugin decided to handle the request asynchronously\n\t\t\t\t\tcallbacks.success();\n\t\t\t\t},\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\t// FIXME\n\t\t\t\t\tcallbacks.error(textStatus + \": \" + errorThrown);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Private method to send a trickle candidate\n\t\tfunction sendTrickleCandidate(handleId, candidate) {\n\t\t\tif(!connected) {\n\t\t\t\tJanus.warn(\"Is the server down? (connected=false)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet request = { \"janus\": \"trickle\", \"candidate\": candidate, \"transaction\": Janus.randomString(12) };\n\t\t\tif(pluginHandle.token)\n\t\t\t\trequest[\"token\"] = pluginHandle.token;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tJanus.vdebug(\"Sending trickle candidate (handle=\" + handleId + \"):\");\n\t\t\tJanus.vdebug(request);\n\t\t\tif(websockets) {\n\t\t\t\trequest[\"session_id\"] = sessionId;\n\t\t\t\trequest[\"handle_id\"] = handleId;\n\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.httpAPICall(server + \"/\" + sessionId + \"/\" + handleId, {\n\t\t\t\tverb: 'POST',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tbody: request,\n\t\t\t\tsuccess: function(json) {\n\t\t\t\t\tJanus.vdebug(\"Candidate sent!\");\n\t\t\t\t\tJanus.vdebug(json);\n\t\t\t\t\tif(json[\"janus\"] !== \"ack\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\t// FIXME\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Private method to create a data channel\n\t\tfunction createDataChannel(handleId, dclabel, dcprotocol, incoming, pendingData) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc) {\n\t\t\t\tJanus.warn(\"Invalid PeerConnection\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet onDataChannelMessage = function(event) {\n\t\t\t\tJanus.log('Received message on data channel:', event);\n\t\t\t\tlet label = event.target.label;\n\t\t\t\tpluginHandle.ondata(event.data, label);\n\t\t\t};\n\t\t\tlet onDataChannelStateChange = function(event) {\n\t\t\t\tJanus.log('Received state change on data channel:', event);\n\t\t\t\tlet label = event.target.label;\n\t\t\t\tlet protocol = event.target.protocol;\n\t\t\t\tlet dcState = config.dataChannel[label] ? config.dataChannel[label].readyState : \"null\";\n\t\t\t\tJanus.log('State change on <' + label + '> data channel: ' + dcState);\n\t\t\t\tif(dcState === 'open') {\n\t\t\t\t\t// Any pending messages to send?\n\t\t\t\t\tif(config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) {\n\t\t\t\t\t\tJanus.log(\"Sending pending messages on <\" + label + \">:\", config.dataChannel[label].pending.length);\n\t\t\t\t\t\tfor(let data of config.dataChannel[label].pending) {\n\t\t\t\t\t\t\tJanus.log(\"Sending data on data channel <\" + label + \">\");\n\t\t\t\t\t\t\tJanus.debug(data);\n\t\t\t\t\t\t\tconfig.dataChannel[label].send(data);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconfig.dataChannel[label].pending = [];\n\t\t\t\t\t}\n\t\t\t\t\t// Notify the open data channel\n\t\t\t\t\tpluginHandle.ondataopen(label, protocol);\n\t\t\t\t}\n\t\t\t};\n\t\t\tlet onDataChannelError = function(error) {\n\t\t\t\tJanus.error('Got error on data channel:', error);\n\t\t\t\t// TODO\n\t\t\t};\n\t\t\tif(!incoming) {\n\t\t\t\t// FIXME Add options (ordered, maxRetransmits, etc.)\n\t\t\t\tlet dcoptions = config.dataChannelOptions;\n\t\t\t\tif(dcprotocol)\n\t\t\t\t\tdcoptions.protocol = dcprotocol;\n\t\t\t\tconfig.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, dcoptions);\n\t\t\t} else {\n\t\t\t\t// The channel was created by Janus\n\t\t\t\tconfig.dataChannel[dclabel] = incoming;\n\t\t\t}\n\t\t\tconfig.dataChannel[dclabel].onmessage = onDataChannelMessage;\n\t\t\tconfig.dataChannel[dclabel].onopen = onDataChannelStateChange;\n\t\t\tconfig.dataChannel[dclabel].onclose = onDataChannelStateChange;\n\t\t\tconfig.dataChannel[dclabel].onerror = onDataChannelError;\n\t\t\tconfig.dataChannel[dclabel].pending = [];\n\t\t\tif(pendingData)\n\t\t\t\tconfig.dataChannel[dclabel].pending.push(pendingData);\n\t\t}\n\n\t\t// Private method to send a data channel message\n\t\tfunction sendData(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tcallbacks.error(\"Invalid handle\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tlet data = callbacks.text || callbacks.data;\n\t\t\tif(!data) {\n\t\t\t\tJanus.warn(\"Invalid data\");\n\t\t\t\tcallbacks.error(\"Invalid data\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel;\n\t\t\tif(!config.dataChannel[label]) {\n\t\t\t\t// Create new data channel and wait for it to open\n\t\t\t\tcreateDataChannel(handleId, label, callbacks.protocol, false, data, callbacks.protocol);\n\t\t\t\tcallbacks.success();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(config.dataChannel[label].readyState !== \"open\") {\n\t\t\t\tconfig.dataChannel[label].pending.push(data);\n\t\t\t\tcallbacks.success();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.log(\"Sending data on data channel <\" + label + \">\");\n\t\t\tJanus.debug(data);\n\t\t\tconfig.dataChannel[label].send(data);\n\t\t\tcallbacks.success();\n\t\t}\n\n\t\t// Private method to send a DTMF tone\n\t\tfunction sendDtmf(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tcallbacks.error(\"Invalid handle\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.dtmfSender) {\n\t\t\t\t// Create the DTMF sender the proper way, if possible\n\t\t\t\tif(config.pc) {\n\t\t\t\t\tlet senders = config.pc.getSenders();\n\t\t\t\t\tlet audioSender = senders.find(function(sender) {\n\t\t\t\t\t\treturn sender.track && sender.track.kind === 'audio';\n\t\t\t\t\t});\n\t\t\t\t\tif(!audioSender) {\n\t\t\t\t\t\tJanus.warn(\"Invalid DTMF configuration (no audio track)\");\n\t\t\t\t\t\tcallbacks.error(\"Invalid DTMF configuration (no audio track)\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconfig.dtmfSender = audioSender.dtmf;\n\t\t\t\t\tif(config.dtmfSender) {\n\t\t\t\t\t\tJanus.log(\"Created DTMF Sender\");\n\t\t\t\t\t\tconfig.dtmfSender.ontonechange = function(tone) { Janus.debug(\"Sent DTMF tone: \" + tone.tone); };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!config.dtmfSender) {\n\t\t\t\t\tJanus.warn(\"Invalid DTMF configuration\");\n\t\t\t\t\tcallbacks.error(\"Invalid DTMF configuration\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlet dtmf = callbacks.dtmf;\n\t\t\tif(!dtmf) {\n\t\t\t\tJanus.warn(\"Invalid DTMF parameters\");\n\t\t\t\tcallbacks.error(\"Invalid DTMF parameters\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet tones = dtmf.tones;\n\t\t\tif(!tones) {\n\t\t\t\tJanus.warn(\"Invalid DTMF string\");\n\t\t\t\tcallbacks.error(\"Invalid DTMF string\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet duration = (typeof dtmf.duration === 'number') ? dtmf.duration : 500; // We choose 500ms as the default duration for a tone\n\t\t\tlet gap = (typeof dtmf.gap === 'number') ? dtmf.gap : 50; // We choose 50ms as the default gap between tones\n\t\t\tJanus.debug(\"Sending DTMF string \" + tones + \" (duration \" + duration + \"ms, gap \" + gap + \"ms)\");\n\t\t\tconfig.dtmfSender.insertDTMF(tones, duration, gap);\n\t\t\tcallbacks.success();\n\t\t}\n\n\t\t// Private method to destroy a plugin handle\n\t\tfunction destroyHandle(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tlet noRequest = (callbacks.noRequest === true);\n\t\t\tJanus.log(\"Destroying handle \" + handleId + \" (only-locally=\" + noRequest + \")\");\n\t\t\tcleanupWebrtc(handleId);\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || pluginHandle.detached) {\n\t\t\t\t// Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here\n\t\t\t\tpluginHandles.delete(handleId);\n\t\t\t\tcallbacks.success();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpluginHandle.detached = true;\n\t\t\tif(noRequest) {\n\t\t\t\t// We're only removing the handle locally\n\t\t\t\tpluginHandles.delete(handleId);\n\t\t\t\tcallbacks.success();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(!connected) {\n\t\t\t\tJanus.warn(\"Is the server down? (connected=false)\");\n\t\t\t\tcallbacks.error(\"Is the server down? (connected=false)\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet request = { \"janus\": \"detach\", \"transaction\": Janus.randomString(12) };\n\t\t\tif(pluginHandle.token)\n\t\t\t\trequest[\"token\"] = pluginHandle.token;\n\t\t\tif(apisecret)\n\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\tif(websockets) {\n\t\t\t\trequest[\"session_id\"] = sessionId;\n\t\t\t\trequest[\"handle_id\"] = handleId;\n\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\tpluginHandles.delete(handleId);\n\t\t\t\tcallbacks.success();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tJanus.httpAPICall(server + \"/\" + sessionId + \"/\" + handleId, {\n\t\t\t\tverb: 'POST',\n\t\t\t\twithCredentials: withCredentials,\n\t\t\t\tbody: request,\n\t\t\t\tsuccess: function(json) {\n\t\t\t\t\tJanus.log(\"Destroyed handle:\");\n\t\t\t\t\tJanus.debug(json);\n\t\t\t\t\tif(json[\"janus\"] !== \"success\") {\n\t\t\t\t\t\tJanus.error(\"Ooops: \" + json[\"error\"].code + \" \" + json[\"error\"].reason);\t// FIXME\n\t\t\t\t\t}\n\t\t\t\t\tpluginHandles.delete(handleId);\n\t\t\t\t\tcallbacks.success();\n\t\t\t\t},\n\t\t\t\terror: function(textStatus, errorThrown) {\n\t\t\t\t\tJanus.error(textStatus + \":\", errorThrown);\t// FIXME\n\t\t\t\t\t// We cleanup anyway\n\t\t\t\t\tpluginHandles.delete(handleId);\n\t\t\t\t\tcallbacks.success();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// WebRTC stuff\n\t\t// Helper function to create a new PeerConnection, if we need one\n\t\tfunction createPeerconnectionIfNeeded(handleId, callbacks) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tthrow \"Invalid handle\";\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(config.pc) {\n\t\t\t\t// Nothing to do, we have a PeerConnection already\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet pc_config = {\n\t\t\t\ticeServers: iceServers,\n\t\t\t\ticeTransportPolicy: iceTransportPolicy,\n\t\t\t\tbundlePolicy: bundlePolicy\n\t\t\t};\n\t\t\tpc_config.sdpSemantics = 'unified-plan';\n\t\t\t// Check if a sender or receiver transform has been provided\n\t\t\tlet insertableStreams = false;\n\t\t\tif(callbacks.tracks) {\n\t\t\t\tfor(let track of callbacks.tracks) {\n\t\t\t\t\tif(track.transforms && (track.transforms.sender || track.transforms.receiver)) {\n\t\t\t\t\t\tinsertableStreams = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(callbacks.externalEncryption) {\n\t\t\t\tinsertableStreams = true;\n\t\t\t\tconfig.externalEncryption = true;\n\t\t\t}\n\t\t\tif(RTCRtpSender && (RTCRtpSender.prototype.createEncodedStreams ||\n\t\t\t\t(RTCRtpSender.prototype.createEncodedAudioStreams &&\n\t\t\t\tRTCRtpSender.prototype.createEncodedVideoStreams)) && insertableStreams) {\n\t\t\t\tconfig.insertableStreams = true;\n\t\t\t\tpc_config.forceEncodedAudioInsertableStreams = true;\n\t\t\t\tpc_config.forceEncodedVideoInsertableStreams = true;\n\t\t\t\tpc_config.encodedInsertableStreams = true;\n\t\t\t}\n\t\t\tJanus.log('Creating PeerConnection');\n\t\t\tconfig.pc = new RTCPeerConnection(pc_config);\n\t\t\tJanus.debug(config.pc);\n\t\t\tif(config.pc.getStats) {\t// FIXME\n\t\t\t\tconfig.volume = {};\n\t\t\t\tconfig.bitrate.value = '0 kbits/sec';\n\t\t\t}\n\t\t\tJanus.log('Preparing local SDP and gathering candidates (trickle=' + config.trickle + ')');\n\t\t\tconfig.pc.onconnectionstatechange = function() {\n\t\t\t\tif(config.pc)\n\t\t\t\t\tpluginHandle.connectionState(config.pc.connectionState);\n\t\t\t};\n\t\t\tconfig.pc.oniceconnectionstatechange = function() {\n\t\t\t\tif(config.pc)\n\t\t\t\t\tpluginHandle.iceState(config.pc.iceConnectionState);\n\t\t\t};\n\t\t\tconfig.pc.onicecandidate = function(event) {\n\t\t\t\tif(!event.candidate || (event.candidate.candidate && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {\n\t\t\t\t\tJanus.log('End of candidates.');\n\t\t\t\t\tconfig.iceDone = true;\n\t\t\t\t\tif(config.trickle === true) {\n\t\t\t\t\t\t// Notify end of candidates\n\t\t\t\t\t\tsendTrickleCandidate(handleId, { completed : true });\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// No trickle, time to send the complete SDP (including all candidates)\n\t\t\t\t\t\tsendSDP(handleId, callbacks);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// JSON.stringify doesn't work on some WebRTC objects anymore\n\t\t\t\t\t// See https://code.google.com/p/chromium/issues/detail?id=467366\n\t\t\t\t\tlet candidate = {\n\t\t\t\t\t\tcandidate: event.candidate.candidate,\n\t\t\t\t\t\tsdpMid: event.candidate.sdpMid,\n\t\t\t\t\t\tsdpMLineIndex: event.candidate.sdpMLineIndex\n\t\t\t\t\t};\n\t\t\t\t\tif(config.trickle === true) {\n\t\t\t\t\t\t// Send candidate\n\t\t\t\t\t\tsendTrickleCandidate(handleId, candidate);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tconfig.pc.ontrack = function(event) {\n\t\t\t\tJanus.log('Handling Remote Track', event);\n\t\t\t\tif(!event.streams)\n\t\t\t\t\treturn;\n\t\t\t\tif(!event.track)\n\t\t\t\t\treturn;\n\t\t\t\t// Notify about the new track event\n\t\t\t\tlet mid = event.transceiver ? event.transceiver.mid : event.track.id;\n\t\t\t\ttry {\n\t\t\t\t\tif(event.transceiver && event.transceiver.mid && event.track.id) {\n\t\t\t\t\t\t// Keep track of the mapping between track ID and mid, since\n\t\t\t\t\t\t// when a track is removed the transceiver may be gone already\n\t\t\t\t\t\tif(!pluginHandle.mids)\n\t\t\t\t\t\t\tpluginHandle.mids = {};\n\t\t\t\t\t\tpluginHandle.mids[event.track.id] = event.transceiver.mid;\n\t\t\t\t\t}\n\t\t\t\t\tpluginHandle.onremotetrack(event.track, mid, true, { reason: 'created' });\n\t\t\t\t} catch(e) {\n\t\t\t\t\tJanus.error(\"Error calling onremotetrack\", e);\n\t\t\t\t}\n\t\t\t\tif(event.track.onended)\n\t\t\t\t\treturn;\n\t\t\t\tlet trackMutedTimeoutId = null;\n\t\t\t\tJanus.log('Adding onended callback to track:', event.track);\n\t\t\t\tevent.track.onended = function(ev) {\n\t\t\t\t\tJanus.log('Remote track removed:', ev);\n\t\t\t\t\tclearTimeout(trackMutedTimeoutId);\n\t\t\t\t\t// Notify the application\n\t\t\t\t\tlet transceivers = config.pc ? config.pc.getTransceivers() : null;\n\t\t\t\t\tlet transceiver = transceivers ? transceivers.find(\n\t\t\t\t\t\tt => t.receiver.track === ev.target) : null;\n\t\t\t\t\tlet mid = transceiver ? transceiver.mid : ev.target.id;\n\t\t\t\t\tif(mid === ev.target.id && pluginHandle.mids && pluginHandle.mids[event.track.id])\n\t\t\t\t\t\tmid = pluginHandle.mids[event.track.id];\n\t\t\t\t\ttry {\n\t\t\t\t\t\tpluginHandle.onremotetrack(ev.target, mid, false, { reason: 'ended' });\n\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\tJanus.error(\"Error calling onremotetrack on removal\", e);\n\t\t\t\t\t}\n\t\t\t\t\tdelete pluginHandle.mids[event.track.id];\n\t\t\t\t};\n\t\t\t\tevent.track.onmute = function(ev) {\n\t\t\t\t\tJanus.log('Remote track muted:', ev);\n\t\t\t\t\tif(!trackMutedTimeoutId) {\n\t\t\t\t\t\ttrackMutedTimeoutId = setTimeout(function() {\n\t\t\t\t\t\t\tJanus.log('Removing remote track');\n\t\t\t\t\t\t\t// Notify the application the track is gone\n\t\t\t\t\t\t\tlet transceivers = config.pc ? config.pc.getTransceivers() : null;\n\t\t\t\t\t\t\tlet transceiver = transceivers ? transceivers.find(\n\t\t\t\t\t\t\t\tt => t.receiver.track === ev.target) : null;\n\t\t\t\t\t\t\tlet mid = transceiver ? transceiver.mid : ev.target.id;\n\t\t\t\t\t\t\tif(mid === ev.target.id && pluginHandle.mids && pluginHandle.mids[event.track.id])\n\t\t\t\t\t\t\t\tmid = pluginHandle.mids[event.track.id];\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tpluginHandle.onremotetrack(ev.target, mid, false, { reason: 'mute' } );\n\t\t\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\t\t\tJanus.error(\"Error calling onremotetrack on mute\", e);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttrackMutedTimeoutId = null;\n\t\t\t\t\t\t\t// Chrome seems to raise mute events only at multiples of 834ms;\n\t\t\t\t\t\t\t// we set the timeout to three times this value (rounded to 840ms)\n\t\t\t\t\t\t}, 3 * 840);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tevent.track.onunmute = function(ev) {\n\t\t\t\t\tJanus.log('Remote track flowing again:', ev);\n\t\t\t\t\tif(trackMutedTimeoutId != null) {\n\t\t\t\t\t\tclearTimeout(trackMutedTimeoutId);\n\t\t\t\t\t\ttrackMutedTimeoutId = null;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Notify the application the track is back\n\t\t\t\t\t\t\tlet transceivers = config.pc ? config.pc.getTransceivers() : null;\n\t\t\t\t\t\t\tlet transceiver = transceivers ? transceivers.find(\n\t\t\t\t\t\t\t\tt => t.receiver.track === ev.target) : null;\n\t\t\t\t\t\t\tlet mid = transceiver ? transceiver.mid : ev.target.id;\n\t\t\t\t\t\t\tpluginHandle.onremotetrack(ev.target, mid, true, { reason: 'unmute' });\n\t\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\t\tJanus.error(\"Error calling onremotetrack on unmute\", e);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t};\n\t\t}\n\n\t\t// Helper function used when creating either an offer or answer: it\n\t\t// prepares what needs to be prepared, including creating a new\n\t\t// PeerConnection (if needed) and updating the tracks configuration,\n\t\t// before invoking the function to actually generate the offer/answer\n\t\tasync function prepareWebrtc(handleId, offer, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : webrtcError;\n\t\t\tlet jsep = callbacks.jsep;\n\t\t\tif(offer && jsep) {\n\t\t\t\tJanus.error(\"Provided a JSEP to a createOffer\");\n\t\t\t\tcallbacks.error(\"Provided a JSEP to a createOffer\");\n\t\t\t\treturn;\n\t\t\t} else if(!offer && (!jsep || !jsep.type || !jsep.sdp)) {\n\t\t\t\tJanus.error(\"A valid JSEP is required for createAnswer\");\n\t\t\t\tcallbacks.error(\"A valid JSEP is required for createAnswer\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// If the deprecated media was provided instead of tracks, translate it\n\t\t\tif(callbacks.media && !callbacks.tracks) {\n\t\t\t\tcallbacks.tracks = Janus.mediaToTracks(callbacks.media);\n\t\t\t\tif(callbacks.simulcast === true || callbacks.simulcast2 === true || callbacks.svc) {\n\t\t\t\t\t// Find the video track and add simulcast/SVC info there\n\t\t\t\t\tfor(let track of callbacks.tracks) {\n\t\t\t\t\t\tif(track.type === 'video') {\n\t\t\t\t\t\t\tif(callbacks.simulcast === true || callbacks.simulcast2 === true)\n\t\t\t\t\t\t\t\ttrack.simulcast = true;\n\t\t\t\t\t\t\telse if(callbacks.svc)\n\t\t\t\t\t\t\t\ttrack.svc = callbacks.svc;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tJanus.warn('Deprecated media object passed, use tracks instead. Automatically translated to:', callbacks.tracks);\n\t\t\t}\n\t\t\t// Check that callbacks.array is a valid array\n\t\t\tif(callbacks.tracks && !Array.isArray(callbacks.tracks)) {\n\t\t\t\tJanus.error(\"Tracks must be an array\");\n\t\t\t\tcallbacks.error(\"Tracks must be an array\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Get the plugin handle\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tcallbacks.error(\"Invalid handle\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tconfig.trickle = isTrickleEnabled(callbacks.trickle);\n\t\t\ttry {\n\t\t\t\t// Create a PeerConnection, if needed\n\t\t\t\tcreatePeerconnectionIfNeeded(handleId, callbacks);\n\t\t\t\tif(offer) {\n\t\t\t\t\t// Capture devices and setup tracks, if needed\n\t\t\t\t\tawait captureDevices(handleId, callbacks);\n\t\t\t\t}\n\t\t\t\t// Create offer or answer now (depending on the context)\n\t\t\t\tif(!jsep) {\n\t\t\t\t\tlet offer = await createOffer(handleId, callbacks);\n\t\t\t\t\tcallbacks.success(offer);\n\t\t\t\t} else {\n\t\t\t\t\tawait config.pc.setRemoteDescription(jsep);\n\t\t\t\t\tJanus.log(\"Remote description accepted!\");\n\t\t\t\t\tconfig.remoteSdp = jsep.sdp;\n\t\t\t\t\t// Any trickle candidate we cached?\n\t\t\t\t\tif(config.candidates && config.candidates.length > 0) {\n\t\t\t\t\t\tfor(let i=0; i<config.candidates.length; i++) {\n\t\t\t\t\t\t\tlet candidate = config.candidates[i];\n\t\t\t\t\t\t\tJanus.debug(\"Adding remote candidate:\", candidate);\n\t\t\t\t\t\t\tif(!candidate || candidate.completed === true) {\n\t\t\t\t\t\t\t\t// end-of-candidates\n\t\t\t\t\t\t\t\tconfig.pc.addIceCandidate(Janus.endOfCandidates);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// New candidate\n\t\t\t\t\t\t\t\tconfig.pc.addIceCandidate(candidate);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconfig.candidates = [];\n\t\t\t\t\t}\n\t\t\t\t\t// Capture devices and setup tracks, if needed\n\t\t\t\t\tawait captureDevices(handleId, callbacks);\n\t\t\t\t\t// Create the answer now\n\t\t\t\t\tlet answer = await createAnswer(handleId, callbacks);\n\t\t\t\t\tcallbacks.success(answer);\n\t\t\t\t}\n\t\t\t} catch(err) {\n\t\t\t\tJanus.error(err);\n\t\t\t\tcallbacks.error(err);\n\t\t\t}\n\t\t}\n\n\t\tfunction prepareWebrtcPeer(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : webrtcError;\n\t\t\tcallbacks.customizeSdp = (typeof callbacks.customizeSdp == \"function\") ? callbacks.customizeSdp : Janus.noop;\n\t\t\tlet jsep = callbacks.jsep;\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tcallbacks.error(\"Invalid handle\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(jsep) {\n\t\t\t\tif(!config.pc) {\n\t\t\t\t\tJanus.warn(\"Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep\");\n\t\t\t\t\tcallbacks.error(\"No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcallbacks.customizeSdp(jsep);\n\t\t\t\tconfig.pc.setRemoteDescription(jsep)\n\t\t\t\t\t.then(function() {\n\t\t\t\t\t\tJanus.log(\"Remote description accepted!\");\n\t\t\t\t\t\tconfig.remoteSdp = jsep.sdp;\n\t\t\t\t\t\t// Any trickle candidate we cached?\n\t\t\t\t\t\tif(config.candidates && config.candidates.length > 0) {\n\t\t\t\t\t\t\tfor(let i=0; i<config.candidates.length; i++) {\n\t\t\t\t\t\t\t\tlet candidate = config.candidates[i];\n\t\t\t\t\t\t\t\tJanus.debug(\"Adding remote candidate:\", candidate);\n\t\t\t\t\t\t\t\tif(!candidate || candidate.completed === true) {\n\t\t\t\t\t\t\t\t\t// end-of-candidates\n\t\t\t\t\t\t\t\t\tconfig.pc.addIceCandidate(Janus.endOfCandidates);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// New candidate\n\t\t\t\t\t\t\t\t\tconfig.pc.addIceCandidate(candidate);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconfig.candidates = [];\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Done\n\t\t\t\t\t\tcallbacks.success();\n\t\t\t\t\t}, callbacks.error);\n\t\t\t} else {\n\t\t\t\tcallbacks.error(\"Invalid JSEP\");\n\t\t\t}\n\t\t}\n\n\t\tasync function createOffer(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.customizeSdp = (typeof callbacks.customizeSdp == \"function\") ? callbacks.customizeSdp : Janus.noop;\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tthrow \"Invalid handle\";\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tJanus.log(\"Creating offer (iceDone=\" + config.iceDone + \")\");\n\t\t\t// https://code.google.com/p/webrtc/issues/detail?id=3508\n\t\t\tlet mediaConstraints = {};\n\t\t\tlet iceRestart = (callbacks.iceRestart === true);\n\t\t\t// If we need an ICE restart, set the related constraint\n\t\t\tif(iceRestart)\n\t\t\t\tmediaConstraints.iceRestart = true;\n\t\t\tJanus.debug(mediaConstraints);\n\t\t\tlet offer = await config.pc.createOffer(mediaConstraints);\n\t\t\tJanus.debug(offer);\n\t\t\t// JSON.stringify doesn't work on some WebRTC objects anymore\n\t\t\t// See https://code.google.com/p/chromium/issues/detail?id=467366\n\t\t\tlet jsep = {\n\t\t\t\ttype: 'offer',\n\t\t\t\tsdp: offer.sdp\n\t\t\t};\n\t\t\tcallbacks.customizeSdp(jsep);\n\t\t\toffer.sdp = jsep.sdp;\n\t\t\tJanus.log(\"Setting local description\");\n\t\t\tconfig.mySdp = {\n\t\t\t\ttype: 'offer',\n\t\t\t\tsdp: offer.sdp\n\t\t\t};\n\t\t\tawait config.pc.setLocalDescription(offer);\n\t\t\tconfig.mediaConstraints = mediaConstraints;\n\t\t\tif(!config.iceDone && !config.trickle) {\n\t\t\t\t// FIXME Don't do anything until we have all candidates\n\t\t\t\tJanus.log(\"Waiting for all candidates...\");\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t// If transforms are present, notify Janus that the media is end-to-end encrypted\n\t\t\tif(config.insertableStreams || config.externalEncryption)\n\t\t\t\toffer.e2ee = true;\n\t\t\treturn offer;\n\t\t}\n\n\t\tasync function createAnswer(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.customizeSdp = (typeof callbacks.customizeSdp == \"function\") ? callbacks.customizeSdp : Janus.noop;\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tthrow \"Invalid handle\";\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tJanus.log(\"Creating answer (iceDone=\" + config.iceDone + \")\");\n\t\t\tlet answer = await config.pc.createAnswer();\n\t\t\tJanus.debug(answer);\n\t\t\t// JSON.stringify doesn't work on some WebRTC objects anymore\n\t\t\t// See https://code.google.com/p/chromium/issues/detail?id=467366\n\t\t\tlet jsep = {\n\t\t\t\ttype: 'answer',\n\t\t\t\tsdp: answer.sdp\n\t\t\t};\n\t\t\tcallbacks.customizeSdp(jsep);\n\t\t\tanswer.sdp = jsep.sdp;\n\t\t\tJanus.log(\"Setting local description\");\n\t\t\tconfig.mySdp = {\n\t\t\t\ttype: 'answer',\n\t\t\t\tsdp: answer.sdp\n\t\t\t};\n\t\t\tawait config.pc.setLocalDescription(answer);\n\t\t\tif(!config.iceDone && !config.trickle) {\n\t\t\t\t// FIXME Don't do anything until we have all candidates\n\t\t\t\tJanus.log(\"Waiting for all candidates...\");\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t// If transforms are present, notify Janus that the media is end-to-end encrypted\n\t\t\tif(config.insertableStreams || config.externalEncryption)\n\t\t\t\tanswer.e2ee = true;\n\t\t\treturn answer;\n\t\t}\n\n\t\tfunction sendSDP(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == \"function\") ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == \"function\") ? callbacks.error : Janus.noop;\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle, not sending anything\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tJanus.log(\"Sending offer/answer SDP...\");\n\t\t\tif(!config.mySdp) {\n\t\t\t\tJanus.warn(\"Local SDP instance is invalid, not sending anything...\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconfig.mySdp = {\n\t\t\t\ttype: config.pc.localDescription.type,\n\t\t\t\tsdp: config.pc.localDescription.sdp\n\t\t\t};\n\t\t\tif(config.trickle === false)\n\t\t\t\tconfig.mySdp[\"trickle\"] = false;\n\t\t\tJanus.debug(callbacks);\n\t\t\tconfig.sdpSent = true;\n\t\t\tcallbacks.success(config.mySdp);\n\t\t}\n\n\t\tasync function replaceTracks(handleId, callbacks) {\n\t\t\tcallbacks = callbacks || {};\n\t\t\tcallbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop;\n\t\t\tcallbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop;\n\t\t\t// Check that callbacks.array is a valid array\n\t\t\tif(callbacks.tracks && !Array.isArray(callbacks.tracks)) {\n\t\t\t\tJanus.error('Tracks must be an array');\n\t\t\t\tcallbacks.error('Tracks must be an array');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Add the replace:true if it's missing\n\t\t\tfor(let track of callbacks.tracks) {\n\t\t\t\tif(track.add || (!track.replace && !track.remove))\n\t\t\t\t\ttrack.replace = true;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait captureDevices(handleId, callbacks);\n\t\t\t\tcallbacks.success();\n\t\t\t} catch(err) {\n\t\t\t\tJanus.error(err);\n\t\t\t\tcallbacks.error(err);\n\t\t\t}\n\t\t}\n\n\t\tasync function captureDevices(handleId, callbacks) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn('Invalid handle, not sending anything');\n\t\t\t\tthrow 'Invalid handle';\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc) {\n\t\t\t\tJanus.warn('Invalid PeerConnection');\n\t\t\t\tthrow 'Invalid PeerConnection';\n\t\t\t}\n\t\t\tlet tracks = callbacks.tracks;\n\t\t\tif(!tracks || !Array.isArray(tracks) || tracks.length === 0) {\n\t\t\t\t// Nothing to do\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet openedConsentDialog = false;\n\t\t\t// Check if we can/should group getUserMedia calls\n\t\t\tlet groups = {};\n\t\t\tfor(let track of tracks) {\n\t\t\t\tdelete track.gumGroup;\n\t\t\t\tif(!track.type || !['audio', 'video'].includes(track.type))\n\t\t\t\t\tcontinue;\n\t\t\t\tif(!track.capture || track.capture instanceof MediaStreamTrack)\n\t\t\t\t\tcontinue;\n\t\t\t\tlet group = track.group ? track.group : 'default';\n\t\t\t\tif(!groups[group])\n\t\t\t\t\tgroups[group] = {};\n\t\t\t\tif(groups[group][track.type])\n\t\t\t\t\tcontinue;\n\t\t\t\ttrack.gumGroup = group;\n\t\t\t\tgroups[group][track.type] = track;\n\t\t\t}\n\t\t\tlet keys = Object.keys(groups);\n\t\t\tfor(let key of keys) {\n\t\t\t\tlet group = groups[key];\n\t\t\t\tif(!group.audio || !group.video) {\n\t\t\t\t\tif(group.audio)\n\t\t\t\t\t\tdelete group.audio.gumGroup;\n\t\t\t\t\tif(group.video)\n\t\t\t\t\t\tdelete group.video.gumGroup;\n\t\t\t\t\tdelete groups[key];\n\t\t\t\t}\n\t\t\t}\n\t\t\tlet answer = (callbacks.jsep ? true : false);\n\t\t\tfor(let track of tracks) {\n\t\t\t\tif(!track.type) {\n\t\t\t\t\tJanus.warn('Missing track type:', track);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(track.type === 'data') {\n\t\t\t\t\t// Easy enough: create a datachannel if we don't have one already\n\t\t\t\t\tif(config.pc.ondatachannel) {\n\t\t\t\t\t\tJanus.warn('Data channel exists already, not creating another one');\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tJanus.log('Creating default data channel');\n\t\t\t\t\tcreateDataChannel(handleId, Janus.dataChanDefaultLabel, null, false);\n\t\t\t\t\tconfig.pc.ondatachannel = function(event) {\n\t\t\t\t\t\tJanus.log('Data channel created by Janus:', event);\n\t\t\t\t\t\tcreateDataChannel(handleId, event.channel.label, event.channel.protocol, event.channel);\n\t\t\t\t\t};\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif((typeof track.add === 'undefined' || track.add === null) &&\n\t\t\t\t\t\t(typeof track.remove === 'undefined' || track.remove === null) &&\n\t\t\t\t\t\t(typeof track.replace === 'undefined' || track.replace === null)) {\n\t\t\t\t\t// Let's default to 'add'\n\t\t\t\t\ttrack.add = true;\n\t\t\t\t}\n\t\t\t\tif((track.add && track.remove) || (track.add && track.remove && track.replace)) {\n\t\t\t\t\tJanus.warn('Conflicting actions for track, ignoring:', track);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(track.add && track.replace) {\n\t\t\t\t\tJanus.warn('Both add and replace provided, falling back to replace:', track);\n\t\t\t\t\tdelete track.add;\n\t\t\t\t} else if(track.remove && track.replace) {\n\t\t\t\t\tJanus.warn('Both remove and replace provided, falling back to remove:', track);\n\t\t\t\t\tdelete track.replace;\n\t\t\t\t}\n\t\t\t\tlet kind = track.type;\n\t\t\t\tif(track.type === 'screen')\n\t\t\t\t\tkind = 'video';\t// FIXME\n\t\t\t\tlet transceiver = null, sender = null;\n\t\t\t\tif(track.mid) {\n\t\t\t\t\t// Search by mid\n\t\t\t\t\ttransceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.mid === track.mid && t.receiver.track.kind === kind));\n\t\t\t\t} else if(!track.add) {\n\t\t\t\t\t// Find the first track of this type\n\t\t\t\t\ttransceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.receiver.track.kind === kind));\n\t\t\t\t}\n\t\t\t\tif(track.replace || track.remove) {\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"Couldn't find a transceiver for track:\", track);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(!transceiver.sender) {\n\t\t\t\t\t\tJanus.warn('No sender in the transceiver for track:', track);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tsender = transceiver.sender;\n\t\t\t\t}\n\t\t\t\tif(answer && !transceiver) {\n\t\t\t\t\ttransceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.receiver.track.kind === kind));\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"Couldn't find a transceiver for track:\", track);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Capture the new track, if we need to\n\t\t\t\tlet nt = null, trackId = null;\n\t\t\t\tif(track.remove || track.replace) {\n\t\t\t\t\tJanus.log('Removing track from PeerConnection', track);\n\t\t\t\t\ttrackId = sender.track ? sender.track.id : null;\n\t\t\t\t\tawait sender.replaceTrack(null);\n\t\t\t\t\t// Get rid of the old track\n\t\t\t\t\tif(trackId && config.myStream) {\n\t\t\t\t\t\tlet rt = null;\n\t\t\t\t\t\tif(kind === 'audio' && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {\n\t\t\t\t\t\t\tfor(let t of config.myStream.getAudioTracks()) {\n\t\t\t\t\t\t\t\tif(t.id === trackId) {\n\t\t\t\t\t\t\t\t\trt = t;\n\t\t\t\t\t\t\t\t\tJanus.log('Removing audio track:', rt);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if(kind === 'video' && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {\n\t\t\t\t\t\t\tfor(let t of config.myStream.getVideoTracks()) {\n\t\t\t\t\t\t\t\tif(t.id === trackId) {\n\t\t\t\t\t\t\t\t\trt = t;\n\t\t\t\t\t\t\t\t\tJanus.log('Removing video track:', rt);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rt) {\n\t\t\t\t\t\t\t// Remove the track and notify the application\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconfig.myStream.removeTrack(rt);\n\t\t\t\t\t\t\t\tpluginHandle.onlocaltrack(rt, false);\n\t\t\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\t\t\tJanus.error(\"Error calling onlocaltrack on removal for renegotiation\", e);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Close the old track (unless we've been asked not to)\n\t\t\t\t\t\t\tif(rt.dontStop !== true) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\trt.stop();\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(track.capture) {\n\t\t\t\t\tif(track.gumGroup && groups[track.gumGroup] && groups[track.gumGroup].stream) {\n\t\t\t\t\t\t// We did a getUserMedia before already\n\t\t\t\t\t\tlet stream = groups[track.gumGroup].stream;\n\t\t\t\t\t\tnt = (track.type === 'audio' ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]);\n\t\t\t\t\t\tdelete groups[track.gumGroup].stream;\n\t\t\t\t\t\tdelete groups[track.gumGroup];\n\t\t\t\t\t\tdelete track.gumGroup;\n\t\t\t\t\t} else if(track.capture instanceof MediaStreamTrack) {\n\t\t\t\t\t\t// An external track was provided, use that\n\t\t\t\t\t\tnt = track.capture;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif(!openedConsentDialog) {\n\t\t\t\t\t\t\topenedConsentDialog = true;\n\t\t\t\t\t\t\tpluginHandle.consentDialog(true);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet constraints = Janus.trackConstraints(track), stream = null;\n\t\t\t\t\t\tif(track.type === 'audio' || track.type === 'video') {\n\t\t\t\t\t\t\t// Use getUserMedia: check if we need to group audio and video together\n\t\t\t\t\t\t\tif(track.gumGroup) {\n\t\t\t\t\t\t\t\tlet otherType = (track.type === 'audio' ? 'video' : 'audio');\n\t\t\t\t\t\t\t\tif(groups[track.gumGroup] && groups[track.gumGroup][otherType]) {\n\t\t\t\t\t\t\t\t\tlet otherTrack = groups[track.gumGroup][otherType];\n\t\t\t\t\t\t\t\t\tlet otherConstraints = Janus.trackConstraints(otherTrack);\n\t\t\t\t\t\t\t\t\tconstraints[otherType] = otherConstraints[otherType];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tstream = await navigator.mediaDevices.getUserMedia(constraints);\n\t\t\t\t\t\t\tif(track.gumGroup && constraints.audio && constraints.video) {\n\t\t\t\t\t\t\t\t// We just performed a grouped getUserMedia, keep track of the\n\t\t\t\t\t\t\t\t// stream so that we can immediately assign the track later\n\t\t\t\t\t\t\t\tgroups[track.gumGroup].stream = stream;\n\t\t\t\t\t\t\t\tdelete track.gumGroup;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use getDisplayMedia\n\t\t\t\t\t\t\tstream = await navigator.mediaDevices.getDisplayMedia(constraints);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnt = (track.type === 'audio' ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]);\n\t\t\t\t\t}\n\t\t\t\t\tif(track.replace) {\n\t\t\t\t\t\t// Replace the track\n\t\t\t\t\t\tawait sender.replaceTrack(nt);\n\t\t\t\t\t\t// Update the transceiver direction\n\t\t\t\t\t\tlet newDirection = 'sendrecv';\n\t\t\t\t\t\tif(track.recv === false || transceiver.direction === 'inactive' || transceiver.direction === 'sendonly')\n\t\t\t\t\t\t\tnewDirection = 'sendonly';\n\t\t\t\t\t\tif(transceiver.setDirection)\n\t\t\t\t\t\t\ttransceiver.setDirection(newDirection);\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\ttransceiver.direction = newDirection;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// FIXME Add as a new track\n\t\t\t\t\t\tif(!config.myStream)\n\t\t\t\t\t\t\tconfig.myStream = new MediaStream();\n\t\t\t\t\t\tif(kind === 'audio' || (!track.simulcast && !track.svc)) {\n\t\t\t\t\t\t\tsender = config.pc.addTrack(nt, config.myStream);\n\t\t\t\t\t\t\ttransceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t\t\t.find(t => (t.sender === sender));\n\t\t\t\t\t\t} else if(track.simulcast) {\n\t\t\t\t\t\t\t// Standard RID\n\t\t\t\t\t\t\tJanus.log('Enabling rid-based simulcasting:', nt);\n\t\t\t\t\t\t\tlet maxBitrates = getMaxBitrates(track.simulcastMaxBitrates);\n\t\t\t\t\t\t\ttransceiver = config.pc.addTransceiver(nt, {\n\t\t\t\t\t\t\t\tdirection: 'sendrecv',\n\t\t\t\t\t\t\t\tstreams: [config.myStream],\n\t\t\t\t\t\t\t\tsendEncodings: track.sendEncodings || [\n\t\t\t\t\t\t\t\t\t{ rid: 'h', active: true, scalabilityMode: 'L1T2', maxBitrate: maxBitrates.high },\n\t\t\t\t\t\t\t\t\t{ rid: 'm', active: true, scalabilityMode: 'L1T2', maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 },\n\t\t\t\t\t\t\t\t\t{ rid: 'l', active: true, scalabilityMode: 'L1T2', maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 }\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJanus.log('Enabling SVC (' + track.svc + '):', nt);\n\t\t\t\t\t\t\ttransceiver = config.pc.addTransceiver(nt, {\n\t\t\t\t\t\t\t\tdirection: 'sendrecv',\n\t\t\t\t\t\t\t\tstreams: [config.myStream],\n\t\t\t\t\t\t\t\tsendEncodings: [\n\t\t\t\t\t\t\t\t\t{ scalabilityMode: track.svc }\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(!sender)\n\t\t\t\t\t\t\tsender = transceiver ? transceiver.sender : null;\n\t\t\t\t\t\t// Check if we need to override some settings\n\t\t\t\t\t\tif(track.codec) {\n\t\t\t\t\t\t\tif(Janus.webRTCAdapter.browserDetails.browser === 'firefox') {\n\t\t\t\t\t\t\t\tJanus.warn('setCodecPreferences not supported in Firefox, ignoring codec for track:', track);\n\t\t\t\t\t\t\t} else if(typeof track.codec !== 'string') {\n\t\t\t\t\t\t\t\tJanus.warn('Invalid codec value, ignoring for track:', track);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlet mimeType = kind + '/' + track.codec.toLowerCase();\n\t\t\t\t\t\t\t\tlet codecs = RTCRtpReceiver.getCapabilities(kind).codecs.filter(function(codec) {\n\t\t\t\t\t\t\t\t\treturn codec.mimeType.toLowerCase() === mimeType;\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tif(!codecs || codecs.length === 0) {\n\t\t\t\t\t\t\t\t\tJanus.warn('Codec not supported in this browser for this track, ignoring:', track);\n\t\t\t\t\t\t\t\t} else if(transceiver) {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\ttransceiver.setCodecPreferences(codecs);\n\t\t\t\t\t\t\t\t\t} catch(err) {\n\t\t\t\t\t\t\t\t\t\tJanus.warn('Failed enforcing codec for this ' + kind + ' track:', err);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(track.bitrate) {\n\t\t\t\t\t\t\t// Override maximum bitrate\n\t\t\t\t\t\t\tif(track.simulcast || track.svc) {\n\t\t\t\t\t\t\t\tJanus.warn('Ignoring bitrate for simulcast/SVC track, use sendEncodings for that');\n\t\t\t\t\t\t\t} else if(isNaN(track.bitrate) || track.bitrate < 0) {\n\t\t\t\t\t\t\t\tJanus.warn('Ignoring invalid bitrate for track:', track);\n\t\t\t\t\t\t\t} else if(sender) {\n\t\t\t\t\t\t\t\tlet params = sender.getParameters();\n\t\t\t\t\t\t\t\tif(!params || !params.encodings || params.encodings.length === 0) {\n\t\t\t\t\t\t\t\t\tJanus.warn('No encodings in the sender parameters, ignoring bitrate for track:', track);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tparams.encodings[0].maxBitrate = track.bitrate;\n\t\t\t\t\t\t\t\t\tawait sender.setParameters(params);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(kind === 'video' && track.framerate) {\n\t\t\t\t\t\t\t// Override maximum framerate\n\t\t\t\t\t\t\tif(track.simulcast || track.svc) {\n\t\t\t\t\t\t\t\tJanus.warn('Ignoring framerate for simulcast/SVC track, use sendEncodings for that');\n\t\t\t\t\t\t\t} else if(isNaN(track.framerate) || track.framerate < 0) {\n\t\t\t\t\t\t\t\tJanus.warn('Ignoring invalid framerate for track:', track);\n\t\t\t\t\t\t\t} else if(sender) {\n\t\t\t\t\t\t\t\tlet params = sender.getParameters();\n\t\t\t\t\t\t\t\tif(!params || !params.encodings || params.encodings.length === 0) {\n\t\t\t\t\t\t\t\t\tJanus.warn('No encodings in the sender parameters, ignoring framerate for track:', track);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tparams.encodings[0].maxFramerate = track.framerate;\n\t\t\t\t\t\t\t\t\tawait sender.setParameters(params);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Check if insertable streams are involved\n\t\t\t\t\t\tif(track.transforms) {\n\t\t\t\t\t\t\tif(sender && track.transforms.sender) {\n\t\t\t\t\t\t\t\t// There's a sender transform, set it on the transceiver sender\n\t\t\t\t\t\t\t\tlet senderStreams = null;\n\t\t\t\t\t\t\t\tif(RTCRtpSender.prototype.createEncodedStreams) {\n\t\t\t\t\t\t\t\t\tsenderStreams = sender.createEncodedStreams();\n\t\t\t\t\t\t\t\t} else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) {\n\t\t\t\t\t\t\t\t\tif(kind === 'audio') {\n\t\t\t\t\t\t\t\t\t\tsenderStreams = sender.createEncodedAudioStreams();\n\t\t\t\t\t\t\t\t\t} else if(kind === 'video') {\n\t\t\t\t\t\t\t\t\t\tsenderStreams = sender.createEncodedVideoStreams();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(senderStreams) {\n\t\t\t\t\t\t\t\t\tconsole.log('Insertable Streams sender transform:', senderStreams);\n\t\t\t\t\t\t\t\t\tif(senderStreams.readableStream && senderStreams.writableStream) {\n\t\t\t\t\t\t\t\t\t\tsenderStreams.readableStream\n\t\t\t\t\t\t\t\t\t\t\t.pipeThrough(track.transforms.sender)\n\t\t\t\t\t\t\t\t\t\t\t.pipeTo(senderStreams.writableStream);\n\t\t\t\t\t\t\t\t\t} else if(senderStreams.readable && senderStreams.writable) {\n\t\t\t\t\t\t\t\t\t\tsenderStreams.readable\n\t\t\t\t\t\t\t\t\t\t\t.pipeThrough(track.transforms.sender)\n\t\t\t\t\t\t\t\t\t\t\t.pipeTo(senderStreams.writable);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(transceiver && transceiver.receiver && track.transforms.receiver) {\n\t\t\t\t\t\t\t\t// There's a receiver transform, set it on the transceiver receiver\n\t\t\t\t\t\t\t\tlet receiverStreams = null;\n\t\t\t\t\t\t\t\tif(RTCRtpReceiver.prototype.createEncodedStreams) {\n\t\t\t\t\t\t\t\t\treceiverStreams = transceiver.receiver.createEncodedStreams();\n\t\t\t\t\t\t\t\t} else if(RTCRtpReceiver.prototype.createAudioEncodedStreams || RTCRtpReceiver.prototype.createEncodedVideoStreams) {\n\t\t\t\t\t\t\t\t\tif(kind === 'audio') {\n\t\t\t\t\t\t\t\t\t\treceiverStreams = transceiver.receiver.createEncodedAudioStreams();\n\t\t\t\t\t\t\t\t\t} else if(kind === 'video') {\n\t\t\t\t\t\t\t\t\t\treceiverStreams = transceiver.receiver.createEncodedVideoStreams();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(receiverStreams) {\n\t\t\t\t\t\t\t\t\tconsole.log('Insertable Streams receiver transform:', receiverStreams);\n\t\t\t\t\t\t\t\t\tif(receiverStreams.readableStream && receiverStreams.writableStream) {\n\t\t\t\t\t\t\t\t\t\treceiverStreams.readableStream\n\t\t\t\t\t\t\t\t\t\t\t.pipeThrough(track.transforms.receiver)\n\t\t\t\t\t\t\t\t\t\t\t.pipeTo(receiverStreams.writableStream);\n\t\t\t\t\t\t\t\t\t} else if(receiverStreams.readable && receiverStreams.writable) {\n\t\t\t\t\t\t\t\t\t\treceiverStreams.readable\n\t\t\t\t\t\t\t\t\t\t\t.pipeThrough(track.transforms.receiver)\n\t\t\t\t\t\t\t\t\t\t\t.pipeTo(receiverStreams.writable);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(nt && track.dontStop === true)\n\t\t\t\t\t\tnt.dontStop = true;\n\t\t\t\t} else if(track.recv) {\n\t\t\t\t\t// Maybe a new recvonly track\n\t\t\t\t\tif(!transceiver)\n\t\t\t\t\t\ttransceiver = config.pc.addTransceiver(kind);\n\t\t\t\t\tif(transceiver) {\n\t\t\t\t\t\t// Check if we need to override some settings\n\t\t\t\t\t\tif(track.codec) {\n\t\t\t\t\t\t\tif(Janus.webRTCAdapter.browserDetails.browser === 'firefox') {\n\t\t\t\t\t\t\t\tJanus.warn('setCodecPreferences not supported in Firefox, ignoring codec for track:', track);\n\t\t\t\t\t\t\t} else if(typeof track.codec !== 'string') {\n\t\t\t\t\t\t\t\tJanus.warn('Invalid codec value, ignoring for track:', track);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlet mimeType = kind + '/' + track.codec.toLowerCase();\n\t\t\t\t\t\t\t\tlet codecs = RTCRtpReceiver.getCapabilities(kind).codecs.filter(function(codec) {\n\t\t\t\t\t\t\t\t\treturn codec.mimeType.toLowerCase() === mimeType;\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tif(!codecs || codecs.length === 0) {\n\t\t\t\t\t\t\t\t\tJanus.warn('Codec not supported in this browser for this track, ignoring:', track);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\ttransceiver.setCodecPreferences(codecs);\n\t\t\t\t\t\t\t\t\t} catch(err) {\n\t\t\t\t\t\t\t\t\t\tJanus.warn('Failed enforcing codec for this ' + kind + ' track:', err);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Check if insertable streams are involved\n\t\t\t\t\t\tif(transceiver.receiver && track.transforms && track.transforms.receiver) {\n\t\t\t\t\t\t\t// There's a receiver transform, set it on the transceiver receiver\n\t\t\t\t\t\t\tlet receiverStreams = null;\n\t\t\t\t\t\t\tif(RTCRtpReceiver.prototype.createEncodedStreams) {\n\t\t\t\t\t\t\t\treceiverStreams = transceiver.receiver.createEncodedStreams();\n\t\t\t\t\t\t\t} else if(RTCRtpReceiver.prototype.createAudioEncodedStreams || RTCRtpReceiver.prototype.createEncodedVideoStreams) {\n\t\t\t\t\t\t\t\tif(kind === 'audio') {\n\t\t\t\t\t\t\t\t\treceiverStreams = transceiver.receiver.createEncodedAudioStreams();\n\t\t\t\t\t\t\t\t} else if(kind === 'video') {\n\t\t\t\t\t\t\t\t\treceiverStreams = transceiver.receiver.createEncodedVideoStreams();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(receiverStreams) {\n\t\t\t\t\t\t\t\tconsole.log('Insertable Streams receiver transform:', receiverStreams);\n\t\t\t\t\t\t\t\tif(receiverStreams.readableStream && receiverStreams.writableStream) {\n\t\t\t\t\t\t\t\t\treceiverStreams.readableStream\n\t\t\t\t\t\t\t\t\t\t.pipeThrough(track.transforms.receiver)\n\t\t\t\t\t\t\t\t\t\t.pipeTo(receiverStreams.writableStream);\n\t\t\t\t\t\t\t\t} else if(receiverStreams.readable && receiverStreams.writable) {\n\t\t\t\t\t\t\t\t\treceiverStreams.readable\n\t\t\t\t\t\t\t\t\t\t.pipeThrough(track.transforms.receiver)\n\t\t\t\t\t\t\t\t\t\t.pipeTo(receiverStreams.writable);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(nt) {\n\t\t\t\t\t// FIXME Add the new track locally\n\t\t\t\t\tconfig.myStream.addTrack(nt);\n\t\t\t\t\t// Notify the application about the new local track, if any\n\t\t\t\t\tnt.onended = function(ev) {\n\t\t\t\t\t\tJanus.log('Local track removed:', ev);\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tpluginHandle.onlocaltrack(ev.target, false);\n\t\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\t\tJanus.error(\"Error calling onlocaltrack following end\", e);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tpluginHandle.onlocaltrack(nt, true);\n\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\tJanus.error(\"Error calling onlocaltrack for track add\", e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Update the direction of the transceiver\n\t\t\t\tif(transceiver) {\n\t\t\t\t\tlet curdir = transceiver.direction, newdir = null;\n\t\t\t\t\tlet send = (nt && transceiver.sender.track),\n\t\t\t\t\t\trecv = (track.recv !== false && transceiver.receiver.track);\n\t\t\t\t\tif(send && recv)\n\t\t\t\t\t\tnewdir = 'sendrecv';\n\t\t\t\t\telse if(send && !recv)\n\t\t\t\t\t\tnewdir = 'sendonly';\n\t\t\t\t\telse if(!send && recv)\n\t\t\t\t\t\tnewdir = 'recvonly';\n\t\t\t\t\telse if(!send && !recv)\n\t\t\t\t\t\tnewdir = 'inactive';\n\t\t\t\t\tif(newdir && newdir !== curdir) {\n\t\t\t\t\t\tJanus.warn('Changing direction of transceiver to ' + newdir + ' (was ' + curdir + ')', track);\n\t\t\t\t\t\tif(transceiver.setDirection)\n\t\t\t\t\t\t\ttransceiver.setDirection(newdir);\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\ttransceiver.direction = newdir;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(openedConsentDialog)\n\t\t\t\tpluginHandle.consentDialog(false);\n\t\t}\n\n\t\tfunction getLocalTracks(handleId) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn('Invalid handle');\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc) {\n\t\t\t\tJanus.warn('Invalid PeerConnection');\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tlet tracks = [];\n\t\t\tlet transceivers = config.pc.getTransceivers();\n\t\t\tfor(let tr of transceivers) {\n\t\t\t\tlet track = null;\n\t\t\t\tif(tr.sender && tr.sender.track) {\n\t\t\t\t\ttrack = { mid: tr.mid };\n\t\t\t\t\ttrack.type = tr.sender.track.kind;\n\t\t\t\t\ttrack.id = tr.sender.track.id;\n\t\t\t\t\ttrack.label = tr.sender.track.label;\n\t\t\t\t}\n\t\t\t\tif(track)\n\t\t\t\t\ttracks.push(track);\n\t\t\t}\n\t\t\treturn tracks;\n\t\t}\n\n\t\tfunction getRemoteTracks(handleId) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn('Invalid handle');\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc) {\n\t\t\t\tJanus.warn('Invalid PeerConnection');\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tlet tracks = [];\n\t\t\tlet transceivers = config.pc.getTransceivers();\n\t\t\tfor(let tr of transceivers) {\n\t\t\t\tlet track = null;\n\t\t\t\tif(tr.receiver && tr.receiver.track) {\n\t\t\t\t\ttrack = { mid: tr.mid };\n\t\t\t\t\ttrack.type = tr.receiver.track.kind;\n\t\t\t\t\ttrack.id = tr.receiver.track.id;\n\t\t\t\t\ttrack.label = tr.receiver.track.label;\n\t\t\t\t}\n\t\t\t\tif(track)\n\t\t\t\t\ttracks.push(track);\n\t\t\t}\n\t\t\treturn tracks;\n\t\t}\n\n\t\tfunction getVolume(handleId, mid, remote, result) {\n\t\t\tresult = (typeof result == \"function\") ? result : Janus.noop;\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\tresult(0);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet stream = remote ? \"remote\" : \"local\";\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.volume[stream])\n\t\t\t\tconfig.volume[stream] = { value: 0 };\n\t\t\t// Start getting the volume, if audioLevel in getStats is supported (apparently\n\t\t\t// they're only available in Chrome/Safari right now: https://webrtc-stats.callstats.io/)\n\t\t\tif(config.pc && config.pc.getStats && (Janus.webRTCAdapter.browserDetails.browser === \"chrome\" ||\n\t\t\t\t\tJanus.webRTCAdapter.browserDetails.browser === \"safari\")) {\n\t\t\t\t// Are we interested in a mid in particular?\n\t\t\t\tlet query = config.pc;\n\t\t\t\tif(mid) {\n\t\t\t\t\tlet transceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.mid === mid && t.receiver.track.kind === \"audio\"));\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"No audio transceiver with mid \" + mid);\n\t\t\t\t\t\tresult(0);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif(remote && !transceiver.receiver) {\n\t\t\t\t\t\tJanus.warn(\"Remote transceiver track unavailable\");\n\t\t\t\t\t\tresult(0);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if(!remote && !transceiver.sender) {\n\t\t\t\t\t\tJanus.warn(\"Local transceiver track unavailable\");\n\t\t\t\t\t\tresult(0);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tquery = remote ? transceiver.receiver : transceiver.sender;\n\t\t\t\t}\n\t\t\t\tquery.getStats()\n\t\t\t\t\t.then(function(stats) {\n\t\t\t\t\t\tstats.forEach(function (res) {\n\t\t\t\t\t\t\tif(!res || res.kind !== \"audio\")\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\tif((remote && !res.remoteSource) || (!remote && res.type !== \"media-source\"))\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\tresult(res.audioLevel ? res.audioLevel : 0);\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\treturn config.volume[stream].value;\n\t\t\t} else {\n\t\t\t\t// audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel\n\t\t\t\t// seems to be available on Chrome and Firefox, but they don't seem to work\n\t\t\t\tJanus.warn(\"Getting the \" + stream + \" volume unsupported by browser\");\n\t\t\t\tresult(0);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tfunction isMuted(handleId, mid, video) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc) {\n\t\t\t\tJanus.warn(\"Invalid PeerConnection\");\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif(!config.myStream) {\n\t\t\t\tJanus.warn(\"Invalid local MediaStream\");\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif(video) {\n\t\t\t\t// Check video track\n\t\t\t\tif(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {\n\t\t\t\t\tJanus.warn(\"No video track\");\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif(mid) {\n\t\t\t\t\tlet transceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.mid === mid && t.receiver.track.kind === \"video\"));\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"No video transceiver with mid \" + mid);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif(!transceiver.sender || !transceiver.sender.track) {\n\t\t\t\t\t\tJanus.warn(\"No video sender with mid \" + mid);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\treturn !transceiver.sender.track.enabled;\n\t\t\t\t} else {\n\t\t\t\t\treturn !config.myStream.getVideoTracks()[0].enabled;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Check audio track\n\t\t\t\tif(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {\n\t\t\t\t\tJanus.warn(\"No audio track\");\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif(mid) {\n\t\t\t\t\tlet transceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.mid === mid && t.receiver.track.kind === \"audio\"));\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"No audio transceiver with mid \" + mid);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif(!transceiver.sender || !transceiver.sender.track) {\n\t\t\t\t\t\tJanus.warn(\"No audio sender with mid \" + mid);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\treturn !transceiver.sender.track.enabled;\n\t\t\t\t} else {\n\t\t\t\t\treturn !config.myStream.getAudioTracks()[0].enabled;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction mute(handleId, mid, video, mute) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc) {\n\t\t\t\tJanus.warn(\"Invalid PeerConnection\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif(!config.myStream) {\n\t\t\t\tJanus.warn(\"Invalid local MediaStream\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif(video) {\n\t\t\t\t// Mute/unmute video track\n\t\t\t\tif(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {\n\t\t\t\t\tJanus.warn(\"No video track\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif(mid) {\n\t\t\t\t\tlet transceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.mid === mid && t.receiver.track.kind === \"video\"));\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"No video transceiver with mid \" + mid);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tif(!transceiver.sender || !transceiver.sender.track) {\n\t\t\t\t\t\tJanus.warn(\"No video sender with mid \" + mid);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\ttransceiver.sender.track.enabled = mute ? false : true;\n\t\t\t\t} else {\n\t\t\t\t\tfor(const videostream of config.myStream.getVideoTracks()) {\n\t\t\t\t\t\tvideostream.enabled = !mute\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Mute/unmute audio track\n\t\t\t\tif(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {\n\t\t\t\t\tJanus.warn(\"No audio track\");\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif(mid) {\n\t\t\t\t\tlet transceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.mid === mid && t.receiver.track.kind === \"audio\"));\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"No audio transceiver with mid \" + mid);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tif(!transceiver.sender || !transceiver.sender.track) {\n\t\t\t\t\t\tJanus.warn(\"No audio sender with mid \" + mid);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\ttransceiver.sender.track.enabled = mute ? false : true;\n\t\t\t\t} else {\n\t\t\t\t\tfor(const audiostream of config.myStream.getAudioTracks()) {\n\t\t\t\t\t\taudiostream.enabled = !mute\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\tfunction getBitrate(handleId, mid) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn(\"Invalid handle\");\n\t\t\t\treturn \"Invalid handle\";\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc)\n\t\t\t\treturn \"Invalid PeerConnection\";\n\t\t\t// Start getting the bitrate, if getStats is supported\n\t\t\tif(config.pc.getStats) {\n\t\t\t\tlet query = config.pc;\n\t\t\t\tlet target = mid ? mid : \"default\";\n\t\t\t\tif(mid) {\n\t\t\t\t\tlet transceiver = config.pc.getTransceivers()\n\t\t\t\t\t\t.find(t => (t.mid === mid && t.receiver.track.kind === \"video\"));\n\t\t\t\t\tif(!transceiver) {\n\t\t\t\t\t\tJanus.warn(\"No video transceiver with mid \" + mid);\n\t\t\t\t\t\treturn (\"No video transceiver with mid \" + mid);\n\t\t\t\t\t}\n\t\t\t\t\tif(!transceiver.receiver) {\n\t\t\t\t\t\tJanus.warn(\"No video receiver with mid \" + mid);\n\t\t\t\t\t\treturn (\"No video receiver with mid \" + mid);\n\t\t\t\t\t}\n\t\t\t\t\tquery = transceiver.receiver;\n\t\t\t\t}\n\t\t\t\tif(!config.bitrate[target]) {\n\t\t\t\t\tconfig.bitrate[target] = {\n\t\t\t\t\t\ttimer: null,\n\t\t\t\t\t\tbsnow: null,\n\t\t\t\t\t\tbsbefore: null,\n\t\t\t\t\t\ttsnow: null,\n\t\t\t\t\t\ttsbefore: null,\n\t\t\t\t\t\tvalue: \"0 kbits/sec\"\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif(!config.bitrate[target].timer) {\n\t\t\t\t\tJanus.log(\"Starting bitrate timer\" + (mid ? (\" for mid \" + mid) : \"\") + \" (via getStats)\");\n\t\t\t\t\tconfig.bitrate[target].timer = setInterval(function() {\n\t\t\t\t\t\tquery.getStats()\n\t\t\t\t\t\t\t.then(function(stats) {\n\t\t\t\t\t\t\t\tstats.forEach(function (res) {\n\t\t\t\t\t\t\t\t\tif(!res)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\tlet inStats = false;\n\t\t\t\t\t\t\t\t\t// Check if these are statistics on incoming media\n\t\t\t\t\t\t\t\t\tif((res.mediaType === \"video\" || res.kind === \"video\" || res.id.toLowerCase().indexOf(\"video\") > -1) &&\n\t\t\t\t\t\t\t\t\t\t\tres.type === \"inbound-rtp\" && res.id.indexOf(\"rtcp\") < 0) {\n\t\t\t\t\t\t\t\t\t\t// New stats\n\t\t\t\t\t\t\t\t\t\tinStats = true;\n\t\t\t\t\t\t\t\t\t} else if(res.type == 'ssrc' && res.bytesReceived &&\n\t\t\t\t\t\t\t\t\t\t\t(res.googCodecName === \"VP8\" || res.googCodecName === \"\")) {\n\t\t\t\t\t\t\t\t\t\t// Older Chromer versions\n\t\t\t\t\t\t\t\t\t\tinStats = true;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Parse stats now\n\t\t\t\t\t\t\t\t\tif(inStats) {\n\t\t\t\t\t\t\t\t\t\tconfig.bitrate[target].bsnow = res.bytesReceived;\n\t\t\t\t\t\t\t\t\t\tconfig.bitrate[target].tsnow = res.timestamp;\n\t\t\t\t\t\t\t\t\t\tif(config.bitrate[target].bsbefore === null || config.bitrate[target].tsbefore === null) {\n\t\t\t\t\t\t\t\t\t\t\t// Skip this round\n\t\t\t\t\t\t\t\t\t\t\tconfig.bitrate[target].bsbefore = config.bitrate[target].bsnow;\n\t\t\t\t\t\t\t\t\t\t\tconfig.bitrate[target].tsbefore = config.bitrate[target].tsnow;\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t// Calculate bitrate\n\t\t\t\t\t\t\t\t\t\t\tlet timePassed = config.bitrate[target].tsnow - config.bitrate[target].tsbefore;\n\t\t\t\t\t\t\t\t\t\t\tif(Janus.webRTCAdapter.browserDetails.browser === \"safari\")\n\t\t\t\t\t\t\t\t\t\t\t\ttimePassed = timePassed/1000;\t// Apparently the timestamp is in microseconds, in Safari\n\t\t\t\t\t\t\t\t\t\t\tlet bitRate = Math.round((config.bitrate[target].bsnow - config.bitrate[target].bsbefore) * 8 / timePassed);\n\t\t\t\t\t\t\t\t\t\t\tif(Janus.webRTCAdapter.browserDetails.browser === \"safari\")\n\t\t\t\t\t\t\t\t\t\t\t\tbitRate = parseInt(bitRate/1000);\n\t\t\t\t\t\t\t\t\t\t\tconfig.bitrate[target].value = bitRate + ' kbits/sec';\n\t\t\t\t\t\t\t\t\t\t\t//~ Janus.log(\"Estimated bitrate is \" + config.bitrate.value);\n\t\t\t\t\t\t\t\t\t\t\tconfig.bitrate[target].bsbefore = config.bitrate[target].bsnow;\n\t\t\t\t\t\t\t\t\t\t\tconfig.bitrate[target].tsbefore = config.bitrate[target].tsnow;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t});\n\t\t\t\t\t}, 1000);\n\t\t\t\t\treturn \"0 kbits/sec\";\t// We don't have a bitrate value yet\n\t\t\t\t}\n\t\t\t\treturn config.bitrate[target].value;\n\t\t\t} else {\n\t\t\t\tJanus.warn(\"Getting the video bitrate unsupported by browser\");\n\t\t\t\treturn \"Feature unsupported by browser\";\n\t\t\t}\n\t\t}\n\n\t\tfunction setBitrate(handleId, mid, bitrate) {\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle || !pluginHandle.webrtcStuff) {\n\t\t\t\tJanus.warn('Invalid handle');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(!config.pc) {\n\t\t\t\tJanus.warn('Invalid PeerConnection');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet transceiver = config.pc.getTransceivers().find(t => (t.mid === mid));\n\t\t\tif(!transceiver) {\n\t\t\t\tJanus.warn('No transceiver with mid', mid);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(!transceiver.sender) {\n\t\t\t\tJanus.warn('No sender for transceiver with mid', mid);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet params = transceiver.sender.getParameters();\n\t\t\tif(!params || !params.encodings || params.encodings.length === 0) {\n\t\t\t\tJanus.warn('No parameters encodings');\n\t\t\t} else if(params.encodings.length > 1) {\n\t\t\t\tJanus.warn('Ignoring bitrate for simulcast track, use sendEncodings for that');\n\t\t\t} else if(isNaN(bitrate) || bitrate < 0) {\n\t\t\t\tJanus.warn('Invalid bitrate (must be a positive integer)');\n\t\t\t} else {\n\t\t\t\tparams.encodings[0].maxBitrate = bitrate;\n\t\t\t\ttransceiver.sender.setParameters(params);\n\t\t\t}\n\t\t}\n\n\t\tfunction webrtcError(error) {\n\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t}\n\n\t\tfunction cleanupWebrtc(handleId, hangupRequest) {\n\t\t\tJanus.log(\"Cleaning WebRTC stuff\");\n\t\t\tlet pluginHandle = pluginHandles.get(handleId);\n\t\t\tif(!pluginHandle) {\n\t\t\t\t// Nothing to clean\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet config = pluginHandle.webrtcStuff;\n\t\t\tif(config) {\n\t\t\t\tif(hangupRequest === true) {\n\t\t\t\t\t// Send a hangup request (we don't really care about the response)\n\t\t\t\t\tlet request = { \"janus\": \"hangup\", \"transaction\": Janus.randomString(12) };\n\t\t\t\t\tif(pluginHandle.token)\n\t\t\t\t\t\trequest[\"token\"] = pluginHandle.token;\n\t\t\t\t\tif(apisecret)\n\t\t\t\t\t\trequest[\"apisecret\"] = apisecret;\n\t\t\t\t\tJanus.debug(\"Sending hangup request (handle=\" + handleId + \"):\");\n\t\t\t\t\tJanus.debug(request);\n\t\t\t\t\tif(websockets) {\n\t\t\t\t\t\trequest[\"session_id\"] = sessionId;\n\t\t\t\t\t\trequest[\"handle_id\"] = handleId;\n\t\t\t\t\t\tws.send(JSON.stringify(request));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJanus.httpAPICall(server + \"/\" + sessionId + \"/\" + handleId, {\n\t\t\t\t\t\t\tverb: 'POST',\n\t\t\t\t\t\t\twithCredentials: withCredentials,\n\t\t\t\t\t\t\tbody: request\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Cleanup stack\n\t\t\t\tif(config.volume) {\n\t\t\t\t\tif(config.volume[\"local\"] && config.volume[\"local\"].timer)\n\t\t\t\t\t\tclearInterval(config.volume[\"local\"].timer);\n\t\t\t\t\tif(config.volume[\"remote\"] && config.volume[\"remote\"].timer)\n\t\t\t\t\t\tclearInterval(config.volume[\"remote\"].timer);\n\t\t\t\t}\n\t\t\t\tfor(let i in config.bitrate) {\n\t\t\t\t\tif(config.bitrate[i].timer)\n\t\t\t\t\t\tclearInterval(config.bitrate[i].timer);\n\t\t\t\t}\n\t\t\t\tconfig.bitrate = {};\n\t\t\t\tif(!config.streamExternal && config.myStream) {\n\t\t\t\t\tJanus.log(\"Stopping local stream tracks\");\n\t\t\t\t\tJanus.stopAllTracks(config.myStream);\n\t\t\t\t}\n\t\t\t\tconfig.streamExternal = false;\n\t\t\t\tconfig.myStream = null;\n\t\t\t\t// Close PeerConnection\n\t\t\t\ttry {\n\t\t\t\t\tconfig.pc.close();\n\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t} catch(e) {\n\t\t\t\t\t// Do nothing\n\t\t\t\t}\n\t\t\t\tconfig.pc = null;\n\t\t\t\tconfig.candidates = null;\n\t\t\t\tconfig.mySdp = null;\n\t\t\t\tconfig.remoteSdp = null;\n\t\t\t\tconfig.iceDone = false;\n\t\t\t\tconfig.dataChannel = {};\n\t\t\t\tconfig.dtmfSender = null;\n\t\t\t\tconfig.insertableStreams = false;\n\t\t\t\tconfig.externalEncryption = false;\n\t\t\t}\n\t\t\tpluginHandle.oncleanup();\n\t\t}\n\n\t\tfunction isTrickleEnabled(trickle) {\n\t\t\tJanus.debug(\"isTrickleEnabled:\", trickle);\n\t\t\treturn (trickle === false) ? false : true;\n\t\t}\n\t}\n\n\treturn Janus;\n\n}));\n"
  },
  {
    "path": "html/demos/multiopus.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Multichannel Opus (surround)</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"multiopus.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='multiopus.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Multichannel Opus (surround)\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>EchoTest</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/echotest\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a variant of the Echo Test demo meant to showcase the support\n\t\t\t\t\t\tfor multichannel Opus, and so surround audio: everything is exactly\n\t\t\t\t\t\tthe same in term of available controls, features, and the like, with\n\t\t\t\t\t\tthe substantial difference that the media being captured and sent does\n\t\t\t\t\t\tnot come from webcam and microphone, but from a pre-recorded surround\n\t\t\t\t\t\tfile instead. More precisely, we downloaded a surround demo video from\n\t\t\t\t\t\t<a target=\"_blank\" href=\"https://www2.iis.fraunhofer.de/AAC/multichannel.html\">Fraunhofer's multichannel tests</a>\n\t\t\t\t\t\tand, using <a target=\"_blank\" href=\"https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/captureStream\">captureStream</a>\n\t\t\t\t\t\ton the video element, get access to the MediaStream to send via WebRTC.\n\t\t\t\t\t\tAs a result, a surround audio/video stream is sent to the EchoTest plugin\n\t\t\t\t\t\tvia WebRTC, which echoes it back, allowing you to demonstrate how the surround\n\t\t\t\t\t\tproperties are preserved through the PeerConnection journey via Janus.\n\t\t\t\t\t\tThe file will loop back to the beginning when it ends, since it's quite short.</p>\n\t\t\t\t\t\t<p>Notice that you'll need a recent version of Chrome for this to work, since\n\t\t\t\t\t\tit's only implemented there and not really available publicly. As a matter of\n\t\t\t\t\t\tfact, Chrome will by default not offer multiopus support by default, and will\n\t\t\t\t\t\teven reject it when offered: it's up to you to munge the SDP to force multiopus\n\t\t\t\t\t\tsupport for a conversation. The <code>janus.js</code> library will <b>NOT</b>\n\t\t\t\t\t\tdo it for you: this demo uses the <code>customizeSdp</code> callback to mess\n\t\t\t\t\t\twith the SDP in an ugly way. In other contexts (e.g., VideoRoom subscribers)\n\t\t\t\t\t\tyou'll need to do something similar when creating the answer instead.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"toggleaudio\">Disable audio</button>\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"togglevideo\">Disable video</button>\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<button id=\"bitrateset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\tBandwidth\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-up\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" placeholder=\"Write a DataChannel message\" autocomplete=\"off\" id=\"datasend\" onkeypress=\"return checkEnter(event);\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream <span class=\"badge bg-primary hide\" id=\"curres\"></span> <span class=\"badge bg-info hide\" id=\"curbitrate\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-down\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"datarecv\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/multiopus.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar echotest = null;\nvar opaqueId = \"multiopus-\"+Janus.randomString(12);\n\nvar remoteTracks = {}, remoteVideos = 0;\nvar bitrateTimer = null;\n\nvar audioenabled = false;\nvar videoenabled = false;\n\nvar acodec = \"multiopus\";\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar vprofile = (getQueryStringValue(\"vprofile\") !== \"\" ? getQueryStringValue(\"vprofile\") : null);\nvar simulcastStarted = false;\n\n// We'll use a pre-recorded with surround audio as our local stream\nvar localStream = null;\n\n$(document).ready(function() {\n\t// Preload the video\n\t$('#videoleft').append(\n\t\t'<video class=\"rounded centered\" id=\"myvideo\" width=\"100%\" height=\"100%\" autoplay playsinline loop>' +\n\t\t'\t<source src=\"surround/ChID-BLITS-EBU.mp4\" type=\"video/mp4\">' +\n\t\t'</video>'\n\t);\n\tlet myvideo = $('#myvideo').get(0);\n\tmyvideo.volume = 0.001;\n\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to EchoTest plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.echotest\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\techotest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + echotest.getPlugin() + \", id=\" + echotest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Use the surround video as our local stream\n\t\t\t\t\t\t\t\t\tlocalStream = myvideo.captureStream();\n\t\t\t\t\t\t\t\t\tmyvideo.play();\n\t\t\t\t\t\t\t\t\t// Negotiate WebRTC\n\t\t\t\t\t\t\t\t\tlet body = { audio: true, video: true };\n\t\t\t\t\t\t\t\t\t// We can try and force a specific codec, by telling the plugin what we'd prefer\n\t\t\t\t\t\t\t\t\t// For simplicity, you can set it via a query string (e.g., ?vcodec=vp9)\n\t\t\t\t\t\t\t\t\tif(acodec)\n\t\t\t\t\t\t\t\t\t\tbody[\"audiocodec\"] = acodec;\n\t\t\t\t\t\t\t\t\tif(vcodec)\n\t\t\t\t\t\t\t\t\t\tbody[\"videocodec\"] = vcodec;\n\t\t\t\t\t\t\t\t\t// For the codecs that support them (VP9 and H.264) you can specify a codec\n\t\t\t\t\t\t\t\t\t// profile as well (e.g., ?vprofile=2 for VP9, or ?vprofile=42e01f for H.264)\n\t\t\t\t\t\t\t\t\tif(vprofile)\n\t\t\t\t\t\t\t\t\t\tbody[\"videoprofile\"] = vprofile;\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Sending message:\", body);\n\t\t\t\t\t\t\t\t\techotest.send({ message: body });\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Trying a createOffer too (audio/video sendrecv)\");\n\t\t\t\t\t\t\t\t\techotest.createOffer(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t// We provide our own stream, plus data channels\n\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: localStream.getAudioTracks()[0], recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'video', capture: localStream.getVideoTracks()[0], recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'data' }\n\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\tcustomizeSdp(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t// Offer multiopus\n\t\t\t\t\t\t\t\t\t\t\t\tjsep.sdp = jsep.sdp\n\t\t\t\t\t\t\t\t\t\t\t\t\t.replace(\"opus/48000/2\", \"multiopus/48000/6\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t.replace(\"useinbandfec=1\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"useinbandfec=1;channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2\");\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').html(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\techotest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result === \"done\") {\n\t\t\t\t\t\t\t\t\t\t\t// The plugin closed the echo test\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The Echo Test is over\");\n\t\t\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// Any loss?\n\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\tif(status === \"slow_link\") {\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Janus apparently missed many packets we sent, maybe we should reduce the bitrate\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted) {\n\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted = true;\n\t\t\t\t\t\t\t\t\t\t\taddSimulcastButtons(msg[\"videocodec\"] === \"vp8\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\tupdateSimulcastButtons(substream, temporal);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\t// We ignore the stream we got here, we're using the static video to render it\n\t\t\t\t\t\t\t\t\tif(echotest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\techotest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tlet addButtons = false;\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\taddButtons = true;\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// FIXME we'll need this for additional videos too\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#peervideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = echotest.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t//~ Janus.debug(\"Current bitrate is \" + echotest.getBitrate());\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#peervideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#peervideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(!addButtons)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// Enable audio/video buttons and bitrate limiter\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = !audioenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(audioenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Enable audio\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { audio: audioenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#togglevideo').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\tvideoenabled = !videoenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(videoenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Enable video\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\techotest.send({ message: { video: videoenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\techotest.send({ message: { bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').removeAttr('disabled');\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\t$('#datarecv').val(data);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').attr('disabled', true);\n\t\t\t\t\t\t\t\t\tsimulcastStarted = false;\n\t\t\t\t\t\t\t\t\t$('#simulcast').remove();\n\t\t\t\t\t\t\t\t\t// Get rid of the local stream\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tlet tracks = localStream.getTracks();\n\t\t\t\t\t\t\t\t\t\tfor(let mst of tracks) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(mst);\n\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\t\t\t\t\t// Do nothing if this fails\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlocalStream = null;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tsendData();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction sendData() {\n\tlet data = $('#datasend').val();\n\tif(data === \"\") {\n\t\tbootbox.alert('Insert a message to send on the DataChannel');\n\t\treturn;\n\t}\n\techotest.data({\n\t\ttext: data,\n\t\terror: function(reason) { bootbox.alert(reason); },\n\t\tsuccess: function() { $('#datasend').val(''); },\n\t});\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helpers to create Simulcast-related UI, if enabled\nfunction addSimulcastButtons(temporal) {\n\t$('#curres').parent().append(\n\t\t'<div id=\"simulcast\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>');\n\tif(Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers\n\t\t$('#tl-2').remove();\n\t}\n\t// Enable the simulcast selection buttons\n\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { substream: 0 }});\n\t\t});\n\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 1 }});\n\t\t});\n\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl-0').parent().removeClass('hide');\n\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { temporal: 0 }});\n\t\t});\n\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 1 }});\n\t\t});\n\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastButtons(substream, temporal) {\n\t// Check the substream\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched simulcast substream! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched simulcast substream! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched simulcast substream! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/mvideoroom.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Video Room Demo (multistream)</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"mvideoroom.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='mvideoroom.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Video Room (multistream)\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>VideoRoom</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/videoroom\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a variant of the <a href=\"videoroomtest.html\">original VideoRoom demo</a>,\n\t\t\t\t\t\tbut taking advantage of the recently added multistream support to use a\n\t\t\t\t\t\tsingle PeerConnection to receive all the contributions, rather than\n\t\t\t\t\t\tcreating a different one for each subscription. Apart from this, the\n\t\t\t\t\t\tsame considerations apply.</p>\n\t\t\t\t\t\t<p>To use the demo, just insert a username to join the default room that\n\t\t\t\t\t\tis configured. This will add you to the list of participants, and allow\n\t\t\t\t\t\tyou to automatically send your audio/video frames and receive the other\n\t\t\t\t\t\tparticipants' feeds. The other participants will appear in separate\n\t\t\t\t\t\tpanels, whose title will be the names they chose when registering at\n\t\t\t\t\t\tthe demo.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videojoin\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<span class=\"badge bg-info\" id=\"you\"></span>\n\t\t\t\t\t<div class=\"col-md-12\" id=\"controls\">\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-1 hide\" id=\"registernow\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-user\"></i></span>\n\t\t\t\t\t\t\t<input autocomplete=\"off\" class=\"form-control\" autocomplete=\"off\" type=\"text\" placeholder=\"Choose a display name\" id=\"username\" onkeypress=\"return checkEnter(this, event);\"></input>\n\t\t\t\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t\t\t\t<button class=\"btn btn-success\" autocomplete=\"off\" id=\"register\">Join the room</button>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Video <span class=\"badge bg-primary hide\" id=\"publisher\"></span>\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<button id=\"bitrateset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\tBandwidth\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videolocal\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #1 <span class=\"badge bg-info hide\" id=\"remote1\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote1\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #2 <span class=\"badge bg-info hide\" id=\"remote2\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote2\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #3 <span class=\"badge bg-info hide\" id=\"remote3\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote3\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #4 <span class=\"badge bg-info hide\" id=\"remote4\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote4\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #5 <span class=\"badge bg-info hide\" id=\"remote5\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote5\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/mvideoroom.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar sfutest = null;\nvar opaqueId = \"videoroomtest-\"+Janus.randomString(12);\n\nvar myroom = 1234;\t// Demo room\nif(getQueryStringValue(\"room\") !== \"\")\n\tmyroom = parseInt(getQueryStringValue(\"room\"));\nvar myusername = null;\nvar myid = null;\nvar mystream = null;\n// We use this other ID just to map our subscriptions to us\nvar mypvtid = null;\n\nvar remoteFeed = null;\nvar feeds = {}, feedStreams = {}, subStreams = {}, slots = {}, mids = {}, subscriptions = {};\nvar localTracks = {}, localVideos = 0, remoteTracks = {};\nvar bitrateTimer = [], simulcastStarted = {}, svcStarted = {};\n\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar doSvc = getQueryStringValue(\"svc\");\nif(doSvc === \"\")\n\tdoSvc = null;\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar subscriber_mode = (getQueryStringValue(\"subscriber-mode\") === \"yes\" || getQueryStringValue(\"subscriber-mode\") === \"true\");\nvar use_msid = (getQueryStringValue(\"msid\") === \"yes\" || getQueryStringValue(\"msid\") === \"true\");\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to video room test plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.videoroom\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tsfutest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + sfutest.getPlugin() + \", id=\" + sfutest.getId() + \")\");\n\t\t\t\t\t\t\t\t\tJanus.log(\"  -- This is a publisher/manager\");\n\t\t\t\t\t\t\t\t\t// Prepare the username registration\n\t\t\t\t\t\t\t\t\t$('#videojoin').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#registernow').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#register').click(registerUsername);\n\t\t\t\t\t\t\t\t\t$('#username').focus();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videolocal\").parent().parent().unblock();\n\t\t\t\t\t\t\t\t\tif(!on)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t$('#publish').remove();\n\t\t\t\t\t\t\t\t\t// This controls allows us to override the global room bitrate cap\n\t\t\t\t\t\t\t\t\t$('#bitrate').parent().parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\tsfutest.send({ message: { request: \"configure\", bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message (publisher) :::\", msg);\n\t\t\t\t\t\t\t\t\tlet event = msg[\"videoroom\"];\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Event: \" + event);\n\t\t\t\t\t\t\t\t\tif(event != undefined && event != null) {\n\t\t\t\t\t\t\t\t\t\tif(event === \"joined\") {\n\t\t\t\t\t\t\t\t\t\t\t// Publisher/manager created, negotiate WebRTC and attach to existing feeds, if any\n\t\t\t\t\t\t\t\t\t\t\tmyid = msg[\"id\"];\n\t\t\t\t\t\t\t\t\t\t\tmypvtid = msg[\"private_id\"];\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Successfully joined room \" + msg[\"room\"] + \" with ID \" + myid);\n\t\t\t\t\t\t\t\t\t\t\tif(subscriber_mode) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videojoin').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\tpublishOwnFeed(true);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// Any new feed to attach to?\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"publishers\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"publishers\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of available publishers/feeds:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tlet sources = null;\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(list[f][\"dummy\"])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = list[f][\"display\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet streams = list[f][\"streams\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in streams) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"id\"] = id;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"display\"] = display;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet slot = feedStreams[id] ? feedStreams[id].slot : null;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet remoteVideos = feedStreams[id] ? feedStreams[id].remoteVideos : 0;\n\t\t\t\t\t\t\t\t\t\t\t\t\tfeedStreams[id] = {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tid: id,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: display,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstreams: streams,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tslot: slot,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tremoteVideos: remoteVideos\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display + \":\", streams);\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(!sources)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsources = [];\n\t\t\t\t\t\t\t\t\t\t\t\t\tsources.push(streams);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tif(sources)\n\t\t\t\t\t\t\t\t\t\t\t\t\tsubscribeTo(sources);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"destroyed\") {\n\t\t\t\t\t\t\t\t\t\t\t// The room has been destroyed\n\t\t\t\t\t\t\t\t\t\t\tJanus.warn(\"The room has been destroyed!\");\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The room has been destroyed\", function() {\n\t\t\t\t\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"event\") {\n\t\t\t\t\t\t\t\t\t\t\t// Any info on our streams or a new feed to attach to?\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"streams\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet streams = msg[\"streams\"];\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in streams) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"id\"] = myid;\n\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"display\"] = myusername;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tfeedStreams[myid] = {\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: myid,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: myusername,\n\t\t\t\t\t\t\t\t\t\t\t\t\tstreams: streams\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"publishers\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"publishers\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of available publishers/feeds:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tlet sources = null;\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(list[f][\"dummy\"])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = list[f][\"display\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet streams = list[f][\"streams\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in streams) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"id\"] = id;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"display\"] = display;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet slot = feedStreams[id] ? feedStreams[id].slot : null;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet remoteVideos = feedStreams[id] ? feedStreams[id].remoteVideos : 0;\n\t\t\t\t\t\t\t\t\t\t\t\t\tfeedStreams[id] = {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tid: id,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: display,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstreams: streams,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tslot: slot,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tremoteVideos: remoteVideos\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display + \":\", streams);\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(!sources)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsources = [];\n\t\t\t\t\t\t\t\t\t\t\t\t\tsources.push(streams);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tif(sources)\n\t\t\t\t\t\t\t\t\t\t\t\t\tsubscribeTo(sources);\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"leaving\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t// One of the publishers has gone away?\n\t\t\t\t\t\t\t\t\t\t\t\tlet leaving = msg[\"leaving\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Publisher left: \" + leaving);\n\t\t\t\t\t\t\t\t\t\t\t\tunsubscribeFrom(leaving);\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"unpublished\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t// One of the publishers has unpublished?\n\t\t\t\t\t\t\t\t\t\t\t\tlet unpublished = msg[\"unpublished\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Publisher left: \" + unpublished);\n\t\t\t\t\t\t\t\t\t\t\t\tif(unpublished === 'ok') {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// That's us\n\t\t\t\t\t\t\t\t\t\t\t\t\tsfutest.hangup();\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tunsubscribeFrom(unpublished);\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"error\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(msg[\"error_code\"] === 426) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// This is a \"no such room\" error: give a more meaningful description\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"<p>Apparently room <code>\" + myroom + \"</code> (the one this demo uses as a test room) \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"does not exist...</p><p>Do you have an updated <code>janus.plugin.videoroom.cfg</code> \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"configuration file? If not, make sure you copy the details of room <code>\" + myroom + \"</code> \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"from that sample in your current configuration file, then restart Janus and try again.\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\tsfutest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t// Check if any of the media we wanted to publish has\n\t\t\t\t\t\t\t\t\t\t// been rejected (e.g., wrong or unsupported codec)\n\t\t\t\t\t\t\t\t\t\tlet audio = msg[\"audio_codec\"];\n\t\t\t\t\t\t\t\t\t\tif(mystream && mystream.getAudioTracks() && mystream.getAudioTracks().length > 0 && !audio) {\n\t\t\t\t\t\t\t\t\t\t\t// Audio has been rejected\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Our audio stream has been rejected, viewers won't hear us\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tlet video = msg[\"video_codec\"];\n\t\t\t\t\t\t\t\t\t\tif(mystream && mystream.getVideoTracks() && mystream.getVideoTracks().length > 0 && !video) {\n\t\t\t\t\t\t\t\t\t\t\t// Video has been rejected\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Our video stream has been rejected, viewers won't see us\");\n\t\t\t\t\t\t\t\t\t\t\t// Hide the webcam video\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend(\n\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\" style=\"height: 100%;\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\" style=\"font-size: 16px;\">Video rejected, no webcam</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a local track event :::\");\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videolocal .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\tif($('#mute').length === 0) {\n\t\t\t\t\t\t\t\t\t\t// Add a 'mute' button\n\t\t\t\t\t\t\t\t\t\t$('#videolocal').append('<button class=\"btn btn-warning btn-xs\" id=\"mute\" style=\"position: absolute; bottom: 0px; left: 0px; margin: 15px;\">Mute</button>');\n\t\t\t\t\t\t\t\t\t\t$('#mute').click(toggleMute);\n\t\t\t\t\t\t\t\t\t\t// Add an 'unpublish' button\n\t\t\t\t\t\t\t\t\t\t$('#videolocal').append('<button class=\"btn btn-warning btn-xs\" id=\"unpublish\" style=\"position: absolute; bottom: 0px; right: 0px; margin: 15px;\">Unpublish</button>');\n\t\t\t\t\t\t\t\t\t\t$('#unpublish').click(unpublishOwnFeed);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videolocal .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videolocal .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tJanus.log(stream.getTracks());\n\t\t\t\t\t\t\t\t\t\tJanus.log(stream.getVideoTracks());\n\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend('<video class=\"rounded centered\" id=\"myvideo' + trackId + '\" width=100% autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(sfutest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\tsfutest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videolocal\").parent().parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\t// The publisher stream is sendonly, we don't expect anything here\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification: we are unpublished now :::\");\n\t\t\t\t\t\t\t\t\tmystream = null;\n\t\t\t\t\t\t\t\t\tdelete feedStreams[myid];\n\t\t\t\t\t\t\t\t\t$('#videolocal').html('<button id=\"publish\" class=\"btn btn-primary\">Publish</button>');\n\t\t\t\t\t\t\t\t\t$('#publish').click(function() { publishOwnFeed(true); });\n\t\t\t\t\t\t\t\t\t$(\"#videolocal\").parent().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#bitrate').parent().parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').unbind('click');\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tregisterUsername();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction registerUsername() {\n\tif($('#username').length === 0) {\n\t\t// Create fields to register\n\t\t$('#register').click(registerUsername);\n\t\t$('#username').focus();\n\t} else {\n\t\t// Try a registration\n\t\t$('#username').attr('disabled', true);\n\t\t$('#register').attr('disabled', true).unbind('click');\n\t\tlet username = $('#username').val();\n\t\tif(username === \"\") {\n\t\t\t$('#you')\n\t\t\t\t.removeClass().addClass('badge bg-warning')\n\t\t\t\t.html(\"Insert your display name (e.g., pippo)\");\n\t\t\t$('#username').removeAttr('disabled');\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\treturn;\n\t\t}\n\t\tif(/[^a-zA-Z0-9]/.test(username)) {\n\t\t\t$('#you')\n\t\t\t\t.removeClass().addClass('badge bg-warning')\n\t\t\t\t.html('Input is not alphanumeric');\n\t\t\t$('#username').removeAttr('disabled').val(\"\");\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\treturn;\n\t\t}\n\t\tlet register = {\n\t\t\trequest: \"join\",\n\t\t\troom: myroom,\n\t\t\tptype: \"publisher\",\n\t\t\tdisplay: username\n\t\t};\n\t\tmyusername = escapeXmlTags(username);\n\t\tsfutest.send({ message: register });\n\t}\n}\n\nfunction publishOwnFeed(useAudio) {\n\t// Publish our stream\n\t$('#publish').attr('disabled', true).unbind('click');\n\n\t// We want sendonly audio and video (uncomment the data track\n\t// too if you want to publish via datachannels as well)\n\tlet tracks = [];\n\tif(useAudio)\n\t\ttracks.push({ type: 'audio', capture: true, recv: false });\n\ttracks.push({ type: 'video', capture: true, recv: false,\n\t\t// We may need to enable simulcast or SVC on the video track\n\t\tsimulcast: doSimulcast,\n\t\t// We only support SVC for VP9 and (still WIP) AV1\n\t\tsvc: ((vcodec === 'vp9' || vcodec === 'av1') && doSvc) ? doSvc : null\n\t});\n\t//~ tracks.push({ type: 'data' });\n\n\tsfutest.createOffer(\n\t\t{\n\t\t\ttracks: tracks,\n\t\t\tsuccess: function(jsep) {\n\t\t\t\tJanus.debug(\"Got publisher SDP!\");\n\t\t\t\tJanus.debug(jsep);\n\t\t\t\tlet publish = { request: \"configure\", audio: useAudio, video: true };\n\t\t\t\t// You can force a specific codec to use when publishing by using the\n\t\t\t\t// audiocodec and videocodec properties, for instance:\n\t\t\t\t// \t\tpublish[\"audiocodec\"] = \"opus\"\n\t\t\t\t// to force Opus as the audio codec to use, or:\n\t\t\t\t// \t\tpublish[\"videocodec\"] = \"vp9\"\n\t\t\t\t// to force VP9 as the videocodec to use. In both case, though, forcing\n\t\t\t\t// a codec will only work if: (1) the codec is actually in the SDP (and\n\t\t\t\t// so the browser supports it), and (2) the codec is in the list of\n\t\t\t\t// allowed codecs in a room. With respect to the point (2) above,\n\t\t\t\t// refer to the text in janus.plugin.videoroom.cfg for more details\n\t\t\t\tif(acodec)\n\t\t\t\t\tpublish[\"audiocodec\"] = acodec;\n\t\t\t\tif(vcodec)\n\t\t\t\t\tpublish[\"videocodec\"] = vcodec;\n\t\t\t\tsfutest.send({ message: publish, jsep: jsep });\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\tif (useAudio) {\n\t\t\t\t\tpublishOwnFeed(false);\n\t\t\t\t} else {\n\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t$('#publish').removeAttr('disabled').click(function() { publishOwnFeed(true); });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n}\n\nfunction toggleMute() {\n\tlet muted = sfutest.isAudioMuted();\n\tJanus.log((muted ? \"Unmuting\" : \"Muting\") + \" local stream...\");\n\tif(muted)\n\t\tsfutest.unmuteAudio();\n\telse\n\t\tsfutest.muteAudio();\n\tmuted = sfutest.isAudioMuted();\n\t$('#mute').html(muted ? \"Unmute\" : \"Mute\");\n}\n\nfunction unpublishOwnFeed() {\n\t// Unpublish our stream\n\t$('#unpublish').attr('disabled', true).unbind('click');\n\tlet unpublish = { request: \"unpublish\" };\n\tsfutest.send({ message: unpublish });\n}\n\nvar creatingSubscription = false;\nfunction subscribeTo(sources) {\n\t// Check if we're still creating the subscription handle\n\tif(creatingSubscription) {\n\t\t// Still working on the handle, send this request later when it's ready\n\t\tsetTimeout(function() {\n\t\t\tsubscribeTo(sources);\n\t\t}, 500);\n\t\treturn;\n\t}\n\t// If we already have a working subscription handle, just update that one\n\tif(remoteFeed) {\n\t\t// Prepare the streams to subscribe to, as an array: we have the list of\n\t\t// streams the feeds are publishing, so we can choose what to pick or skip\n\t\tlet added = null, removed = null;\n\t\tfor(let s in sources) {\n\t\t\tlet streams = sources[s];\n\t\t\tfor(let i in streams) {\n\t\t\t\tlet stream = streams[i];\n\t\t\t\t// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video\n\t\t\t\tif(stream.type === \"video\" && Janus.webRTCAdapter.browserDetails.browser === \"safari\" &&\n\t\t\t\t\t\t((stream.codec === \"vp9\" && !Janus.safariVp9) || (stream.codec === \"vp8\" && !Janus.safariVp8))) {\n\t\t\t\t\ttoastr.warning(\"Publisher is using \" + stream.codec.toUpperCase +\n\t\t\t\t\t\t\", but Safari doesn't support it: disabling video stream #\" + stream.mindex);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(stream.disabled) {\n\t\t\t\t\tJanus.log(\"Disabled stream:\", stream);\n\t\t\t\t\t// Unsubscribe\n\t\t\t\t\tif(!removed)\n\t\t\t\t\t\tremoved = [];\n\t\t\t\t\tremoved.push({\n\t\t\t\t\t\tfeed: stream.id,\t// This is mandatory\n\t\t\t\t\t\tmid: stream.mid\t\t// This is optional (all streams, if missing)\n\t\t\t\t\t});\n\t\t\t\t\tdelete subscriptions[stream.id][stream.mid];\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(subscriptions[stream.id] && subscriptions[stream.id][stream.mid]) {\n\t\t\t\t\tJanus.log(\"Already subscribed to stream, skipping:\", stream);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Find an empty slot in the UI for each new source\n\t\t\t\tif(!feedStreams[stream.id].slot) {\n\t\t\t\t\tlet slot;\n\t\t\t\t\tfor(let i=1;i<6;i++) {\n\t\t\t\t\t\tif(!feeds[i]) {\n\t\t\t\t\t\t\tslot = i;\n\t\t\t\t\t\t\tfeeds[slot] = stream.id;\n\t\t\t\t\t\t\tfeedStreams[stream.id].slot = slot;\n\t\t\t\t\t\t\tfeedStreams[stream.id].remoteVideos = 0;\n\t\t\t\t\t\t\t$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).removeClass('hide');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Subscribe\n\t\t\t\tif(!added)\n\t\t\t\t\tadded = [];\n\t\t\t\tadded.push({\n\t\t\t\t\tfeed: stream.id,\t// This is mandatory\n\t\t\t\t\tmid: stream.mid\t\t// This is optional (all streams, if missing)\n\t\t\t\t});\n\t\t\t\tif(!subscriptions[stream.id])\n\t\t\t\t\tsubscriptions[stream.id] = {};\n\t\t\t\tsubscriptions[stream.id][stream.mid] = true;\n\t\t\t}\n\t\t}\n\t\tif((!added || added.length === 0) && (!removed || removed.length === 0)) {\n\t\t\t// Nothing to do\n\t\t\treturn;\n\t\t}\n\t\tlet update = { request: 'update' };\n\t\tif(added)\n\t\t\tupdate.subscribe = added;\n\t\tif(removed)\n\t\t\tupdate.unsubscribe = removed;\n\t\tremoteFeed.send({ message: update });\n\t\t// Nothing else we need to do\n\t\treturn;\n\t}\n\t// If we got here, we're creating a new handle for the subscriptions (we only need one)\n\tcreatingSubscription = true;\n\tjanus.attach(\n\t\t{\n\t\t\tplugin: \"janus.plugin.videoroom\",\n\t\t\topaqueId: opaqueId,\n\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\tremoteFeed = pluginHandle;\n\t\t\t\tremoteTracks = {};\n\t\t\t\tJanus.log(\"Plugin attached! (\" + remoteFeed.getPlugin() + \", id=\" + remoteFeed.getId() + \")\");\n\t\t\t\tJanus.log(\"  -- This is a multistream subscriber\");\n\t\t\t\t// Prepare the streams to subscribe to, as an array: we have the list of\n\t\t\t\t// streams the feed is publishing, so we can choose what to pick or skip\n\t\t\t\tlet subscription = [];\n\t\t\t\tfor(let s in sources) {\n\t\t\t\t\tlet streams = sources[s];\n\t\t\t\t\tfor(let i in streams) {\n\t\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t\t// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video\n\t\t\t\t\t\tif(stream.type === \"video\" && Janus.webRTCAdapter.browserDetails.browser === \"safari\" &&\n\t\t\t\t\t\t\t\t((stream.codec === \"vp9\" && !Janus.safariVp9) || (stream.codec === \"vp8\" && !Janus.safariVp8))) {\n\t\t\t\t\t\t\ttoastr.warning(\"Publisher is using \" + stream.codec.toUpperCase +\n\t\t\t\t\t\t\t\t\", but Safari doesn't support it: disabling video stream #\" + stream.mindex);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream.disabled) {\n\t\t\t\t\t\t\tJanus.log(\"Disabled stream:\", stream);\n\t\t\t\t\t\t\t// TODO Skipping for now, we should unsubscribe\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJanus.log(\"Subscribed to \" + stream.id + \"/\" + stream.mid + \"?\", subscriptions);\n\t\t\t\t\t\tif(subscriptions[stream.id] && subscriptions[stream.id][stream.mid]) {\n\t\t\t\t\t\t\tJanus.log(\"Already subscribed to stream, skipping:\", stream);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Find an empty slot in the UI for each new source\n\t\t\t\t\t\tif(!feedStreams[stream.id].slot) {\n\t\t\t\t\t\t\tlet slot;\n\t\t\t\t\t\t\tfor(let i=1;i<6;i++) {\n\t\t\t\t\t\t\t\tif(!feeds[i]) {\n\t\t\t\t\t\t\t\t\tslot = i;\n\t\t\t\t\t\t\t\t\tfeeds[slot] = stream.id;\n\t\t\t\t\t\t\t\t\tfeedStreams[stream.id].slot = slot;\n\t\t\t\t\t\t\t\t\tfeedStreams[stream.id].remoteVideos = 0;\n\t\t\t\t\t\t\t\t\t$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).removeClass('hide');\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsubscription.push({\n\t\t\t\t\t\t\tfeed: stream.id,\t// This is mandatory\n\t\t\t\t\t\t\tmid: stream.mid\t\t// This is optional (all streams, if missing)\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif(!subscriptions[stream.id])\n\t\t\t\t\t\t\tsubscriptions[stream.id] = {};\n\t\t\t\t\t\tsubscriptions[stream.id][stream.mid] = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// We wait for the plugin to send us an offer\n\t\t\t\tlet subscribe = {\n\t\t\t\t\trequest: \"join\",\n\t\t\t\t\troom: myroom,\n\t\t\t\t\tptype: \"subscriber\",\n\t\t\t\t\tstreams: subscription,\n\t\t\t\t\tuse_msid: use_msid,\n\t\t\t\t\tprivate_id: mypvtid\n\t\t\t\t};\n\t\t\t\tremoteFeed.send({ message: subscribe });\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t},\n\t\t\ticeState: function(state) {\n\t\t\t\tJanus.log(\"ICE state (remote feed) changed to \" + state);\n\t\t\t},\n\t\t\twebrtcState: function(on) {\n\t\t\t\tJanus.log(\"Janus says this WebRTC PeerConnection (remote feed) is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t},\n\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t},\n\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\tJanus.debug(\" ::: Got a message (subscriber) :::\", msg);\n\t\t\t\tlet event = msg[\"videoroom\"];\n\t\t\t\tJanus.debug(\"Event: \" + event);\n\t\t\t\tif(msg[\"error\"]) {\n\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t} else if(event) {\n\t\t\t\t\tif(event === \"attached\") {\n\t\t\t\t\t\t// Now we have a working subscription, next requests will update this one\n\t\t\t\t\t\tcreatingSubscription = false;\n\t\t\t\t\t\tJanus.log(\"Successfully attached to feed in room \" + msg[\"room\"]);\n\t\t\t\t\t} else if(event === \"event\") {\n\t\t\t\t\t\t// Check if we got an event on a simulcast-related event from this publisher\n\t\t\t\t\t\tlet mid = msg[\"mid\"];\n\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t// Check which this feed this refers to\n\t\t\t\t\t\t\tlet slot = slots[mid];\n\t\t\t\t\t\t\tif(!simulcastStarted[slot]) {\n\t\t\t\t\t\t\t\tsimulcastStarted[slot] = true;\n\t\t\t\t\t\t\t\t// Add some new buttons\n\t\t\t\t\t\t\t\taddSimulcastSvcButtons(slot, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\tupdateSimulcastSvcButtons(slot, substream, temporal);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Or maybe SVC?\n\t\t\t\t\t\tlet spatial = msg[\"spatial_layer\"];\n\t\t\t\t\t\ttemporal = msg[\"temporal_layer\"];\n\t\t\t\t\t\tif((spatial !== null && spatial !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\tlet slot = slots[mid];\n\t\t\t\t\t\t\tif(!svcStarted[slot]) {\n\t\t\t\t\t\t\t\tsvcStarted[slot] = true;\n\t\t\t\t\t\t\t\t// Add some new buttons\n\t\t\t\t\t\t\t\taddSimulcastSvcButtons(slot, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\tupdateSimulcastSvcButtons(slot, spatial, temporal);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// What has just happened?\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(msg[\"streams\"]) {\n\t\t\t\t\t// Update map of subscriptions by mid\n\t\t\t\t\tfor(let i in msg[\"streams\"]) {\n\t\t\t\t\t\tlet mid = msg[\"streams\"][i][\"mid\"];\n\t\t\t\t\t\tsubStreams[mid] = msg[\"streams\"][i];\n\t\t\t\t\t\tlet feed = feedStreams[msg[\"streams\"][i][\"feed_id\"]];\n\t\t\t\t\t\tif(feed && feed.slot) {\n\t\t\t\t\t\t\tslots[mid] = feed.slot;\n\t\t\t\t\t\t\tmids[feed.slot] = mid;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(jsep) {\n\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t// Answer and attach\n\t\t\t\t\tremoteFeed.createAnswer(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t// We only specify data channels here, as this way in\n\t\t\t\t\t\t\t// case they were offered we'll enable them. Since we\n\t\t\t\t\t\t\t// don't mention audio or video tracks, we autoaccept them\n\t\t\t\t\t\t\t// as recvonly (since we won't capture anything ourselves)\n\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t{ type: 'data' }\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\");\n\t\t\t\t\t\t\t\tJanus.debug(jsep);\n\t\t\t\t\t\t\t\tlet body = { request: \"start\", room: myroom };\n\t\t\t\t\t\t\t\tremoteFeed.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t// The subscriber stream is recvonly, we don't expect anything here\n\t\t\t},\n\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\tJanus.debug(\n\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t(metadata ? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t);\n\t\t\t\t// Which publisher are we getting on this mid?\n\t\t\t\tlet sub = subStreams[mid];\n\t\t\t\tlet feed = feedStreams[sub.feed_id];\n\t\t\t\tJanus.debug(\" >> This track is coming from feed \" + sub.feed_id + \":\", feed);\n\t\t\t\tlet slot = slots[mid];\n\t\t\t\tif(feed && !slot) {\n\t\t\t\t\tslot = feed.slot;\n\t\t\t\t\tslots[mid] = feed.slot;\n\t\t\t\t\tmids[feed.slot] = mid;\n\t\t\t\t}\n\t\t\t\tJanus.debug(\" >> mid \" + mid + \" is in slot \" + slot);\n\t\t\t\tif(!on) {\n\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t$('#remotevideo' + slot + '-' + mid).remove();\n\t\t\t\t\tif(track.kind === \"video\" && feed) {\n\t\t\t\t\t\tfeed.remoteVideos--;\n\t\t\t\t\t\tif(feed.remoteVideos === 0) {\n\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\tif($('#videoremote' + slot + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t$('#videoremote' + slot).append(\n\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\tdelete slots[mid];\n\t\t\t\t\tdelete mids[slot];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// If we're here, a new track was added\n\t\t\t\tif($('#remotevideo' + slot + '-' + mid).length > 0)\n\t\t\t\t\treturn;\n\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t$('#videoremote' + slot).append('<audio class=\"hide\" id=\"remotevideo' + slot + '-' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\tJanus.attachMediaStream($('#remotevideo' + slot + '-' + mid).get(0), stream);\n\t\t\t\t\tif(feed.remoteVideos === 0) {\n\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\tif($('#videoremote' + slot + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t$('#videoremote' + slot).append(\n\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\tfeed.remoteVideos++;\n\t\t\t\t\t$('#videoremote' + slot + ' .no-video-container').remove();\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t$('#videoremote' + slot).append('<video class=\"rounded centered\" id=\"remotevideo' + slot + '-' + mid + '\" width=100% autoplay playsinline/>');\n\t\t\t\t\t$('#videoremote' + slot).append(\n\t\t\t\t\t\t'<span class=\"badge bg-primary hide\" id=\"curres'+slot+'\" style=\"position: absolute; bottom: 0px; left: 0px; margin: 15px;\"></span>' +\n\t\t\t\t\t\t'<span class=\"badge bg-info hide\" id=\"curbitrate'+slot+'\" style=\"position: absolute; bottom: 0px; right: 0px; margin: 15px;\"></span>');\n\t\t\t\t\tJanus.attachMediaStream($('#remotevideo' + slot + '-' + mid).get(0), stream);\n\t\t\t\t\t// Note: we'll need this for additional videos too\n\t\t\t\t\tif(!bitrateTimer[slot]) {\n\t\t\t\t\t\t$('#curbitrate' + slot).removeClass('hide');\n\t\t\t\t\t\tbitrateTimer[slot] = setInterval(function() {\n\t\t\t\t\t\t\tif(!$(\"#videoremote\" + slot + ' video').get(0))\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\tlet bitrate = remoteFeed.getBitrate(mid);\n\t\t\t\t\t\t\t$('#curbitrate' + slot).text(bitrate);\n\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\tlet width = $(\"#videoremote\" + slot + ' video').get(0).videoWidth;\n\t\t\t\t\t\t\tlet height = $(\"#videoremote\" + slot + ' video').get(0).videoHeight;\n\t\t\t\t\t\t\tif(width > 0 && height > 0) {\n\t\t\t\t\t\t\t\tlet res = width + 'x' + height;\n\t\t\t\t\t\t\t\tif(simulcastStarted[slot])\n\t\t\t\t\t\t\t\t\tres += ' (simulcast)';\n\t\t\t\t\t\t\t\telse if(svcStarted[slot])\n\t\t\t\t\t\t\t\t\tres += ' (SVC)';\n\t\t\t\t\t\t\t\t$('#curres' + slot).removeClass('hide').text(res).removeClass('hide');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\toncleanup: function() {\n\t\t\t\tJanus.log(\" ::: Got a cleanup notification (remote feed) :::\");\n\t\t\t\tfor(let i=1;i<6;i++) {\n\t\t\t\t\t$('#videoremote'+i).empty();\n\t\t\t\t\tif(bitrateTimer[i])\n\t\t\t\t\t\tclearInterval(bitrateTimer[i]);\n\t\t\t\t\tbitrateTimer[i] = null;\n\t\t\t\t\tfeedStreams[i].simulcastStarted = false;\n\t\t\t\t\tfeedStreams[i].svcStarted = false;\n\t\t\t\t\tfeedStreams[i].remoteVideos = 0;\n\t\t\t\t\t$('#simulcast'+i).remove();\n\t\t\t\t}\n\t\t\t\tremoteTracks = {};\n\t\t\t}\n\t\t});\n}\n\nfunction unsubscribeFrom(id) {\n\t// Unsubscribe from this publisher\n\tlet feed = feedStreams[id];\n\tif(!feed)\n\t\treturn;\n\tJanus.debug(\"Feed \" + id + \" (\" + feed.display + \") has left the room, detaching\");\n\tif(bitrateTimer[feed.slot])\n\t\tclearInterval(bitrateTimer[feed.slot]);\n\tbitrateTimer[feed.slot] = null;\n\t$('#remote' + feed.slot).empty().addClass('hide');\n\t$('#videoremote' + feed.slot).empty();\n\tdelete simulcastStarted[feed.slot];\n\tdelete svcStarted[feed.slot];\n\t$('#simulcast' + feed.slot).remove();\n\tdelete feeds[feed.slot];\n\tfeeds.slot = 0;\n\tdelete feedStreams[id];\n\t// Send an unsubscribe request\n\tlet unsubscribe = {\n\t\trequest: \"unsubscribe\",\n\t\tstreams: [{ feed: id }]\n\t};\n\tif(remoteFeed != null)\n\t\tremoteFeed.send({ message: unsubscribe });\n\tdelete subscriptions[id];\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n\n// Helpers to create Simulcast- or SVC-related UI, if enabled\nfunction addSimulcastSvcButtons(feed, temporal) {\n\tlet index = feed;\n\tlet simulcast = simulcastStarted[index];\n\tlet what = (simulcast ? 'simulcast' : 'SVC');\n\tlet layer = (simulcast ? 'substream' : 'layer');\n\t$('#remote'+index).parent().append(\n\t\t'<div id=\"simulcast'+index+'\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl'+index+'-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl'+index+'-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl'+index+'-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl'+index+'-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl'+index+'-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl'+index+'-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>'\n\t);\n\tif(simulcast && Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers, when doing simulcast\n\t\t$('#tl'+index+'-2').remove();\n\t}\n\t// Enable the simulcast selection buttons\n\t$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\tlet index = $(this).attr('id').split('sl')[1].split('-')[0];\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \" (mid=\" + mids[index] + \"), wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(simulcastStarted[index])\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], substream: 0 }});\n\t\t\telse\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], spatial_layer: 0 }});\n\t\t});\n\t$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\tlet index = $(this).attr('id').split('sl')[1].split('-')[0];\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \" (mid=\" + mids[index] + \"), wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted[index])\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], substream: 1 }});\n\t\t\telse\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], spatial_layer: 1 }});\n\t\t});\n\t$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\tlet index = $(this).attr('id').split('sl')[1].split('-')[0];\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \" (mid=\" + mids[index] + \"), wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted[index])\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], substream: 2 }});\n\t\t\telse\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], spatial_layer: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl' + index + '-0').parent().removeClass('hide');\n\t$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\tlet index = $(this).attr('id').split('tl')[1].split('-')[0];\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer (mid=\" + mids[index] + \"), wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(simulcastStarted[index])\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], temporal: 0 }});\n\t\t\telse\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], temporal_layer: 0 }});\n\t\t});\n\t$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\tlet index = $(this).attr('id').split('tl')[1].split('-')[0];\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer (mid=\" + mids[index] + \"), wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted[index])\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], temporal: 1 }});\n\t\t\telse\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], temporal_layer: 1 }});\n\t\t});\n\t$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\tlet index = $(this).attr('id').split('tl')[1].split('-')[0];\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer (mid=\" + mids[index] + \"), wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(simulcastStarted[index])\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], temporal: 2 }});\n\t\t\telse\n\t\t\t\tremoteFeed.send({ message: { request: \"configure\", mid: mids[index], temporal_layer: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastSvcButtons(feed, substream, temporal) {\n\t// Check the substream\n\tlet index = feed;\n\tlet simulcast = simulcastStarted[index];\n\tlet what = (simulcast ? 'simulcast' : 'SVC');\n\tlet layer = (simulcast ? 'substream' : 'layer');\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/navbar.html",
    "content": "<div class=\"container\">\n\t<a class=\"navbar-brand\" href=\"../index.html\">Janus (multistream)</a>\n\t<button type=\"button\" class=\"navbar-toggler\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarResponsive\" aria-controls=\"navbarResponsive\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n\t\t<span class=\"navbar-toggler-icon\"></span>\n\t</button>\n\t<div class=\"navbar-collapse collapse\" id=\"navbarResponsive\">\n\t\t<ul class=\"navbar-nav\">\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../index.html\">Home</a></li>\n\t\t\t<li class=\"nav-item dropdown\">\n\t\t\t\t<a class=\"nav-link dropdown-toggle\" data-bs-toggle=\"dropdown\" href=\"#\" id=\"demos\">Demos</a>\n\t\t\t\t<div class=\"dropdown-menu\" aria-labelledby=\"demos\">\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"index.html\">Index</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"echotest.html\">Echo Test</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"streaming.html\">Streaming</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"videocall.html\">Video Call</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"sip.html\">SIP Gateway</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"videoroom.html\">Video Room</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"mvideoroom.html\">Video Room (multistream)</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"audiobridge.html\">Audio Bridge</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"textroom.html\">Text Room</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"recordplay.html\">Recorder/Playout</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"screensharing.html\">Screen Sharing</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"nosip.html\">NoSIP (SDP/RTP)</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"devices.html\">Device Selection</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"e2e.html\">End-to-end Encryption</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"multiopus.html\">Multichannel Opus (surround)</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"canvas.html\">Canvas Capture</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"webaudio.html\">Web Audio Processing</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"virtualbg.html\">Virtual Background</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"admin.html\">Admin/Monitor</a>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../docs\">Documentation</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../citeus.html\">Papers</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../support.html\">Need help?</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus-legacy.conf.meetecho.com/\">Janus (0.x)</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link januscon\" target=\"_blank\" href=\"https://januscon.it\">JanusCon!</a></li>\n\t\t</ul>\n\t\t<ul class=\"navbar-nav ms-auto\">\n\t\t\t<li class=\"nav-item\">\n\t\t\t\t<a class=\"nav-link meetecho-logo\" target=\"_blank\" href=\"https://www.meetecho.com\">\n\t\t\t\t\t<img src=\"../meetecho-logo.png\"/>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</div>\n"
  },
  {
    "path": "html/demos/nosip.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): NoSIP (SDP/RTP)</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"nosip.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='nosip.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: NoSIP (SDP/RTP)\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>NoSIP</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/nosip\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a demo that complements the one <a href=\"siptest.html\">showcasing the SIP plugin</a>. In\n\t\t\t\t\t\tfact, while the SIP plugin allows you to not worry about SIP details, which are implemented within\n\t\t\t\t\t\tthe plugin itself, the NoSIP plugin doesn't mess with signalling itself, leaving it up to the\n\t\t\t\t\t\tapplication. As such, it provided an alternative to those that still want to interact with a legacy\n\t\t\t\t\t\tinfrastructure (e.g., a pre-existing SIP-based one), but still want to be able to have control\n\t\t\t\t\t\ton the signalling themselves, rather than completely delegating it to the SIP plugin.</p>\n\t\t\t\t\t\t<p>All this plugin does, as a consequence, is taking care of the translation between WebRTC\n\t\t\t\t\t\tempowered SDPs, and barebone SDPs that can be used with legacy peers. The barebone SDPs the\n\t\t\t\t\t\tplugin generates are crafted so that media is handled by the plugin itself, thus implementing\n\t\t\t\t\t\tthe same RTP/RTCP gateway functionality the SIP plugin provides, but without the constraint\n\t\t\t\t\t\tof the signalling. It is up to the appplication to transport a generated offer in whatever\n\t\t\t\t\t\tsignalling they want to use (e.g., SIP, IAX, XMPP, etc.) and make sure the offer/answer from\n\t\t\t\t\t\tthe peer is passed to the plugin, so that the session can be completed.</p>\n\t\t\t\t\t\t<p>Considering this plugin is very much generic and signalling-agnostic, this demo does NOT\n\t\t\t\t\t\tinvolve any signalling at all. On the contrary, it will show how a WebRTC peer can establish\n\t\t\t\t\t\ta session with another WebRTC peer (for the sake of simplicity located in the same page)\n\t\t\t\t\t\tby passing through the RTP/RTCP gatewaying functionality. This should as a result make it easier\n\t\t\t\t\t\tfor you to understand how a NoSIP caller and a NoSIP callee would need to be implemented. The\n\t\t\t\t\t\tbarebone SDPs generated/processed as a consequence will be displayed as a proof of concept.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Caller\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right\">\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"togglevideo\" disabled>Disable video</button>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<pre class=\"card card-body bg-gray mt-3\" id=\"localsdp\"></pre>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Callee</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<pre class=\"card card-body bg-gray mt-3\" id=\"remotesdp\"></pre>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/nosip.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\n\n// We'll need two handles for this demo: a caller and a callee\nvar caller = null, callee = null;\nvar opaqueId = Janus.randomString(12);\n// The local and remote tracks only refer to the caller, though (we ignore the callee)\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\n\nvar callstarted = false, videoenabled = true;\nvar srtp = undefined ; // use \"sdes_mandatory\" to test SRTP-SDES\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to NoSIP plugin as a caller\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.nosip\",\n\t\t\t\t\t\t\t\topaqueId: \"nosiptest-caller-\"+opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tcaller = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"[caller] Plugin attached! (\" + caller.getPlugin() + \", id=\" + caller.getId() + \")\");\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t// Negotiate WebRTC in a second (just to make sure both caller and callee handles exist)\n\t\t\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"[caller] Trying a createOffer too (audio/video sendrecv)\");\n\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and video by default\n\t\t\t\t\t\t\t\t\t\tcaller.createOffer(\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'video', capture: true, recv: true }\n\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"[caller] Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t// We now have a WebRTC SDP: to get a barebone SDP legacy\n\t\t\t\t\t\t\t\t\t\t\t\t\t// peers can digest, we ask the NoSIP plugin to generate\n\t\t\t\t\t\t\t\t\t\t\t\t\t// an offer for us. For the sake of simplicity, no SRTP:\n\t\t\t\t\t\t\t\t\t\t\t\t\t// if you need SRTP support, you can use the same syntax\n\t\t\t\t\t\t\t\t\t\t\t\t\t// the SIP plugin uses (mandatory vs. optional). We'll\n\t\t\t\t\t\t\t\t\t\t\t\t\t// get the result in an event called \"generated\" here.\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trequest: \"generate\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsrtp: srtp\n\t\t\t\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\t\t\t\tcaller.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').html(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"[caller]   -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"[caller] Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"[caller] Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[caller] ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[caller] Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[caller] Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\tcallstarted = true;\n\t\t\t\t\t\t\t\t\t\t$('#togglevideo').removeAttr('disabled').click(renegotiateVideo);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"[caller] Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"[caller]  ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\t// Any error?\n\t\t\t\t\t\t\t\t\tlet error = msg[\"error\"];\n\t\t\t\t\t\t\t\t\tif(error) {\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(error);\n\t\t\t\t\t\t\t\t\t\tcaller.hangup();\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tlet event = result[\"event\"];\n\t\t\t\t\t\t\t\t\t\tif(event === \"generated\") {\n\t\t\t\t\t\t\t\t\t\t\t// We got the barebone SDP offer we wanted, let's have\n\t\t\t\t\t\t\t\t\t\t\t// the callee handle it as if it arrived via signalling\n\t\t\t\t\t\t\t\t\t\t\tlet sdp = result[\"sdp\"];\n\t\t\t\t\t\t\t\t\t\t\t$('#localsdp').text(\n\t\t\t\t\t\t\t\t\t\t\t\t\"[\" + result[\"type\"] + \"]\\n\" + sdp);\n\t\t\t\t\t\t\t\t\t\t\t// This will result in a \"processed\" event on the callee handle\n\t\t\t\t\t\t\t\t\t\t\tlet processOffer = {\n\t\t\t\t\t\t\t\t\t\t\t\trequest: \"process\",\n\t\t\t\t\t\t\t\t\t\t\t\ttype: result[\"type\"],\n\t\t\t\t\t\t\t\t\t\t\t\tsdp: result[\"sdp\"],\n\t\t\t\t\t\t\t\t\t\t\t\tupdate: result[\"update\"],\n\t\t\t\t\t\t\t\t\t\t\t\tsrtp: srtp\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tcallee.send({ message: processOffer });\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"processed\") {\n\t\t\t\t\t\t\t\t\t\t\t// As a caller, this means the remote, barebone SDP answer\n\t\t\t\t\t\t\t\t\t\t\t// we got from the legacy peer has been turned into a full\n\t\t\t\t\t\t\t\t\t\t\t// WebRTC SDP answer we can consume here, let's do that\n\t\t\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"[caller] Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\tcaller.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t// If this was a renegotiation, update the button\n\t\t\t\t\t\t\t\t\t\t\t\tif(callstarted) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t.text(videoenabled ? 'Disable video' : 'Enable video')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t.removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideot' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#videoleft video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoleft .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoleft').append('<video class=\"rounded centered\" id=\"myvideot' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideot' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(caller.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\tcaller.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Calling...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"[caller] Remote track (mid=\" + mid + \") \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#peervideo' + mid).length > 0)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t$('#videoright').parent().find('h3').html(\n\t\t\t\t\t\t\t\t\t\t\t'Send DTMF: <span id=\"dtmf\" class=\"btn-group btn-group-xs\"></span>');\n\t\t\t\t\t\t\t\t\t\tfor(let i=0; i<12; i++) {\n\t\t\t\t\t\t\t\t\t\t\tif(i<10)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#dtmf').append('<button class=\"btn btn-info dtmf\">' + i + '</button>');\n\t\t\t\t\t\t\t\t\t\t\telse if(i == 10)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#dtmf').append('<button class=\"btn btn-info dtmf\">#</button>');\n\t\t\t\t\t\t\t\t\t\t\telse if(i == 11)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#dtmf').append('<button class=\"btn btn-info dtmf\">*</button>');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('.dtmf').click(function() {\n\t\t\t\t\t\t\t\t\t\t\t// Send DTMF tone (inband)\n\t\t\t\t\t\t\t\t\t\t\tcaller.dtmf({dtmf: { tones: $(this).text()}});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"[caller] Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"[caller] Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[caller]  ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#dtmf').parent().html(\"Remote UA\");\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t// Attach to NoSIP plugin as a callee\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.nosip\",\n\t\t\t\t\t\t\t\topaqueId: \"nosiptest-callee-\"+opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\tcallee = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"[callee] Plugin attached! (\" + callee.getPlugin() + \", id=\" + callee.getId() + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"[callee]   -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"[callee] Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"[callee] Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[callee] ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[callee] Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[callee] Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"[callee] Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"[callee]  ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\t// Any error?\n\t\t\t\t\t\t\t\t\tlet error = msg[\"error\"];\n\t\t\t\t\t\t\t\t\tif(error) {\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(error);\n\t\t\t\t\t\t\t\t\t\tcallee.hangup();\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tlet event = result[\"event\"];\n\t\t\t\t\t\t\t\t\t\tif(event === \"processed\") {\n\t\t\t\t\t\t\t\t\t\t\t// Since we're a callee, this means that the barebone SDP offer\n\t\t\t\t\t\t\t\t\t\t\t// the caller gave us (and that we assumed had been sent via\n\t\t\t\t\t\t\t\t\t\t\t// signalling)has been processed, and we got a JSEP SDP to process:\n\t\t\t\t\t\t\t\t\t\t\t// we need to come up with our own answer now, so let's do that\n\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"[callee] Trying a createAnswer too (audio/video sendrecv)\");\n\t\t\t\t\t\t\t\t\t\t\tlet update = result[\"update\"];\n\t\t\t\t\t\t\t\t\t\t\tcallee.createAnswer(\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t// This is the WebRTC enriched offer the plugin gave us\n\t\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and video, if offered\n\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'video', capture: true, recv: true }\n\t\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"[callee] Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We now have a WebRTC SDP: to get a barebone SDP legacy\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// peers can digest, we ask the NoSIP plugin to generate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// an answer for us, just as we did for the caller's offer.\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We'll get the result in an event called \"generated\" here.\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trequest: \"generate\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tupdate: update,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsrtp: srtp\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallee.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"generated\") {\n\t\t\t\t\t\t\t\t\t\t\t// As a callee, we get this when our barebone answer has been\n\t\t\t\t\t\t\t\t\t\t\t// generated from the original JSEP answer. Let's have\n\t\t\t\t\t\t\t\t\t\t\t// the caller handle it as if it arrived via signalling\n\t\t\t\t\t\t\t\t\t\t\tlet sdp = result[\"sdp\"];\n\t\t\t\t\t\t\t\t\t\t\t$('#remotesdp').text(\n\t\t\t\t\t\t\t\t\t\t\t\t\"[\" + result[\"type\"] + \"]\\n\" + sdp);\n\t\t\t\t\t\t\t\t\t\t\t// This will result in a \"processed\" event on the caller handle\n\t\t\t\t\t\t\t\t\t\t\tlet processAnswer = {\n\t\t\t\t\t\t\t\t\t\t\t\trequest: \"process\",\n\t\t\t\t\t\t\t\t\t\t\t\ttype: result[\"type\"],\n\t\t\t\t\t\t\t\t\t\t\t\tsdp: result[\"sdp\"],\n\t\t\t\t\t\t\t\t\t\t\t\tupdate: result[\"update\"],\n\t\t\t\t\t\t\t\t\t\t\t\tsrtp: srtp\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tcaller.send({ message: processAnswer });\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\t// The callee is our fake peer, we don't display anything\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\t// The callee is our fake peer, we don't display anything\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\"[callee] ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// We use this helper function to remove/add video to the call\nfunction renegotiateVideo() {\n\t$('#togglevideo').attr('disabled', true);\n\tlet modifiedTrack = null;\n\tif(videoenabled) {\n\t\t// Renegotiate the call removing local video\n\t\tvideoenabled = false;\n\t\t// We only want to modify the video track, removing our own\n\t\tmodifiedTrack = [{ type: 'video', mid: '1', remove: true }]\n\t} else {\n\t\t// Renegotiate the call removing local video\n\t\tvideoenabled = true;\n\t\t// We only want to modify the video track, adding our own\n\t\tmodifiedTrack = [{ type: 'video', mid: '1', replace: true, capture: true }]\n\t}\n\t// Create an updated offer\n\tcaller.createOffer(\n\t\t{\n\t\t\ttracks: modifiedTrack,\n\t\t\tsuccess: function(jsep) {\n\t\t\t\tJanus.debug(\"[caller] Got SDP!\", jsep);\n\t\t\t\t// As before, we ask the NoSIP plugin to generate a\n\t\t\t\t// plain SDP we can then pass to the callee handle\n\t\t\t\tlet body = {\n\t\t\t\t\trequest: \"generate\",\n\t\t\t\t\tsrtp: srtp\n\t\t\t\t};\n\t\t\t\tcaller.send({ message: body, jsep: jsep });\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t}\n\t\t});\n}\n"
  },
  {
    "path": "html/demos/recordplay.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Recorder/Playout Demo</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"recordplay.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='recordplay.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Recorder/Playout\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>Record&amp;Play</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/recordplay\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This demo shows how you can record a WebRTC session, and then replay it. You\n\t\t\t\t\t\tcan choose to either record a new session (e.g., a videomessage) or watch any of\n\t\t\t\t\t\tthe recordings that may be available (including those you made yourself),\n\t\t\t\t\t\tassuming they weren't marked as private (in which case they won't be listed).</p>\n\t\t\t\t\t\t<p>This application makes use of the integrated recording feature in Janus,\n\t\t\t\t\t\tspecifically the individual recording of audio and video streams in <code>.mjr</code>\n\t\t\t\t\t\tformat: these individual recordings are then used for a live broadcasting\n\t\t\t\t\t\tof the dumped RTP packets through a sendonly WebRTC PeerConnection\n\t\t\t\t\t\twhen you choose to replay them. To post-process these recordings in a\n\t\t\t\t\t\tmore usable format (e.g., <code>.webm</code> for video or <code>.opus</code>\n\t\t\t\t\t\tfor audio) you can make use of the <code>janus-pp-rec</code> tool that\n\t\t\t\t\t\tis available as part of the Janus code.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"recordplay\">\n\t\t\t\t<div id=\"demo\" class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\" id=\"controls\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Recorder/Playout</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\">\n\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" disabled autocomplete=\"off\" id=\"record\">Record</button>\n\t\t\t\t\t\t\t\t\t<button class=\"btn btn-success\" disabled autocomplete=\"off\" id=\"play\">Play</button>\n\t\t\t\t\t\t\t\t\t<button class=\"btn btn-primary\" disabled autocomplete=\"off\" id=\"list\">Update <i id=\"update-list\" class=\"fa-solid fa-rotate\"></i></button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<br/>\n\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm w-100\">\n\t\t\t\t\t\t\t\t\t<button autocomplete=\"off\" id=\"recset\" class=\"btn btn-secondary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\tRecordings list\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t<ul id=\"recslist\" class=\"dropdown-menu\" role=\"menu\" style=\"max-height: 300px; overflow: auto;\">\n\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6 invisible\" id=\"video\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">\n\t\t\t\t\t\t\t\t\t<span id=\"videotitle\">Remote Video</span>\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right\">\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-primary\" autocomplete=\"off\" id=\"pause-resume\">Pause</button>\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"stop\">Stop</button>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videobox\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3 hide\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-down\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"datafield\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/recordplay.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar recordplay = null;\nvar opaqueId = \"recordplaytest-\"+Janus.randomString(12);\n\nvar private_recordings = undefined;\t\t// Set to true if you want recordings to be private\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\nvar bandwidth = 1024 * 1024;\n\nvar myname = null;\nvar recording = false;\nvar playing = false;\nvar recordingId = null;\nvar selectedRecording = null;\nvar selectedRecordingInfo = null;\n\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar vprofile = (getQueryStringValue(\"vprofile\") !== \"\" ? getQueryStringValue(\"vprofile\") : null);\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar doOpusred = (getQueryStringValue(\"opusred\") === \"yes\" || getQueryStringValue(\"opusred\") === \"true\");\nvar recordData = (getQueryStringValue(\"data\") !== \"\" ? getQueryStringValue(\"data\") : null);\nif(recordData !== \"text\" && recordData !== \"binary\")\n\trecordData = null;\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to Record&Play plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.recordplay\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\trecordplay = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + recordplay.getPlugin() + \", id=\" + recordplay.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Prepare the name prompt\n\t\t\t\t\t\t\t\t\t$('#recordplay').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tupdateRecsList();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"  -- Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videobox\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result[\"status\"]) {\n\t\t\t\t\t\t\t\t\t\t\tlet event = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\t\tif(event === 'preparing' || event === 'refreshing') {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Preparing the recording playout\");\n\t\t\t\t\t\t\t\t\t\t\t\trecordplay.createAnswer(\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We only specify data channels here, as this way in\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// case they were offered we'll enable them. Since we\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// don't mention audio or video tracks, we autoaccept them\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// as recvonly (since we won't capture anything ourselves)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'data' }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"start\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trecordplay.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\tif(result[\"warning\"])\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(result[\"warning\"]);\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'recording') {\n\t\t\t\t\t\t\t\t\t\t\t\t// Got an ANSWER to our recording OFFER\n\t\t\t\t\t\t\t\t\t\t\t\tif(jsep)\n\t\t\t\t\t\t\t\t\t\t\t\t\trecordplay.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\tlet id = result[\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\tif(id) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"The ID of the current recording is \" + id);\n\t\t\t\t\t\t\t\t\t\t\t\t\trecordingId = id;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'playing') {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Playout has started!\");\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'done' || event === 'stopped') {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Session has stopped!\");\n\t\t\t\t\t\t\t\t\t\t\t\tlet id = result[\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\tif(recordingId) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(recordingId !== id) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.warn(\"Not a stop to our recording? (\" + recordingId + \", \" + id + \")\");\n\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Show a prompt to replay the recording immediately\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet text = 'Do you want to replay your recording right now?';\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(result['is_private'])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttext += ' This is a private recording, so if you don\\'t play it now you won\\'t be able to replay it later.';\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.confirm(text, function(res) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(res) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tselectedRecording = '' + id;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tselectedRecordingInfo = escapeXmlTags(myname);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstartPlayout();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t\trecordingId = null;\n\t\t\t\t\t\t\t\t\t\t\t\t} else if(selectedRecording) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(parseInt(selectedRecording) !== id) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.warn(\"Not a stop to our playout?\");\n\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t// FIXME Reset status\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videobox').empty();\n\t\t\t\t\t\t\t\t\t\t\t\t$('#video').addClass('invisible');\n\t\t\t\t\t\t\t\t\t\t\t\trecording = false;\n\t\t\t\t\t\t\t\t\t\t\t\tplaying = false;\n\t\t\t\t\t\t\t\t\t\t\t\trecordplay.hangup();\n\t\t\t\t\t\t\t\t\t\t\t\t$('#record').removeAttr('disabled').click(startRecording);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#play').removeAttr('disabled').click(startPlayout);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#list').removeAttr('disabled').click(updateRecsList);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#recset').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#recslist').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t\tupdateRecsList();\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else if(msg[\"error\"]) {\n\t\t\t\t\t\t\t\t\t\t// FIXME Error?\n\t\t\t\t\t\t\t\t\t\tlet error = msg[\"error\"];\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(error);\n\t\t\t\t\t\t\t\t\t\t// FIXME Reset status\n\t\t\t\t\t\t\t\t\t\t$('#videobox').empty();\n\t\t\t\t\t\t\t\t\t\t$('#video').addClass('invisible');\n\t\t\t\t\t\t\t\t\t\trecording = false;\n\t\t\t\t\t\t\t\t\t\tplaying = false;\n\t\t\t\t\t\t\t\t\t\trecordplay.hangup();\n\t\t\t\t\t\t\t\t\t\t$('#record').removeAttr('disabled').click(startRecording);\n\t\t\t\t\t\t\t\t\t\t$('#play').removeAttr('disabled').click(startPlayout);\n\t\t\t\t\t\t\t\t\t\t$('#list').removeAttr('disabled').click(updateRecsList);\n\t\t\t\t\t\t\t\t\t\t$('#recset').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t$('#recslist').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\tupdateRecsList();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tif(playing === true)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#thevideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videobox .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videobox').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t$('#videotitle').html(\"Recording...\");\n\t\t\t\t\t\t\t\t\t$('#stop').unbind('click').click(stopRecPlay);\n\t\t\t\t\t\t\t\t\t$('#video').removeClass('invisible');\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videobox .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videobox').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videobox .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videobox').append('<video class=\"rounded centered\" id=\"thevideo' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#thevideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(recordplay.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\trecordplay.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videobox\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tif(playing === false)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#thevideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videobox .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videobox').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#thevideo' + mid).length > 0)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tif($('#videobox audio').length === 0 && $('#videobox video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videotitle').html(selectedRecordingInfo);\n\t\t\t\t\t\t\t\t\t\t$('#stop').unbind('click').click(stopRecPlay);\n\t\t\t\t\t\t\t\t\t\t$('#video').removeClass('invisible');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videobox').append('<audio class=\"hide\" id=\"thevideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#thevideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videobox .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videobox').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videobox .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videobox').append('<video class=\"rounded centered\" id=\"thevideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#thevideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif($('#curres').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t$('#videobox').append(\n\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"badge bg-primary bottom-left m-3\" id=\"curres' +'\"></span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"badge bg-info bottom-right m-3\" id=\"curbw' +'\"></span>');\n\t\t\t\t\t\t\t\t\t\t\t$('#thevideo' + mid).bind(\"playing\", function () {\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = this.videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = this.videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').text(width + 'x' + height);\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\trecordplay.bitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = recordplay.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbw').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\tlet video = $('video').get(0);\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = video.videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = video.videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').text(width + 'x' + height);\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t$('#datafield').parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\tif(playing === false) {\n\t\t\t\t\t\t\t\t\t\t// We're recording, use this field to send data\n\t\t\t\t\t\t\t\t\t\t$('#datafield').attr('placeholder', 'Write a message to record');\n\t\t\t\t\t\t\t\t\t\t$('#datafield').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\tif(playing === true)\n\t\t\t\t\t\t\t\t\t\t$('#datafield').val(data);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t// FIXME Reset status\n\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\tif(recordplay.bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(recordplay.bitrateTimer);\n\t\t\t\t\t\t\t\t\tdelete recordplay.bitrateTimer;\n\t\t\t\t\t\t\t\t\t$('#videobox').empty();\n\t\t\t\t\t\t\t\t\t$(\"#videobox\").parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#video').addClass('invisible');\n\t\t\t\t\t\t\t\t\t$('#datafield').attr('disabled', true).attr('placeholder', '').val('');\n\t\t\t\t\t\t\t\t\t$('#datafield').parent().addClass('hide');\n\t\t\t\t\t\t\t\t\trecording = false;\n\t\t\t\t\t\t\t\t\tplaying = false;\n\t\t\t\t\t\t\t\t\t$('#record').removeAttr('disabled').click(startRecording);\n\t\t\t\t\t\t\t\t\t$('#play').removeAttr('disabled').click(startPlayout);\n\t\t\t\t\t\t\t\t\t$('#list').removeAttr('disabled').click(updateRecsList);\n\t\t\t\t\t\t\t\t\t$('#recset').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t$('#recslist').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t\tupdateRecsList();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tsendData();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction sendData() {\n\tlet data = $('#datafield').val();\n\tif(data === \"\") {\n\t\tbootbox.alert('Insert a message to send on the DataChannel');\n\t\treturn;\n\t}\n\trecordplay.data({\n\t\ttext: data,\n\t\terror: function(reason) { bootbox.alert(reason); },\n\t\tsuccess: function() { $('#datafield').val(''); },\n\t});\n}\n\nfunction updateRecsList() {\n\t$('#list').unbind('click');\n\t$('#update-list').addClass('fa-spin');\n\tlet body = { request: \"list\" };\n\t// A list request will only obtain the list of recordings that were\n\t// not marked as private. To return the list of private recordings\n\t// as well, you need to provide the plugin admin key too, e.g.:\n\t//     body['admin_key'] = 'supersecret';\n\tJanus.debug(\"Sending message:\", body);\n\trecordplay.send({ message: body, success: function(result) {\n\t\tsetTimeout(function() {\n\t\t\t$('#list').click(updateRecsList);\n\t\t\t$('#update-list').removeClass('fa-spin');\n\t\t}, 500);\n\t\tif(!result) {\n\t\t\tbootbox.alert(\"Got no response to our query for available recordings\");\n\t\t\treturn;\n\t\t}\n\t\tif(result[\"list\"]) {\n\t\t\t$('#recslist').empty();\n\t\t\t$('#record').removeAttr('disabled').click(startRecording);\n\t\t\t$('#list').removeAttr('disabled').click(updateRecsList);\n\t\t\tlet list = result[\"list\"];\n\t\t\tlist.sort(function(a, b) {return (a[\"date\"] < b[\"date\"]) ? 1 : ((b[\"date\"] < a[\"date\"]) ? -1 : 0);} );\n\t\t\tJanus.debug(\"Got a list of available recordings:\", list);\n\t\t\tfor(let mp in list) {\n\t\t\t\tJanus.debug(\"  >> [\" + list[mp][\"id\"] + \"] \" + list[mp][\"name\"] + \" (\" + list[mp][\"date\"] + \")\");\n\t\t\t\t$('#recslist').append(\"<a class='dropdown-item' href='#' id='\" + list[mp][\"id\"] + \"'>\" + escapeXmlTags(list[mp][\"name\"]) + \" [\" + list[mp][\"date\"] + \"]\" + \"</a>\");\n\t\t\t}\n\t\t\t$('#recslist a').unbind('click').click(function() {\n\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\tselectedRecording = $(this).attr(\"id\");\n\t\t\t\tselectedRecordingInfo = escapeXmlTags($(this).text());\n\t\t\t\t$('#recset').html($(this).html()).parent().removeClass('open');\n\t\t\t\t$('#play').removeAttr('disabled').click(startPlayout);\n\t\t\t\treturn false;\n\t\t\t});\n\t\t}\n\t}});\n}\n\nfunction startRecording() {\n\tif(recording)\n\t\treturn;\n\t// Start a recording\n\trecording = true;\n\tplaying = false;\n\tbootbox.prompt(\"Insert a name for the recording (e.g., John Smith says hello)\", function(result) {\n\t\tif(!result) {\n\t\t\trecording = false;\n\t\t\treturn;\n\t\t}\n\t\tmyname = result;\n\t\t$('#record').unbind('click').attr('disabled', true);\n\t\t$('#play').unbind('click').attr('disabled', true);\n\t\t$('#list').unbind('click').attr('disabled', true);\n\t\t$('#recset').attr('disabled', true);\n\t\t$('#recslist').attr('disabled', true);\n\t\t$('#pause-resume').removeClass('hide');\n\n\t\t// bitrate and keyframe interval can be set at any time:\n\t\t// before, after, during recording\n\t\trecordplay.send({\n\t\t\tmessage: {\n\t\t\t\trequest: 'configure',\n\t\t\t\t'video-bitrate-max': bandwidth,\t\t// a quarter megabit\n\t\t\t\t'video-keyframe-interval': 15000\t// 15 seconds\n\t\t\t}\n\t\t});\n\n\t\trecordplay.createOffer(\n\t\t\t{\n\t\t\t\t// We want sendonly audio and video, since we'll just send\n\t\t\t\t// media to Janus and not receive any back in this scenario\n\t\t\t\t// (uncomment the data track if you want to also record data\n\t\t\t\t// channels, even though there's no UI for that in the demo)\n\t\t\t\ttracks: [\n\t\t\t\t\t{ type: 'audio', capture: true, recv: false },\n\t\t\t\t\t{ type: 'video', capture: true, recv: false, simulcast: doSimulcast },\n\t\t\t\t\t//~ { type: 'data' },\n\t\t\t\t],\n\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\tlet body = { request: \"record\", name: myname, is_private: private_recordings };\n\t\t\t\t\t// We can try and force a specific codec, by telling the plugin what we'd prefer\n\t\t\t\t\t// For simplicity, you can set it via a query string (e.g., ?vcodec=vp9)\n\t\t\t\t\tif(acodec)\n\t\t\t\t\t\tbody[\"audiocodec\"] = acodec;\n\t\t\t\t\tif(vcodec)\n\t\t\t\t\t\tbody[\"videocodec\"] = vcodec;\n\t\t\t\t\t// For the codecs that support them (VP9 and H.264) you can specify a codec\n\t\t\t\t\t// profile as well (e.g., ?vprofile=2 for VP9, or ?vprofile=42e01f for H.264)\n\t\t\t\t\tif(vprofile)\n\t\t\t\t\t\tbody[\"videoprofile\"] = vprofile;\n\t\t\t\t\t// You can use RED for Opus, if the browser supports it\n\t\t\t\t\tif(doOpusred)\n\t\t\t\t\t\tbody[\"opusred\"] = true;\n\t\t\t\t\t// If we're going to send binary data, let's tell the plugin\n\t\t\t\t\tif(recordData === \"binary\")\n\t\t\t\t\t\tbody[\"textdata\"] = false;\n\t\t\t\t\trecordplay.send({ message: body, jsep: jsep });\n\t\t\t\t},\n\t\t\t\terror: function(error) {\n\t\t\t\t\tJanus.error(\"WebRTC error...\", error);\n\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\trecordplay.hangup();\n\t\t\t\t}\n\t\t\t});\n\t\t$('#pause-resume').unbind('click').on('click', function() {\n\t\t\tif($(this).text() === 'Pause') {\n\t\t\t\trecordplay.send({message: {request: 'pause'}});\n\t\t\t\t$(this).text('Resume');\n\t\t\t} else {\n\t\t\t\trecordplay.send({message: {request: 'resume'}});\n\t\t\t\t$(this).text('Pause');\n\t\t\t}\n\t\t});\n\t});\n}\n\nfunction startPlayout() {\n\tif(playing)\n\t\treturn;\n\t// Start a playout\n\trecording = false;\n\tplaying = true;\n\tif(!selectedRecording) {\n\t\tplaying = false;\n\t\treturn;\n\t}\n\t$('#record').unbind('click').attr('disabled', true);\n\t$('#play').unbind('click').attr('disabled', true);\n\t$('#list').unbind('click').attr('disabled', true);\n\t$('#recset').attr('disabled', true);\n\t$('#recslist').attr('disabled', true);\n\t$('#pause-resume').addClass('hide');\n\tlet play = { request: \"play\", id: parseInt(selectedRecording) };\n\trecordplay.send({ message: play });\n}\n\nfunction stopRecPlay() {\n\t// Stop a recording/playout\n\t$('#stop').unbind('click');\n\tlet stop = { request: \"stop\" };\n\trecordplay.send({ message: stop });\n\trecordplay.hangup();\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n"
  },
  {
    "path": "html/demos/screensharing.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Screen Sharing Demo</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"screensharing.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='screensharing.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Screen Sharing\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>VideoRoom</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/videoroom\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This demo, as the Video Conferencing one, makes use of the Video Room plugin. Unlike\n\t\t\t\t\t\tthe video conferencing scenario, though, this demo implements a webinar kind of scenario:\n\t\t\t\t\t\tthat is, it allows a single user to share their screen with a set of passive\n\t\t\t\t\t\tviewers.</p>\n\t\t\t\t\t\t<p>When started, the demo asks you whether you want to be the one sharing the screen\n\t\t\t\t\t\t(or an application you're using, if your browser version is recent enough)\n\t\t\t\t\t\tor a viewer to an existing session. When sharing your screen/application, an ID will be returned\n\t\t\t\t\t\tthat you'll be able to share with other people to act as viewers.</p>\n\t\t\t\t\t\t<div class=\"alert alert-info\"><b>Note well!</b> If you want to share your screen, you may need to open\n\t\t\t\t\t\tthe <b>HTTPS</b> version of this page. If Janus is not behind the same webserver\n\t\t\t\t\t\tas the pages that are served (that is, you didn't configure a proxying of HTTP requests\n\t\t\t\t\t\tto Janus via a web frontend, e.g., Apache HTTPD), make sure you started it\n\t\t\t\t\t\twith HTTPS support as well, since for security reasons you cannot contact an HTTP\n\t\t\t\t\t\tbackend if the page has been served via HTTPS. Besides, if you configured Janus\n\t\t\t\t\t\tto make use of self-signed certificates, try and open a generic link served by Janus\n\t\t\t\t\t\tin the browser itself, or otherwise AJAX requests to it will fail due to the unsafe\n\t\t\t\t\t\tnature of the certificate.</div>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-5 hide\" id=\"screenmenu\">\n\t\t\t\t<div class=\"row col-md-12\">\n\t\t\t\t\t<div class=\"input-group mt-3 mb-1 hide\" id=\"createnow\">\n\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-users\"></i></span>\n\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Insert a title for the session\" autocomplete=\"off\" id=\"desc\" onkeypress=\"return checkEnterShare(this, event);\" />\n\t\t\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t\t\t<button class=\"btn btn-success\" autocomplete=\"off\" id=\"create\">Share your screen</button>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"divider col-md-12\">\n\t\t\t\t\t<hr class=\"pull-left\"/>or<hr class=\"pull-right\"/>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row col-md-12\">\n\t\t\t\t\t<div class=\"input-group mt-1 mb-1 hide\" id=\"joinnow\">\n\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-circle-play\"></i></span>\n\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Insert the numeric session identifier\" autocomplete=\"off\" id=\"roomid\" onkeypress=\"return checkEnterJoin(this, event);\" />\n\t\t\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t\t\t<button class=\"btn btn-success\" autocomplete=\"off\" id=\"join\">Join an existing session</button>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"room\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Screen Capture <span class=\"badge bg-info\" id=\"title\"></span> <span class=\"badge bg-success\" id=\"session\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"screencapture\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/screensharing.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar screentest = null;\nvar opaqueId = \"screensharingtest-\"+Janus.randomString(12);\n\nvar myusername = null;\nvar myid = null;\n\nvar capture = null;\nvar role = null;\nvar room = null;\nvar source = null;\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to VideoRoom plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.videoroom\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tscreentest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + screentest.getPlugin() + \", id=\" + screentest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Prepare the username registration\n\t\t\t\t\t\t\t\t\t$('#screenmenu').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#createnow').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#create').click(preShareScreen);\n\t\t\t\t\t\t\t\t\t$('#joinnow').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#join').click(joinScreen);\n\t\t\t\t\t\t\t\t\t$('#desc').focus();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#screencapture\").parent().parent().unblock();\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Your screen sharing session just started: pass the <b>\" + room + \"</b> session identifier to those who want to attend.\");\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Your screen sharing session just stopped.\", function() {\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message (publisher) :::\", msg);\n\t\t\t\t\t\t\t\t\tlet event = msg[\"videoroom\"];\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Event: \" + event);\n\t\t\t\t\t\t\t\t\tif(event) {\n\t\t\t\t\t\t\t\t\t\tif(event === \"joined\") {\n\t\t\t\t\t\t\t\t\t\t\tmyid = msg[\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t$('#session').html(room);\n\t\t\t\t\t\t\t\t\t\t\t$('#title').html(escapeXmlTags(msg[\"description\"]));\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Successfully joined room \" + msg[\"room\"] + \" with ID \" + myid);\n\t\t\t\t\t\t\t\t\t\t\tif(role === \"publisher\") {\n\t\t\t\t\t\t\t\t\t\t\t\t// This is our session, publish our stream\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Negotiating WebRTC stream for our screen (capture \" + capture + \")\");\n\t\t\t\t\t\t\t\t\t\t\t\t// Safari expects a user gesture to share the screen: see issue #2455\n\t\t\t\t\t\t\t\t\t\t\t\tif(Janus.webRTCAdapter.browserDetails.browser === \"safari\") {\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Safari requires a user gesture before the screen can be shared: close this dialog to do that. See issue #2455 for more details\", function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tscreentest.createOffer(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We want to capture the screen and audio, but sendonly\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: false },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'screen', capture: true, recv: false }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got publisher SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet publish = { request: \"configure\", audio: true, video: true };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tscreentest.send({ message: publish, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Other browsers should be fine, we try to call getDisplayMedia directly\n\t\t\t\t\t\t\t\t\t\t\t\t\tscreentest.createOffer(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We want sendonly audio and screensharing\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: false },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'screen', capture: true, recv: false }\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got publisher SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet publish = { request: \"configure\", audio: true, video: true };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tscreentest.send({ message: publish, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t// We're just watching a session, any feed to attach to?\n\t\t\t\t\t\t\t\t\t\t\t\tif(msg[\"publishers\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"publishers\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of available publishers/feeds:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(list[f][\"dummy\"])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = list[f][\"display\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnewRemoteFeed(id, display)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"event\") {\n\t\t\t\t\t\t\t\t\t\t\t// Any feed to attach to?\n\t\t\t\t\t\t\t\t\t\t\tif(role === \"listener\" && msg[\"publishers\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"publishers\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of available publishers/feeds:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(list[f][\"dummy\"])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = list[f][\"display\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display);\n\t\t\t\t\t\t\t\t\t\t\t\t\tnewRemoteFeed(id, display)\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"leaving\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t// One of the publishers has gone away?\n\t\t\t\t\t\t\t\t\t\t\t\tlet leaving = msg[\"leaving\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Publisher left: \" + leaving);\n\t\t\t\t\t\t\t\t\t\t\t\tif(role === \"listener\" && msg[\"leaving\"] === source) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The screen sharing session is over, the publisher left\", function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"error\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\tscreentest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#screenvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#screencapture .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#screencapture').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t$('#screenmenu').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#room').removeClass('hide');\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#screencapture .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#screencapture').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#screencapture .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#screencapture').append('<video class=\"rounded centered\" id=\"screenvideo' + trackId + '\" width=100% autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#screenvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(screentest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\tscreentest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#screencapture\").parent().parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\t// The publisher stream is sendonly, we don't expect anything here\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$('#screencapture').empty();\n\t\t\t\t\t\t\t\t\t$(\"#screencapture\").parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#room').addClass('hide');\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnterShare(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tpreShareScreen();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction preShareScreen() {\n\tif(!Janus.isExtensionEnabled()) {\n\t\tbootbox.alert(\"This browser doesn't support screensharing (getDisplayMedia unavailable)\", function() {\n\t\t\twindow.location.reload();\n\t\t});\n\t\treturn;\n\t}\n\t// Create a new room\n\t$('#desc').attr('disabled', true);\n\t$('#create').attr('disabled', true).unbind('click');\n\t$('#roomid').attr('disabled', true);\n\t$('#join').attr('disabled', true).unbind('click');\n\tif($('#desc').val() === \"\") {\n\t\tbootbox.alert(\"Please insert a description for the room\");\n\t\t$('#desc').removeAttr('disabled', true);\n\t\t$('#create').removeAttr('disabled', true).click(preShareScreen);\n\t\t$('#roomid').removeAttr('disabled', true);\n\t\t$('#join').removeAttr('disabled', true).click(joinScreen);\n\t\treturn;\n\t}\n\tcapture = \"screen\";\n\tshareScreen();\n}\n\nfunction shareScreen() {\n\t// Create a new room\n\tlet desc = $('#desc').val();\n\trole = \"publisher\";\n\tlet create = {\n\t\trequest: \"create\",\n\t\tdescription: desc,\n\t\tbitrate: 500000,\n\t\tpublishers: 1\n\t};\n\tscreentest.send({ message: create, success: function(result) {\n\t\tif(result[\"error\"]) {\n\t\t\tbootbox.alert(\"Couldn't create room: \" + result[\"error\"]);\n\t\t\treturn;\n\t\t}\n\t\tlet event = result[\"videoroom\"];\n\t\tJanus.debug(\"Event: \" + event);\n\t\tif(event) {\n\t\t\t// Our own screen sharing session has been created, join it\n\t\t\troom = result[\"room\"];\n\t\t\tJanus.log(\"Screen sharing session created: \" + room);\n\t\t\tmyusername = Janus.randomString(12);\n\t\t\tlet register = {\n\t\t\t\trequest: \"join\",\n\t\t\t\troom: room,\n\t\t\t\tptype: \"publisher\",\n\t\t\t\tdisplay: myusername\n\t\t\t};\n\t\t\tscreentest.send({ message: register });\n\t\t}\n\t}});\n}\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnterJoin(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tjoinScreen();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction joinScreen() {\n\t// Join an existing screen sharing session\n\t$('#desc').attr('disabled', true);\n\t$('#create').attr('disabled', true).unbind('click');\n\t$('#roomid').attr('disabled', true);\n\t$('#join').attr('disabled', true).unbind('click');\n\tlet roomid = $('#roomid').val();\n\tif(isNaN(roomid)) {\n\t\tbootbox.alert(\"Session identifiers are numeric only\");\n\t\t$('#desc').removeAttr('disabled', true);\n\t\t$('#create').removeAttr('disabled', true).click(preShareScreen);\n\t\t$('#roomid').removeAttr('disabled', true);\n\t\t$('#join').removeAttr('disabled', true).click(joinScreen);\n\t\treturn;\n\t}\n\troom = parseInt(roomid);\n\trole = \"listener\";\n\tmyusername = Janus.randomString(12);\n\tlet register = {\n\t\trequest: \"join\",\n\t\troom: room,\n\t\tptype: \"publisher\",\n\t\tdisplay: myusername\n\t};\n\tscreentest.send({ message: register });\n}\n\nfunction newRemoteFeed(id, display) {\n\t// A new feed has been published, create a new plugin handle and attach to it as a listener\n\tsource = id;\n\tlet remoteFeed = null;\n\tjanus.attach(\n\t\t{\n\t\t\tplugin: \"janus.plugin.videoroom\",\n\t\t\topaqueId: opaqueId,\n\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\tremoteFeed = pluginHandle;\n\t\t\t\tremoteFeed.remoteTracks = {};\n\t\t\t\tremoteFeed.remoteVideos = 0;\n\t\t\t\tJanus.log(\"Plugin attached! (\" + remoteFeed.getPlugin() + \", id=\" + remoteFeed.getId() + \")\");\n\t\t\t\tJanus.log(\"  -- This is a subscriber\");\n\t\t\t\t// We wait for the plugin to send us an offer\n\t\t\t\tlet listen = {\n\t\t\t\t\trequest: \"join\",\n\t\t\t\t\troom: room,\n\t\t\t\t\tptype: \"subscriber\",\n\t\t\t\t\tfeed: id\n\t\t\t\t};\n\t\t\t\tremoteFeed.send({ message: listen });\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t},\n\t\t\ticeState: function(state) {\n\t\t\t\tJanus.log(\"ICE state (feed #\" + remoteFeed.rfindex + \") changed to \" + state);\n\t\t\t},\n\t\t\twebrtcState: function(on) {\n\t\t\t\tJanus.log(\"Janus says this WebRTC PeerConnection (feed #\" + remoteFeed.rfindex + \") is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t},\n\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t},\n\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\tJanus.debug(\" ::: Got a message (listener) :::\", msg);\n\t\t\t\tlet event = msg[\"videoroom\"];\n\t\t\t\tJanus.debug(\"Event: \" + event);\n\t\t\t\tif(event) {\n\t\t\t\t\tif(event === \"attached\") {\n\t\t\t\t\t\t// Subscriber created and attached\n\t\t\t\t\t\tJanus.log(\"Successfully attached to feed \" + id + \" (\" + display + \") in room \" + msg[\"room\"]);\n\t\t\t\t\t\t$('#screenmenu').addClass('hide');\n\t\t\t\t\t\t$('#room').removeClass('hide');\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// What has just happened?\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(jsep) {\n\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t// Answer and attach\n\t\t\t\t\tremoteFeed.createAnswer(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t// We only specify data channels here, as this way in\n\t\t\t\t\t\t\t// case they were offered we'll enable them. Since we\n\t\t\t\t\t\t\t// don't mention audio or video tracks, we autoaccept them\n\t\t\t\t\t\t\t// as recvonly (since we won't capture anything ourselves)\n\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t{ type: 'data' }\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\tlet body = { request: \"start\", room: room };\n\t\t\t\t\t\t\t\tremoteFeed.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t// The subscriber stream is recvonly, we don't expect anything here\n\t\t\t},\n\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\tJanus.debug(\n\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t(metadata? \" (\" + metadata.reason + \") \" : \"\") + \":\", track\n\t\t\t\t);\n\t\t\t\t// Screen sharing tracks are sometimes muted/unmuted by browser\n\t\t\t\t// when data is not flowing fast enough; this can make streams blink.\n\t\t\t\t// We can ignore these.\n\t\t\t\tif(track.kind === \"video\" && metadata && (metadata.reason === \"mute\" || metadata.reason === \"unmute\")) {\n\t\t\t\t\tJanus.log(\"Ignoring mute/unmute on screen-sharing track.\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif(!on) {\n\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t$('#screenvideo' + mid).remove();\n\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\tif($('#screencapture .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t$('#screencapture').append(\n\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// If we're here, a new track was added\n\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t$('#screencapture').append('<audio class=\"hide\" id=\"screenvideo' + mid + '\" playsinline/>');\n\t\t\t\t\t$('#screenvideo' + mid).get(0).volume = 0;\n\t\t\t\t\tJanus.attachMediaStream($('#screenvideo' + mid).get(0), stream);\n\t\t\t\t\t$('#screenvideo' + mid).get(0).play();\n\t\t\t\t\t$('#screenvideo' + mid).get(0).volume = 1;\n\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\tif($('#screencapture .no-video-container').length === 0) {\n\t\t\t\t\t\t\t$('#screencapture').append(\n\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t$('#screencapture .no-video-container').remove();\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\tremoteFeed.remoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t$('#screencapture').append('<video class=\"rounded centered\" id=\"screenvideo' + mid + '\" width=100% playsinline/>');\n\t\t\t\t\t$('#screenvideo' + mid).get(0).volume = 0;\n\t\t\t\t\tJanus.attachMediaStream($('#screenvideo' + mid).get(0), stream);\n\t\t\t\t\t$('#screenvideo' + mid).get(0).play();\n\t\t\t\t\t$('#screenvideo' + mid).get(0).volume = 1;\n\t\t\t\t}\n\t\t\t},\n\t\t\toncleanup: function() {\n\t\t\t\tJanus.log(\" ::: Got a cleanup notification (remote feed \" + id + \") :::\");\n\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\tremoteFeed.remoteTracks = {};\n\t\t\t\tremoteFeed.remoteVideos = 0;\n\t\t\t}\n\t\t});\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n"
  },
  {
    "path": "html/demos/settings.js",
    "content": "/* eslint-disable no-unused-vars */\n\n// We use this shared JavaScript file as a simple way to have all demos\n// refer to the same settings, e.g., in terms of which server to connect\n// to or which STUN/TURN servers to use. This is helpful any time Janus\n// and its demos need to be deployed in a different environment, and\n// so all demos can be installed as are, by just updating the settings.js\n// file accordingly to account for the custom changes.\n//\n// We make use of this 'server' variable to provide the address of the\n// Janus API backend. By default, in this example we assume that Janus is\n// co-located with the web server hosting the HTML pages but listening\n// on a different port (8088, the default for HTTP in Janus), which is\n// why we make use of the 'window.location.hostname' base address. Since\n// Janus can also do HTTPS, and considering we don't really want to make\n// use of HTTP for Janus if your demos are served on HTTPS, we also rely\n// on the 'window.location.protocol' prefix to build the variable, in\n// particular to also change the port used to contact Janus (8088 for\n// HTTP and 8089 for HTTPS, if enabled).\n// In case you place Janus behind an Apache frontend (as we did on the\n// online demos at http://janus.conf.meetecho.com) you can just use a\n// relative path for the variable, e.g.:\n//\n// \t\tvar server = \"/janus\";\n//\n// which will take care of this on its own.\n//\n// If you want to use the WebSockets frontend to Janus, instead (which\n// is what we recommend, since they're more efficient than the long polling\n// we do with HTTP), you'll have to pass a different kind of address, e.g.:\n//\n// \t\tvar server = \"ws://\" + window.location.hostname + \":8188\";\n//\n// Of course this assumes that support for WebSockets has been built in\n// when compiling the server. Notice that the \"ws://\" prefix assumes\n// plain HTTP usage, so \"wss://\" should be used instead when using\n// WebSockets on HTTPS.//\n//\n// If you have multiple options available, and want to let the library\n// autodetect the best way to contact your server (or pool of servers),\n// you can also pass an array of servers, e.g., to provide alternative\n// means of access (e.g., try WebSockets first and, if that fails, fall\n// back to plain HTTP) or just have failover servers:\n//\n//\t\tvar server = [\n//\t\t\t\"ws://\" + window.location.hostname + \":8188\",\n//\t\t\t\"/janus\"\n//\t\t];\n//\n// This will tell the library to try connecting to each of the servers\n// in the presented order. The first working server will be used for\n// the whole session.\n//\nvar server = null;\nif(window.location.protocol === 'http:')\n\tserver = \"http://\" + window.location.hostname + \":8088/janus\";\nelse\n\tserver = \"https://\" + window.location.hostname + \":8089/janus\";\n\n// When creating a Janus object, we can also specify which STUN/TURN\n// servers we'd like to use to gather additional candidates. This is\n// done by passing an \"iceServers\" property when creating the Janus\n// object, meaning that the same set of servers will be used for all\n// PeerConnections that will be initialized within the context of the\n// new Janus session. When no iceServers object is provided, the janus.js\n// library automatically uses the free Google STUN servers, which means\n// it's equivalent to setting:\n//\n//\t\tvar iceServers = [{urls: \"stun:stun.l.google.com:19302\"}];\n//\n// Here are some examples of how an iceServers field may look like to\n// support TURN instead. Notice that, when a TURN server is configured,\n// there's no need to set a STUN one as well, since the TURN server will\n// be automatically contacted as a STUN server too, meaning it will be\n// used to gather both server reflexive and relay candidates.\n//\n//\t\tvar iceServers = [{urls: \"turn:yourturnserver.com:3478\", username: \"janususer\", credential: \"januspwd\"}]\n//\t\tvar iceServers = [{urls: \"turn:yourturnserver.com:443?transport=tcp\", username: \"janususer\", credential: \"januspwd\"}]\n//\t\tvar iceServers = [{urls: \"turns:yourturnserver.com:443?transport=tcp\", username: \"janususer\", credential: \"januspwd\"}]\n//\n// By default we leave the iceServers variable empty, which again means\n// janus.js will fallback to the Google STUN server by default:\n//\nvar iceServers = null;\n"
  },
  {
    "path": "html/demos/sip.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): SIP Gateway Demo</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.6.0/js/md5.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"sip.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='sip.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: SIP Gateway\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>SIP</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/sip\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This demo shows how you can make use of the SIP plugin to interact with a SIP\n\t\t\t\t\t\tProxy (e.g., Kamailio or OpenSIPS) or PBX (e.g., Asterisk or FreeSwitch) in order to\n\t\t\t\t\t\tplace or receive calls to and from other SIP clients. Specifically, it uses the\n\t\t\t\t\t\tSofia-based SIP plugin. Notice the plugin only exchange SIP messages from within the\n\t\t\t\t\t\tplugin itself: no SIP is done in JavaScript, except for references to SIP URIs.</p>\n\t\t\t\t\t\t<p>When started, the demo will allow you to insert a minimum set of information\n\t\t\t\t\t\trequired to REGISTER the web page as a SIP client at a SIP Proxy or PBX you specify.\n\t\t\t\t\t\tThis will allow you to call SIP URIs, or receive calls through the SIP Server itself.\n\t\t\t\t\t\tDuring a call, you'll also be able to interact with the PBX via DTMF tones, e.g.,\n\t\t\t\t\t\tto drive an Interactive Voice Response (IVR) menu that you're being presented with.</p>\n\t\t\t\t\t\t<div class=\"alert alert-info\"><b>Note well!</b> Please notice that, while audio support\n\t\t\t\t\t\thas been tested extensively, video hasn't as much, and as such may not work as expected.\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"sipcall\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6 container invisible\" id=\"login\">\n\t\t\t\t\t\t<div class=\"input-group mt-1 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-up\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"SIP Registrar (e.g., sip:host:port)\" autocomplete=\"off\" id=\"server\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-1 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-user\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"SIP Identity (e.g., sip:goofy@example.com)\" autocomplete=\"off\" id=\"username\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t\t<button id=\"addhelper\" class=\"btn btn-xs btn-info hide\" title=\"Add a new line\">\n\t\t\t\t\t\t\t\t<i class=\"fa-solid fa-plus\"></i>\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-1 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-user-plus\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Username (e.g., goofy, overrides the one in the SIP identity if provided)\" autocomplete=\"off\" id=\"authuser\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-1 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-key\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"password\" placeholder=\"Secret (e.g., mysupersecretpassword)\" autocomplete=\"off\" id=\"password\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-1 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-quote-right\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Display name (e.g., Alice Smith)\" autocomplete=\"off\" id=\"displayname\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"btn-group btn-group-sm w-100\">\n\t\t\t\t\t\t\t<button class=\"btn btn-primary\" autocomplete=\"off\" id=\"register\" style=\"width: 30%\">Register</button>\n\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\" style=\"width: 70%\">\n\t\t\t\t\t\t\t\t<button autocomplete=\"off\" id=\"registerset\" class=\"btn btn-secondary dropdown-toggle\" data-bs-toggle=\"dropdown\" style=\"width: 100%\">\n\t\t\t\t\t\t\t\t\tRegistration approach\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t<ul id=\"registerlist\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href='#' id='secret'>Register using plain secret</a>\n\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href='#' id='ha1secret'>Register using HA1 secret</a>\n\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href='#' id='guest'>Register as a guest (no secret)</a>\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6 container invisible\" id=\"phone\">\n\t\t\t\t\t\t<div class=\"input-group mt-1 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-phone\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"SIP URI to call (e.g., sip:1000@example.com)\" autocomplete=\"off\" id=\"peer\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"btn btn-success mb-1\" autocomplete=\"off\" id=\"call\">Call</button> <input autocomplete=\"off\" id=\"dovideo\" type=\"checkbox\" />Use Video\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"videos\" class=\"row mt-2 mb-2 hide\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">You</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote UA</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/sip.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n/* global md5:readonly */\n\nvar janus = null;\nvar sipcall = null;\nvar opaqueId = \"siptest-\"+Janus.randomString(12);\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\n\nvar selectedApproach = null;\nvar registered = false;\nvar masterId = null, helpers = {}, helpersCount = 0;\n\nvar incoming = null;\n\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to SIP plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.sip\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tsipcall = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + sipcall.getPlugin() + \", id=\" + sipcall.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Prepare the username registration\n\t\t\t\t\t\t\t\t\t$('#sipcall').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#login').removeClass('invisible').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#registerlist a').unbind('click').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tselectedApproach = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\t$('#registerset').html($(this).html()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\tif(selectedApproach === \"guest\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#password').empty().attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t$('#password').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tswitch(selectedApproach) {\n\t\t\t\t\t\t\t\t\t\t\tcase \"secret\":\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Using this approach you'll provide a plain secret to REGISTER\");\n\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\tcase \"ha1secret\":\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Using this approach might not work with Asterisk because the generated HA1 secret could have the wrong realm\");\n\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\tcase \"guest\":\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Using this approach you'll try to REGISTER as a guest, that is without providing any secret\");\n\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#register').click(registerUsername);\n\t\t\t\t\t\t\t\t\t$('#server').focus();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"  -- Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\t// Any error?\n\t\t\t\t\t\t\t\t\tlet error = msg[\"error\"];\n\t\t\t\t\t\t\t\t\tif(error) {\n\t\t\t\t\t\t\t\t\t\tif(!registered) {\n\t\t\t\t\t\t\t\t\t\t\t$('#server').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#username').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#authuser').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#displayname').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#password').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\t\t\t\t\t\t\t\t\t$('#registerset').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t// Reset status\n\t\t\t\t\t\t\t\t\t\t\tsipcall.hangup();\n\t\t\t\t\t\t\t\t\t\t\t$('#dovideo').removeAttr('disabled').val('');\n\t\t\t\t\t\t\t\t\t\t\t$('#peer').removeAttr('disabled').val('');\n\t\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Call')\n\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-danger\").addClass(\"btn-success\")\n\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doCall);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(error);\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet callId = msg[\"call_id\"];\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result && result[\"event\"]) {\n\t\t\t\t\t\t\t\t\t\tlet event = result[\"event\"];\n\t\t\t\t\t\t\t\t\t\tif(event === 'registration_failed') {\n\t\t\t\t\t\t\t\t\t\t\tJanus.warn(\"Registration failed: \" + result[\"code\"] + \" \" + result[\"reason\"]);\n\t\t\t\t\t\t\t\t\t\t\t$('#server').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#username').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#authuser').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#displayname').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#password').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\t\t\t\t\t\t\t\t\t$('#registerset').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(result[\"code\"] + \" \" + result[\"reason\"]);\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(event === 'registered') {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Successfully registered as \" + result[\"username\"] + \"!\");\n\t\t\t\t\t\t\t\t\t\t\t$('#you').removeClass('hide').text(\"Registered as '\" + result[\"username\"] + \"'\");\n\t\t\t\t\t\t\t\t\t\t\t// TODO Enable buttons to call now\n\t\t\t\t\t\t\t\t\t\t\tif(!registered) {\n\t\t\t\t\t\t\t\t\t\t\t\tregistered = true;\n\t\t\t\t\t\t\t\t\t\t\t\tmasterId = result[\"master_id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t$('#server').parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#authuser').parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#displayname').parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#password').parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#register').parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#registerset').parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#addhelper').removeClass('hide').click(addHelper);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#phone').removeClass('invisible').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#call').unbind('click').click(doCall);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#peer').focus();\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'calling') {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Waiting for the peer to answer...\");\n\t\t\t\t\t\t\t\t\t\t\t// TODO Any ringtone?\n\t\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Hangup')\n\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-success\").addClass(\"btn-danger\")\n\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doHangup);\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'incomingcall') {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Incoming call from \" + result[\"username\"] + \"!\");\n\t\t\t\t\t\t\t\t\t\t\tsipcall.callId = callId;\n\t\t\t\t\t\t\t\t\t\t\tlet doAudio = true, doVideo = true;\n\t\t\t\t\t\t\t\t\t\t\tlet offerlessInvite = false;\n\t\t\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t// What has been negotiated?\n\t\t\t\t\t\t\t\t\t\t\t\tdoAudio = (jsep.sdp.indexOf(\"m=audio \") > -1);\n\t\t\t\t\t\t\t\t\t\t\t\tdoVideo = (jsep.sdp.indexOf(\"m=video \") > -1);\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Audio \" + (doAudio ? \"has\" : \"has NOT\") + \" been negotiated\");\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Video \" + (doVideo ? \"has\" : \"has NOT\") + \" been negotiated\");\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"This call doesn't contain an offer... we'll need to provide one ourselves\");\n\t\t\t\t\t\t\t\t\t\t\t\tofferlessInvite = true;\n\t\t\t\t\t\t\t\t\t\t\t\t// In case you want to offer video when reacting to an offerless call, set this to true\n\t\t\t\t\t\t\t\t\t\t\t\tdoVideo = false;\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// Is this the result of a transfer?\n\t\t\t\t\t\t\t\t\t\t\tlet transfer = \"\";\n\t\t\t\t\t\t\t\t\t\t\tlet referredBy = result[\"referred_by\"];\n\t\t\t\t\t\t\t\t\t\t\tif(referredBy) {\n\t\t\t\t\t\t\t\t\t\t\t\ttransfer = \" (referred by \" + referredBy + \")\";\n\t\t\t\t\t\t\t\t\t\t\t\ttransfer = transfer.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\t\t\t\t\t\t\ttransfer = transfer.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// Any security offered? A missing \"srtp\" attribute means plain RTP\n\t\t\t\t\t\t\t\t\t\t\tlet rtpType = \"\";\n\t\t\t\t\t\t\t\t\t\t\tlet srtp = result[\"srtp\"];\n\t\t\t\t\t\t\t\t\t\t\tif(srtp === \"sdes_optional\")\n\t\t\t\t\t\t\t\t\t\t\t\trtpType = \" (SDES-SRTP offered)\";\n\t\t\t\t\t\t\t\t\t\t\telse if(srtp === \"sdes_mandatory\")\n\t\t\t\t\t\t\t\t\t\t\t\trtpType = \" (SDES-SRTP mandatory)\";\n\t\t\t\t\t\t\t\t\t\t\t// Notify user\n\t\t\t\t\t\t\t\t\t\t\tbootbox.hideAll();\n\t\t\t\t\t\t\t\t\t\t\tlet extra = \"\";\n\t\t\t\t\t\t\t\t\t\t\tif(offerlessInvite)\n\t\t\t\t\t\t\t\t\t\t\t\textra = \" (no SDP offer provided)\"\n\t\t\t\t\t\t\t\t\t\t\tincoming = bootbox.dialog({\n\t\t\t\t\t\t\t\t\t\t\t\tmessage: \"Incoming call from \" + result[\"username\"] + \"!\" + transfer + rtpType + extra,\n\t\t\t\t\t\t\t\t\t\t\t\ttitle: \"Incoming call\",\n\t\t\t\t\t\t\t\t\t\t\t\tcloseButton: false,\n\t\t\t\t\t\t\t\t\t\t\t\tbuttons: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Answer\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-success\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tincoming = null;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#peer').val(result[\"username\"]).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Notice that we can only answer if we got an offer: if this was\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// an offerless call, we'll need to create an offer ourselves\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet sipcallAction = (offerlessInvite ? sipcall.createOffer : sipcall.createAnswer);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and/or video\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = [];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(doAudio)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks.push({ type: 'audio', capture: true, recv: true });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(doVideo)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks.push({ type: 'video', capture: true, recv: true });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcallAction(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: tracks,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP \" + jsep.type + \"! audio=\" + doAudio + \", video=\" + doVideo + \":\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.doAudio = doAudio;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.doVideo = doVideo;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"accept\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Note: as with \"call\", you can add a \"srtp\" attribute to\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// negotiate/mandate SDES support for this incoming call.\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// The default behaviour is to automatically use it if\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// the caller negotiated it, but you may choose to require\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// SDES support by setting \"srtp\" to \"sdes_mandatory\", e.g.:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t//\t\tlet body = { request: \"accept\", srtp: \"sdes_mandatory\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// This way you'll tell the plugin to accept the call, but ONLY\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// if SDES is available, and you don't want plain RTP. If it\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// is not available, you'll get an error (452) back. You can\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// also specify the SRTP profile to negotiate by setting the\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// \"srtp_profile\" property accordingly (the default if not\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// set in the request is \"AES_CM_128_HMAC_SHA1_80\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Note 2: by default, the SIP plugin auto-answers incoming\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// re-INVITEs, without involving the browser/client: this is\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// for backwards compatibility with older Janus clients that\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// may not be able to handle them. Since we want to receive\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// re-INVITES to handle them ourselves, we specify it here:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbody[\"autoaccept_reinvites\"] = false;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Hangup')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-success\").addClass(\"btn-danger\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doHangup);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Don't keep the caller waiting any longer, but use a 480 instead of the default 486 to clarify the cause\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"decline\", code: 480 };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: body });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tdanger: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Decline\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-danger\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tincoming = null;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"decline\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: body });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'accepting') {\n\t\t\t\t\t\t\t\t\t\t\t// Response to an offerless INVITE, let's wait for an 'accepted'\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'progress') {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"There's early media from \" + result[\"username\"] + \", wairing for the call!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t// Call can start already: handle the remote answer\n\t\t\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\tsipcall.handleRemoteJsep({ jsep: jsep, error: doHangup });\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\ttoastr.info(\"Early media...\");\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'accepted') {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(result[\"username\"] + \" accepted the call!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t// Call can start, now: handle the remote answer\n\t\t\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\tsipcall.handleRemoteJsep({ jsep: jsep, error: doHangup });\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\ttoastr.success(\"Call accepted!\");\n\t\t\t\t\t\t\t\t\t\t\tsipcall.callId = callId;\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'updatingcall') {\n\t\t\t\t\t\t\t\t\t\t\t// We got a re-INVITE: while we may prompt the user (e.g.,\n\t\t\t\t\t\t\t\t\t\t\t// to notify about media changes), to keep things simple\n\t\t\t\t\t\t\t\t\t\t\t// we just accept the update and send an answer right away\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Got re-INVITE\");\n\t\t\t\t\t\t\t\t\t\t\tlet doAudio = (jsep.sdp.indexOf(\"m=audio \") > -1),\n\t\t\t\t\t\t\t\t\t\t\t\tdoVideo = (jsep.sdp.indexOf(\"m=video \") > -1);\n\t\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and/or video, but only\n\t\t\t\t\t\t\t\t\t\t\t// populate tracks if we weren't sending something before\n\t\t\t\t\t\t\t\t\t\t\tlet tracks = [];\n\t\t\t\t\t\t\t\t\t\t\tif(doAudio && !sipcall.doAudio) {\n\t\t\t\t\t\t\t\t\t\t\t\tsipcall.doAudio = true;\n\t\t\t\t\t\t\t\t\t\t\t\ttracks.push({ type: 'audio', capture: true, recv: true });\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tif(doVideo && !sipcall.doVideo) {\n\t\t\t\t\t\t\t\t\t\t\t\tsipcall.doVideo = true;\n\t\t\t\t\t\t\t\t\t\t\t\ttracks.push({ type: 'video', capture: true, recv: true });\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tsipcall.createAnswer(\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: tracks,\n\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP \" + jsep.type + \"! audio=\" + doAudio + \", video=\" + doVideo + \":\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"update\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'message') {\n\t\t\t\t\t\t\t\t\t\t\t// We got a MESSAGE\n\t\t\t\t\t\t\t\t\t\t\tlet sender = result[\"displayname\"] ? result[\"displayname\"] : result[\"sender\"];\n\t\t\t\t\t\t\t\t\t\t\tlet content = result[\"content\"];\n\t\t\t\t\t\t\t\t\t\t\tcontent = content.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\t\t\t\t\t\tcontent = content.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\t\t\t\t\t\ttoastr.success(content, \"Message from \" + sender);\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'info') {\n\t\t\t\t\t\t\t\t\t\t\t// We got an INFO\n\t\t\t\t\t\t\t\t\t\t\tlet sender = result[\"displayname\"] ? result[\"displayname\"] : result[\"sender\"];\n\t\t\t\t\t\t\t\t\t\t\tlet content = result[\"content\"];\n\t\t\t\t\t\t\t\t\t\t\tcontent = content.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\t\t\t\t\t\tcontent = content.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\t\t\t\t\t\ttoastr.info(content, \"Info from \" + sender);\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'notify') {\n\t\t\t\t\t\t\t\t\t\t\t// We got a NOTIFY\n\t\t\t\t\t\t\t\t\t\t\tlet notify = result[\"notify\"];\n\t\t\t\t\t\t\t\t\t\t\tlet content = result[\"content\"];\n\t\t\t\t\t\t\t\t\t\t\ttoastr.info(content, \"Notify (\" + notify + \")\");\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'transfer') {\n\t\t\t\t\t\t\t\t\t\t\t// We're being asked to transfer the call, ask the user what to do\n\t\t\t\t\t\t\t\t\t\t\tlet referTo = result[\"refer_to\"];\n\t\t\t\t\t\t\t\t\t\t\tlet referredBy = result[\"referred_by\"] ? result[\"referred_by\"] : \"an unknown party\";\n\t\t\t\t\t\t\t\t\t\t\tlet referId = result[\"refer_id\"];\n\t\t\t\t\t\t\t\t\t\t\tlet replaces = result[\"replaces\"];\n\t\t\t\t\t\t\t\t\t\t\tlet extra = (\"referred by \" + referredBy);\n\t\t\t\t\t\t\t\t\t\t\tif(replaces)\n\t\t\t\t\t\t\t\t\t\t\t\textra += (\", replaces call-ID \" + replaces);\n\t\t\t\t\t\t\t\t\t\t\textra = extra.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\t\t\t\t\t\textra = extra.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\t\t\t\t\t\tbootbox.confirm(\"Transfer the call to \" + referTo + \"? (\" + extra + \")\",\n\t\t\t\t\t\t\t\t\t\t\t\tfunction(result) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Call the person we're being transferred to\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(!sipcall.webrtcStuff.pc) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Do it here\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#peer').val(referTo).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactuallyDoCall(sipcall, referTo, false, referId);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We're in a call already, use a helper\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet h = -1;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(Object.keys(helpers).length > 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// See if any of the helpers if available\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in helpers) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(!helpers[i].sipcall.webrtcStuff.pc) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\th = parseInt(i);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(h !== -1) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Do in this helper\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#peer' + h).val(referTo).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactuallyDoCall(helpers[h].sipcall, referTo, false, referId);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Create a new helper\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\taddHelper(function(id) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Do it here\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#peer' + id).val(referTo).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactuallyDoCall(helpers[id].sipcall, referTo, false, referId);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We're rejecting the transfer\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"decline\", refer_id: referId };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: body });\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'hangup') {\n\t\t\t\t\t\t\t\t\t\t\tif(incoming != null) {\n\t\t\t\t\t\t\t\t\t\t\t\tincoming.modal('hide');\n\t\t\t\t\t\t\t\t\t\t\t\tincoming = null;\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Call hung up (\" + result[\"code\"] + \" \" + result[\"reason\"] + \")!\");\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(result[\"code\"] + \" \" + result[\"reason\"]);\n\t\t\t\t\t\t\t\t\t\t\t// Reset status\n\t\t\t\t\t\t\t\t\t\t\tsipcall.hangup();\n\t\t\t\t\t\t\t\t\t\t\t$('#dovideo').removeAttr('disabled').val('');\n\t\t\t\t\t\t\t\t\t\t\t$('#peer').removeAttr('disabled').val('');\n\t\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Call')\n\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-danger\").addClass(\"btn-success\")\n\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doCall);\n\t\t\t\t\t\t\t\t\t\t} else if(event === 'messagedelivery') {\n\t\t\t\t\t\t\t\t\t\t\t// message delivery status\n\t\t\t\t\t\t\t\t\t\t\tlet reason = result[\"reason\"];\n\t\t\t\t\t\t\t\t\t\t\tlet code = result[\"code\"];\n\t\t\t\t\t\t\t\t\t\t\tlet callid = msg['call_id'];\n\t\t\t\t\t\t\t\t\t\t\tif (code == 200) {\n\t\t\t\t\t\t\t\t\t\t\t\ttoastr.success(`${callid} Delivery Status: ${code} ${reason}`);\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\ttoastr.error(`${callid} Delivery Status: ${code} ${reason}`);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideot' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#videoleft video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoleft .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoleft').append('<video class=\"rounded centered\" id=\"myvideot' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideot' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(sipcall.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\tsipcall.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Calling...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Remote track (mid=\" + mid + \") \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideom' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t$('#videoright').parent().find('span').html(\n\t\t\t\t\t\t\t\t\t\t\t'Send DTMF: <span id=\"dtmf\" class=\"btn-group btn-group-xs\"></span>' +\n\t\t\t\t\t\t\t\t\t\t\t'<span id=\"ctrls\" class=\"top-right btn-group btn-group-xs\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t'<button id=\"msg\" title=\"Send message\" class=\"btn btn-info\"><i class=\"fa-solid fa-envelope\"></i></button>' +\n\t\t\t\t\t\t\t\t\t\t\t\t'<button id=\"info\" title=\"Send INFO\" class=\"btn btn-info\"><i class=\"fa-solid fa-info\"></i></button>' +\n\t\t\t\t\t\t\t\t\t\t\t\t'<button id=\"transfer\" title=\"Transfer call\" class=\"btn btn-info\"><i class=\"fa-solid fa-share\"></i></button>' +\n\t\t\t\t\t\t\t\t\t\t\t'</span>');\n\t\t\t\t\t\t\t\t\t\tfor(let i=0; i<12; i++) {\n\t\t\t\t\t\t\t\t\t\t\tif(i<10)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#dtmf').append('<button class=\"btn btn-info dtmf\">' + i + '</button>');\n\t\t\t\t\t\t\t\t\t\t\telse if(i == 10)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#dtmf').append('<button class=\"btn btn-info dtmf\">#</button>');\n\t\t\t\t\t\t\t\t\t\t\telse if(i == 11)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#dtmf').append('<button class=\"btn btn-info dtmf\">*</button>');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#dtmf .dtmf').click(function() {\n\t\t\t\t\t\t\t\t\t\t\t// Send DTMF tone (inband)\n\t\t\t\t\t\t\t\t\t\t\tsipcall.dtmf({dtmf: { tones: $(this).text()}});\n\t\t\t\t\t\t\t\t\t\t\t// Notice you can also send DTMF tones using SIP INFO\n\t\t\t\t\t\t\t\t\t\t\t// \t\tsipcall.send({message: {request: \"dtmf_info\", digit: $(this).text()}});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t$('#msg').click(function() {\n\t\t\t\t\t\t\t\t\t\t\tbootbox.prompt(\"Insert message to send\", function(result) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(result && result !== '') {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Send the message\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet msg = { request: \"message\", content: result };\n\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: msg });\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t$('#info').click(function() {\n\t\t\t\t\t\t\t\t\t\t\tbootbox.dialog({\n\t\t\t\t\t\t\t\t\t\t\t\tmessage: 'Type: <input class=\"form-control\" type=\"text\" id=\"type\" placeholder=\"e.g., application/xml\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<br/>Content: <input class=\"form-control\" type=\"text\" id=\"content\" placeholder=\"e.g., <message>hi</message>\">',\n\t\t\t\t\t\t\t\t\t\t\t\ttitle: \"Insert the type and content to send\",\n\t\t\t\t\t\t\t\t\t\t\t\tbuttons: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tcancel: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Cancel\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-secondary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Do nothing\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tok: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"OK\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Send the INFO\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet type = $('#type').val();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet content = $('#content').val();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(type === '' || content === '')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet msg = { request: \"info\", type: type, content: content };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: msg });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t$('#transfer').click(function() {\n\t\t\t\t\t\t\t\t\t\t\tbootbox.dialog({\n\t\t\t\t\t\t\t\t\t\t\t\tmessage: '<input class=\"form-control\" type=\"text\" id=\"transferto\" placeholder=\"e.g., sip:goofy@example.com\">',\n\t\t\t\t\t\t\t\t\t\t\t\ttitle: \"Insert the address to transfer the call to\",\n\t\t\t\t\t\t\t\t\t\t\t\tbuttons: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tcancel: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Cancel\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-secondary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Do nothing\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tblind: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Blind transfer\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-info\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Start a blind transfer\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet address = $('#transferto').val();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(address === '')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet msg = { request: \"transfer\", uri: address };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: msg });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tattended: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Attended transfer\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Start an attended transfer\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet address = $('#transferto').val();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif(address === '')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Add the call-id to replace to the transfer\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet msg = { request: \"transfer\", uri: address, replace: sipcall.callId };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsipcall.send({ message: msg });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideom' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideom' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideom' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideom' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#videos').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#dtmf').parent().html(\"Remote UA\");\n\t\t\t\t\t\t\t\t\tif(sipcall) {\n\t\t\t\t\t\t\t\t\t\tdelete sipcall.callId;\n\t\t\t\t\t\t\t\t\t\tdelete sipcall.doAudio;\n\t\t\t\t\t\t\t\t\t\tdelete sipcall.doVideo;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tif(field.id == 'server' || field.id == 'username' || field.id == 'password' || field.id == 'displayname')\n\t\t\tregisterUsername();\n\t\telse if(field.id == 'peer')\n\t\t\tdoCall();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction registerUsername() {\n\tif(!selectedApproach) {\n\t\tbootbox.alert(\"Please select a registration approach from the dropdown menu\");\n\t\treturn;\n\t}\n\t// Try a registration\n\t$('#server').attr('disabled', true);\n\t$('#username').attr('disabled', true);\n\t$('#authuser').attr('disabled', true);\n\t$('#displayname').attr('disabled', true);\n\t$('#password').attr('disabled', true);\n\t$('#register').attr('disabled', true).unbind('click');\n\t$('#registerset').attr('disabled', true);\n\t// Let's see if the user provided a server address\n\t// \t\tNOTE WELL! Even though the attribute we set in the request is called \"proxy\",\n\t//\t\tthis is actually the _registrar_. If you want to set an outbound proxy (for this\n\t//\t\tREGISTER request and for all INVITEs that will follow), you'll need to set the\n\t//\t\t\"outbound_proxy\" property in the request instead. The two are of course not\n\t//\t\tmutually exclusive. If you set neither, the domain part of the user identity\n\t//\t\twill be used as the target of the REGISTER request the plugin might send.\n\tlet sipserver = $('#server').val();\n\tif(sipserver !== \"\" && sipserver.indexOf(\"sip:\") != 0 && sipserver.indexOf(\"sips:\") !=0) {\n\t\tbootbox.alert(\"Please insert a valid SIP server (e.g., sip:192.168.0.1:5060)\");\n\t\t$('#server').removeAttr('disabled');\n\t\t$('#username').removeAttr('disabled');\n\t\t$('#authuser').removeAttr('disabled');\n\t\t$('#displayname').removeAttr('disabled');\n\t\t$('#password').removeAttr('disabled');\n\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t$('#registerset').removeAttr('disabled');\n\t\treturn;\n\t}\n\tif(selectedApproach === \"guest\") {\n\t\t// We're registering as guests, no username/secret provided\n\t\tlet register = {\n\t\t\trequest: \"register\",\n\t\t\ttype: \"guest\"\n\t\t};\n\t\tif(sipserver !== \"\") {\n\t\t\tregister[\"proxy\"] = sipserver;\n\t\t\t// Uncomment this if you want to see an outbound proxy too\n\t\t\t//~ register[\"outbound_proxy\"] = \"sip:outbound.example.com\";\n\t\t}\n\t\tlet username = $('#username').val();\n\t\tif(!username === \"\" || username.indexOf(\"sip:\") != 0 || username.indexOf(\"@\") < 0) {\n\t\t\tbootbox.alert(\"Please insert a valid SIP address (e.g., sip:goofy@example.com): this doesn't need to exist for guests, but is required\");\n\t\t\t$('#server').removeAttr('disabled');\n\t\t\t$('#username').removeAttr('disabled');\n\t\t\t$('#authuser').removeAttr('disabled');\n\t\t\t$('#displayname').removeAttr('disabled');\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\t$('#registerset').removeAttr('disabled');\n\t\t\treturn;\n\t\t}\n\t\tregister.username = username;\n\t\tlet displayname = $('#displayname').val();\n\t\tif(displayname) {\n\t\t\tregister.display_name = displayname;\n\t\t}\n\t\tif(sipserver === \"\") {\n\t\t\tbootbox.confirm(\"You didn't specify a SIP Registrar to use: this will cause the plugin to try and conduct a standard (<a href='https://tools.ietf.org/html/rfc3263' target='_blank'>RFC3263</a>) lookup. If this is not what you want or you don't know what this means, hit Cancel and provide a SIP Registrar instead'\",\n\t\t\t\tfunction(result) {\n\t\t\t\t\tif(result) {\n\t\t\t\t\t\tsipcall.send({ message: register });\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$('#server').removeAttr('disabled');\n\t\t\t\t\t\t$('#username').removeAttr('disabled');\n\t\t\t\t\t\t$('#authuser').removeAttr('disabled');\n\t\t\t\t\t\t$('#displayname').removeAttr('disabled');\n\t\t\t\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\t\t\t\t$('#registerset').removeAttr('disabled');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t} else {\n\t\t\tsipcall.send({ message: register });\n\t\t}\n\t\treturn;\n\t}\n\tlet username = $('#username').val();\n\tif(username === \"\" || username.indexOf(\"sip:\") != 0 || username.indexOf(\"@\") < 0) {\n\t\tbootbox.alert('Please insert a valid SIP identity address (e.g., sip:goofy@example.com)');\n\t\t$('#server').removeAttr('disabled');\n\t\t$('#username').removeAttr('disabled');\n\t\t$('#authuser').removeAttr('disabled');\n\t\t$('#displayname').removeAttr('disabled');\n\t\t$('#password').removeAttr('disabled');\n\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t$('#registerset').removeAttr('disabled');\n\t\treturn;\n\t}\n\tlet password = $('#password').val();\n\tif(password === \"\") {\n\t\tbootbox.alert(\"Insert the username secret (e.g., mypassword)\");\n\t\t$('#server').removeAttr('disabled');\n\t\t$('#username').removeAttr('disabled');\n\t\t$('#authuser').removeAttr('disabled');\n\t\t$('#displayname').removeAttr('disabled');\n\t\t$('#password').removeAttr('disabled');\n\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t$('#registerset').removeAttr('disabled');\n\t\treturn;\n\t}\n\tlet register = {\n\t\trequest: \"register\",\n\t\tusername: username\n\t};\n\t// By default, the SIP plugin tries to extract the username part from the SIP\n\t// identity to register; if the username is different, you can provide it here\n\tlet authuser = $('#authuser').val();\n\tif(authuser !== \"\") {\n\t\tregister.authuser = authuser;\n\t}\n\t// The display name is only needed when you want a friendly name to appear when you call someone\n\tlet displayname = $('#displayname').val();\n\tif(displayname !== \"\") {\n\t\tregister.display_name = displayname;\n\t}\n\tif(selectedApproach === \"secret\") {\n\t\t// Use the plain secret\n\t\tregister[\"secret\"] = password;\n\t} else if(selectedApproach === \"ha1secret\") {\n\t\tlet sip_user = username.substring(4, username.indexOf('@'));    /* skip sip: */\n\t\tlet sip_domain = username.substring(username.indexOf('@')+1);\n\t\tregister[\"ha1_secret\"] = md5(sip_user+':'+sip_domain+':'+password);\n\t}\n\t// Should you want the SIP stack to add some custom headers to the\n\t// REGISTER, you can do so by adding an additional \"headers\" object,\n\t// containing each of the headers as key-value, e.g.:\n\t//\t\tregister[\"headers\"] = {\n\t//\t\t\t\"My-Header\": \"value\",\n\t//\t\t\t\"AnotherHeader\": \"another string\"\n\t//\t\t};\n\t// Similarly, a \"contact_params\" object will allow you to\n\t// inject custom Contact URI params, e.g.:\n\t//\t\tregister[\"contact_params\"] = {\n\t//\t\t\t\"pn-provider\": \"acme\",\n\t//\t\t\t\"pn-param\": \"acme-param\",\n\t//\t\t\t\"pn-prid\": \"ZTY4ZDJlMzODE1NmUgKi0K\"\n\t//\t\t};\n\tif(sipserver === \"\") {\n\t\tbootbox.confirm(\"You didn't specify a SIP Registrar: this will cause the plugin to try and conduct a standard (<a href='https://tools.ietf.org/html/rfc3263' target='_blank'>RFC3263</a>) lookup. If this is not what you want or you don't know what this means, hit Cancel and provide a SIP Registrar instead'\",\n\t\t\tfunction(result) {\n\t\t\t\tif(result) {\n\t\t\t\t\tsipcall.send({ message: register });\n\t\t\t\t} else {\n\t\t\t\t\t$('#server').removeAttr('disabled');\n\t\t\t\t\t$('#username').removeAttr('disabled');\n\t\t\t\t\t$('#authuser').removeAttr('disabled');\n\t\t\t\t\t$('#displayname').removeAttr('disabled');\n\t\t\t\t\t$('#password').removeAttr('disabled');\n\t\t\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\t\t\t$('#registerset').removeAttr('disabled');\n\t\t\t\t}\n\t\t\t});\n\t} else {\n\t\tregister[\"proxy\"] = sipserver;\n\t\t// Uncomment this if you want to see an outbound proxy too\n\t\t//~ register[\"outbound_proxy\"] = \"sip:outbound.example.com\";\n\t\tsipcall.send({ message: register });\n\t}\n}\n\nfunction doCall(ev) {\n\t// Call someone (from the main session or one of the helpers)\n\tlet button = ev ? ev.currentTarget.id : \"call\";\n\tlet helperId = button.split(\"call\")[1];\n\tif(helperId === \"\")\n\t\thelperId = null;\n\telse\n\t\thelperId = parseInt(helperId);\n\tlet handle = helperId ? helpers[helperId].sipcall : sipcall;\n\tlet prefix = helperId ? (\"[Helper #\" + helperId + \"]\") : \"\";\n\tlet suffix = helperId ? (\"\"+helperId) : \"\";\n\t$('#peer' + suffix).attr('disabled', true);\n\t$('#call' + suffix).attr('disabled', true).unbind('click');\n\t$('#dovideo' + suffix).attr('disabled', true);\n\tlet username = $('#peer' + suffix).val();\n\tif(username === \"\") {\n\t\tbootbox.alert('Please insert a valid SIP address (e.g., sip:pluto@example.com)');\n\t\t$('#peer' + suffix).removeAttr('disabled');\n\t\t$('#dovideo' + suffix).removeAttr('disabled');\n\t\t$('#call' + suffix).removeAttr('disabled').click(function(ev) { doCall(ev); });\n\t\treturn;\n\t}\n\tif(username.indexOf(\"sip:\") != 0 || username.indexOf(\"@\") < 0) {\n\t\tbootbox.alert('Please insert a valid SIP address (e.g., sip:pluto@example.com)');\n\t\t$('#peer' + suffix).removeAttr('disabled').val(\"\");\n\t\t$('#dovideo' + suffix).removeAttr('disabled').val(\"\");\n\t\t$('#call' + suffix).removeAttr('disabled').click(function(ev) { doCall(ev); });\n\t\treturn;\n\t}\n\t// Call this URI\n\tlet doVideo = $('#dovideo' + suffix).is(':checked');\n\tJanus.log(prefix + \"This is a SIP \" + (doVideo ? \"video\" : \"audio\") + \" call (dovideo=\" + doVideo + \")\");\n\tactuallyDoCall(handle, $('#peer' + suffix).val(), doVideo);\n}\nfunction actuallyDoCall(handle, uri, doVideo, referId) {\n\t// We want bidirectional audio for sure, and maybe video\n\thandle.doAudio = true;\n\thandle.doVideo = doVideo;\n\tlet tracks = [{ type: 'audio', capture: true, recv: true }];\n\tif(doVideo)\n\t\ttracks.push({ type: 'video', capture: true, recv: true });\n\thandle.createOffer(\n\t\t{\n\t\t\ttracks: tracks,\n\t\t\tsuccess: function(jsep) {\n\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t// By default, you only pass the SIP URI to call as an\n\t\t\t\t// argument to a \"call\" request. Should you want the\n\t\t\t\t// SIP stack to add some custom headers to the INVITE,\n\t\t\t\t// you can do so by adding an additional \"headers\" object,\n\t\t\t\t// containing each of the headers as key-value, e.g.:\n\t\t\t\t//\t\tlet body = { request: \"call\", uri: $('#peer').val(),\n\t\t\t\t//\t\t\theaders: {\n\t\t\t\t//\t\t\t\t\"My-Header\": \"value\",\n\t\t\t\t//\t\t\t\t\"AnotherHeader\": \"another string\"\n\t\t\t\t//\t\t\t}\n\t\t\t\t//\t\t};\n\t\t\t\tlet body = { request: \"call\", uri: uri };\n\t\t\t\t// Note: you can also ask the plugin to negotiate SDES-SRTP, instead of the\n\t\t\t\t// default plain RTP, by adding a \"srtp\" attribute to the request. Valid\n\t\t\t\t// values are \"sdes_optional\" and \"sdes_mandatory\", e.g.:\n\t\t\t\t//\t\tlet body = { request: \"call\", uri: $('#peer').val(), srtp: \"sdes_optional\" };\n\t\t\t\t// \"sdes_optional\" will negotiate RTP/AVP and add a crypto line,\n\t\t\t\t// \"sdes_mandatory\" will set the protocol to RTP/SAVP instead.\n\t\t\t\t// Just beware that some endpoints will NOT accept an INVITE\n\t\t\t\t// with a crypto line in it if the protocol is not RTP/SAVP,\n\t\t\t\t// so if you want SDES use \"sdes_optional\" with care.\n\t\t\t\t// Note 2: by default, the SIP plugin auto-answers incoming\n\t\t\t\t// re-INVITEs, without involving the browser/client: this is\n\t\t\t\t// for backwards compatibility with older Janus clients that\n\t\t\t\t// may not be able to handle them. Since we want to receive\n\t\t\t\t// re-INVITES to handle them ourselves, we specify it here:\n\t\t\t\tbody[\"autoaccept_reinvites\"] = false;\n\t\t\t\tif(referId) {\n\t\t\t\t\t// In case we're originating this call because of a call\n\t\t\t\t\t// transfer, we need to provide the internal reference ID\n\t\t\t\t\tbody[\"refer_id\"] = referId;\n\t\t\t\t}\n\t\t\t\thandle.send({ message: body, jsep: jsep });\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"WebRTC error...\", error);\n\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t}\n\t\t});\n}\n\nfunction doHangup(ev) {\n\t// Hangup a call (on the main session or one of the helpers)\n\tlet button = ev ? ev.currentTarget.id : \"call\";\n\tlet helperId = button.split(\"call\")[1];\n\tif(helperId === \"\")\n\t\thelperId = null;\n\telse\n\t\thelperId = parseInt(helperId);\n\tif(!helperId) {\n\t\t$('#call').attr('disabled', true).unbind('click');\n\t\tlet hangup = { request: \"hangup\" };\n\t\tsipcall.send({ message: hangup });\n\t\tsipcall.hangup();\n\t} else {\n\t\t$('#call' + helperId).attr('disabled', true).unbind('click');\n\t\tlet hangup = { request: \"hangup\" };\n\t\thelpers[helperId].sipcall.send({ message: hangup });\n\t\thelpers[helperId].sipcall.hangup();\n\t}\n}\n\n// The following code is only needed if you're interested in supporting multiple\n// calls at the same time. As explained in the Janus documentation, each Janus\n// handle can only do one PeerConnection at a time, which means you normally\n// cannot do multiple calls. If that's something you need (e.g., because you\n// need to do a SIP transfer, or want to be in two calls), then the SIP plugin\n// provides the so-called \"helpers\": basically additional handles attached to\n// the SIP plugin, and associated to your SIP identity. They can be used to\n// originate and receive calls exactly as the main handle: notice that incoming\n// calls will be rejected with a \"486 Busy\" if you're in a call already and there\n// are no available \"helpers\", which means you should add one in advance for that.\n// In this demo, creating a \"helper\" adds a new row for calls that looks and\n// works exactly as the default one: you can add more than one \"helper\", and\n// obviously the more you have, the more concurrent calls you can have.\nfunction addHelper(helperCreated) {\n\thelperCreated = (typeof helperCreated == \"function\") ? helperCreated : Janus.noop;\n\thelpersCount++;\n\tlet helperId = helpersCount;\n\thelpers[helperId] = { id: helperId,\n\t\tlocalTracks: {}, localVideos: 0,\n\t\tremoteTracks: {}, remoteVideos: 0 };\n\t// Add another row with a new \"phone\"\n\t$('.footer').before(\n\t\t'<div class=\"container\" id=\"sipcall' + helperId + '\">' +\n\t\t'\t<div class=\"row\">' +\n\t\t'\t\t<div class=\"col-md-6 container\">' +\n\t\t'\t\t\t<span class=\"badge bg-info\">Helper #' + helperId +\n\t\t'\t\t\t\t<i class=\"fa-solid fa-rectangle-xmark\" id=\"rmhelper' + helperId + '\" style=\"cursor: pointer;\" title=\"Remove this helper\"></i>' +\n\t\t'\t\t\t</span>' +\n\t\t'\t\t</div>' +\n\t\t'\t\t<div class=\"col-md-6 container\" id=\"phone' + helperId + '\">' +\n\t\t'\t\t\t<div class=\"input-group mt-1 mb-1\">' +\n\t\t'\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-phone\"></i></span>' +\n\t\t'\t\t\t\t<input disabled class=\"form-control\" type=\"text\" placeholder=\"SIP URI to call (e.g., sip:1000@example.com)\" autocomplete=\"off\" id=\"peer' + helperId + '\" onkeypress=\"return checkEnter(this, event, ' + helperId + ');\"></input>' +\n\t\t'\t\t\t</div>' +\n\t\t'\t\t\t<button disabled class=\"btn btn-success mb-1\" autocomplete=\"off\" id=\"call' + helperId + '\">Call</button> <input autocomplete=\"off\" id=\"dovideo' + helperId + '\" type=\"checkbox\">Use Video</input>' +\n\t\t'\t\t</div>' +\n\t\t'\t</div>' +\n\t\t'\t<div id=\"videos' + helperId + '\" class=\"row mt-2 mb-2 hide\">' +\n\t\t'\t\t<div class=\"col-md-6\">' +\n\t\t'\t\t\t<div class=\"card\">' +\n\t\t'\t\t\t\t<div class=\"card-header\">' +\n\t\t'\t\t\t\t\t<span class=\"card-title\">You</span>' +\n\t\t'\t\t\t\t</div>' +\n\t\t'\t\t\t\t<div class=\"card-body\" id=\"videoleft' + helperId + '\"></div>' +\n\t\t'\t\t\t</div>' +\n\t\t'\t\t</div>' +\n\t\t'\t\t<div class=\"col-md-6\">' +\n\t\t'\t\t\t<div class=\"card\">' +\n\t\t'\t\t\t\t<div class=\"card-header\">' +\n\t\t'\t\t\t\t\t<span class=\"card-title\">Remote UA</span>' +\n\t\t'\t\t\t\t</div>' +\n\t\t'\t\t\t\t<div class=\"card-body\" id=\"videoright' + helperId + '\"></div>' +\n\t\t'\t\t\t</div>' +\n\t\t'\t\t</div>' +\n\t\t'\t</div>' +\n\t\t'</div>'\n\t);\n\t$('#rmhelper' + helperId).click(function() {\n\t\tlet hid = $(this).attr('id').split(\"rmhelper\")[1];\n\t\tconsole.log(hid);\n\t\tremoveHelper(hid);\n\t});\n\t// Attach to SIP plugin, but only register as an helper for the master session\n\tjanus.attach(\n\t\t{\n\t\t\tplugin: \"janus.plugin.sip\",\n\t\t\topaqueId: opaqueId,\n\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\thelpers[helperId].sipcall = pluginHandle;\n\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Plugin attached! (\" + helpers[helperId].sipcall.getPlugin() + \", id=\" + helpers[helperId].sipcall.getId() + \")\");\n\t\t\t\t// TODO Send the \"register\"\n\t\t\t\thelpers[helperId].sipcall.send({\n\t\t\t\t\tmessage: {\n\t\t\t\t\t\trequest: \"register\",\n\t\t\t\t\t\ttype: \"helper\",\n\t\t\t\t\t\tusername: $('#username').val(),\t// We use the same username as the master session\n\t\t\t\t\t\tmaster_id: masterId\t\t\t\t// Then we add the ID of the master session, nothing else\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"[Helper #\" + helperId + \"]   -- Error attaching plugin...\", error);\n\t\t\t\tbootbox.alert(\"  -- Error attaching plugin... \" + error);\n\t\t\t\tremoveHelper(helperId);\n\t\t\t},\n\t\t\tconsentDialog: function(on) {\n\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"] Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\tif(on) {\n\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t} });\n\t\t\t\t} else {\n\t\t\t\t\t// Restore screen\n\t\t\t\t\t$.unblockUI();\n\t\t\t\t}\n\t\t\t},\n\t\t\ticeState: function(state) {\n\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] ICE state changed to \" + state);\n\t\t\t},\n\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t},\n\t\t\twebrtcState: function(on) {\n\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t$(\"#videoleft\" + helperId).parent().unblock();\n\t\t\t},\n\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t},\n\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"]  ::: Got a message :::\", msg);\n\t\t\t\t// Any error?\n\t\t\t\tlet error = msg[\"error\"];\n\t\t\t\tif(error) {\n\t\t\t\t\tbootbox.alert(error);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet callId = msg[\"call_id\"];\n\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\tif(result && result[\"event\"]) {\n\t\t\t\t\tlet event = result[\"event\"];\n\t\t\t\t\tif(event === 'registration_failed') {\n\t\t\t\t\t\tJanus.warn(\"[Helper #\" + helperId + \"] Registration failed: \" + result[\"code\"] + \" \" + result[\"reason\"]);\n\t\t\t\t\t\tbootbox.alert(result[\"code\"] + \" \" + result[\"reason\"]);\n\t\t\t\t\t\t// Get rid of the helper\n\t\t\t\t\t\tremoveHelper(helperId);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif(event === 'registered') {\n\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Successfully registered as \" + result[\"username\"] + \"!\");\n\t\t\t\t\t\t// Unlock the \"phone\" controls\n\t\t\t\t\t\t$('#peer' + helperId).removeAttr('disabled');\n\t\t\t\t\t\t$('#call' + helperId).removeAttr('disabled').html('Call')\n\t\t\t\t\t\t\t.removeClass(\"btn-danger\").addClass(\"btn-success\")\n\t\t\t\t\t\t\t.unbind('click').click(doCall);\n\t\t\t\t\t\tif(helperCreated)\n\t\t\t\t\t\t\thelperCreated(helperId);\n\t\t\t\t\t} else if(event === 'calling') {\n\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Waiting for the peer to answer...\");\n\t\t\t\t\t\t// TODO Any ringtone?\n\t\t\t\t\t\t$('#call' + helperId).removeAttr('disabled').html('Hangup')\n\t\t\t\t\t\t\t.removeClass(\"btn-success\").addClass(\"btn-danger\")\n\t\t\t\t\t\t\t.unbind('click').click(doHangup);\n\t\t\t\t\t} else if(event === 'incomingcall') {\n\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Incoming call from \" + result[\"username\"] + \"! (on helper #\" + helperId + \")\");\n\t\t\t\t\t\thelpers[helperId].sipcall.callId = callId;\n\t\t\t\t\t\tlet doAudio = true, doVideo = true;\n\t\t\t\t\t\tlet offerlessInvite = false;\n\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t// What has been negotiated?\n\t\t\t\t\t\t\tdoAudio = (jsep.sdp.indexOf(\"m=audio \") > -1);\n\t\t\t\t\t\t\tdoVideo = (jsep.sdp.indexOf(\"m=video \") > -1);\n\t\t\t\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"] Audio \" + (doAudio ? \"has\" : \"has NOT\") + \" been negotiated\");\n\t\t\t\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"] Video \" + (doVideo ? \"has\" : \"has NOT\") + \" been negotiated\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] This call doesn't contain an offer... we'll need to provide one ourselves\");\n\t\t\t\t\t\t\tofferlessInvite = true;\n\t\t\t\t\t\t\t// In case you want to offer video when reacting to an offerless call, set this to true\n\t\t\t\t\t\t\tdoVideo = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Is this the result of a transfer?\n\t\t\t\t\t\tlet transfer = \"\";\n\t\t\t\t\t\tlet referredBy = result[\"referred_by\"];\n\t\t\t\t\t\tlet replaces = result[\"replaces\"];\n\t\t\t\t\t\tif(referredBy && replaces)\n\t\t\t\t\t\t\ttransfer = \" (referred by \" + referredBy + \", replaces call-ID \" + replaces + \")\";\n\t\t\t\t\t\telse if(referredBy && !replaces)\n\t\t\t\t\t\t\ttransfer = \" (referred by \" + referredBy + \")\";\n\t\t\t\t\t\telse if(!referredBy && replaces)\n\t\t\t\t\t\t\ttransfer = \" (replaces call-ID \" + replaces + \")\";\n\t\t\t\t\t\ttransfer = transfer.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\ttransfer = transfer.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\t// Any security offered? A missing \"srtp\" attribute means plain RTP\n\t\t\t\t\t\tlet rtpType = \"\";\n\t\t\t\t\t\tlet srtp = result[\"srtp\"];\n\t\t\t\t\t\tif(srtp === \"sdes_optional\")\n\t\t\t\t\t\t\trtpType = \" (SDES-SRTP offered)\";\n\t\t\t\t\t\telse if(srtp === \"sdes_mandatory\")\n\t\t\t\t\t\t\trtpType = \" (SDES-SRTP mandatory)\";\n\t\t\t\t\t\t// Notify user\n\t\t\t\t\t\tbootbox.hideAll();\n\t\t\t\t\t\tlet extra = \"\";\n\t\t\t\t\t\tif(offerlessInvite)\n\t\t\t\t\t\t\textra = \" (no SDP offer provided)\"\n\t\t\t\t\t\tincoming = bootbox.dialog({\n\t\t\t\t\t\t\tmessage: \"Incoming call from \" + result[\"username\"] + \"!\" + transfer + rtpType + extra + \" (on helper #\" + helperId + \")\",\n\t\t\t\t\t\t\ttitle: \"Incoming call (helper \" + helperId + \")\",\n\t\t\t\t\t\t\tcloseButton: false,\n\t\t\t\t\t\t\tbuttons: {\n\t\t\t\t\t\t\t\tsuccess: {\n\t\t\t\t\t\t\t\t\tlabel: \"Answer\",\n\t\t\t\t\t\t\t\t\tclassName: \"btn-success\",\n\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\tincoming = null;\n\t\t\t\t\t\t\t\t\t\t$('#peer' + helperId).val(result[\"username\"]).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t// Notice that we can only answer if we got an offer: if this was\n\t\t\t\t\t\t\t\t\t\t// an offerless call, we'll need to create an offer ourselves\n\t\t\t\t\t\t\t\t\t\tlet sipcallAction = (offerlessInvite ? helpers[helperId].sipcall.createOffer : helpers[helperId].sipcall.createAnswer);\n\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and/or video\n\t\t\t\t\t\t\t\t\t\tlet tracks = [];\n\t\t\t\t\t\t\t\t\t\tif(doAudio)\n\t\t\t\t\t\t\t\t\t\t\ttracks.push({ type: 'audio', capture: true, recv: true });\n\t\t\t\t\t\t\t\t\t\tif(doVideo)\n\t\t\t\t\t\t\t\t\t\t\ttracks.push({ type: 'video', capture: true, recv: true });\n\t\t\t\t\t\t\t\t\t\tsipcallAction(\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\ttracks: tracks,\n\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"] Got SDP \" + jsep.type + \"! audio=\" + doAudio + \", video=\" + doVideo + \":\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.doAudio = doAudio;\n\t\t\t\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.doVideo = doVideo;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"accept\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Note: as with \"call\", you can add a \"srtp\" attribute to\n\t\t\t\t\t\t\t\t\t\t\t\t\t// negotiate/mandate SDES support for this incoming call.\n\t\t\t\t\t\t\t\t\t\t\t\t\t// The default behaviour is to automatically use it if\n\t\t\t\t\t\t\t\t\t\t\t\t\t// the caller negotiated it, but you may choose to require\n\t\t\t\t\t\t\t\t\t\t\t\t\t// SDES support by setting \"srtp\" to \"sdes_mandatory\", e.g.:\n\t\t\t\t\t\t\t\t\t\t\t\t\t//\t\tlet body = { request: \"accept\", srtp: \"sdes_mandatory\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t// This way you'll tell the plugin to accept the call, but ONLY\n\t\t\t\t\t\t\t\t\t\t\t\t\t// if SDES is available, and you don't want plain RTP. If it\n\t\t\t\t\t\t\t\t\t\t\t\t\t// is not available, you'll get an error (452) back. You can\n\t\t\t\t\t\t\t\t\t\t\t\t\t// also specify the SRTP profile to negotiate by setting the\n\t\t\t\t\t\t\t\t\t\t\t\t\t// \"srtp_profile\" property accordingly (the default if not\n\t\t\t\t\t\t\t\t\t\t\t\t\t// set in the request is \"AES_CM_128_HMAC_SHA1_80\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Note 2: by default, the SIP plugin auto-answers incoming\n\t\t\t\t\t\t\t\t\t\t\t\t\t// re-INVITEs, without involving the browser/client: this is\n\t\t\t\t\t\t\t\t\t\t\t\t\t// for backwards compatibility with older Janus clients that\n\t\t\t\t\t\t\t\t\t\t\t\t\t// may not be able to handle them. Since we want to receive\n\t\t\t\t\t\t\t\t\t\t\t\t\t// re-INVITES to handle them ourselves, we specify it here:\n\t\t\t\t\t\t\t\t\t\t\t\t\tbody[\"autoaccept_reinvites\"] = false;\n\t\t\t\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#call' + helperId).removeAttr('disabled').html('Hangup')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-success\").addClass(\"btn-danger\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doHangup);\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"[Helper #\" + helperId + \"] WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Don't keep the caller waiting any longer, but use a 480 instead of the default 486 to clarify the cause\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"decline\", code: 480 };\n\t\t\t\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: body });\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdanger: {\n\t\t\t\t\t\t\t\t\tlabel: \"Decline\",\n\t\t\t\t\t\t\t\t\tclassName: \"btn-danger\",\n\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\tincoming = null;\n\t\t\t\t\t\t\t\t\t\tlet body = { request: \"decline\" };\n\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: body });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if(event === 'accepting') {\n\t\t\t\t\t\t// Response to an offerless INVITE, let's wait for an 'accepted'\n\t\t\t\t\t} else if(event === 'progress') {\n\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] There's early media from \" + result[\"username\"] + \", wairing for the call!\", jsep);\n\t\t\t\t\t\t// Call can start already: handle the remote answer\n\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\thelpers[helperId].sipcall.handleRemoteJsep({ jsep: jsep, error: function() {\n\t\t\t\t\t\t\t\t// Simulate an hangup from this helper's button\n\t\t\t\t\t\t\t\tdoHangup({ currentTarget: { id: \"call\" + helperId } });\n\t\t\t\t\t\t\t}});\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttoastr.info(\"Early media...\");\n\t\t\t\t\t} else if(event === 'accepted') {\n\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] \" + result[\"username\"] + \" accepted the call!\", jsep);\n\t\t\t\t\t\t// Call can start, now: handle the remote answer\n\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\thelpers[helperId].sipcall.handleRemoteJsep({ jsep: jsep, error: function() {\n\t\t\t\t\t\t\t\t// Simulate an hangup from this helper's button\n\t\t\t\t\t\t\t\tdoHangup({ currentTarget: { id: \"call\" + helperId } });\n\t\t\t\t\t\t\t}});\n\t\t\t\t\t\t}\n\t\t\t\t\t\thelpers[helperId].sipcall.callId = callId;\n\t\t\t\t\t\ttoastr.success(\"Call accepted!\");\n\t\t\t\t\t} else if(event === 'updatingcall') {\n\t\t\t\t\t\t// We got a re-INVITE: while we may prompt the user (e.g.,\n\t\t\t\t\t\t// to notify about media changes), to keep things simple\n\t\t\t\t\t\t// we just accept the update and send an answer right away\n\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Got re-INVITE\");\n\t\t\t\t\t\tlet doAudio = (jsep.sdp.indexOf(\"m=audio \") > -1),\n\t\t\t\t\t\t\tdoVideo = (jsep.sdp.indexOf(\"m=video \") > -1);\n\t\t\t\t\t\t// We want bidirectional audio and/or video, but only\n\t\t\t\t\t\t// populate tracks if we weren't sending something before\n\t\t\t\t\t\tlet tracks = [];\n\t\t\t\t\t\tif(doAudio && !sipcall.doAudio) {\n\t\t\t\t\t\t\thelpers[helperId].sipcall.doAudio = true;\n\t\t\t\t\t\t\ttracks.push({ type: 'audio', capture: true, recv: true });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(doVideo && !sipcall.doVideo) {\n\t\t\t\t\t\t\thelpers[helperId].sipcall.doVideo = true;\n\t\t\t\t\t\t\ttracks.push({ type: 'video', capture: true, recv: true });\n\t\t\t\t\t\t}\n\t\t\t\t\t\thelpers[helperId].sipcall.createAnswer(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\ttracks: tracks,\n\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"] Got SDP \" + jsep.type + \"! audio=\" + doAudio + \", video=\" + doVideo + \":\", jsep);\n\t\t\t\t\t\t\t\t\tlet body = { request: \"update\" };\n\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"[Helper #\" + helperId + \"] WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t} else if(event === 'message') {\n\t\t\t\t\t\t// We got a MESSAGE\n\t\t\t\t\t\tlet sender = result[\"displayname\"] ? result[\"displayname\"] : result[\"sender\"];\n\t\t\t\t\t\tlet content = result[\"content\"];\n\t\t\t\t\t\tcontent = content.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\tcontent = content.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\ttoastr.success(content, \"Message from \" + sender);\n\t\t\t\t\t} else if(event === 'info') {\n\t\t\t\t\t\t// We got an INFO\n\t\t\t\t\t\tlet sender = result[\"displayname\"] ? result[\"displayname\"] : result[\"sender\"];\n\t\t\t\t\t\tlet content = result[\"content\"];\n\t\t\t\t\t\tcontent = content.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\tcontent = content.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\ttoastr.info(content, \"Info from \" + sender);\n\t\t\t\t\t} else if(event === 'notify') {\n\t\t\t\t\t\t// We got a NOTIFY\n\t\t\t\t\t\tlet notify = result[\"notify\"];\n\t\t\t\t\t\tlet content = result[\"content\"];\n\t\t\t\t\t\ttoastr.info(content, \"Notify (\" + notify + \")\");\n\t\t\t\t\t} else if(event === 'transfer') {\n\t\t\t\t\t\t// We're being asked to transfer the call, ask the user what to do\n\t\t\t\t\t\tlet referTo = result[\"refer_to\"];\n\t\t\t\t\t\tlet referredBy = result[\"referred_by\"] ? result[\"referred_by\"] : \"an unknown party\";\n\t\t\t\t\t\tlet referId = result[\"refer_id\"];\n\t\t\t\t\t\tlet replaces = result[\"replaces\"];\n\t\t\t\t\t\tlet extra = (\"referred by \" + referredBy);\n\t\t\t\t\t\tif(replaces)\n\t\t\t\t\t\t\textra += (\", replaces call-ID \" + replaces);\n\t\t\t\t\t\textra = extra.replace(new RegExp('<', 'g'), '&lt');\n\t\t\t\t\t\textra = extra.replace(new RegExp('>', 'g'), '&gt');\n\t\t\t\t\t\tbootbox.confirm(\"Transfer the call to \" + referTo + \"? (\" + extra + \", helper \" + helperId + \")\",\n\t\t\t\t\t\t\tfunction(result) {\n\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t// Call the person we're being transferred to\n\t\t\t\t\t\t\t\t\tif(!helpers[helperId].sipcall.webrtcStuff.pc) {\n\t\t\t\t\t\t\t\t\t\t// Do it here\n\t\t\t\t\t\t\t\t\t\t$('#peer' + helperId).val(referTo).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\tactuallyDoCall(helpers[helperId].sipcall, referTo, false, referId);\n\t\t\t\t\t\t\t\t\t} else if(!sipcall.webrtcStuff.pc) {\n\t\t\t\t\t\t\t\t\t\t// Do it on the main handle\n\t\t\t\t\t\t\t\t\t\t$('#peer').val(referTo).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\tactuallyDoCall(sipcall, referTo, false, referId);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// We're in a call already, use the main handle or a helper\n\t\t\t\t\t\t\t\t\t\tlet h = -1;\n\t\t\t\t\t\t\t\t\t\tif(Object.keys(helpers).length > 0) {\n\t\t\t\t\t\t\t\t\t\t\t// See if any of the helpers if available\n\t\t\t\t\t\t\t\t\t\t\tfor(let i in helpers) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!helpers[i].sipcall.webrtcStuff.pc) {\n\t\t\t\t\t\t\t\t\t\t\t\t\th = parseInt(i);\n\t\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(h !== -1) {\n\t\t\t\t\t\t\t\t\t\t\t// Do in this helper\n\t\t\t\t\t\t\t\t\t\t\t$('#peer' + h).val(referTo).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tactuallyDoCall(helpers[h].sipcall, referTo, false, referId);\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t// Create a new helper\n\t\t\t\t\t\t\t\t\t\t\taddHelper(function(id) {\n\t\t\t\t\t\t\t\t\t\t\t\t// Do it here\n\t\t\t\t\t\t\t\t\t\t\t\t$('#peer' + id).val(referTo).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\tactuallyDoCall(helpers[id].sipcall, referTo, false, referId);\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// We're rejecting the transfer\n\t\t\t\t\t\t\t\t\tlet body = { request: \"decline\", refer_id: referId };\n\t\t\t\t\t\t\t\t\tsipcall.send({ message: body });\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t} else if(event === 'hangup') {\n\t\t\t\t\t\tif(incoming != null) {\n\t\t\t\t\t\t\tincoming.modal('hide');\n\t\t\t\t\t\t\tincoming = null;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Call hung up (\" + result[\"code\"] + \" \" + result[\"reason\"] + \")!\");\n\t\t\t\t\t\tbootbox.alert(result[\"code\"] + \" \" + result[\"reason\"]);\n\t\t\t\t\t\t// Reset status\n\t\t\t\t\t\thelpers[helperId].sipcall.hangup();\n\t\t\t\t\t\t$('#dovideo' + helperId).removeAttr('disabled').val('');\n\t\t\t\t\t\t$('#peer' + helperId).removeAttr('disabled').val('');\n\t\t\t\t\t\t$('#call' + helperId).removeAttr('disabled').html('Call')\n\t\t\t\t\t\t\t.removeClass(\"btn-danger\").addClass(\"btn-success\")\n\t\t\t\t\t\t\t.unbind('click').click(doCall);\n\t\t\t\t\t} else if(event === 'messagedelivery') {\n\t\t\t\t\t\t// message delivery status\n\t\t\t\t\t\tlet reason = result[\"reason\"];\n\t\t\t\t\t\tlet code = result[\"code\"];\n\t\t\t\t\t\tlet callid = msg['call_id'];\n\t\t\t\t\t\tif (code == 200) {\n\t\t\t\t\t\t\ttoastr.success(`${callid}/${helperId} Delivery Status: ${code} ${reason}`);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttoastr.error(`${callid}/${helperId} Delivery Status: ${code} ${reason}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"] Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\tif(!on) {\n\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\tlet stream = helpers[helperId].localTracks[trackId];\n\t\t\t\t\tif(stream) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t}\n\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t$('#myvideo' + helperId + 't' + trackId).remove();\n\t\t\t\t\t\thelpers[helperId].localVideos--;\n\t\t\t\t\t\tif(helpers[helperId].localVideos === 0) {\n\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\tif($('#videoleft' + helperId + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t$('#videoleft' + helperId).append(\n\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete helpers[helperId].localTracks[trackId];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// If we're here, a new track was added\n\t\t\t\tlet stream = helpers[helperId].localTracks[trackId];\n\t\t\t\tif(stream) {\n\t\t\t\t\t// We've been here already\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif($('#videoleft' + helperId + ' video').length === 0) {\n\t\t\t\t\t$('#videos' + helperId).removeClass('hide');\n\t\t\t\t}\n\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\tif(helpers[helperId].localVideos === 0) {\n\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\tif($('#videoleft' + helperId + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t$('#videoleft' + helperId).append(\n\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\thelpers[helperId].localVideos++;\n\t\t\t\t\t$('#videoleft' + helperId + ' .no-video-container').remove();\n\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\thelpers[helperId].localTracks[trackId] = stream;\n\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Created local stream:\", stream);\n\t\t\t\t\t$('#videoleft' + helperId).append('<video class=\"rounded centered\" id=\"myvideo' + helperId + 't' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + helperId + 't' + trackId).get(0), stream);\n\t\t\t\t}\n\t\t\t\tif(helpers[helperId].sipcall.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\thelpers[helperId].sipcall.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t$(\"#videoleft\" + helperId).parent().block({\n\t\t\t\t\t\tmessage: '<b>Calling...</b>',\n\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\tJanus.debug(\"[Helper #\" + helperId + \"] Remote track (mid=\" + mid + \") \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\tif(!on) {\n\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t$('#peervideo' + helperId + 'm' + mid).remove();\n\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\tif($('#videoright' + helperId + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete helpers[helperId].remoteTracks[mid];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// If we're here, a new track was added\n\t\t\t\tif($('#videoright' + helperId + ' audio').length === 0 && $('#videoright' + helperId + ' video').length === 0) {\n\t\t\t\t\t$('#videos' + helperId).removeClass('hide');\n\t\t\t\t\t$('#videoright' + helperId).parent().find('span').html(\n\t\t\t\t\t\t'Send DTMF: <span id=\"dtmf' + helperId + '\" class=\"btn-group btn-group-xs\"></span>' +\n\t\t\t\t\t\t'<span id=\"ctrls\" class=\"top-right btn-group btn-group-xs\">' +\n\t\t\t\t\t\t\t'<button id=\"msg' + helperId + '\" title=\"Send message\" class=\"btn btn-info\"><i class=\"fa-solid fa-envelope\"></i></button>' +\n\t\t\t\t\t\t\t'<button id=\"info' + helperId + '\" title=\"Send INFO\" class=\"btn btn-info\"><i class=\"fa-solid fa-info\"></i></button>' +\n\t\t\t\t\t\t\t'<button id=\"transfer' + helperId + '\" title=\"Transfer call\" class=\"btn btn-info\"><i class=\"fa-solid fa-share\"></i></button>' +\n\t\t\t\t\t\t'</span>');\n\t\t\t\t\tfor(let i=0; i<12; i++) {\n\t\t\t\t\t\tif(i<10)\n\t\t\t\t\t\t\t$('#dtmf' + helperId).append('<button class=\"btn btn-info dtmf\">' + i + '</button>');\n\t\t\t\t\t\telse if(i == 10)\n\t\t\t\t\t\t\t$('#dtmf' + helperId).append('<button class=\"btn btn-info dtmf\">#</button>');\n\t\t\t\t\t\telse if(i == 11)\n\t\t\t\t\t\t\t$('#dtmf' + helperId).append('<button class=\"btn btn-info dtmf\">*</button>');\n\t\t\t\t\t}\n\t\t\t\t\t$('#dtmf' + helperId + ' .dtmf').click(function() {\n\t\t\t\t\t\t// Send DTMF tone (inband)\n\t\t\t\t\t\thelpers[helperId].sipcall.dtmf({dtmf: { tones: $(this).text()}});\n\t\t\t\t\t\t// Notice you can also send DTMF tones using SIP INFO\n\t\t\t\t\t\t// \t\thelpers[helperId].sipcall.send({ message: { request: \"dtmf_info\", digit: $(this).text() }});\n\t\t\t\t\t});\n\t\t\t\t\t$('#msg' + helperId).click(function() {\n\t\t\t\t\t\tbootbox.prompt(\"Insert message to send\", function(result) {\n\t\t\t\t\t\t\tif(result && result !== '') {\n\t\t\t\t\t\t\t\t// Send the message\n\t\t\t\t\t\t\t\tlet msg = { request: \"message\", content: result };\n\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: msg });\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t\t$('#info' + helperId).click(function() {\n\t\t\t\t\t\tbootbox.dialog({\n\t\t\t\t\t\t\tmessage: 'Type: <input class=\"form-control\" type=\"text\" id=\"type\" placeholder=\"e.g., application/xml\">' +\n\t\t\t\t\t\t\t\t'<br/>Content: <input class=\"form-control\" type=\"text\" id=\"content\" placeholder=\"e.g., <message>hi</message>\">',\n\t\t\t\t\t\t\ttitle: \"Insert the type and content to send\",\n\t\t\t\t\t\t\tbuttons: {\n\t\t\t\t\t\t\t\tcancel: {\n\t\t\t\t\t\t\t\t\tlabel: \"Cancel\",\n\t\t\t\t\t\t\t\t\tclassName: \"btn-secondary\",\n\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t// Do nothing\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tok: {\n\t\t\t\t\t\t\t\t\tlabel: \"OK\",\n\t\t\t\t\t\t\t\t\tclassName: \"btn-primary\",\n\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t// Send the INFO\n\t\t\t\t\t\t\t\t\t\tlet type = $('#type').val();\n\t\t\t\t\t\t\t\t\t\tlet content = $('#content').val();\n\t\t\t\t\t\t\t\t\t\tif(type === '' || content === '')\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\tlet msg = { request: \"info\", type: type, content: content };\n\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: msg });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t\t$('#transfer' + helperId).click(function() {\n\t\t\t\t\t\tbootbox.dialog({\n\t\t\t\t\t\t\tmessage: '<input class=\"form-control\" type=\"text\" id=\"transferto\" placeholder=\"e.g., sip:goofy@example.com\">',\n\t\t\t\t\t\t\ttitle: \"Insert the address to transfer the call to\",\n\t\t\t\t\t\t\tbuttons: {\n\t\t\t\t\t\t\t\tcancel: {\n\t\t\t\t\t\t\t\t\tlabel: \"Cancel\",\n\t\t\t\t\t\t\t\t\tclassName: \"btn-secondary\",\n\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t// Do nothing\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tblind: {\n\t\t\t\t\t\t\t\t\tlabel: \"Blind transfer\",\n\t\t\t\t\t\t\t\t\tclassName: \"btn-info\",\n\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t// Start a blind transfer\n\t\t\t\t\t\t\t\t\t\tlet address = $('#transferto').val();\n\t\t\t\t\t\t\t\t\t\tif(address === '')\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\tlet msg = {\n\t\t\t\t\t\t\t\t\t\t\trequest: \"transfer\",\n\t\t\t\t\t\t\t\t\t\t\turi: address\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: msg });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tattended: {\n\t\t\t\t\t\t\t\t\tlabel: \"Attended transfer\",\n\t\t\t\t\t\t\t\t\tclassName: \"btn-primary\",\n\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t// Start an attended transfer\n\t\t\t\t\t\t\t\t\t\tlet address = $('#transferto').val();\n\t\t\t\t\t\t\t\t\t\tif(address === '')\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t// Add the call-id to replace to the transfer\n\t\t\t\t\t\t\t\t\t\tlet msg = {\n\t\t\t\t\t\t\t\t\t\t\trequest: \"transfer\",\n\t\t\t\t\t\t\t\t\t\t\turi: address,\n\t\t\t\t\t\t\t\t\t\t\treplace: helpers[helperId].sipcall.callId\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\thelpers[helperId].sipcall.send({ message: msg });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\thelpers[helperId].remoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Created remote audio stream:\", stream);\n\t\t\t\t\t$('#videoright' + helperId).append('<audio class=\"hide\" id=\"peervideo' + helperId + 'm' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + helperId + 'm' + mid).get(0), stream);\n\t\t\t\t\tif(helpers[helperId].remoteVideos === 0) {\n\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\tif($('#videoright' + helperId + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t$('#videoright' + helperId).append(\n\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\thelpers[helperId].remoteVideos++;\n\t\t\t\t\t$('#videoright' + helperId + ' .no-video-container').remove();\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\thelpers[helperId].remoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"] Created remote video stream:\", stream);\n\t\t\t\t\t$('#videoright' + helperId).append('<video class=\"rounded centered\" id=\"peervideo' + helperId + 'm' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + helperId + 'm' + mid).get(0), stream);\n\t\t\t\t}\n\t\t\t},\n\t\t\toncleanup: function() {\n\t\t\t\tJanus.log(\"[Helper #\" + helperId + \"]  ::: Got a cleanup notification :::\");\n\t\t\t\t$('#videoleft' + helperId).empty().parent().unblock();\n\t\t\t\t$('#videoleft' + helperId).empty();\n\t\t\t\t$('#videos' + helperId).addClass('hide');\n\t\t\t\t$('#dtmf' + helperId).parent().html(\"Remote UA\");\n\t\t\t\tif(helpers[helperId] && helpers[helperId].sipcall) {\n\t\t\t\t\tdelete helpers[helperId].sipcall.callId;\n\t\t\t\t\tdelete helpers[helperId].sipcall.doAudio;\n\t\t\t\t\tdelete helpers[helperId].sipcall.doVideo;\n\t\t\t\t}\n\t\t\t\tif(helpers[helperId]) {\n\t\t\t\t\thelpers[helperId].localTracks = {};\n\t\t\t\t\thelpers[helperId].localVideos = 0;\n\t\t\t\t\thelpers[helperId].remoteTracks = {};\n\t\t\t\t\thelpers[helperId].remoteVideos = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n}\nfunction removeHelper(helperId) {\n\tif(helpers[helperId] && helpers[helperId].sipcall) {\n\t\t// Detach from the helper's Janus handle\n\t\thelpers[helperId].sipcall.detach();\n\t\tdelete helpers[helperId];\n\t\t// Remove the related UI too\n\t\t$('#sipcall'+helperId).remove();\n\t}\n}\n"
  },
  {
    "path": "html/demos/streaming.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Streaming Demo</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"streaming.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='streaming.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Streaming\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>Streaming</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/streaming\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This demo showcases the functionality provided by the Streaming plugin.\n\t\t\t\t\t\tIn particular, it provides three different streaming approaches, namely:</p>\n\t\t\t\t\t\t<p><ol>\n\t\t\t\t\t\t\t<li>An on-demand stream originated by a file (a song, in this case):\n\t\t\t\t\t\t\tdifferent users accessing this stream would receive a personal view\n\t\t\t\t\t\t\tof the stream itself.</li>\n\t\t\t\t\t\t\t<li>A pseudo-live stream, still originated by a file (an audio recording\n\t\t\t\t\t\t\tof a radio running commentary): different users accessing this stream\n\t\t\t\t\t\t\twould all receive the same, shared view of the stream.</li>\n\t\t\t\t\t\t\t<li>A live stream, originated by a gstreamer script: as for the pseudo-live\n\t\t\t\t\t\t\tstream, different users will get the same feed.</li>\n\t\t\t\t\t\t</ol></p>\n\t\t\t\t\t\t<p>You can try them all within the same session: just choose the stream\n\t\t\t\t\t\tyou're interested in and press the <code>Watch</code> button to start\n\t\t\t\t\t\tthe playout. Stopping it will allow you to switch to a different one.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"streams\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card w-100\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Streams <i id=\"update-streams\" class=\"fa-solid fa-rotate\" title=\"Update list of streams\" style=\"cursor: pointer;\"></i></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"list\">\n\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t<button class=\"btn btn-primary\" autocomplete=\"off\" id=\"watch\">Watch</button>\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t<button autocomplete=\"off\" id=\"streamset\" class=\"btn btn-secondary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\tStreams list\n\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t<ul id=\"streamslist\" class=\"dropdown-menu\" role=\"menu\" style=\"max-height: 300px; overflow: auto;\">\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"card mt-4 w-100 hide\" id=\"info\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\"><i class=\"fa-solid fa-circle-info\"></i> Metadata</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\">\n\t\t\t\t\t\t\t\t<pre id=\"metadata\" class=\"card card-body bg-gray mt-3\"></pre>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\" id=\"videos\">\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/streaming.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar streaming = null;\nvar opaqueId = \"streamingtest-\"+Janus.randomString(12);\n\nvar remoteTracks = {}, remoteVideos = 0, dataMid = null;\nvar bitrateTimer = {};\n\nvar simulcastStarted = {}, svcStarted = {};\n\nvar streamsList = {};\nvar selectedStream = null;\n\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to Streaming plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.streaming\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tstreaming = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + streaming.getPlugin() + \", id=\" + streaming.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Setup streaming session\n\t\t\t\t\t\t\t\t\t$('#update-streams').click(updateStreamsList);\n\t\t\t\t\t\t\t\t\tupdateStreamsList();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tfor(let i in bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer[i]);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = {};\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t\t$('#streamslist').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#watch').attr('disabled', true).unbind('click');\n\t\t\t\t\t\t\t\t\t\t\t$('#start').attr('disabled', true).html(\"Bye\").unbind('click');\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin... \", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result[\"status\"]) {\n\t\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\t\tif(status === 'starting')\n\t\t\t\t\t\t\t\t\t\t\t\t$('#status').removeClass('hide').text(\"Starting, please wait...\").removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(status === 'started')\n\t\t\t\t\t\t\t\t\t\t\t\t$('#status').removeClass('hide').text(\"Started\").removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\telse if(status === 'stopped')\n\t\t\t\t\t\t\t\t\t\t\t\tstopStream();\n\t\t\t\t\t\t\t\t\t\t} else if(msg[\"streaming\"] === \"event\") {\n\t\t\t\t\t\t\t\t\t\t\t// Does this event refer to a mid in particular?\n\t\t\t\t\t\t\t\t\t\t\tlet mid = result[\"mid\"] ? result[\"mid\"] : \"0\";\n\t\t\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\t\t\tlet substream = result[\"substream\"];\n\t\t\t\t\t\t\t\t\t\t\tlet temporal = result[\"temporal\"];\n\t\t\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted[mid]) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted[mid] = true;\n\t\t\t\t\t\t\t\t\t\t\t\t\taddSimulcastButtons(mid);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\t\t\tupdateSimulcastButtons(mid, substream, temporal);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// Is VP9/SVC in place?\n\t\t\t\t\t\t\t\t\t\t\tlet spatial = result[\"spatial_layer\"];\n\t\t\t\t\t\t\t\t\t\t\ttemporal = result[\"temporal_layer\"];\n\t\t\t\t\t\t\t\t\t\t\tif((spatial !== null && spatial !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!svcStarted[mid]) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsvcStarted[mid] = true;\n\t\t\t\t\t\t\t\t\t\t\t\t\taddSvcButtons(mid);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\t\t\tupdateSvcButtons(mid, spatial, temporal);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else if(msg[\"error\"]) {\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t\t\t\t\t\t\tstopStream();\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\tlet stereo = (jsep.sdp.indexOf(\"stereo=1\") !== -1);\n\t\t\t\t\t\t\t\t\t\t// Offer from the plugin, let's answer\n\t\t\t\t\t\t\t\t\t\tstreaming.createAnswer(\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t// We only specify data channels here, as this way in\n\t\t\t\t\t\t\t\t\t\t\t\t// case they were offered we'll enable them. Since we\n\t\t\t\t\t\t\t\t\t\t\t\t// don't mention audio or video tracks, we autoaccept them\n\t\t\t\t\t\t\t\t\t\t\t\t// as recvonly (since we won't capture anything ourselves)\n\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'data' }\n\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\tcustomizeSdp: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(stereo && jsep.sdp.indexOf(\"stereo=1\") == -1) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Make sure that our offer contains stereo too\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tjsep.sdp = jsep.sdp.replace(\"useinbandfec=1\", \"useinbandfec=1;stereo=1\");\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"start\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\tstreaming.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#watch').html(\"Stop\").removeAttr('disabled').unbind('click').click(stopStream);\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata ? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tlet mstreamId = \"mstream\"+mid;\n\t\t\t\t\t\t\t\t\tif(streamsList[selectedStream] && streamsList[selectedStream].legacy)\n\t\t\t\t\t\t\t\t\t\tmstreamId = \"mstream0\";\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#remotevideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#'+mstreamId+' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#'+mstreamId).append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#remotevideo' + mid).length > 0)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner' + mid).remove();\n\t\t\t\t\t\t\t\t\tlet stream = null;\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#'+mstreamId).append('<audio class=\"hide\" id=\"remotevideo' + mid + '\" playsinline/>');\n\t\t\t\t\t\t\t\t\t\t$('#remotevideo'+mid).get(0).volume = 0;\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#'+mstreamId+' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#'+mstreamId).append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container audioonly\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('.no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#'+mstreamId).append('<video class=\"rounded centered hide\" id=\"remotevideo' + mid + '\" width=\"100%\" height=\"100%\" playsinline/>');\n\t\t\t\t\t\t\t\t\t\t$('#remotevideo'+mid).get(0).volume = 0;\n\t\t\t\t\t\t\t\t\t\t// Use a custom timer for this stream\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer[mid]) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate'+mid).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer[mid] = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#remotevideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = streaming.getBitrate(mid);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate'+mid).text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#remotevideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#remotevideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres'+mid).removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Play the stream when we get a playing event\n\t\t\t\t\t\t\t\t\t$(\"#remotevideo\" + mid).bind(\"playing\", function (ev) {\n\t\t\t\t\t\t\t\t\t\t$('.waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\tif(!this.videoWidth)\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t$('#'+ev.target.id).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\tlet width = this.videoWidth;\n\t\t\t\t\t\t\t\t\t\tlet height = this.videoHeight;\n\t\t\t\t\t\t\t\t\t\t$('#curres'+mid).removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\tif(Janus.webRTCAdapter.browserDetails.browser === \"firefox\") {\n\t\t\t\t\t\t\t\t\t\t\t// Firefox Stable has a bug: width and height are not immediately available after a playing\n\t\t\t\t\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $('#'+ev.target.id).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $('#'+ev.target.id).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curres'+mid).removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 2000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#remotevideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\tlet playPromise = $('#remotevideo' + mid).get(0).play();\n\t\t\t\t\t\t\t\t\tif (playPromise !== undefined) {\n\t\t\t\t\t\t\t\t\t\tplayPromise\n\t\t\t\t\t\t\t\t\t\t\t.then(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log('Started playing')\n\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t.catch(function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.error('Failed to play', error)\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t$('#remotevideo' + mid).get(0).volume = 1;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t$('.waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t$('#mstream' + dataMid).append(\n\t\t\t\t\t\t\t\t\t\t'<input class=\"form-control\" type=\"text\" id=\"datarecv\" disabled></input>'\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\t$('#datarecv').val(data);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$('#videos').empty();\n\t\t\t\t\t\t\t\t\t$('#info').addClass('hide');\n\t\t\t\t\t\t\t\t\tfor(let i in bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer[i]);\n\t\t\t\t\t\t\t\t\tbitrateTimer = {};\n\t\t\t\t\t\t\t\t\tsimulcastStarted = false;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t\tdataMid = null;\n\t\t\t\t\t\t\t\t\t$('#streamset').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t$('#streamslist').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t$('#watch').html(\"Watch\").removeAttr('disabled')\n\t\t\t\t\t\t\t\t\t\t.unbind('click').click(startStream);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\nfunction updateStreamsList() {\n\t$('#update-streams').unbind('click').addClass('fa-spin');\n\tlet body = { request: \"list\" };\n\tJanus.debug(\"Sending message:\", body);\n\tstreaming.send({ message: body, success: function(result) {\n\t\tsetTimeout(function() {\n\t\t\t$('#update-streams').removeClass('fa-spin').unbind('click').click(updateStreamsList);\n\t\t}, 500);\n\t\tif(!result) {\n\t\t\tbootbox.alert(\"Got no response to our query for available streams\");\n\t\t\treturn;\n\t\t}\n\t\tif(result[\"list\"]) {\n\t\t\t$('#streams').removeClass('hide');\n\t\t\t$('#streamslist').empty();\n\t\t\t$('#watch').attr('disabled', true).unbind('click');\n\t\t\tlet list = result[\"list\"];\n\t\t\tif(list && Array.isArray(list)) {\n\t\t\t\tlist.sort(function(a, b) {\n\t\t\t\t\tif(!a || a.id < (b ? b.id : 0))\n\t\t\t\t\t\treturn -1;\n\t\t\t\t\tif(!b || b.id < (a ? a.id : 0))\n\t\t\t\t\t\treturn 1;\n\t\t\t\t\treturn 0;\n\t\t\t\t});\n\t\t\t}\n\t\t\tJanus.log(\"Got a list of available streams:\", list);\n\t\t\tstreamsList = {};\n\t\t\tfor(let mp in list) {\n\t\t\t\tJanus.debug(\"  >> [\" + list[mp][\"id\"] + \"] \" + list[mp][\"description\"] + \" (\" + list[mp][\"type\"] + \")\");\n\t\t\t\t$('#streamslist').append(\"<a class='dropdown-item' href='#' id='\" + list[mp][\"id\"] + \"'>\" + escapeXmlTags(list[mp][\"description\"]) + \" (\" + list[mp][\"type\"] + \")\" + \"</a>\");\n\t\t\t\t// Check the nature of the available streams, and if there are some multistream ones\n\t\t\t\tlist[mp].legacy = true;\n\t\t\t\tif(list[mp].media) {\n\t\t\t\t\tlet audios = 0, videos = 0;\n\t\t\t\t\tfor(let mi in list[mp].media) {\n\t\t\t\t\t\tif(!list[mp].media[mi])\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\tif(list[mp].media[mi].type === \"audio\")\n\t\t\t\t\t\t\taudios++;\n\t\t\t\t\t\telse if(list[mp].media[mi].type === \"video\")\n\t\t\t\t\t\t\tvideos++;\n\t\t\t\t\t\tif(audios > 1 || videos > 1) {\n\t\t\t\t\t\t\tlist[mp].legacy = false;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Keep track of all the available streams\n\t\t\t\tstreamsList[list[mp][\"id\"]] = list[mp];\n\t\t\t}\n\t\t\t$('#streamslist a').off('click').on('click', function() {\n\t\t\t\tselectedStream = $(this).attr(\"id\");\n\t\t\t\t$('#streamset').html($(this).html());\n\t\t\t});\n\t\t\t$('#watch').removeAttr('disabled').unbind('click').click(startStream);\n\t\t}\n\t}});\n}\n\nfunction getStreamInfo() {\n\t$('#metadata').empty();\n\t$('#info').addClass('hide');\n\tif(!selectedStream || !streamsList[selectedStream])\n\t\treturn;\n\t// Send a request for more info on the mountpoint we subscribed to\n\tlet body = { request: \"info\", id: parseInt(selectedStream) || selectedStream };\n\tstreaming.send({ message: body, success: function(result) {\n\t\tif(result && result.info && result.info.metadata) {\n\t\t\t$('#metadata').html(escapeXmlTags(result.info.metadata));\n\t\t\t$('#info').removeClass('hide');\n\t\t}\n\t}});\n}\n\nfunction startStream() {\n\tJanus.log(\"Selected video id #\" + selectedStream);\n\tif(!selectedStream || !streamsList[selectedStream]) {\n\t\tbootbox.alert(\"Select a stream from the list\");\n\t\treturn;\n\t}\n\t$('#streamset').attr('disabled', true);\n\t$('#streamslist').attr('disabled', true);\n\t$('#watch').attr('disabled', true).unbind('click');\n\t// Add some panels to host the remote streams\n\tif(streamsList[selectedStream].legacy) {\n\t\t// At max 1-audio/1-video, so use a single panel\n\t\tlet mid = null;\n\t\tfor(let mi in streamsList[selectedStream].media) {\n\t\t\t// Add a new panel\n\t\t\tlet type = streamsList[selectedStream].media[mi].type;\n\t\t\tif(type === \"video\") {\n\t\t\t\tmid = streamsList[selectedStream].media[mi].mid;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif($('#mstream0').length === 0) {\n\t\t\taddPanel(\"0\", (mid ? mid : \"0\"));\n\t\t\t// No remote video yet\n\t\t\t$('#mstream0').append('<video class=\"rounded centered waitingvideo\" id=\"waitingvideo0\" width=\"100%\" height=\"100%\" />');\n\t\t}\n\t\tdataMid = \"0\";\n\t} else {\n\t\t// Multistream mountpoint, create a panel for each stream\n\t\tfor(let mi in streamsList[selectedStream].media) {\n\t\t\t// Add a new panel\n\t\t\tlet type = streamsList[selectedStream].media[mi].type;\n\t\t\tlet mid = streamsList[selectedStream].media[mi].mid;\n\t\t\tlet label = streamsList[selectedStream].media[mi].label;\n\t\t\tif($('#mstream'+mid).length === 0) {\n\t\t\t\taddPanel(mid, mid, label);\n\t\t\t\t// No remote media yet\n\t\t\t\t$('#mstream'+mid).append('<video class=\"rounded centered waitingvideo\" id=\"waitingvideo'+mid+'\" width=\"100%\" height=\"100%\" />');\n\t\t\t}\n\t\t\tif(type === 'data')\n\t\t\t\tdataMid = mid;\n\t\t}\n\t}\n\t// Prepare the request to start streaming and send it\n\tlet body = { request: \"watch\", id: parseInt(selectedStream) || selectedStream };\n\t// Notice that, for RTP mountpoints, you can subscribe to a subset\n\t// of the mountpoint media, rather than them all, by adding a \"stream\"\n\t// array containing the list of stream mids you're interested in, e.g.:\n\t//\n\t//\t\tbody.streams = [ \"0\", \"2\" ];\n\t//\n\t// to only subscribe to the first and third stream, and skip the second\n\t// (assuming those are the mids you got from a \"list\" or \"info\" request).\n\t// By default, you always subscribe to all the streams in a mountpoint\n\tstreaming.send({ message: body });\n\t// Get some more info for the mountpoint to display, if any\n\tgetStreamInfo();\n}\n\nfunction stopStream() {\n\t$('#watch').attr('disabled', true).unbind('click');\n\tlet body = { request: \"stop\" };\n\tstreaming.send({ message: body });\n\tstreaming.hangup();\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n\n// Helper to add a new panel to the 'videos' div\nfunction addPanel(panelId, mid, desc) {\n\t$('#videos').append(\n\t\t'<div class=\"row mb-3\" id=\"panel' + panelId + '\">' +\n\t\t'\t<div class=\"card w-100\">' +\n\t\t'\t\t<div class=\"card-header\">' +\n\t\t'\t\t\t<span class=\"card-title\">' + (desc ? desc : \"Stream\") +\n\t\t'\t\t\t\t<span class=\"badge bg-info hide\" id=\"status' + mid + '\"></span>' +\n\t\t'\t\t\t\t<span class=\"badge bg-primary hide\" id=\"curres' + mid + '\"></span>' +\n\t\t'\t\t\t\t<span class=\"badge bg-info hide\" id=\"curbitrate' + mid + '\"></span>' +\n\t\t'\t\t\t</span>' +\n\t\t'\t\t</div>' +\n\t\t'\t\t<div class=\"card-body\" id=\"mstream' + panelId + '\">' +\n\t\t'\t\t\t<div class=\"text-center\">' +\n\t\t'\t\t\t\t<div id=\"spinner' + mid + '\" class=\"spinner-border\" role=\"status\">' +\n\t\t'\t\t\t\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t'\t\t\t\t</div>' +\n\t\t'\t\t\t</div>' +\n\t\t'\t\t</div>' +\n\t\t'\t</div>' +\n\t\t'</div>'\n\t);\n}\n\n// Helpers to create Simulcast-related UI, if enabled\nfunction addSimulcastButtons(mid) {\n\t$('#curres'+mid).parent().append(\n\t\t'<div id=\"simulcast'+mid+'\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"m-'+mid+'-sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"m-'+mid+'-sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"m-'+mid+'-sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"m-'+mid+'-tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"m-'+mid+'-tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"m-'+mid+'-tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>');\n\t// Enable the simulcast selection buttons\n\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#m-'+mid+'-sl-2').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#m-'+mid+'-sl-1').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, substream: 0 }});\n\t\t});\n\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#m-'+mid+'-sl-2').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#m-'+mid+'-sl-0').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, substream: 1 }});\n\t\t});\n\t$('#m-'+mid+'-sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#m-'+mid+'-sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#m-'+mid+'-sl-1').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#m-'+mid+'-sl-0').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, substream: 2 }});\n\t\t});\n\t// We always add temporal layer buttons too, even though these will only work with vP8\n\t$('#m-'+mid+'-tl-0').parent().removeClass('hide');\n\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#m-'+mid+'-tl-2').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#m-'+mid+'-tl-1').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, temporal: 0 }});\n\t\t});\n\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#m-'+mid+'-tl-2').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#m-'+mid+'-tl-0').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, temporal: 1 }});\n\t\t});\n\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#m-'+mid+'-tl-1').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#m-'+mid+'-tl-0').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, temporal: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastButtons(mid, substream, temporal) {\n\t// Check the substream\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched simulcast substream! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched simulcast substream! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched simulcast substream! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n\n// Helpers to create SVC-related UI for a new viewer\nfunction addSvcButtons(mid) {\n\tif($('#svc').length > 0)\n\t\treturn;\n\t$('#curres'+mid).parent().append(\n\t\t'<div id=\"svc'+mid+'\" class=\"btn-group-vertical btn-group-vertical-xs top-right\">' +\n\t\t'\t<div class\"row\">' +\n\t\t'\t\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t\t<button id=\"m-'+mid+'-sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal resolution\">SL 1</button>' +\n\t\t'\t\t\t<button id=\"m-'+mid+'-sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to low resolution\">SL 0</button>' +\n\t\t'\t\t</div>' +\n\t\t'\t</div>' +\n\t\t'\t<div class\"row\">' +\n\t\t'\t\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t\t<button id=\"m-'+mid+'-tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2 (high FPS)\">TL 2</button>' +\n\t\t'\t\t\t<button id=\"m-'+mid+'-tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1 (medium FPS)\">TL 1</button>' +\n\t\t'\t\t\t<button id=\"m-'+mid+'-tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0 (low FPS)\">TL 0</button>' +\n\t\t'\t\t</div>' +\n\t\t'\t</div>' +\n\t\t'</div>'\n\t);\n\t// Enable the SVC selection buttons\n\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching SVC spatial layer, wait for it... (low resolution)\", null, {timeOut: 2000});\n\t\t\tif(!$('#m-'+mid+'-sl-1').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, spatial_layer: 0 }});\n\t\t});\n\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching SVC spatial layer, wait for it... (normal resolution)\", null, {timeOut: 2000});\n\t\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#m-'+mid+'-sl-0').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, spatial_layer: 1 }});\n\t\t});\n\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping SVC temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#m-'+mid+'-tl-2').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#m-'+mid+'-tl-1').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, temporal_layer: 0 }});\n\t\t});\n\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping SVC temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#m-'+mid+'-tl-2').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#m-'+mid+'-tl-0').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, temporal_layer: 1 }});\n\t\t});\n\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping SVC temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#m-'+mid+'-tl-1').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#m-'+mid+'-tl-0').hasClass('btn-success'))\n\t\t\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tstreaming.send({ message: { request: \"configure\", mid: mid, temporal_layer: 2 }});\n\t\t});\n}\n\nfunction updateSvcButtons(mid, spatial, temporal) {\n\t// Check the spatial layer\n\tif(spatial === 0) {\n\t\ttoastr.success(\"Switched SVC spatial layer! (lower resolution)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(spatial === 1) {\n\t\ttoastr.success(\"Switched SVC spatial layer! (normal resolution)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#m-'+mid+'-sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped SVC temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped SVC temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped SVC temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#m-'+mid+'-tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#m-'+mid+'-tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#m-'+mid+'-tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/surround/ChID-BLITS-EBU.txt",
    "content": "File downloaded from https://www2.iis.fraunhofer.de/AAC/multichannel.html\n"
  },
  {
    "path": "html/demos/textroom.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Text Room</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"textroom.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='textroom.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Text Room\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>TextRoom</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/textroom\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>The Text Room demo is a simple example of how you can use this text\n\t\t\t\t\t\tbroadcasting plugin, which uses Data Channels, to implement something\n\t\t\t\t\t\tsimilar to a chatroom. More specifically, the demo allows you to join\n\t\t\t\t\t\ta previously created and configured text room together with other\n\t\t\t\t\t\tparticipants, and send/receive public and private messages to\n\t\t\t\t\t\tindividual participants. To send messages on the chatroom, just type\n\t\t\t\t\t\tyour text and send. To send private messages to individual participants,\n\t\t\t\t\t\tclick the participant name in the list on the right and a custom dialog\n\t\t\t\t\t\twill appear.</p>\n\t\t\t\t\t\t<p>To try the demo, just insert a username to join the room. This will\n\t\t\t\t\t\tadd you to chatroom, and allow you to interact with the other participants.</p>\n\t\t\t\t\t\t<p>Notice that this is just a very basic demo, and that is just one of\n\t\t\t\t\t\tthe several different ways you can use this plugin for. The plugin\n\t\t\t\t\t\tactually allows you to join multiple rooms at the same time, and also\n\t\t\t\t\t\tto forward incoming messages to the room to external components. This\n\t\t\t\t\t\tmakes it a useful tool whenever you have to interact with third party\n\t\t\t\t\t\tapplications and exchange text data.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"roomjoin\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<span class=\"badge bg-info\" id=\"you\"></span>\n\t\t\t\t\t<div class=\"col-md-12\" id=\"controls\">\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-1 hide\" id=\"registernow\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-user\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Choose a display name\" autocomplete=\"off\" id=\"username\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t\t\t\t<button class=\"btn btn-success\" autocomplete=\"off\" id=\"register\">Join the room</button>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"room\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Participants <span class=\"badge bg-info hide\" id=\"participant\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\">\n\t\t\t\t\t\t\t\t<ul id=\"list\" class=\"list-group\">\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-8\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Public Chatroom</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" style=\"overflow-x: auto;\" id=\"chatroom\">\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-footer\">\n\t\t\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-up\"></i></span>\n\t\t\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Write a chatroom message\" autocomplete=\"off\" id=\"datasend\" onkeypress=\"return checkEnter(this, event);\" disabled />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/textroom.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar textroom = null;\nvar opaqueId = \"textroomtest-\"+Janus.randomString(12);\n\nvar myroom = 1234;\t// Demo room\nif(getQueryStringValue(\"room\") !== \"\")\n\tmyroom = parseInt(getQueryStringValue(\"room\"));\nvar myusername = null;\nvar myid = null;\nvar participants = {}\nvar transactions = {}\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to TextRoom plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.textroom\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\ttextroom = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + textroom.getPlugin() + \", id=\" + textroom.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Setup the DataChannel\n\t\t\t\t\t\t\t\t\tlet body = { request: \"setup\" };\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Sending message:\", body);\n\t\t\t\t\t\t\t\t\ttextroom.send({ message: body });\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(msg[\"error\"]) {\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\t// Answer\n\t\t\t\t\t\t\t\t\t\ttextroom.createAnswer(\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t// We only use datachannels\n\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'data' }\n\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"ack\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\ttextroom.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t// Prompt for a display name to join the default room\n\t\t\t\t\t\t\t\t\t$('#roomjoin').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#registernow').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#register').click(registerUsername);\n\t\t\t\t\t\t\t\t\t$('#username').focus();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\t//~ $('#datarecv').val(data);\n\t\t\t\t\t\t\t\t\tlet json = JSON.parse(data);\n\t\t\t\t\t\t\t\t\tlet transaction = json[\"transaction\"];\n\t\t\t\t\t\t\t\t\tif(transactions[transaction]) {\n\t\t\t\t\t\t\t\t\t\t// Someone was waiting for this\n\t\t\t\t\t\t\t\t\t\ttransactions[transaction](json);\n\t\t\t\t\t\t\t\t\t\tdelete transactions[transaction];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet what = json[\"textroom\"];\n\t\t\t\t\t\t\t\t\tif(what === \"message\") {\n\t\t\t\t\t\t\t\t\t\t// Incoming message: public or private?\n\t\t\t\t\t\t\t\t\t\tlet msg = escapeXmlTags(json[\"text\"]);\n\t\t\t\t\t\t\t\t\t\tlet from = json[\"from\"];\n\t\t\t\t\t\t\t\t\t\tlet dateString = getDateString(json[\"date\"]);\n\t\t\t\t\t\t\t\t\t\tlet whisper = json[\"whisper\"];\n\t\t\t\t\t\t\t\t\t\tlet sender = participants[from] ? participants[from] : escapeXmlTags(json[\"display\"]);\n\t\t\t\t\t\t\t\t\t\tif(whisper === true) {\n\t\t\t\t\t\t\t\t\t\t\t// Private message\n\t\t\t\t\t\t\t\t\t\t\t$('#chatroom').append('<p style=\"color: purple;\">[' + dateString + '] <b>[whisper from ' + sender + ']</b> ' + msg);\n\t\t\t\t\t\t\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t// Public message\n\t\t\t\t\t\t\t\t\t\t\t$('#chatroom').append('<p>[' + dateString + '] <b>' + sender + ':</b> ' + msg);\n\t\t\t\t\t\t\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else if(what === \"announcement\") {\n\t\t\t\t\t\t\t\t\t\t// Room announcement\n\t\t\t\t\t\t\t\t\t\tlet msg = escapeXmlTags(json[\"text\"]);\n\t\t\t\t\t\t\t\t\t\tlet dateString = getDateString(json[\"date\"]);\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').append('<p style=\"color: purple;\">[' + dateString + '] <i>' + msg + '</i>');\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t\t\t\t\t\t} else if(what === \"join\") {\n\t\t\t\t\t\t\t\t\t\t// Somebody joined\n\t\t\t\t\t\t\t\t\t\tlet username = json[\"username\"];\n\t\t\t\t\t\t\t\t\t\tlet display = json[\"display\"];\n\t\t\t\t\t\t\t\t\t\tparticipants[username] = escapeXmlTags(display ? display : username);\n\t\t\t\t\t\t\t\t\t\tif(username !== myid && $('#rp' + username).length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// Add to the participants list\n\t\t\t\t\t\t\t\t\t\t\t$('#list').append('<li id=\"rp' + username + '\" class=\"list-group-item\">' + participants[username] + '</li>');\n\t\t\t\t\t\t\t\t\t\t\t$('#rp' + username).css('cursor', 'pointer').click(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tlet username = $(this).attr('id').split(\"rp\")[1];\n\t\t\t\t\t\t\t\t\t\t\t\tsendPrivateMsg(username);\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').append('<p style=\"color: green;\">[' + getDateString() + '] <i>' + participants[username] + ' joined</i></p>');\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t\t\t\t\t\t} else if(what === \"leave\") {\n\t\t\t\t\t\t\t\t\t\t// Somebody left\n\t\t\t\t\t\t\t\t\t\tlet username = json[\"username\"];\n\t\t\t\t\t\t\t\t\t\t$('#rp' + username).remove();\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').append('<p style=\"color: green;\">[' + getDateString() + '] <i>' + participants[username] + ' left</i></p>');\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t\t\t\t\t\t\tdelete participants[username];\n\t\t\t\t\t\t\t\t\t} else if(what === \"kicked\") {\n\t\t\t\t\t\t\t\t\t\t// Somebody was kicked\n\t\t\t\t\t\t\t\t\t\tlet username = json[\"username\"];\n\t\t\t\t\t\t\t\t\t\t$('#rp' + username).remove();\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').append('<p style=\"color: green;\">[' + getDateString() + '] <i>' + participants[username] + ' was kicked from the room</i></p>');\n\t\t\t\t\t\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t\t\t\t\t\t\tdelete participants[username];\n\t\t\t\t\t\t\t\t\t\tif(username === myid) {\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"You have been kicked from the room\", function() {\n\t\t\t\t\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else if(what === \"destroyed\") {\n\t\t\t\t\t\t\t\t\t\tif(json[\"room\"] !== myroom)\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t// Room was destroyed, goodbye!\n\t\t\t\t\t\t\t\t\t\tJanus.warn(\"The room has been destroyed!\");\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The room has been destroyed\", function() {\n\t\t\t\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$('#datasend').attr('disabled', true);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tif(field.id == 'username')\n\t\t\tregisterUsername();\n\t\telse if(field.id == 'datasend')\n\t\t\tsendData();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction registerUsername() {\n\tif($('#username').length === 0) {\n\t\t// Create fields to register\n\t\t$('#register').click(registerUsername);\n\t\t$('#username').focus();\n\t} else {\n\t\t// Try a registration\n\t\t$('#username').attr('disabled', true);\n\t\t$('#register').attr('disabled', true).unbind('click');\n\t\tlet username = $('#username').val();\n\t\tif(username === \"\") {\n\t\t\t$('#you')\n\t\t\t\t.removeClass().addClass('badge bg-warning')\n\t\t\t\t.html(\"Insert your display name (e.g., pippo)\");\n\t\t\t$('#username').removeAttr('disabled');\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\treturn;\n\t\t}\n\t\tmyid = Janus.randomString(12);\n\t\tlet transaction = Janus.randomString(12);\n\t\tlet register = {\n\t\t\ttextroom: \"join\",\n\t\t\ttransaction: transaction,\n\t\t\troom: myroom,\n\t\t\tusername: myid,\n\t\t\tdisplay: username\n\t\t};\n\t\tmyusername = escapeXmlTags(username);\n\t\ttransactions[transaction] = function(response) {\n\t\t\tif(response[\"textroom\"] === \"error\") {\n\t\t\t\t// Something went wrong\n\t\t\t\tif(response[\"error_code\"] === 417) {\n\t\t\t\t\t// This is a \"no such room\" error: give a more meaningful description\n\t\t\t\t\tbootbox.alert(\n\t\t\t\t\t\t\"<p>Apparently room <code>\" + myroom + \"</code> (the one this demo uses as a test room) \" +\n\t\t\t\t\t\t\"does not exist...</p><p>Do you have an updated <code>janus.plugin.textroom.jcfg</code> \" +\n\t\t\t\t\t\t\"configuration file? If not, make sure you copy the details of room <code>\" + myroom + \"</code> \" +\n\t\t\t\t\t\t\"from that sample in your current configuration file, then restart Janus and try again.\"\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tbootbox.alert(response[\"error\"]);\n\t\t\t\t}\n\t\t\t\t$('#username').removeAttr('disabled').val(\"\");\n\t\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// We're in\n\t\t\t$('#roomjoin').addClass('hide');\n\t\t\t$('#room').removeClass('hide');\n\t\t\t$('#participant').removeClass('hide').html(myusername).removeClass('hide');\n\t\t\t$('#chatroom').css('height', ($(window).height()-420)+\"px\");\n\t\t\t$('#datasend').removeAttr('disabled');\n\t\t\t// Any participants already in?\n\t\t\tconsole.log(\"Participants:\", response.participants);\n\t\t\tif(response.participants && response.participants.length > 0) {\n\t\t\t\tfor(let i in response.participants) {\n\t\t\t\t\tlet p = response.participants[i];\n\t\t\t\t\tparticipants[p.username] = escapeXmlTags(p.display ? p.display : p.username);\n\t\t\t\t\tif(p.username !== myid && $('#rp' + p.username).length === 0) {\n\t\t\t\t\t\t// Add to the participants list\n\t\t\t\t\t\t$('#list').append('<li id=\"rp' + p.username + '\" class=\"list-group-item\">' + participants[p.username] + '</li>');\n\t\t\t\t\t\t$('#rp' + p.username).css('cursor', 'pointer').click(function() {\n\t\t\t\t\t\t\tlet username = $(this).attr('id').split(\"rp\")[1];\n\t\t\t\t\t\t\tsendPrivateMsg(username);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t$('#chatroom').append('<p style=\"color: green;\">[' + getDateString() + '] <i>' + participants[p.username] + ' joined</i></p>');\n\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\ttextroom.data({\n\t\t\ttext: JSON.stringify(register),\n\t\t\terror: function(reason) {\n\t\t\t\tbootbox.alert(reason);\n\t\t\t\t$('#username').removeAttr('disabled').val(\"\");\n\t\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\t}\n\t\t});\n\t}\n}\n\nfunction sendPrivateMsg(username) {\n\tlet display = participants[username];\n\tif(!display)\n\t\treturn;\n\tbootbox.prompt(\"Private message to \" + display, function(result) {\n\t\tif(result && result !== \"\") {\n\t\t\tlet message = {\n\t\t\t\ttextroom: \"message\",\n\t\t\t\ttransaction: Janus.randomString(12),\n\t\t\t\troom: myroom,\n\t\t\t\tto: username,\n\t\t\t\ttext: result\n\t\t\t};\n\t\t\ttextroom.data({\n\t\t\t\ttext: JSON.stringify(message),\n\t\t\t\terror: function(reason) { bootbox.alert(reason); },\n\t\t\t\tsuccess: function() {\n\t\t\t\t\t$('#chatroom').append('<p style=\"color: purple;\">[' + getDateString() + '] <b>[whisper to ' + display + ']</b> ' + escapeXmlTags(result));\n\t\t\t\t\t$('#chatroom').get(0).scrollTop = $('#chatroom').get(0).scrollHeight;\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t});\n\treturn;\n}\n\nfunction sendData() {\n\tlet data = $('#datasend').val();\n\tif(data === \"\") {\n\t\tbootbox.alert('Insert a message to send on the DataChannel');\n\t\treturn;\n\t}\n\tlet message = {\n\t\ttextroom: \"message\",\n\t\ttransaction: Janus.randomString(12),\n\t\troom: myroom,\n\t\ttext: data,\n\t};\n\t// Note: messages are always acknowledged by default. This means that you'll\n\t// always receive a confirmation back that the message has been received by the\n\t// server and forwarded to the recipients. If you do not want this to happen,\n\t// just add an ack:false property to the message above, and server won't send\n\t// you a response (meaning you just have to hope it succeeded).\n\ttextroom.data({\n\t\ttext: JSON.stringify(message),\n\t\terror: function(reason) { bootbox.alert(reason); },\n\t\tsuccess: function() { $('#datasend').val(''); }\n\t});\n}\n\n// Helper to format times\nfunction getDateString(jsonDate) {\n\tlet when = new Date();\n\tif(jsonDate) {\n\t\twhen = new Date(Date.parse(jsonDate));\n\t}\n\tlet dateString =\n\t\t\t(\"0\" + when.getUTCHours()).slice(-2) + \":\" +\n\t\t\t(\"0\" + when.getUTCMinutes()).slice(-2) + \":\" +\n\t\t\t(\"0\" + when.getUTCSeconds()).slice(-2);\n\treturn dateString;\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n"
  },
  {
    "path": "html/demos/videocall.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Video Call Demo</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"videocall.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='videocall.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Video Call\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>VideoCall</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/videocall\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This Video Call demo is basically an example of how you can achieve a\n\t\t\t\t\t\tscenario like the famous AppRTC demo but with media flowing through Janus. It\n\t\t\t\t\t\tbasically is an extension to the Echo Test demo, where in this case the media\n\t\t\t\t\t\tpackets and statistics are forwarded between the two involved peers.</p>\n\t\t\t\t\t\t<p>Using the demo is simple. Just choose a simple username to register\n\t\t\t\t\t\tat the plugin, and then either call another user (provided you know\n\t\t\t\t\t\twhich username was picked) or share your username with a friend and\n\t\t\t\t\t\twait for a call. At that point, you'll be in a video call with the\n\t\t\t\t\t\tremote peer, and you'll have the same controls the Echo Test demo\n\t\t\t\t\t\tprovides to try and control the media: that is, a button to mute/unmute\n\t\t\t\t\t\tyour audio and video, and a knob to try and limit your bandwidth. If\n\t\t\t\t\t\tthe browser supports it, you'll also get a view of the bandwidth\n\t\t\t\t\t\tcurrently used by your peer for the video stream.</p>\n\t\t\t\t\t\t<p>If you're interested in testing how simulcasting can be used within\n\t\t\t\t\t\tthe context of this sample videocall application, just pass the\n\t\t\t\t\t\t<code>?simulcast=true</code> query string to the url of this page and\n\t\t\t\t\t\treload it. If you're using a browser that does support simulcasting\n\t\t\t\t\t\t(Chrome or Firefox) and the call will end up using VP8, you'll\n\t\t\t\t\t\tsend multiple qualities of the video you're capturing. Notice that\n\t\t\t\t\t\tsimulcasting will only occur if the browser thinks there is enough\n\t\t\t\t\t\tbandwidth, so you'll have to play with the Bandwidth selector to\n\t\t\t\t\t\tincrease it. New buttons to play with the feature will automatically\n\t\t\t\t\t\tappear for your peer; at the same time, if your peer enabled simulcasting\n\t\t\t\t\t\tnew buttons will appear for you when watching the remote stream. Notice that\n\t\t\t\t\t\tno simulcast support is needed for watching, only for publishing.</p>\n\t\t\t\t\t\t<p>A very simple chat based on Data Channels is available as well:\n\t\t\t\t\t\tjust use the text area under your local video to send messages\n\t\t\t\t\t\tto your peer. Incoming messages will be displayed below the\n\t\t\t\t\t\tremote video instead.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container hide\" id=\"videocall\">\n\t\t\t\t<div class=\"row mt-4\">\n\t\t\t\t\t<div class=\"col-md-6 container invisible\" id=\"login\">\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-user\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Choose a username\" autocomplete=\"off\" id=\"username\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"btn btn-success mb-1\" autocomplete=\"off\" id=\"register\">Register</button> <span class=\"hide badge bg-info\" id=\"youok\"></span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6 container invisible\" id=\"phone\">\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-1\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-phone\"></i></span>\n\t\t\t\t\t\t\t<input class=\"form-control\" type=\"text\" placeholder=\"Who should we call?\" autocomplete=\"off\" id=\"peer\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button class=\"btn btn-success mb-1\" autocomplete=\"off\" id=\"call\">Call</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div id=\"videos\" class=\"row mt-4 hide\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"toggleaudio\">Disable audio</button>\n\t\t\t\t\t\t\t\t\t\t<button class=\"btn btn-danger\" autocomplete=\"off\" id=\"togglevideo\">Disable video</button>\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<button autocomplete=\"off\" id=\"bitrateset\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\tBandwidth\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-up\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" placeholder=\"Write a DataChannel message\" autocomplete=\"off\" id=\"datasend\" onkeypress=\"return checkEnter(this, event);\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream\n\t\t\t\t\t\t\t\t\t<span class=\"badge bg-info hide\" id=\"callee\"></span>\n\t\t\t\t\t\t\t\t\t<span class=\"badge bg-primary hide\" id=\"curres\"></span>\n\t\t\t\t\t\t\t\t\t<span class=\"badge bg-info hide\" id=\"curbitrate\"></span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-3\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-cloud-arrow-down\"></i></span>\n\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"datarecv\" disabled>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/videocall.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar videocall = null;\nvar opaqueId = \"videocalltest-\"+Janus.randomString(12);\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\nvar bitrateTimer = null;\n\nvar audioenabled = false;\nvar videoenabled = false;\n\nvar myusername = null;\nvar yourusername = null;\n\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar simulcastStarted = false;\n\n$(document).ready(function() {\n\t// Initialize the library (console debug enabled)\n\tJanus.init({debug: true, callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to VideoCall plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.videocall\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tvideocall = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + videocall.getPlugin() + \", id=\" + videocall.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Prepare the username registration\n\t\t\t\t\t\t\t\t\t$('#videocall').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#login').removeClass('invisible');\n\t\t\t\t\t\t\t\t\t$('#registernow').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#register').click(registerUsername);\n\t\t\t\t\t\t\t\t\t$('#username').focus();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"  -- Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result[\"list\"]) {\n\t\t\t\t\t\t\t\t\t\t\tlet list = result[\"list\"];\n\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of registered peers:\", list);\n\t\t\t\t\t\t\t\t\t\t\tfor(let mp in list) {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + list[mp] + \"]\");\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else if(result[\"event\"]) {\n\t\t\t\t\t\t\t\t\t\t\tlet event = result[\"event\"];\n\t\t\t\t\t\t\t\t\t\t\tif(event === 'registered') {\n\t\t\t\t\t\t\t\t\t\t\t\tmyusername = escapeXmlTags(result[\"username\"]);\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Successfully registered as \" + myusername + \"!\");\n\t\t\t\t\t\t\t\t\t\t\t\t$('#youok').removeClass('hide').html(\"Registered as '\" + myusername + \"'\");\n\t\t\t\t\t\t\t\t\t\t\t\t// Get a list of available peers, just for fun\n\t\t\t\t\t\t\t\t\t\t\t\tvideocall.send({ message: { request: \"list\" }});\n\t\t\t\t\t\t\t\t\t\t\t\t// Enable buttons to call now\n\t\t\t\t\t\t\t\t\t\t\t\t$('#phone').removeClass('invisible');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#call').unbind('click').click(doCall);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#peer').focus();\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'calling') {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Waiting for the peer to answer...\");\n\t\t\t\t\t\t\t\t\t\t\t\t// TODO Any ringtone?\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"Waiting for the peer to answer...\");\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'incomingcall') {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Incoming call from \" + result[\"username\"] + \"!\");\n\t\t\t\t\t\t\t\t\t\t\t\tyourusername = escapeXmlTags(result[\"username\"]);\n\t\t\t\t\t\t\t\t\t\t\t\t// Notify user\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.hideAll();\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.dialog({\n\t\t\t\t\t\t\t\t\t\t\t\t\tmessage: \"Incoming call from \" + yourusername + \"!\",\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle: \"Incoming call\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tcloseButton: false,\n\t\t\t\t\t\t\t\t\t\t\t\t\tbuttons: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Answer\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-success\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#peer').val(result[\"username\"]).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvideocall.createAnswer(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and video, if offered,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// plus data channels too if they were negotiated\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'video', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'data' },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"accept\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvideocall.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#peer').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Hangup')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-success\").addClass(\"btn-danger\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doHangup);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdanger: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: \"Decline\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName: \"btn-danger\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdoHangup();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'accepted') {\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.hideAll();\n\t\t\t\t\t\t\t\t\t\t\t\tlet peer = escapeXmlTags(result[\"username\"]);\n\t\t\t\t\t\t\t\t\t\t\t\tif(!peer) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Call started!\");\n\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(peer + \" accepted the call!\");\n\t\t\t\t\t\t\t\t\t\t\t\t\tyourusername = peer;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t// Video call can start\n\t\t\t\t\t\t\t\t\t\t\t\tif(jsep)\n\t\t\t\t\t\t\t\t\t\t\t\t\tvideocall.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Hangup')\n\t\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-success\").addClass(\"btn-danger\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doHangup);\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'update') {\n\t\t\t\t\t\t\t\t\t\t\t\t// An 'update' event may be used to provide renegotiation attempts\n\t\t\t\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(jsep.type === \"answer\") {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvideocall.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvideocall.createAnswer(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// We want bidirectional audio and video, if offered,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// plus data channels too if they were negotiated\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'audio', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'video', capture: true, recv: true },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{ type: 'data' },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet body = { request: \"set\" };\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvideocall.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === 'hangup') {\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Call hung up by \" + result[\"username\"] + \" (\" + result[\"reason\"] + \")!\");\n\t\t\t\t\t\t\t\t\t\t\t\t// Reset status\n\t\t\t\t\t\t\t\t\t\t\t\tbootbox.hideAll();\n\t\t\t\t\t\t\t\t\t\t\t\tvideocall.hangup();\n\t\t\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videos').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#peer').removeAttr('disabled').val('');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Call')\n\t\t\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-danger\").addClass(\"btn-success\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doCall);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t} else if(event === \"simulcast\") {\n\t\t\t\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\t\t\t\tlet substream = result[\"substream\"];\n\t\t\t\t\t\t\t\t\t\t\t\tlet temporal = result[\"temporal\"];\n\t\t\t\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted = true;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taddSimulcastButtons(result[\"videocodec\"] === \"vp8\");\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\t\t\t\tupdateSimulcastButtons(substream, temporal);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// FIXME Error?\n\t\t\t\t\t\t\t\t\t\tlet error = msg[\"error\"];\n\t\t\t\t\t\t\t\t\t\tbootbox.alert(error);\n\t\t\t\t\t\t\t\t\t\tif(error.indexOf(\"already taken\") > 0) {\n\t\t\t\t\t\t\t\t\t\t\t// FIXME Use status codes...\n\t\t\t\t\t\t\t\t\t\t\t$('#username').removeAttr('disabled').val(\"\");\n\t\t\t\t\t\t\t\t\t\t\t$('#register').removeAttr('disabled').unbind('click').click(registerUsername);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// TODO Reset status\n\t\t\t\t\t\t\t\t\t\tvideocall.hangup();\n\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t$('#videos').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t$('#peer').removeAttr('disabled').val('');\n\t\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Call')\n\t\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-danger\").addClass(\"btn-success\")\n\t\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doCall);\n\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst !== null && mst !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#videoleft video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoleft .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoleft').append('<video class=\"rounded centered\" id=\"myvideo' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(videocall.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\tvideocall.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata ? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#peervideo' + mid).length > 0)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tlet addButtons = false;\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\taddButtons = true;\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// Note: we'll need this for additional videos too\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#peervideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = videocall.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t//~ Janus.debug(\"Current bitrate is \" + videocall.getBitrate());\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#peervideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#peervideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(!addButtons)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// Enable audio/video buttons and bitrate limiter\n\t\t\t\t\t\t\t\t\taudioenabled = true;\n\t\t\t\t\t\t\t\t\tvideoenabled = true;\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').removeAttr('disabled').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\taudioenabled = !audioenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(audioenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Disable audio\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#toggleaudio').html(\"Enable audio\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\tvideocall.send({ message: { request: \"set\", audio: audioenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#togglevideo').removeAttr('disabled').click(\n\t\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\t\tvideoenabled = !videoenabled;\n\t\t\t\t\t\t\t\t\t\t\tif(videoenabled)\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Disable video\").removeClass(\"btn-success\").addClass(\"btn-danger\");\n\t\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\t\t$('#togglevideo').html(\"Enable video\").removeClass(\"btn-danger\").addClass(\"btn-success\");\n\t\t\t\t\t\t\t\t\t\t\tvideocall.send({ message: { request: \"set\", video: videoenabled }});\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').removeAttr('disabled').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\tvideocall.send({ message: { request: \"set\", bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tondataopen: function(label, protocol) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"The DataChannel is available!\");\n\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#datasend').removeAttr('disabled');\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tondata: function(data) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"We got data from the DataChannel!\", data);\n\t\t\t\t\t\t\t\t\t$('#datarecv').val(data);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#callee').empty().addClass('hide');\n\t\t\t\t\t\t\t\t\tyourusername = null;\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#videos').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#toggleaudio').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#togglevideo').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#bitrate').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t$('#videos').addClass('hide');\n\t\t\t\t\t\t\t\t\tsimulcastStarted = false;\n\t\t\t\t\t\t\t\t\t$('#simulcast').remove();\n\t\t\t\t\t\t\t\t\t$('#peer').removeAttr('disabled').val('');\n\t\t\t\t\t\t\t\t\t$('#call').removeAttr('disabled').html('Call')\n\t\t\t\t\t\t\t\t\t\t.removeClass(\"btn-danger\").addClass(\"btn-success\")\n\t\t\t\t\t\t\t\t\t\t.unbind('click').click(doCall);\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tif(field.id == 'username')\n\t\t\tregisterUsername();\n\t\telse if(field.id == 'peer')\n\t\t\tdoCall();\n\t\telse if(field.id == 'datasend')\n\t\t\tsendData();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction registerUsername() {\n\t// Try a registration\n\t$('#username').attr('disabled', true);\n\t$('#register').attr('disabled', true).unbind('click');\n\tlet username = $('#username').val();\n\tif(username === \"\") {\n\t\tbootbox.alert(\"Insert a username to register (e.g., pippo)\");\n\t\t$('#username').removeAttr('disabled');\n\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\treturn;\n\t}\n\tif(/[^a-zA-Z0-9]/.test(username)) {\n\t\tbootbox.alert('Input is not alphanumeric');\n\t\t$('#username').removeAttr('disabled').val(\"\");\n\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\treturn;\n\t}\n\tlet register = { request: \"register\", username: username };\n\tvideocall.send({ message: register });\n}\n\nfunction doCall() {\n\t// Call someone\n\t$('#peer').attr('disabled', true);\n\t$('#call').attr('disabled', true).unbind('click');\n\tlet username = $('#peer').val();\n\tif(username === \"\") {\n\t\tbootbox.alert(\"Insert a username to call (e.g., pluto)\");\n\t\t$('#peer').removeAttr('disabled');\n\t\t$('#call').removeAttr('disabled').click(doCall);\n\t\treturn;\n\t}\n\tif(/[^a-zA-Z0-9]/.test(username)) {\n\t\tbootbox.alert('Input is not alphanumeric');\n\t\t$('#peer').removeAttr('disabled').val(\"\");\n\t\t$('#call').removeAttr('disabled').click(doCall);\n\t\treturn;\n\t}\n\t// Call this user\n\tvideocall.createOffer(\n\t\t{\n\t\t\t// We want bidirectional audio and video, plus data channels\n\t\t\ttracks: [\n\t\t\t\t{ type: 'audio', capture: true, recv: true },\n\t\t\t\t{ type: 'video', capture: true, recv: true, simulcast: doSimulcast },\n\t\t\t\t{ type: 'data' },\n\t\t\t],\n\t\t\tsuccess: function(jsep) {\n\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\tlet body = { request: \"call\", username: $('#peer').val() };\n\t\t\t\tvideocall.send({ message: body, jsep: jsep });\n\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t$('#videoright').html(\n\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t'</div>');\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"WebRTC error...\", error);\n\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t}\n\t\t});\n}\n\nfunction doHangup() {\n\t// Hangup a call\n\t$('#call').attr('disabled', true).unbind('click');\n\tlet hangup = { request: \"hangup\" };\n\tvideocall.send({ message: hangup });\n\tvideocall.hangup();\n\tyourusername = null;\n}\n\nfunction sendData() {\n\tlet data = $('#datasend').val();\n\tif(data === \"\") {\n\t\tbootbox.alert('Insert a message to send on the DataChannel to your peer');\n\t\treturn;\n\t}\n\tvideocall.data({\n\t\ttext: data,\n\t\terror: function(reason) { bootbox.alert(reason); },\n\t\tsuccess: function() { $('#datasend').val(''); },\n\t});\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n\n// Helpers to create Simulcast-related UI, if enabled\nfunction addSimulcastButtons(temporal) {\n\t$('#curres').parent().append(\n\t\t'<div id=\"simulcast\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>');\n\tif(Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers\n\t\t$('#tl-2').remove();\n\t}\n\t// Enable the simulcast selection buttons\n\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tvideocall.send({ message: { request: \"set\", substream: 0 }});\n\t\t});\n\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tvideocall.send({ message: { request: \"set\", substream: 1 }});\n\t\t});\n\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tvideocall.send({ message: { request: \"set\", substream: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl-0').parent().removeClass('hide');\n\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tvideocall.send({ message: { request: \"set\", temporal: 0 }});\n\t\t});\n\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tvideocall.send({ message: { request: \"set\", temporal: 1 }});\n\t\t});\n\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tvideocall.send({ message: { request: \"set\", temporal: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastButtons(substream, temporal) {\n\t// Check the substream\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched simulcast substream! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched simulcast substream! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched simulcast substream! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/videoroom.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Video Room Demo</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"videoroom.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='videoroom.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Video Room\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>VideoRoom</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/videoroom\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This demo is an example of how you can use the Video Room plugin to\n\t\t\t\t\t\timplement a simple videoconferencing application. In particular, this\n\t\t\t\t\t\tdemo page allows you to have up to 6 active participants at the same time:\n\t\t\t\t\t\tmore participants joining the room will be instead just passive users.\n\t\t\t\t\t\tNo mixing is involved: all media are just relayed in a publisher/subscriber\n\t\t\t\t\t\tapproach. This means that the plugin acts as a SFU (Selective Forwarding Unit)\n\t\t\t\t\t\trather than an MCU (Multipoint Control Unit).</p>\n\t\t\t\t\t\t<div class=\"alert alert-info\">Notice that this is the <b>original</b> VideoRoom\n\t\t\t\t\t\tdemo, and uses a different PeerConnections per each subscription: if\n\t\t\t\t\t\tyou want to test the new multistream support, instead, try the\n\t\t\t\t\t\t<a href=\"mvideoroom.html\">multistream VideoRoom demo</a>\n\t\t\t\t\t\tinstead. The two demos are interoperable, if you want to see how\n\t\t\t\t\t\tdifferent subscription mechanisms are used on the same sources.</div>\n\t\t\t\t\t\t<p>If you're interested in testing how simulcasting or SVC can be used within\n\t\t\t\t\t\tthe context of a videoconferencing application, just pass a\n\t\t\t\t\t\t<code>?simulcast=true</code> (for simulcast) or <code>?svc=&lt;mode&gt;</code>\n\t\t\t\t\t\t(for SVC) query string to the url of this page and reload it. Notice that\n\t\t\t\t\t\tsimulcast will only work when using VP8 or H.264 (or, if you're using a\n\t\t\t\t\t\trecent version of Chrome, VP9 and AV1 too), while SVC will only work\n\t\t\t\t\t\tif you're using VP9 or AV1 on a browser that supports setting the <code>scalabilityMode</code>.\n\t\t\t\t\t\tBesides, notice that simulcasting/SVC will only be sent if the browser thinks\n\t\t\t\t\t\tthere is enough bandwidth, so you may have to play with the Bandwidth selector to\n\t\t\t\t\t\tincrease it. New buttons to play with the feature will automatically\n\t\t\t\t\t\tappear for viewers when receiving any simulcast/SVC stream. Notice that\n\t\t\t\t\t\tno simulcast/SVC support is needed for watching, only for publishing.</p>\n\t\t\t\t\t\t<p>To use the demo, just insert a username to join the default room that\n\t\t\t\t\t\tis configured. This will add you to the list of participants, and allow\n\t\t\t\t\t\tyou to automatically send your audio/video frames and receive the other\n\t\t\t\t\t\tparticipants' feeds. The other participants will appear in separate\n\t\t\t\t\t\tpanels, whose title will be the names they chose when registering at\n\t\t\t\t\t\tthe demo.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videojoin\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<span class=\"badge bg-info\" id=\"you\"></span>\n\t\t\t\t\t<div class=\"col-md-12\" id=\"controls\">\n\t\t\t\t\t\t<div class=\"input-group mt-3 mb-1 hide\" id=\"registernow\">\n\t\t\t\t\t\t\t<span class=\"input-group-text\"><i class=\"fa-solid fa-user\"></i></span>\n\t\t\t\t\t\t\t<input autocomplete=\"off\" class=\"form-control\" type=\"text\" placeholder=\"Choose a display name\" id=\"username\" onkeypress=\"return checkEnter(this, event);\" />\n\t\t\t\t\t\t\t<span class=\"input-group-btn\">\n\t\t\t\t\t\t\t\t<button class=\"btn btn-success\" autocomplete=\"off\" id=\"register\">Join the room</button>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Video <span class=\"badge bg-primary hide\" id=\"publisher\"></span>\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<button id=\"bitrateset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\tBandwidth\n\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t<ul id=\"bitrate\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"0\">No limit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"128\">Cap to 128kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"256\">Cap to 256kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"512\">Cap to 512kbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1024\">Cap to 1mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"1500\">Cap to 1.5mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"2000\">Cap to 2mbit</a>\n\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videolocal\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #1 <span class=\"badge bg-info hide\" id=\"remote1\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote1\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #2 <span class=\"badge bg-info hide\" id=\"remote2\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote2\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #3 <span class=\"badge bg-info hide\" id=\"remote3\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote3\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #4 <span class=\"badge bg-info hide\" id=\"remote4\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote4\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-4\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Video #5 <span class=\"badge bg-info hide\" id=\"remote5\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body relative\" id=\"videoremote5\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/videoroom.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n\nvar janus = null;\nvar sfutest = null;\nvar opaqueId = \"videoroomtest-\"+Janus.randomString(12);\n\nvar myroom = 1234;\t// Demo room\nif(getQueryStringValue(\"room\") !== \"\")\n\tmyroom = parseInt(getQueryStringValue(\"room\"));\nvar myusername = null;\nvar myid = null;\nvar mystream = null;\n// We use this other ID just to map our subscriptions to us\nvar mypvtid = null;\n\nvar localTracks = {}, localVideos = 0;\nvar feeds = [], feedStreams = {};\nvar bitrateTimer = [];\n\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar doSvc = getQueryStringValue(\"svc\");\nif(doSvc === \"\")\n\tdoSvc = null;\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar doDtx = (getQueryStringValue(\"dtx\") === \"yes\" || getQueryStringValue(\"dtx\") === \"true\");\nvar subscriber_mode = (getQueryStringValue(\"subscriber-mode\") === \"yes\" || getQueryStringValue(\"subscriber-mode\") === \"true\");\nvar use_msid = (getQueryStringValue(\"msid\") === \"yes\" || getQueryStringValue(\"msid\") === \"true\");\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to VideoRoom plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.videoroom\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\tsfutest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + sfutest.getPlugin() + \", id=\" + sfutest.getId() + \")\");\n\t\t\t\t\t\t\t\t\tJanus.log(\"  -- This is a publisher/manager\");\n\t\t\t\t\t\t\t\t\t// Prepare the username registration\n\t\t\t\t\t\t\t\t\t$('#videojoin').removeClass('hide').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#registernow').removeClass('hide').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#register').click(registerUsername);\n\t\t\t\t\t\t\t\t\t$('#username').focus();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videolocal\").parent().parent().unblock();\n\t\t\t\t\t\t\t\t\tif(!on)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t$('#publish').remove();\n\t\t\t\t\t\t\t\t\t// This controls allows us to override the global room bitrate cap\n\t\t\t\t\t\t\t\t\t$('#bitrate').parent().parent().removeClass('hide').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tlet id = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tlet bitrate = parseInt(id)*1000;\n\t\t\t\t\t\t\t\t\t\tif(bitrate === 0) {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Not limiting bandwidth via REMB\");\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Capping bandwidth to \" + bitrate + \" via REMB\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t$('#bitrateset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\tsfutest.send({ message: { request: \"configure\", bitrate: bitrate }});\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message (publisher) :::\", msg);\n\t\t\t\t\t\t\t\t\tlet event = msg[\"videoroom\"];\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Event: \" + event);\n\t\t\t\t\t\t\t\t\tif(event) {\n\t\t\t\t\t\t\t\t\t\tif(event === \"joined\") {\n\t\t\t\t\t\t\t\t\t\t\t// Publisher/manager created, negotiate WebRTC and attach to existing feeds, if any\n\t\t\t\t\t\t\t\t\t\t\tmyid = msg[\"id\"];\n\t\t\t\t\t\t\t\t\t\t\tmypvtid = msg[\"private_id\"];\n\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Successfully joined room \" + msg[\"room\"] + \" with ID \" + myid);\n\t\t\t\t\t\t\t\t\t\t\tif(subscriber_mode) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videojoin').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\tpublishOwnFeed(true);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// Any new feed to attach to?\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"publishers\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"publishers\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of available publishers/feeds:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(list[f][\"dummy\"])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet streams = list[f][\"streams\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = list[f][\"display\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in streams) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"id\"] = id;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"display\"] = display;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tfeedStreams[id] = streams;\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display + \":\", streams);\n\t\t\t\t\t\t\t\t\t\t\t\t\tnewRemoteFeed(id, display, streams);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"destroyed\") {\n\t\t\t\t\t\t\t\t\t\t\t// The room has been destroyed\n\t\t\t\t\t\t\t\t\t\t\tJanus.warn(\"The room has been destroyed!\");\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The room has been destroyed\", function() {\n\t\t\t\t\t\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t\t} else if(event === \"event\") {\n\t\t\t\t\t\t\t\t\t\t\t// Any info on our streams or a new feed to attach to?\n\t\t\t\t\t\t\t\t\t\t\tif(msg[\"streams\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet streams = msg[\"streams\"];\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in streams) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"id\"] = myid;\n\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"display\"] = myusername;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tfeedStreams[myid] = streams;\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"publishers\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tlet list = msg[\"publishers\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Got a list of available publishers/feeds:\", list);\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let f in list) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(list[f][\"dummy\"])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet id = list[f][\"id\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet display = list[f][\"display\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet streams = list[f][\"streams\"];\n\t\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in streams) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"id\"] = id;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstream[\"display\"] = display;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tfeedStreams[id] = streams;\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"  >> [\" + id + \"] \" + display + \":\", streams);\n\t\t\t\t\t\t\t\t\t\t\t\t\tnewRemoteFeed(id, display, streams);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"leaving\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t// One of the publishers has gone away?\n\t\t\t\t\t\t\t\t\t\t\t\tlet leaving = msg[\"leaving\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Publisher left: \" + leaving);\n\t\t\t\t\t\t\t\t\t\t\t\tlet remoteFeed = null;\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i=1; i<6; i++) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(feeds[i] && feeds[i].rfid == leaving) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tremoteFeed = feeds[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tif(remoteFeed) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Feed \" + remoteFeed.rfid + \" (\" + remoteFeed.rfdisplay + \") has left the room, detaching\");\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#remote'+remoteFeed.rfindex).empty().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex).empty();\n\t\t\t\t\t\t\t\t\t\t\t\t\tfeeds[remoteFeed.rfindex] = null;\n\t\t\t\t\t\t\t\t\t\t\t\t\tremoteFeed.detach();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tdelete feedStreams[leaving];\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"unpublished\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\t// One of the publishers has unpublished?\n\t\t\t\t\t\t\t\t\t\t\t\tlet unpublished = msg[\"unpublished\"];\n\t\t\t\t\t\t\t\t\t\t\t\tJanus.log(\"Publisher left: \" + unpublished);\n\t\t\t\t\t\t\t\t\t\t\t\tif(unpublished === 'ok') {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// That's us\n\t\t\t\t\t\t\t\t\t\t\t\t\tsfutest.hangup();\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tlet remoteFeed = null;\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i=1; i<6; i++) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(feeds[i] && feeds[i].rfid == unpublished) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tremoteFeed = feeds[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tif(remoteFeed) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Feed \" + remoteFeed.rfid + \" (\" + remoteFeed.rfdisplay + \") has left the room, detaching\");\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#remote'+remoteFeed.rfindex).empty().addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex).empty();\n\t\t\t\t\t\t\t\t\t\t\t\t\tfeeds[remoteFeed.rfindex] = null;\n\t\t\t\t\t\t\t\t\t\t\t\t\tremoteFeed.detach();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tdelete feedStreams[unpublished];\n\t\t\t\t\t\t\t\t\t\t\t} else if(msg[\"error\"]) {\n\t\t\t\t\t\t\t\t\t\t\t\tif(msg[\"error_code\"] === 426) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// This is a \"no such room\" error: give a more meaningful description\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"<p>Apparently room <code>\" + myroom + \"</code> (the one this demo uses as a test room) \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"does not exist...</p><p>Do you have an updated <code>janus.plugin.videoroom.jcfg</code> \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"configuration file? If not, make sure you copy the details of room <code>\" + myroom + \"</code> \" +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"from that sample in your current configuration file, then restart Janus and try again.\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\tsfutest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t\t// Check if any of the media we wanted to publish has\n\t\t\t\t\t\t\t\t\t\t// been rejected (e.g., wrong or unsupported codec)\n\t\t\t\t\t\t\t\t\t\tlet audio = msg[\"audio_codec\"];\n\t\t\t\t\t\t\t\t\t\tif(mystream && mystream.getAudioTracks() && mystream.getAudioTracks().length > 0 && !audio) {\n\t\t\t\t\t\t\t\t\t\t\t// Audio has been rejected\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Our audio stream has been rejected, viewers won't hear us\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tlet video = msg[\"video_codec\"];\n\t\t\t\t\t\t\t\t\t\tif(mystream && mystream.getVideoTracks() && mystream.getVideoTracks().length > 0 && !video) {\n\t\t\t\t\t\t\t\t\t\t\t// Video has been rejected\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Our video stream has been rejected, viewers won't see us\");\n\t\t\t\t\t\t\t\t\t\t\t// Hide the webcam video\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend(\n\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\" style=\"height: 100%;\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\" style=\"font-size: 16px;\">Video rejected, no webcam</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst !== null && mst !== undefined)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videolocal .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide').removeClass('hide');\n\t\t\t\t\t\t\t\t\tif($('#mute').length === 0) {\n\t\t\t\t\t\t\t\t\t\t// Add a 'mute' button\n\t\t\t\t\t\t\t\t\t\t$('#videolocal').append('<button class=\"btn btn-warning btn-sm bottom-left m-2\" id=\"mute\">Mute</button>');\n\t\t\t\t\t\t\t\t\t\t$('#mute').click(toggleMute);\n\t\t\t\t\t\t\t\t\t\t// Add an 'unpublish' button\n\t\t\t\t\t\t\t\t\t\t$('#videolocal').append('<button class=\"btn btn-warning btn-sm bottom-right m-2\" id=\"unpublish\">Unpublish</button>');\n\t\t\t\t\t\t\t\t\t\t$('#unpublish').click(unpublishOwnFeed);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videolocal .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videolocal .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tJanus.log(stream.getTracks());\n\t\t\t\t\t\t\t\t\t\tJanus.log(stream.getVideoTracks());\n\t\t\t\t\t\t\t\t\t\t$('#videolocal').prepend('<video class=\"rounded centered\" id=\"myvideo' + trackId + '\" width=100% autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(sfutest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\tsfutest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videolocal\").parent().parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on) {\n\t\t\t\t\t\t\t\t\t// The publisher stream is sendonly, we don't expect anything here\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification: we are unpublished now :::\");\n\t\t\t\t\t\t\t\t\tmystream = null;\n\t\t\t\t\t\t\t\t\tdelete feedStreams[myid];\n\t\t\t\t\t\t\t\t\t$('#videolocal').html('<button id=\"publish\" class=\"btn btn-primary\">Publish</button>');\n\t\t\t\t\t\t\t\t\t$('#publish').click(function() { publishOwnFeed(true); });\n\t\t\t\t\t\t\t\t\t$(\"#videolocal\").parent().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#bitrate').parent().parent().addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#bitrate a').unbind('click');\n\t\t\t\t\t\t\t\t\tlocalTracks = {};\n\t\t\t\t\t\t\t\t\tlocalVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(field, event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\tregisterUsername();\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\nfunction registerUsername() {\n\tif($('#username').length === 0) {\n\t\t// Create fields to register\n\t\t$('#register').click(registerUsername);\n\t\t$('#username').focus();\n\t} else {\n\t\t// Try a registration\n\t\t$('#username').attr('disabled', true);\n\t\t$('#register').attr('disabled', true).unbind('click');\n\t\tlet username = $('#username').val();\n\t\tif(username === \"\") {\n\t\t\t$('#you')\n\t\t\t\t.removeClass().addClass('badge bg-warning')\n\t\t\t\t.html(\"Insert your display name (e.g., pippo)\");\n\t\t\t$('#username').removeAttr('disabled');\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\treturn;\n\t\t}\n\t\tif(/[^a-zA-Z0-9]/.test(username)) {\n\t\t\t$('#you')\n\t\t\t\t.removeClass().addClass('badge bg-warning')\n\t\t\t\t.html('Input is not alphanumeric');\n\t\t\t$('#username').removeAttr('disabled').val(\"\");\n\t\t\t$('#register').removeAttr('disabled').click(registerUsername);\n\t\t\treturn;\n\t\t}\n\t\tlet register = {\n\t\t\trequest: \"join\",\n\t\t\troom: myroom,\n\t\t\tptype: \"publisher\",\n\t\t\tdisplay: username\n\t\t};\n\t\tmyusername = escapeXmlTags(username);\n\t\tsfutest.send({ message: register });\n\t}\n}\n\nfunction publishOwnFeed(useAudio) {\n\t// Publish our stream\n\t$('#publish').attr('disabled', true).unbind('click');\n\n\t// We want sendonly audio and video (uncomment the data track\n\t// too if you want to publish via datachannels as well)\n\tlet tracks = [];\n\tif(useAudio)\n\t\ttracks.push({ type: 'audio', capture: true, recv: false });\n\ttracks.push({ type: 'video', capture: true, recv: false,\n\t\t// We may need to enable simulcast or SVC on the video track\n\t\tsimulcast: doSimulcast,\n\t\t// We only support SVC for VP9 and (still WIP) AV1\n\t\tsvc: ((vcodec === 'vp9' || vcodec === 'av1') && doSvc) ? doSvc : null\n\t});\n\t//~ tracks.push({ type: 'data' });\n\n\tsfutest.createOffer(\n\t\t{\n\t\t\ttracks: tracks,\n\t\t\tcustomizeSdp: function(jsep) {\n\t\t\t\t// If DTX is enabled, munge the SDP\n\t\t\t\tif(doDtx) {\n\t\t\t\t\tjsep.sdp = jsep.sdp\n\t\t\t\t\t\t.replace(\"useinbandfec=1\", \"useinbandfec=1;usedtx=1\")\n\t\t\t\t}\n\t\t\t},\n\t\t\tsuccess: function(jsep) {\n\t\t\t\tJanus.debug(\"Got publisher SDP!\", jsep);\n\t\t\t\tlet publish = { request: \"configure\", audio: useAudio, video: true };\n\t\t\t\t// You can force a specific codec to use when publishing by using the\n\t\t\t\t// audiocodec and videocodec properties, for instance:\n\t\t\t\t// \t\tpublish[\"audiocodec\"] = \"opus\"\n\t\t\t\t// to force Opus as the audio codec to use, or:\n\t\t\t\t// \t\tpublish[\"videocodec\"] = \"vp9\"\n\t\t\t\t// to force VP9 as the videocodec to use. In both case, though, forcing\n\t\t\t\t// a codec will only work if: (1) the codec is actually in the SDP (and\n\t\t\t\t// so the browser supports it), and (2) the codec is in the list of\n\t\t\t\t// allowed codecs in a room. With respect to the point (2) above,\n\t\t\t\t// refer to the text in janus.plugin.videoroom.jcfg for more details.\n\t\t\t\t// We allow people to specify a codec via query string, for demo purposes\n\t\t\t\tif(acodec)\n\t\t\t\t\tpublish[\"audiocodec\"] = acodec;\n\t\t\t\tif(vcodec)\n\t\t\t\t\tpublish[\"videocodec\"] = vcodec;\n\t\t\t\tsfutest.send({ message: publish, jsep: jsep });\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\tif(useAudio) {\n\t\t\t\t\tpublishOwnFeed(false);\n\t\t\t\t} else {\n\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t$('#publish').removeAttr('disabled').click(function() { publishOwnFeed(true); });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n}\n\nfunction toggleMute() {\n\tlet muted = sfutest.isAudioMuted();\n\tJanus.log((muted ? \"Unmuting\" : \"Muting\") + \" local stream...\");\n\tif(muted)\n\t\tsfutest.unmuteAudio();\n\telse\n\t\tsfutest.muteAudio();\n\tmuted = sfutest.isAudioMuted();\n\t$('#mute').html(muted ? \"Unmute\" : \"Mute\");\n}\n\nfunction unpublishOwnFeed() {\n\t// Unpublish our stream\n\t$('#unpublish').attr('disabled', true).unbind('click');\n\tlet unpublish = { request: \"unpublish\" };\n\tsfutest.send({ message: unpublish });\n}\n\n// eslint-disable-next-line no-unused-vars\nfunction newRemoteFeed(id, display, streams) {\n\t// A new feed has been published, create a new plugin handle and attach to it as a subscriber\n\tlet remoteFeed = null;\n\tif(!streams)\n\t\tstreams = feedStreams[id];\n\tjanus.attach(\n\t\t{\n\t\t\tplugin: \"janus.plugin.videoroom\",\n\t\t\topaqueId: opaqueId,\n\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\tremoteFeed = pluginHandle;\n\t\t\t\tremoteFeed.remoteTracks = {};\n\t\t\t\tremoteFeed.remoteVideos = 0;\n\t\t\t\tremoteFeed.simulcastStarted = false;\n\t\t\t\tremoteFeed.svcStarted = false;\n\t\t\t\tJanus.log(\"Plugin attached! (\" + remoteFeed.getPlugin() + \", id=\" + remoteFeed.getId() + \")\");\n\t\t\t\tJanus.log(\"  -- This is a subscriber\");\n\t\t\t\t// Prepare the streams to subscribe to, as an array: we have the list of\n\t\t\t\t// streams the feed is publishing, so we can choose what to pick or skip\n\t\t\t\tlet subscription = [];\n\t\t\t\tfor(let i in streams) {\n\t\t\t\t\tlet stream = streams[i];\n\t\t\t\t\t// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video\n\t\t\t\t\tif(stream.type === \"video\" && Janus.webRTCAdapter.browserDetails.browser === \"safari\" &&\n\t\t\t\t\t\t\t((stream.codec === \"vp9\" && !Janus.safariVp9) || (stream.codec === \"vp8\" && !Janus.safariVp8))) {\n\t\t\t\t\t\ttoastr.warning(\"Publisher is using \" + stream.codec.toUpperCase +\n\t\t\t\t\t\t\t\", but Safari doesn't support it: disabling video stream #\" + stream.mindex);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tsubscription.push({\n\t\t\t\t\t\tfeed: stream.id,\t// This is mandatory\n\t\t\t\t\t\tmid: stream.mid\t\t// This is optional (all streams, if missing)\n\t\t\t\t\t});\n\t\t\t\t\t// FIXME Right now, this is always the same feed: in the future, it won't\n\t\t\t\t\tremoteFeed.rfid = stream.id;\n\t\t\t\t\tremoteFeed.rfdisplay = escapeXmlTags(stream.display);\n\t\t\t\t}\n\t\t\t\t// We wait for the plugin to send us an offer\n\t\t\t\tlet subscribe = {\n\t\t\t\t\trequest: \"join\",\n\t\t\t\t\troom: myroom,\n\t\t\t\t\tptype: \"subscriber\",\n\t\t\t\t\tstreams: subscription,\n\t\t\t\t\tuse_msid: use_msid,\n\t\t\t\t\tprivate_id: mypvtid\n\t\t\t\t};\n\t\t\t\tremoteFeed.send({ message: subscribe });\n\t\t\t},\n\t\t\terror: function(error) {\n\t\t\t\tJanus.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t},\n\t\t\ticeState: function(state) {\n\t\t\t\tJanus.log(\"ICE state (feed #\" + remoteFeed.rfindex + \") changed to \" + state);\n\t\t\t},\n\t\t\twebrtcState: function(on) {\n\t\t\t\tJanus.log(\"Janus says this WebRTC PeerConnection (feed #\" + remoteFeed.rfindex + \") is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t},\n\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t},\n\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\tJanus.debug(\" ::: Got a message (subscriber) :::\", msg);\n\t\t\t\tlet event = msg[\"videoroom\"];\n\t\t\t\tJanus.debug(\"Event: \" + event);\n\t\t\t\tif(msg[\"error\"]) {\n\t\t\t\t\tbootbox.alert(msg[\"error\"]);\n\t\t\t\t} else if(event) {\n\t\t\t\t\tif(event === \"attached\") {\n\t\t\t\t\t\t// Subscriber created and attached\n\t\t\t\t\t\tfor(let i=1;i<6;i++) {\n\t\t\t\t\t\t\tif(!feeds[i]) {\n\t\t\t\t\t\t\t\tfeeds[i] = remoteFeed;\n\t\t\t\t\t\t\t\tremoteFeed.rfindex = i;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJanus.log(\"Successfully attached to feed in room \" + msg[\"room\"]);\n\t\t\t\t\t\t$('#remote'+remoteFeed.rfindex).removeClass('hide').html(remoteFeed.rfdisplay).removeClass('hide');\n\t\t\t\t\t} else if(event === \"event\") {\n\t\t\t\t\t\t// Check if we got a simulcast-related event from this publisher\n\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\tif(!remoteFeed.simulcastStarted) {\n\t\t\t\t\t\t\t\tremoteFeed.simulcastStarted = true;\n\t\t\t\t\t\t\t\t// Add some new buttons\n\t\t\t\t\t\t\t\taddSimulcastSvcButtons(remoteFeed.rfindex, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\tupdateSimulcastSvcButtons(remoteFeed.rfindex, substream, temporal);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Or maybe SVC?\n\t\t\t\t\t\tlet spatial = msg[\"spatial_layer\"];\n\t\t\t\t\t\ttemporal = msg[\"temporal_layer\"];\n\t\t\t\t\t\tif((spatial !== null && spatial !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\tif(!remoteFeed.svcStarted) {\n\t\t\t\t\t\t\t\tremoteFeed.svcStarted = true;\n\t\t\t\t\t\t\t\t// Add some new buttons\n\t\t\t\t\t\t\t\taddSimulcastSvcButtons(remoteFeed.rfindex, true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\tupdateSimulcastSvcButtons(remoteFeed.rfindex, spatial, temporal);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// What has just happened?\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(jsep) {\n\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\tlet stereo = (jsep.sdp.indexOf(\"stereo=1\") !== -1);\n\t\t\t\t\t// Answer and attach\n\t\t\t\t\tremoteFeed.createAnswer(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tjsep: jsep,\n\t\t\t\t\t\t\t// We only specify data channels here, as this way in\n\t\t\t\t\t\t\t// case they were offered we'll enable them. Since we\n\t\t\t\t\t\t\t// don't mention audio or video tracks, we autoaccept them\n\t\t\t\t\t\t\t// as recvonly (since we won't capture anything ourselves)\n\t\t\t\t\t\t\ttracks: [\n\t\t\t\t\t\t\t\t{ type: 'data' }\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tcustomizeSdp: function(jsep) {\n\t\t\t\t\t\t\t\tif(stereo && jsep.sdp.indexOf(\"stereo=1\") == -1) {\n\t\t\t\t\t\t\t\t\t// Make sure that our offer contains stereo too\n\t\t\t\t\t\t\t\t\tjsep.sdp = jsep.sdp.replace(\"useinbandfec=1\", \"useinbandfec=1;stereo=1\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\t\tlet body = { request: \"start\", room: myroom };\n\t\t\t\t\t\t\t\tremoteFeed.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t// The subscriber stream is recvonly, we don't expect anything here\n\t\t\t},\n\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\tJanus.debug(\n\t\t\t\t\t\"Remote feed #\" + remoteFeed.rfindex +\n\t\t\t\t\t\", remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t(metadata? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t);\n\t\t\t\tif(!on) {\n\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t$('#remotevideo'+remoteFeed.rfindex + '-' + mid).remove();\n\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\tremoteFeed.remoteVideos--;\n\t\t\t\t\t\tif(remoteFeed.remoteVideos === 0) {\n\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\tif($('#videoremote'+remoteFeed.rfindex + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex).append(\n\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete remoteFeed.remoteTracks[mid];\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// If we're here, a new track was added\n\t\t\t\tif($('#remotevideo' + remoteFeed.rfindex + '-' + mid).length > 0)\n\t\t\t\t\treturn;\n\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\tremoteFeed.remoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex).append('<audio class=\"hide\" id=\"remotevideo' + remoteFeed.rfindex + '-' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\tJanus.attachMediaStream($('#remotevideo' + remoteFeed.rfindex + '-' + mid).get(0), stream);\n\t\t\t\t\tif(remoteFeed.remoteVideos === 0) {\n\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\tif($('#videoremote'+remoteFeed.rfindex + ' .no-video-container').length === 0) {\n\t\t\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex).append(\n\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\tremoteFeed.remoteVideos++;\n\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex + ' .no-video-container').remove();\n\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\tremoteFeed.remoteTracks[mid] = stream;\n\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex).append('<video class=\"rounded centered\" id=\"remotevideo' + remoteFeed.rfindex + '-' + mid + '\" width=100% autoplay playsinline/>');\n\t\t\t\t\t$('#videoremote'+remoteFeed.rfindex).append(\n\t\t\t\t\t\t'<span class=\"badge bg-primary bottom-left m-3 hide\" id=\"curres'+remoteFeed.rfindex+'\"></span>' +\n\t\t\t\t\t\t'<span class=\"badge bg-info bottom-right m-3 hide\" id=\"curbitrate'+remoteFeed.rfindex+'\"></span>');\n\t\t\t\t\tJanus.attachMediaStream($('#remotevideo' + remoteFeed.rfindex + '-' + mid).get(0), stream);\n\t\t\t\t\t// Note: we'll need this for additional videos too\n\t\t\t\t\tif(!bitrateTimer[remoteFeed.rfindex]) {\n\t\t\t\t\t\t$('#curbitrate'+remoteFeed.rfindex).removeClass('hide').removeClass('hide');\n\t\t\t\t\t\tbitrateTimer[remoteFeed.rfindex] = setInterval(function() {\n\t\t\t\t\t\t\tif(!$(\"#videoremote\" + remoteFeed.rfindex + ' video').get(0))\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\tlet bitrate = remoteFeed.getBitrate();\n\t\t\t\t\t\t\t$('#curbitrate'+remoteFeed.rfindex).text(bitrate);\n\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\tlet width = $(\"#videoremote\" + remoteFeed.rfindex + ' video').get(0).videoWidth;\n\t\t\t\t\t\t\tlet height = $(\"#videoremote\" + remoteFeed.rfindex + ' video').get(0).videoHeight;\n\t\t\t\t\t\t\tif(width > 0 && height > 0) {\n\t\t\t\t\t\t\t\tlet res = width + 'x' + height;\n\t\t\t\t\t\t\t\tif(remoteFeed.simulcastStarted)\n\t\t\t\t\t\t\t\t\tres += ' (simulcast)';\n\t\t\t\t\t\t\t\telse if(remoteFeed.svcStarted)\n\t\t\t\t\t\t\t\t\tres += ' (SVC)';\n\t\t\t\t\t\t\t\t$('#curres'+remoteFeed.rfindex).removeClass('hide').text(res).removeClass('hide');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\toncleanup: function() {\n\t\t\t\tJanus.log(\" ::: Got a cleanup notification (remote feed \" + id + \") :::\");\n\t\t\t\t$('#remotevideo'+remoteFeed.rfindex).remove();\n\t\t\t\t$('#waitingvideo'+remoteFeed.rfindex).remove();\n\t\t\t\t$('#novideo'+remoteFeed.rfindex).remove();\n\t\t\t\t$('#curbitrate'+remoteFeed.rfindex).remove();\n\t\t\t\t$('#curres'+remoteFeed.rfindex).remove();\n\t\t\t\tif(bitrateTimer[remoteFeed.rfindex])\n\t\t\t\t\tclearInterval(bitrateTimer[remoteFeed.rfindex]);\n\t\t\t\tbitrateTimer[remoteFeed.rfindex] = null;\n\t\t\t\tremoteFeed.simulcastStarted = false;\n\t\t\t\t$('#simulcast'+remoteFeed.rfindex).remove();\n\t\t\t\tremoteFeed.remoteTracks = {};\n\t\t\t\tremoteFeed.remoteVideos = 0;\n\t\t\t}\n\t\t});\n}\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// Helper to escape XML tags\nfunction escapeXmlTags(value) {\n\tif(value) {\n\t\tlet escapedValue = value.replace(new RegExp('<', 'g'), '&lt');\n\t\tescapedValue = escapedValue.replace(new RegExp('>', 'g'), '&gt');\n\t\treturn escapedValue;\n\t}\n}\n\n// Helpers to create Simulcast- or SVC-related UI, if enabled\nfunction addSimulcastSvcButtons(feed, temporal) {\n\tlet index = feed;\n\tlet f = feeds[index];\n\tlet simulcast = (f && f.simulcastStarted);\n\tlet what = (simulcast ? 'simulcast' : 'SVC');\n\tlet layer = (simulcast ? 'substream' : 'layer');\n\t$('#remote'+index).parent().append(\n\t\t'<div id=\"simulcast'+index+'\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl'+index+'-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl'+index+'-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl'+index+'-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl'+index+'-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl'+index+'-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl'+index+'-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>'\n\t);\n\tif(simulcast && Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers, when doing simulcast\n\t\t$('#tl'+index+'-2').remove();\n\t}\n\t// Enable the simulcast/SVC selection buttons\n\t$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \", wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tlet f = feeds[index];\n\t\t\tif(f.simulcastStarted)\n\t\t\t\tf.send({ message: { request: \"configure\", substream: 0 }});\n\t\t\telse\n\t\t\t\tf.send({ message: { request: \"configure\", spatial_layer: 0 }});\n\t\t});\n\t$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \", wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tlet f = feeds[index];\n\t\t\tif(f.simulcastStarted)\n\t\t\t\tf.send({ message: { request: \"configure\", substream: 1 }});\n\t\t\telse\n\t\t\t\tf.send({ message: { request: \"configure\", spatial_layer: 1 }});\n\t\t});\n\t$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching \" + what + \" \" + layer + \", wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tlet f = feeds[index];\n\t\t\tif(f.simulcastStarted)\n\t\t\t\tf.send({ message: { request: \"configure\", substream: 2 }});\n\t\t\telse\n\t\t\t\tf.send({ message: { request: \"configure\", spatial_layer: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl' + index + '-0').parent().removeClass('hide');\n\t$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tlet f = feeds[index];\n\t\t\tif(f.simulcastStarted)\n\t\t\t\tf.send({ message: { request: \"configure\", temporal: 0 }});\n\t\t\telse\n\t\t\t\tf.send({ message: { request: \"configure\", temporal_layer: 0 }});\n\t\t});\n\t$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl' + index + '-2').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tlet f = feeds[index];\n\t\t\tif(f.simulcastStarted)\n\t\t\t\tf.send({ message: { request: \"configure\", temporal: 1 }});\n\t\t\telse\n\t\t\t\tf.send({ message: { request: \"configure\", temporal_layer: 1 }});\n\t\t});\n\t$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping \" + what + \" temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl' + index + '-1').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl' + index + '-0').hasClass('btn-success'))\n\t\t\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tlet f = feeds[index];\n\t\t\tif(f.simulcastStarted)\n\t\t\t\tf.send({ message: { request: \"configure\", temporal: 2 }});\n\t\t\telse\n\t\t\t\tf.send({ message: { request: \"configure\", temporal_layer: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastSvcButtons(feed, substream, temporal) {\n\t// Check the substream\n\tlet index = feed;\n\tlet f = feeds[index];\n\tlet simulcast = (f && f.simulcastStarted);\n\tlet what = (simulcast ? 'simulcast' : 'SVC');\n\tlet layer = (simulcast ? 'substream' : 'layer');\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched \" + what + \" \" + layer + \"! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped \" + what + \" temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/virtualbg.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Virtual Background</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/selfie_segmentation.js\" crossorigin=\"anonymous\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"virtualbg.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='virtualbg.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Virtual Background\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>EchoTest</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/echotest\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a variant of the Echo Test and Canvas demos meant to\n\t\t\t\t\t\t showcase how you can use libraries like\n\t\t\t\t\t\t <a href=\"https://google.github.io/mediapipe/\" target=\"_blank\">MediaPipe</a>\n\t\t\t\t\t\tto add a virtual background to your webcam capture before sending it\n\t\t\t\t\t\tto Janus: everything is exactly the same in term of available controls,\n\t\t\t\t\t\tfeatures, and the like, with the substantial difference that we'll\n\t\t\t\t\t\tplay a bit with what we'll send on the video stream.</p>\n\t\t\t\t\t\t<p>More precisely, the demo captures the webcam feed via a\n\t\t\t\t\t\t<code>getUserMedia</code> call, and then will make use of MediaPipe\n\t\t\t\t\t\tto segment the video and replace the background by drawing on a\n\t\t\t\t\t\t<code>canvas</code> element. The <code>canvas</code> element is then used\n\t\t\t\t\t\tas the actual source of media for our PeerConnection, which means the\n\t\t\t\t\t\tvideo we get back from the EchoTest plugin should reflect the\n\t\t\t\t\t\ttweaks we've made on the stream.</p>\n\t\t\t\t\t\t<p>Notice that this is a very naive implementation, which is heavily\n\t\t\t\t\t\tbased on the <a href=\"https://google.github.io/mediapipe/solutions/selfie_segmentation.html\" target=\"_blank\">example</a>\n\t\t\t\t\t\tfrom the MediaPipe Selfie Segmentation documentation.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"videos\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\" id=\"self\">\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream\n\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm top-right hide\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"btn-group btn-group-sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t<button id=\"backgroundset\" autocomplete=\"off\" class=\"btn btn-primary dropdown-toggle\" data-bs-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\t\t\tBackground\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t<ul id=\"background\" class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"synthwave\">Synthwave</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"office\">Office</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"brickwall\">Brick Wall</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"blur\">Blur</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a class=\"dropdown-item\" href=\"#\" id=\"none\">None</a>\n\t\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoleft\">\n\t\t\t\t\t\t\t\t\t<video class=\"rounded centered hide\" id=\"myvideo\" width=\"100%\" height=\"100%\" muted=\"muted\"></video>\n\t\t\t\t\t\t\t\t\t<canvas id=\"canvas\" class=\"hide\" width=\"640\" height=\"480\" style=\"display: block; margin: auto; padding: 0\"></canvas>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream <span class=\"badge bg-primary hide\" id=\"curres\"></span> <span class=\"badge bg-info hide\" id=\"curbitrate\"></span></span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"videoright\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/virtualbg.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global iceServers:readonly, Janus:readonly, server:readonly */\n/* global SelfieSegmentation:readonly */\n\nvar janus = null;\nvar echotest = null;\nvar opaqueId = \"canvas-\"+Janus.randomString(12);\n\nvar localTracks = {}, localVideos = 0,\n\tremoteTracks = {}, remoteVideos = 0;\nvar bitrateTimer = null;\n\nvar doSimulcast = (getQueryStringValue(\"simulcast\") === \"yes\" || getQueryStringValue(\"simulcast\") === \"true\");\nvar acodec = (getQueryStringValue(\"acodec\") !== \"\" ? getQueryStringValue(\"acodec\") : null);\nvar vcodec = (getQueryStringValue(\"vcodec\") !== \"\" ? getQueryStringValue(\"vcodec\") : null);\nvar vprofile = (getQueryStringValue(\"vprofile\") !== \"\" ? getQueryStringValue(\"vprofile\") : null);\nvar simulcastStarted = false;\n\n// Canvas object\nvar canvas = null;\nvar context = null;\nvar canvasStream = null;\nvar width = doSimulcast ? 1280 : 640,\n\theight = doSimulcast ? 720 : 360;\n\n// We can use a few different images as our virtual background\nvar bg = 'synthwave';\nvar images = {\n\t'synthwave': './background/retro.webp',\n\t'office': './background/office.jpeg',\n\t'brickwall': './background/brick-wall.jpeg'\n};\nconst image = new Image();\nimage.src = images[bg];\n\n$(document).ready(function() {\n\tcanvas = document.getElementById('canvas');\n\tcontext = canvas.getContext('2d');\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\ticeServers: iceServers,\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to EchoTest plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: \"janus.plugin.echotest\",\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\techotest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + echotest.getPlugin() + \", id=\" + echotest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// We're connected to the plugin, create and populate the canvas element\n\t\t\t\t\t\t\t\t\tcreateCanvas();\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\techotest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result === \"done\") {\n\t\t\t\t\t\t\t\t\t\t\t// The plugin closed the echo test\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The Echo Test is over\");\n\t\t\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#peervideo').remove();\n\t\t\t\t\t\t\t\t\t\t\t$('#background').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// Any loss?\n\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\tif(status === \"slow_link\") {\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Janus apparently missed many packets we sent, maybe we should reduce the bitrate\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// Is simulcast in place?\n\t\t\t\t\t\t\t\t\tlet substream = msg[\"substream\"];\n\t\t\t\t\t\t\t\t\tlet temporal = msg[\"temporal\"];\n\t\t\t\t\t\t\t\t\tif((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {\n\t\t\t\t\t\t\t\t\t\tif(!simulcastStarted) {\n\t\t\t\t\t\t\t\t\t\t\tsimulcastStarted = true;\n\t\t\t\t\t\t\t\t\t\t\taddSimulcastButtons(msg[\"videocodec\"] === \"vp8\");\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// We just received notice that there's been a switch, update the buttons\n\t\t\t\t\t\t\t\t\t\tupdateSimulcastButtons(substream, temporal);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\t// We use the track ID as name of the element, but it may contain invalid characters\n\t\t\t\t\t\t\t\t\tlet trackId = track.id.replace(/[{}]/g, \"\");\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\t\tlet tracks = stream.getTracks();\n\t\t\t\t\t\t\t\t\t\t\t\tfor(let i in tracks) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tlet mst = tracks[i];\n\t\t\t\t\t\t\t\t\t\t\t\t\tif(mst)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmst.stop();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// eslint-disable-next-line no-unused-vars\n\t\t\t\t\t\t\t\t\t\t\t} catch(e) {}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\t$('#myvideo' + trackId).remove();\n\t\t\t\t\t\t\t\t\t\t\tlocalVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete localTracks[trackId];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\tlet stream = localTracks[trackId];\n\t\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\t\t// We've been here already\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif($('#videoleft video').length === 0) {\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// We ignore local audio tracks, they'd generate echo anyway\n\t\t\t\t\t\t\t\t\t\tif(localVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoleft .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoleft').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tlocalVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoleft .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tstream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tlocalTracks[trackId] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created local stream:\", stream);\n\t\t\t\t\t\t\t\t\t\t$('#videoleft').append('<video class=\"rounded centered\" id=\"myvideo' + trackId + '\" width=\"100%\" height=\"100%\" autoplay playsinline muted=\"muted\"/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#myvideo' + trackId).get(0), stream);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(echotest.webrtcStuff.pc.iceConnectionState !== \"completed\" &&\n\t\t\t\t\t\t\t\t\t\t\techotest.webrtcStuff.pc.iceConnectionState !== \"connected\") {\n\t\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().block({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<b>Publishing...</b>',\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: 'white'\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peervideo' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tif(track.kind === \"video\") {\n\t\t\t\t\t\t\t\t\t\t\tremoteVideos--;\n\t\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No remote video available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tlet addButtons = false;\n\t\t\t\t\t\t\t\t\tif($('#videoright audio').length === 0 && $('#videoright video').length === 0) {\n\t\t\t\t\t\t\t\t\t\taddButtons = true;\n\t\t\t\t\t\t\t\t\t\t$('#videos').removeClass('hide');\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<audio class=\"hide\" id=\"peervideo' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\tif(remoteVideos === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// No video, at least for now: show a placeholder\n\t\t\t\t\t\t\t\t\t\t\tif($('#videoright .no-video-container').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'<div class=\"no-video-container\">' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<i class=\"fa-solid fa-video fa-xl no-video-icon\"></i>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'<span class=\"no-video-text\">No webcam available</span>' +\n\t\t\t\t\t\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// New video track: create a stream out of it\n\t\t\t\t\t\t\t\t\t\tremoteVideos++;\n\t\t\t\t\t\t\t\t\t\t$('#videoright .no-video-container').remove();\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote video stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peervideo'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#videoright').append('<video class=\"rounded centered\" id=\"peervideo' + mid + '\" width=\"100%\" height=\"100%\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peervideo' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// FIXME we'll need this for additional videos too\n\t\t\t\t\t\t\t\t\t\tif(!bitrateTimer) {\n\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = setInterval(function() {\n\t\t\t\t\t\t\t\t\t\t\t\tif(!$(\"#peervideo\" + mid).get(0))\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t\t\t// Display updated bitrate, if supported\n\t\t\t\t\t\t\t\t\t\t\t\tlet bitrate = echotest.getBitrate();\n\t\t\t\t\t\t\t\t\t\t\t\t//~ Janus.debug(\"Current bitrate is \" + echotest.getBitrate());\n\t\t\t\t\t\t\t\t\t\t\t\t$('#curbitrate').text(bitrate);\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the resolution changed too\n\t\t\t\t\t\t\t\t\t\t\t\tlet width = $(\"#peervideo\" + mid).get(0).videoWidth;\n\t\t\t\t\t\t\t\t\t\t\t\tlet height = $(\"#peervideo\" + mid).get(0).videoHeight;\n\t\t\t\t\t\t\t\t\t\t\t\tif(width > 0 && height > 0)\n\t\t\t\t\t\t\t\t\t\t\t\t\t$('#curres').removeClass('hide').text(width+'x'+height).removeClass('hide');\n\t\t\t\t\t\t\t\t\t\t\t}, 1000);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(!addButtons)\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t// Enable background image selector\n\t\t\t\t\t\t\t\t\t$('#background').parent().parent().removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#background a').click(function() {\n\t\t\t\t\t\t\t\t\t\t$('.dropdown-toggle').dropdown('hide');\n\t\t\t\t\t\t\t\t\t\tbg = $(this).attr(\"id\");\n\t\t\t\t\t\t\t\t\t\tif(bg !== 'none')\n\t\t\t\t\t\t\t\t\t\t\timage.src = images[bg];\n\t\t\t\t\t\t\t\t\t\t$('#backgroundset').text($(this).text()).parent().removeClass('open');\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t$('video').remove();\n\t\t\t\t\t\t\t\t\t$('#waitingvideo').remove();\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").empty().parent().unblock();\n\t\t\t\t\t\t\t\t\t$('#videoright').empty();\n\t\t\t\t\t\t\t\t\t$('#background').attr('disabled', true);\n\t\t\t\t\t\t\t\t\t$('#curbitrate').addClass('hide');\n\t\t\t\t\t\t\t\t\t$('#curres').addClass('hide');\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t\tremoteVideos = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// This is the callback we invoke on the segmentation result\nfunction handleSegmentationResults(results) {\n\t// Prepare the new frame\n\tcontext.save();\n\tcontext.clearRect(0, 0, canvas.width, canvas.height);\n\tcontext.drawImage(results.segmentationMask, 0, 0, canvas.width, canvas.height);\n\tif(bg === 'none' || bg === 'blur') {\n\t\t// No background selected, just draw the segmented background (and maybe blur)\n\t\tif(bg === 'blur')\n\t\t\tcontext.filter = 'blur(10px)';\n\t\tcontext.globalCompositeOperation = 'source-out';\n\t\tcontext.drawImage(results.image, 0, 0, canvas.width, canvas.height);\n\t\tif(bg === 'blur')\n\t\t\tcontext.filter = 'blur(0px)';\n\t} else {\n\t\t// Draw the image as the new background, and the segmented video on top of that\n\t\tcontext.globalCompositeOperation = 'source-out';\n\t\tcontext.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);\n\t}\n\tcontext.globalCompositeOperation = 'destination-atop';\n\tcontext.drawImage(results.image, 0, 0, canvas.width, canvas.height);\n\t// Done\n\tcontext.restore();\n}\n// This is MediaPipe's Selfie Segmentation loaded\nconst selfieSegmentation = new SelfieSegmentation({locateFile: (file) => {\n\treturn `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/${file}`;\n}});\nselfieSegmentation.setOptions({\n\tmodelSelection: 1,\n});\nselfieSegmentation.onResults(handleSegmentationResults);\n\n// Helper function to create (and populate) our canvas element\nfunction createCanvas() {\n\t// Capture the local webcam\n\tnavigator.mediaDevices.getUserMedia(\n\t\t{\n\t\t\taudio: true,\n\t\t\tvideo: {\n\t\t\t\twidth: { ideal: width },\n\t\t\t\theight: { ideal: height }\n\t\t\t}\n\t\t})\n\t\t.then(function(stream) {\n\t\t\t// We have our video\n\t\t\tJanus.debug(stream);\n\t\t\tJanus.attachMediaStream($('#myvideo').get(0), stream);\n\t\t\t$('#myvideo').get(0).muted = \"muted\";\n\t\t\t$('#myvideo').get(0).play();\n\t\t\t// As soon as the video is ready, start the segmentation\n\t\t\t$('#myvideo').get(0).addEventListener('playing', function () {\n\t\t\t\tlet myvideo = this;\n\t\t\t\t// Adapt the canvas to the size of the video element\n\t\t\t\tif(width !== myvideo.videoWidth || height !== myvideo.videoHeight) {\n\t\t\t\t\twidth = myvideo.videoWidth;\n\t\t\t\t\theight = myvideo.videoHeight;\n\t\t\t\t}\n\t\t\t\tcanvas.width = width;\n\t\t\t\tcanvas.height = height;\n\t\t\t\t// Draw the video element on top of the canvas\n\t\t\t\tlet lastTime = new Date();\n\t\t\t\tasync function getFrames() {\n\t\t\t\t\tconst now = myvideo.currentTime;\n\t\t\t\t\tif(now > lastTime)\n\t\t\t\t\t\tawait selfieSegmentation.send({image: myvideo});\n\t\t\t\t\tlastTime = now;\n\t\t\t\t\trequestAnimationFrame(getFrames);\n\t\t\t\t}\n\t\t\t\tgetFrames();\n\t\t\t\t// Capture the canvas as a local MediaStream\n\t\t\t\tcanvasStream = canvas.captureStream();\n\t\t\t\tcanvasStream.addTrack(stream.getAudioTracks()[0]);\n\t\t\t\t// Now that the stream is ready, we can create the PeerConnection\n\t\t\t\tlet body = { audio: true, video: true };\n\t\t\t\t// We can try and force a specific codec, by telling the plugin what we'd prefer\n\t\t\t\t// For simplicity, you can set it via a query string (e.g., ?vcodec=vp9)\n\t\t\t\tif(acodec)\n\t\t\t\t\tbody[\"audiocodec\"] = acodec;\n\t\t\t\tif(vcodec)\n\t\t\t\t\tbody[\"videocodec\"] = vcodec;\n\t\t\t\t// For the codecs that support them (VP9 and H.264) you can specify a codec\n\t\t\t\t// profile as well (e.g., ?vprofile=2 for VP9, or ?vprofile=42e01f for H.264)\n\t\t\t\tif(vprofile)\n\t\t\t\t\tbody[\"videoprofile\"] = vprofile;\n\t\t\t\tJanus.debug(\"Sending message:\", body);\n\t\t\t\techotest.send({ message: body });\n\t\t\t\tJanus.debug(\"Trying a createOffer too (audio/video sendrecv)\");\n\t\t\t\t// We need to pass the canvas MediaStream tracks we\n\t\t\t\t// captured here, so we tell janus.js to use those\n\t\t\t\tlet canvasTracks = [];\n\t\t\t\tif(canvasStream.getAudioTracks().length > 0)\n\t\t\t\t\tcanvasTracks.push({ type: 'audio', capture: canvasStream.getAudioTracks()[0], recv: true });\n\t\t\t\tif(canvasStream.getVideoTracks().length > 0)\n\t\t\t\t\tcanvasTracks.push({ type: 'video', capture: canvasStream.getVideoTracks()[0], recv: true, simulcast: doSimulcast });\n\t\t\t\techotest.createOffer(\n\t\t\t\t\t{\n\t\t\t\t\t\ttracks: canvasTracks,\n\t\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\t\techotest.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t\t\t$('#videoright').html(\n\t\t\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t\t},\n\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t}, 0);\n\t\t})\n\t\t.catch(function(error) {\n\t\t\tJanus.error(error);\n\t\t\tbootbox.alert(error);\n\t\t});\n}\n\n// Helpers to create Simulcast-related UI, if enabled\nfunction addSimulcastButtons(temporal) {\n\t$('#curres').parent().append(\n\t\t'<div id=\"simulcast\" class=\"btn-group-vertical btn-group-xs top-right\">' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"sl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to higher quality\">SL 2</button>' +\n\t\t'\t\t<button id=\"sl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to normal quality\">SL 1</button>' +\n\t\t'\t\t<button id=\"sl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Switch to lower quality\">SL 0</button>' +\n\t\t'\t</div>' +\n\t\t'\t<div class=\"btn-group btn-group-xs d-flex hide\" style=\"width: 100%\">' +\n\t\t'\t\t<button id=\"tl-2\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 2\">TL 2</button>' +\n\t\t'\t\t<button id=\"tl-1\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 1\">TL 1</button>' +\n\t\t'\t\t<button id=\"tl-0\" type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"tooltip\" title=\"Cap to temporal layer 0\">TL 0</button>' +\n\t\t'\t</div>' +\n\t\t'</div>');\n\tif(Janus.webRTCAdapter.browserDetails.browser !== \"firefox\") {\n\t\t// Chromium-based browsers only have two temporal layers\n\t\t$('#tl-2').remove();\n\t}\n\t// Enable the simulcast selection buttons\n\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (lower quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { substream: 0 }});\n\t\t});\n\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (normal quality)\", null, {timeOut: 2000});\n\t\t\tif(!$('#sl-2').hasClass('btn-success'))\n\t\t\t\t$('#sl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 1 }});\n\t\t});\n\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Switching simulcast substream, wait for it... (higher quality)\", null, {timeOut: 2000});\n\t\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#sl-1').hasClass('btn-success'))\n\t\t\t\t$('#sl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#sl-0').hasClass('btn-success'))\n\t\t\t\t$('#sl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { substream: 2 }});\n\t\t});\n\tif(!temporal)\t// No temporal layer support\n\t\treturn;\n\t$('#tl-0').parent().removeClass('hide');\n\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (lowest FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\techotest.send({ message: { temporal: 0 }});\n\t\t});\n\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (medium FPS)\", null, {timeOut: 2000});\n\t\t\tif(!$('#tl-2').hasClass('btn-success'))\n\t\t\t\t$('#tl-2').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-info');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 1 }});\n\t\t});\n\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary')\n\t\t.unbind('click').click(function() {\n\t\t\ttoastr.info(\"Capping simulcast temporal layer, wait for it... (highest FPS)\", null, {timeOut: 2000});\n\t\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');\n\t\t\tif(!$('#tl-1').hasClass('btn-success'))\n\t\t\t\t$('#tl-1').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\tif(!$('#tl-0').hasClass('btn-success'))\n\t\t\t\t$('#tl-0').removeClass('btn-primary btn-info').addClass('btn-primary');\n\t\t\techotest.send({ message: { temporal: 2 }});\n\t\t});\n}\n\nfunction updateSimulcastButtons(substream, temporal) {\n\t// Check the substream\n\tif(substream === 0) {\n\t\ttoastr.success(\"Switched simulcast substream! (lower quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(substream === 1) {\n\t\ttoastr.success(\"Switched simulcast substream! (normal quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(substream === 2) {\n\t\ttoastr.success(\"Switched simulcast substream! (higher quality)\", null, {timeOut: 2000});\n\t\t$('#sl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n\t// Check the temporal layer\n\tif(temporal === 0) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (lowest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t} else if(temporal === 1) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (medium FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t} else if(temporal === 2) {\n\t\ttoastr.success(\"Capped simulcast temporal layer! (highest FPS)\", null, {timeOut: 2000});\n\t\t$('#tl-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');\n\t\t$('#tl-1').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t\t$('#tl-0').removeClass('btn-primary btn-success').addClass('btn-primary');\n\t}\n}\n"
  },
  {
    "path": "html/demos/webaudio.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Web Audio Processing</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/9.0.3/adapter.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/6.0.0/bootbox.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js\"></script>\n<script type=\"text/javascript\" src=\"settings.js\" ></script>\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n<script type=\"text/javascript\" src=\"webaudio.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top li.dropdown\").addClass(\"active\");\n\t\t\t$(\".fixed-top a[href='webaudio.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"../footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"../forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Plugin Demo: Web Audio Processing\n\t\t\t\t\t<button class=\"btn btn-secondary\" autocomplete=\"off\" id=\"start\">Start</button>\n\t\t\t\t</h1>\n\t\t\t</div>\n\t\t\t<div class=\"container\" id=\"details\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"alert alert-primary mt-2 mb-5\">\n\t\t\t\t\t\tWant to learn more about the <strong>EchoTest</strong> plugin?\n\t\t\t\t\t\tCheck the <a target=\"_blank\" href=\"https://janus.conf.meetecho.com/docs/echotest\">Documentation</a>.\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t\t<h3>Demo details</h3>\n\t\t\t\t\t\t<p>This is a variant of the Echo Test demo meant to showcase how you can use the\n\t\t\t\t\t\t<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API\">Web Audio API</a>\n\t\t\t\t\t\tto manipulate the audio from the microphone, before sending it to Janus,\n\t\t\t\t\t\tusing some dynamic controls to tweak the output. The remote audio, echoed\n\t\t\t\t\t\tback by Janus, is also processed via Web Audio and rendered visually.</p>\n\t\t\t\t\t\t<p>Notice that this is just a basic example, and is not meant as a\n\t\t\t\t\t\tcomprehensive tutorial on how to use Web Audio, but only as a simple\n\t\t\t\t\t\tindication of how they can indeed be used with Janus WebRTC streams.</p>\n\t\t\t\t\t\t<p>Also notice that this demo does not negotiates video for the sake of\n\t\t\t\t\t\tsimplicity, in order to only focus on audio controls and visualization.</p>\n\t\t\t\t\t\t<p>Press the <code>Start</code> button above to launch the demo.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"container mt-4 hide\" id=\"demo\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Local Stream (compressor)</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\">\n\t\t\t\t\t\t\t\t<div class=\"input-group mb-2\">\n\t\t\t\t\t\t\t\t\t<span class=\"input-group-text w-100\">Threshold</span>\n\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"threshold\" name=\"threshold\" placeholder=\"Threshold\" value=\"\" onkeypress=\"return checkEnter(event);\" disabled></input>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"input-group mt-2 mb-2\">\n\t\t\t\t\t\t\t\t\t<span class=\"input-group-text w-100\">Knee</span>\n\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"knee\" name=\"knee\" placeholder=\"Knee\" value=\"\" onkeypress=\"return checkEnter(event);\" disabled></input>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"input-group mt-2 mb-2\">\n\t\t\t\t\t\t\t\t\t<span class=\"input-group-text w-100\">Ratio</span>\n\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"ratio\" name=\"ratio\" placeholder=\"Ratio\" value=\"\" onkeypress=\"return checkEnter(event);\" disabled></input>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"input-group mt-2 mb-2\">\n\t\t\t\t\t\t\t\t\t<span class=\"input-group-text w-100\">Attack</span>\n\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"attack\" name=\"attack\" placeholder=\"Attack\" value=\"\" onkeypress=\"return checkEnter(event);\" disabled></input>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"input-group mt-2 mb-2\">\n\t\t\t\t\t\t\t\t\t<span class=\"input-group-text w-100\">Release</span>\n\t\t\t\t\t\t\t\t\t<input type=\"text\" class=\"form-control\" id=\"release\" name=\"release\" placeholder=\"Release\" value=\"\" onkeypress=\"return checkEnter(event);\" disabled></input>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t\t<div class=\"card\">\n\t\t\t\t\t\t\t<div class=\"card-header\">\n\t\t\t\t\t\t\t\t<span class=\"card-title\">Remote Stream (visualizer)</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"card-body\" id=\"remote\">\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "html/demos/webaudio.js",
    "content": "// We import the settings.js file to know which address we should contact\n// to talk to Janus, and optionally which STUN/TURN servers should be\n// used as well. Specifically, that file defines the \"server\" and\n// \"iceServers\" properties we'll pass when creating the Janus session.\n\n/* global Janus:readonly, server:readonly */\n\nvar janus = null;\nvar echotest = null;\nvar opaqueId = \"echotest-\"+Janus.randomString(12);\n\nvar remoteTracks = {};\nvar bitrateTimer = null;\n\n\n// Web Audio context and filters\nvar audioContext = new (window.AudioContext || window.webkitAudioContext)();\nvar compressor = null, analyser = null;\n// Canvas and visualizer data\nvar canvasContext = null, dataArray = null;\n\n// By default we talk to the \"regular\" EchoTest plugin\nvar echotestPluginBackend = \"janus.plugin.echotest\";\n// We can use query string arguments to talk to the Lua or Duktape EchoTest\n// demo scripts instead. Notice that this assumes that the Lua or Duktape\n// plugins are configured to run the sample scripts that comes with the repo\nif(getQueryStringValue(\"plugin\") === \"lua\")\n\techotestPluginBackend = \"janus.plugin.echolua\";\nelse if(getQueryStringValue(\"plugin\") === \"duktape\")\n\techotestPluginBackend = \"janus.plugin.echojs\";\n\n$(document).ready(function() {\n\t// Initialize the library (all console debuggers enabled)\n\tJanus.init({debug: \"all\", callback: function() {\n\t\t// Use a button to start the demo\n\t\t$('#start').one('click', function() {\n\t\t\t$(this).attr('disabled', true).unbind('click');\n\t\t\t// Make sure the browser supports WebRTC\n\t\t\tif(!Janus.isWebrtcSupported()) {\n\t\t\t\tbootbox.alert(\"No WebRTC support... \");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Create session\n\t\t\tjanus = new Janus(\n\t\t\t\t{\n\t\t\t\t\tserver: server,\n\t\t\t\t\t// No \"iceServers\" is provided, meaning janus.js will use a default STUN server\n\t\t\t\t\t// Here are some examples of how an iceServers field may look like to support TURN\n\t\t\t\t\t// \t\ticeServers: [{urls: \"turn:yourturnserver.com:3478\", username: \"janususer\", credential: \"januspwd\"}],\n\t\t\t\t\t// \t\ticeServers: [{urls: \"turn:yourturnserver.com:443?transport=tcp\", username: \"janususer\", credential: \"januspwd\"}],\n\t\t\t\t\t// \t\ticeServers: [{urls: \"turns:yourturnserver.com:443?transport=tcp\", username: \"janususer\", credential: \"januspwd\"}],\n\t\t\t\t\t// Should the Janus API require authentication, you can specify either the API secret or user token here too\n\t\t\t\t\t//\t\ttoken: \"mytoken\",\n\t\t\t\t\t//\tor\n\t\t\t\t\t//\t\tapisecret: \"serversecret\",\n\t\t\t\t\tsuccess: function() {\n\t\t\t\t\t\t// Attach to EchoTest plugin\n\t\t\t\t\t\tjanus.attach(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tplugin: echotestPluginBackend,\n\t\t\t\t\t\t\t\topaqueId: opaqueId,\n\t\t\t\t\t\t\t\tsuccess: function(pluginHandle) {\n\t\t\t\t\t\t\t\t\t$('#details').remove();\n\t\t\t\t\t\t\t\t\techotest = pluginHandle;\n\t\t\t\t\t\t\t\t\tJanus.log(\"Plugin attached! (\" + echotest.getPlugin() + \", id=\" + echotest.getId() + \")\");\n\t\t\t\t\t\t\t\t\t// Capture the webcam and create the Web Audio processors\n\t\t\t\t\t\t\t\t\tsetupWebAudioDemo();\n\t\t\t\t\t\t\t\t\t// Done\n\t\t\t\t\t\t\t\t\t$('#demo').removeClass('hide');\n\t\t\t\t\t\t\t\t\t$('#start').removeAttr('disabled').html(\"Stop\")\n\t\t\t\t\t\t\t\t\t\t.click(function() {\n\t\t\t\t\t\t\t\t\t\t\t$(this).attr('disabled', true);\n\t\t\t\t\t\t\t\t\t\t\tif(bitrateTimer)\n\t\t\t\t\t\t\t\t\t\t\t\tclearInterval(bitrateTimer);\n\t\t\t\t\t\t\t\t\t\t\tbitrateTimer = null;\n\t\t\t\t\t\t\t\t\t\t\tjanus.destroy();\n\t\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\t\t\t\tconsole.error(\"  -- Error attaching plugin...\", error);\n\t\t\t\t\t\t\t\t\tbootbox.alert(\"Error attaching plugin... \" + error);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tconsentDialog: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Consent dialog should be \" + (on ? \"on\" : \"off\") + \" now\");\n\t\t\t\t\t\t\t\t\tif(on) {\n\t\t\t\t\t\t\t\t\t\t// Darken screen and show hint\n\t\t\t\t\t\t\t\t\t\t$.blockUI({\n\t\t\t\t\t\t\t\t\t\t\tmessage: '<div><img src=\"up_arrow.png\"/></div>',\n\t\t\t\t\t\t\t\t\t\t\tbaseZ: 3001,\n\t\t\t\t\t\t\t\t\t\t\tcss: {\n\t\t\t\t\t\t\t\t\t\t\t\tborder: 'none',\n\t\t\t\t\t\t\t\t\t\t\t\tpadding: '15px',\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: 'transparent',\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: '#aaa',\n\t\t\t\t\t\t\t\t\t\t\t\ttop: '10px',\n\t\t\t\t\t\t\t\t\t\t\t\tleft: '100px'\n\t\t\t\t\t\t\t\t\t\t\t} });\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Restore screen\n\t\t\t\t\t\t\t\t\t\t$.unblockUI();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ticeState: function(state) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"ICE state changed to \" + state);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tmediaState: function(medium, on, mid) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus \" + (on ? \"started\" : \"stopped\") + \" receiving our \" + medium + \" (mid=\" + mid + \")\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\twebrtcState: function(on) {\n\t\t\t\t\t\t\t\t\tJanus.log(\"Janus says our WebRTC PeerConnection is \" + (on ? \"up\" : \"down\") + \" now\");\n\t\t\t\t\t\t\t\t\t$(\"#videoleft\").parent().unblock();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslowLink: function(uplink, lost, mid) {\n\t\t\t\t\t\t\t\t\tJanus.warn(\"Janus reports problems \" + (uplink ? \"sending\" : \"receiving\") +\n\t\t\t\t\t\t\t\t\t\t\" packets on mid \" + mid + \" (\" + lost + \" lost packets)\");\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonmessage: function(msg, jsep) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\" ::: Got a message :::\", msg);\n\t\t\t\t\t\t\t\t\tif(jsep) {\n\t\t\t\t\t\t\t\t\t\tJanus.debug(\"Handling SDP as well...\", jsep);\n\t\t\t\t\t\t\t\t\t\techotest.handleRemoteJsep({ jsep: jsep });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tlet result = msg[\"result\"];\n\t\t\t\t\t\t\t\t\tif(result) {\n\t\t\t\t\t\t\t\t\t\tif(result === \"done\") {\n\t\t\t\t\t\t\t\t\t\t\t// The plugin closed the echo test\n\t\t\t\t\t\t\t\t\t\t\tbootbox.alert(\"The test is over\");\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t// Any loss?\n\t\t\t\t\t\t\t\t\t\tlet status = result[\"status\"];\n\t\t\t\t\t\t\t\t\t\tif(status === \"slow_link\") {\n\t\t\t\t\t\t\t\t\t\t\ttoastr.warning(\"Janus apparently missed many packets we sent, maybe we should reduce the bitrate\", \"Packet loss?\", {timeOut: 2000});\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonlocaltrack: function(track, on) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\"Local track \" + (on ? \"added\" : \"removed\") + \":\", track);\n\t\t\t\t\t\t\t\t\t// We don't do anything here, since we captured the stream ourselves\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonremotetrack: function(track, mid, on, metadata) {\n\t\t\t\t\t\t\t\t\tJanus.debug(\n\t\t\t\t\t\t\t\t\t\t\"Remote track (mid=\" + mid + \") \" +\n\t\t\t\t\t\t\t\t\t\t(on ? \"added\" : \"removed\") +\n\t\t\t\t\t\t\t\t\t\t(metadata? \" (\" + metadata.reason + \") \": \"\") + \":\", track\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t// Now that we're aware of the remote stream, we process it to visualize it\n\t\t\t\t\t\t\t\t\tif(!on) {\n\t\t\t\t\t\t\t\t\t\t// Track removed, get rid of the stream and the rendering\n\t\t\t\t\t\t\t\t\t\t$('#peeraudio' + mid).remove();\n\t\t\t\t\t\t\t\t\t\tdelete remoteTracks[mid];\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t// If we're here, a new track was added\n\t\t\t\t\t\t\t\t\t$('#spinner').remove();\n\t\t\t\t\t\t\t\t\tif(track.kind === \"audio\") {\n\t\t\t\t\t\t\t\t\t\t// New audio track: create a stream out of it, and use a hidden <audio> element\n\t\t\t\t\t\t\t\t\t\tlet stream = new MediaStream([track]);\n\t\t\t\t\t\t\t\t\t\tremoteTracks[mid] = stream;\n\t\t\t\t\t\t\t\t\t\tJanus.log(\"Created remote audio stream:\", stream);\n\t\t\t\t\t\t\t\t\t\tif($('#peeraudio'+mid).length === 0)\n\t\t\t\t\t\t\t\t\t\t\t$('#remote').append('<audio class=\"hide\" id=\"peeraudio' + mid + '\" autoplay playsinline/>');\n\t\t\t\t\t\t\t\t\t\tJanus.attachMediaStream($('#peeraudio' + mid).get(0), stream);\n\t\t\t\t\t\t\t\t\t\t// Do we have a visualizer already?\n\t\t\t\t\t\t\t\t\t\tif($('#canvas').length === 0) {\n\t\t\t\t\t\t\t\t\t\t\t// Create a new visualizer: since we're lazy we use this existing example:\n\t\t\t\t\t\t\t\t\t\t\t// https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API\n\t\t\t\t\t\t\t\t\t\t\t$('#remote').append('<canvas id=\"canvas\" width=\"432\" height=\"240\" style=\"display: block; margin: auto; padding: 0\"></canvas>');\n\t\t\t\t\t\t\t\t\t\t\tlet canvas = $('#canvas').get(0);\n\t\t\t\t\t\t\t\t\t\t\tcanvasContext = canvas.getContext('2d');\n\t\t\t\t\t\t\t\t\t\t\tanalyser = audioContext.createAnalyser();\n\t\t\t\t\t\t\t\t\t\t\tanalyser.fftSize = 256;\n\t\t\t\t\t\t\t\t\t\t\tdataArray = new Uint8Array(analyser.frequencyBinCount);\n\t\t\t\t\t\t\t\t\t\t\tcanvasContext.clearRect(0, 0, 432, 240);\n\t\t\t\t\t\t\t\t\t\t\tlet source = audioContext.createMediaStreamSource(stream);\n\t\t\t\t\t\t\t\t\t\t\tsource.connect(analyser);\n\t\t\t\t\t\t\t\t\t\t\tdrawVisualizer();\n\t\t\t\t\t\t\t\t\t\t\t// Also unlock the compressor controls\n\t\t\t\t\t\t\t\t\t\t\t$('#threshold').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#knee').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#ratio').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#attack').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t\t$('#release').removeAttr('disabled');\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t// Video? Ignore\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toncleanup: function() {\n\t\t\t\t\t\t\t\t\tJanus.log(\" ::: Got a cleanup notification :::\");\n\t\t\t\t\t\t\t\t\tremoteTracks = {};\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(error);\n\t\t\t\t\t\tbootbox.alert(error, function() {\n\t\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\tdestroyed: function() {\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}});\n});\n\n// Helper to parse query string\nfunction getQueryStringValue(name) {\n\tname = name.replace(/[[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\");\n\tlet regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"),\n\t\tresults = regex.exec(location.search);\n\treturn results === null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \"));\n}\n\n// eslint-disable-next-line no-unused-vars\nfunction checkEnter(event) {\n\tlet theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;\n\tif(theCode == 13) {\n\t\t// Update the compressor values, if needed\n\t\tlet threshold = parseFloat($('#threshold').val());\n\t\tif(threshold !== compressor.threshold.value) {\n\t\t\tif(threshold < compressor.threshold.minValue || threshold > compressor.threshold.maxValue) {\n\t\t\t\ttoastr[\"warning\"](\"Invalid threshold value\");\n\t\t\t} else {\n\t\t\t\tcompressor.threshold.setValueAtTime(threshold, audioContext.currentTime);\n\t\t\t\ttoastr[\"success\"](\"Threshold updated\");\n\t\t\t}\n\t\t\t$('#threshold').val('' + compressor.threshold.value);\n\t\t}\n\t\tlet knee = parseFloat($('#knee').val());\n\t\tif(knee !== compressor.knee.value) {\n\t\t\tif(knee < compressor.knee.minValue || knee > compressor.knee.maxValue) {\n\t\t\t\ttoastr[\"warning\"](\"Invalid knee value\");\n\t\t\t} else {\n\t\t\t\tcompressor.knee.setValueAtTime(knee, audioContext.currentTime);\n\t\t\t\ttoastr[\"success\"](\"Knee updated\");\n\t\t\t}\n\t\t\t$('#knee').val('' + compressor.knee.value);\n\t\t}\n\t\tlet ratio = parseFloat($('#ratio').val());\n\t\tif(ratio !== compressor.ratio.value) {\n\t\t\tif(ratio < compressor.ratio.minValue || ratio > compressor.ratio.maxValue) {\n\t\t\t\ttoastr[\"warning\"](\"Invalid ratio value\");\n\t\t\t} else {\n\t\t\t\tcompressor.ratio.setValueAtTime(ratio, audioContext.currentTime);\n\t\t\t\ttoastr[\"success\"](\"Ratio updated\");\n\t\t\t}\n\t\t\t$('#ratio').val('' + compressor.ratio.value);\n\t\t}\n\t\tlet attack = parseFloat($('#attack').val());\n\t\tif(attack !== compressor.attack.value) {\n\t\t\tif(attack < compressor.attack.minValue || attack > compressor.attack.maxValue) {\n\t\t\t\ttoastr[\"warning\"](\"Invalid attack value\");\n\t\t\t} else {\n\t\t\t\tcompressor.attack.setValueAtTime(attack, audioContext.currentTime);\n\t\t\t\ttoastr[\"success\"](\"Attack updated\");\n\t\t\t}\n\t\t\t$('#attack').val('' + compressor.attack.value);\n\t\t}\n\t\tlet release = parseFloat($('#release').val());\n\t\tif(release !== compressor.release.value) {\n\t\t\tif(release < compressor.release.minValue || release > compressor.release.maxValue) {\n\t\t\t\ttoastr[\"warning\"](\"Invalid release value\");\n\t\t\t} else {\n\t\t\t\tcompressor.release.setValueAtTime(release, audioContext.currentTime);\n\t\t\t\ttoastr[\"success\"](\"Release updated\");\n\t\t\t}\n\t\t\t$('#release').val('' + compressor.release.value);\n\t\t}\n\t\treturn false;\n\t} else {\n\t\treturn true;\n\t}\n}\n\n// We setup the Web Audio resources here\nfunction setupWebAudioDemo() {\n\t// We have a context already, let's capture the microphone (with gain control disabled)\n\tnavigator.mediaDevices.getUserMedia({ audio: { autoGainControl: false }, video: false })\n\t\t.then(function(audioStream) {\n\t\t// Let's create a source from the microphone stream\n\t\t\tlet microphone = audioContext.createMediaStreamSource(audioStream);\n\t\t\t// Create a compressor node with some default values\n\t\t\tcompressor = audioContext.createDynamicsCompressor();\n\t\t\tcompressor.threshold.setValueAtTime(-18.0, audioContext.currentTime);\n\t\t\tcompressor.knee.setValueAtTime(9.0, audioContext.currentTime);\n\t\t\tcompressor.ratio.setValueAtTime(3.0, audioContext.currentTime);\n\t\t\tcompressor.attack.setValueAtTime(0.02, audioContext.currentTime);\n\t\t\tcompressor.release.setValueAtTime(0.25, audioContext.currentTime);\n\t\t\t// Update the content of the compressor UI with the current settings\n\t\t\t$('#threshold').val('' + compressor.threshold.value);\n\t\t\t$('#knee').val('' + compressor.knee.value);\n\t\t\t$('#ratio').val('' + compressor.ratio.value);\n\t\t\t$('#attack').val('' + compressor.attack.value);\n\t\t\t$('#release').val('' + compressor.release.value);\n\t\t\t// Use the compressor as a filter to get a new stream\n\t\t\tlet peer = audioContext.createMediaStreamDestination();\n\t\t\tmicrophone.connect(compressor);\n\t\t\tcompressor.connect(peer);\n\t\t\t// Let's use the compressed stream as source for our PeerConnection\n\t\t\techotest.createOffer(\n\t\t\t\t{\n\t\t\t\t// We provide our own stream\n\t\t\t\t\ttracks: [\n\t\t\t\t\t\t{ type: 'audio', capture: peer.stream.getAudioTracks()[0], recv: true }\n\t\t\t\t\t],\n\t\t\t\t\tsuccess: function(jsep) {\n\t\t\t\t\t\tJanus.debug(\"Got SDP!\", jsep);\n\t\t\t\t\t\tlet body = { audio: true, video: true };\n\t\t\t\t\t\techotest.send({ message: body, jsep: jsep });\n\t\t\t\t\t\t// Create a spinner waiting for the remote video\n\t\t\t\t\t\t$('#remote').html(\n\t\t\t\t\t\t\t'<div class=\"text-center\">' +\n\t\t\t\t\t\t\t'\t<div id=\"spinner\" class=\"spinner-border\" role=\"status\">' +\n\t\t\t\t\t\t\t'\t\t<span class=\"visually-hidden\">Loading...</span>' +\n\t\t\t\t\t\t\t'\t</div>' +\n\t\t\t\t\t\t\t'</div>');\n\t\t\t\t\t},\n\t\t\t\t\terror: function(error) {\n\t\t\t\t\t\tJanus.error(\"WebRTC error:\", error);\n\t\t\t\t\t\tbootbox.alert(\"WebRTC error... \" + error.message);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n\n// This is our callback to draw the visualizer for the remore audio data\nfunction drawVisualizer() {\n\trequestAnimationFrame(drawVisualizer);\n\tanalyser.getByteFrequencyData(dataArray);\n\tcanvasContext.fillStyle = 'rgb(0, 0, 0)';\n\tcanvasContext.fillRect(0, 0, 432, 240);\n\tlet barWidth = (432 / analyser.frequencyBinCount) * 2.5;\n\tlet barHeight;\n\tlet x = 0;\n\tfor(let i=0; i < analyser.frequencyBinCount; i++) {\n\t\tbarHeight = dataArray[i]/2;\n\t\tcanvasContext.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';\n\t\tcanvasContext.fillRect(x, 240-barHeight/2, barWidth, barHeight);\n\t\tx += barWidth + 1;\n\t}\n}\n"
  },
  {
    "path": "html/docs/index.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server: Documentation</title>\n<script type=\"text/javascript\" src=\"jquery.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"../css/demo.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n<div class=\"container\">\n\t<a class=\"navbar-brand\" href=\"index.html\">Janus (multistream)</a>\n\t<button type=\"button\" class=\"navbar-toggler\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarResponsive\" aria-controls=\"navbarResponsive\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n\t\t<span class=\"navbar-toggler-icon\"></span>\n\t</button>\n\t<div class=\"navbar-collapse collapse\" id=\"navbarResponsive\">\n\t\t<ul class=\"navbar-nav\">\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../index.html\">Home</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../demos/\">Demos</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link active\" href=\"index.html\">Documentation</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../citeus.html\">Papers</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"../support.html\">Need help?</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus-legacy.conf.meetecho.com/\">Janus (0.x)</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link januscon\" target=\"_blank\" href=\"https://januscon.it\">JanusCon!</a></li>\n\t\t</ul>\n\t\t<ul class=\"navbar-nav ms-auto\">\n\t\t\t<li class=\"nav-item\">\n\t\t\t\t<a class=\"nav-link meetecho-logo\" target=\"_blank\" href=\"https://www.meetecho.com\">\n\t\t\t\t\t<img src=\"meetecho-logo.png\"/>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</div>\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Documentation</h1>\n\t\t\t</div>\n\t\t\t<h3>Janus WebRTC Server: Documentation</h3>\n\t\t\t<p>You can build a documentation for the server adding:</p>\n\t\t\t<pre class=\"card card-body bg-gray\">--enable-docs</pre>\n\t\t\t<p>to the configure options. You'll need <a href=\"http://www.doxygen.org\">Doxygen</a> and <a href=\"http://www.graphviz.org/\">Graphviz</a> installed.</p>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t<p>Janus WebRTC Server &copy; <a href=\"http://www.meetecho.com\">Meetecho</a> 2014-2023</p>\n\t</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "html/footer.html",
    "content": "\t<p>Janus WebRTC Server &copy; <a href=\"http://www.meetecho.com\">Meetecho</a> 2014-2026</p>\n"
  },
  {
    "path": "html/index.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): About Janus</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top a[href='index.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"css/demo.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>About</h1>\n\t\t\t</div>\n\t\t\t<h3 class=\"mt-3\">Janus: the general purpose WebRTC server</h3>\n\t\t\t<div class=\"alert alert-warning\">\n\t\t\t\t<b>Note Well:</b> these are the demos and documentation for the <code>multistream</code> version\n\t\t\t\tof Janus, which is a new version. If you want to check the previous version of\n\t\t\t\tJanus instead (i.e., <code>0.x</code>, a.k.a. \"legacy\") click\n\t\t\t\t<a href=\"https://janus-legacy.conf.meetecho.com\">here</a> instead.\n\t\t\t</div>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-md-8\">\n\t\t\t\t\t<p>Janus is a WebRTC Server developed by <a href=\"http://www.meetecho.com\">Meetecho</a>\n\t\t\t\t\tconceived to be a general purpose one. As such, it doesn't provide any functionality per\n\t\t\t\t\tse other than implementing the means to set up a WebRTC media communication with a browser,\n\t\t\t\t\texchanging JSON messages with it, and relaying RTP/RTCP and messages between browsers and\n\t\t\t\t\tthe server-side application logic they're attached to. Any specific feature/application\n\t\t\t\t\tis provided by server side plugins, that browsers can then contact via Janus to take advantage of the\n\t\t\t\t\tfunctionality they provide. Example of such plugins can be implementations of applications\n\t\t\t\t\tlike echo tests, conference bridges, media recorders, SIP gateways and the like.</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-md-4 mt-auto mb-auto\">\n\t\t\t\t\t<img src=\"janus-logo.png\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t\t<p>The reason for this is simple: we wanted something that would have a\n\t\t\t\t\t<code>small footprint</code> (hence a C implementation) and that we could only\n\t\t\t\t\tequip with what was <code>really needed</code> (hence pluggable modules). That is,\n\t\t\t\t\tsomething that would allow us to deploy either a full-fledged WebRTC\n\t\t\t\t\tgateway on the cloud, or a small nettop/box to handle a specific use case.</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"alert alert-primary\">Check the <a class=\"alert-link\" href=\"docs/\">Documentation</a>\n\t\t\t\tfor more details about Janus, or check out the <a class=\"alert-link\" href=\"demos/\">Demos</a>\n\t\t\t\tto see it in action.</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "html/navbar.html",
    "content": "<div class=\"container\">\n\t<a class=\"navbar-brand\" href=\"index.html\">Janus (multistream)</a>\n\t<button type=\"button\" class=\"navbar-toggler\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarResponsive\" aria-controls=\"navbarResponsive\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n\t\t<span class=\"navbar-toggler-icon\"></span>\n\t</button>\n\t<div class=\"navbar-collapse collapse\" id=\"navbarResponsive\">\n\t\t<ul class=\"navbar-nav\">\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"index.html\">Home</a></li>\n\t\t\t<li class=\"nav-item dropdown\">\n\t\t\t\t<a class=\"nav-link dropdown-toggle\" data-bs-toggle=\"dropdown\" href=\"#\" id=\"demos\">Demos</a>\n\t\t\t\t<div class=\"dropdown-menu\" aria-labelledby=\"demos\">\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/\">Index</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/echotest.html\">Echo Test</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/streaming.html\">Streaming</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/videocall.html\">Video Call</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/sip.html\">SIP Gateway</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/videoroom.html\">Video Room</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/mvideoroom.html\">Video Room (multistream)</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/audiobridge.html\">Audio Bridge</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/textroom.html\">Text Room</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/recordplay.html\">Recorder/Playout</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/screensharing.html\">Screen Sharing</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/nosip.html\">NoSIP (SDP/RTP)</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/devices.html\">Device Selection</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/e2e.html\">End-to-end Encryption</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/multiopus.html\">Multichannel Opus (surround)</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/canvas.html\">Canvas Capture</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/webaudio.html\">Web Audio Processing</a>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/virtualbg.html\">Virtual Background</a>\n\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t<a class=\"dropdown-item\" href=\"demos/admin.html\">Admin/Monitor</a>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"docs\">Documentation</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"citeus.html\">Papers</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"support.html\">Need help?</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"https://janus-legacy.conf.meetecho.com/\">Janus (0.x)</a></li>\n\t\t\t<li class=\"nav-item\"><a class=\"nav-link januscon\" target=\"_blank\" href=\"https://januscon.it\">JanusCon!</a></li>\n\t\t</ul>\n\t\t<ul class=\"navbar-nav ms-auto\">\n\t\t\t<li class=\"nav-item\">\n\t\t\t\t<a class=\"nav-link meetecho-logo\" target=\"_blank\" href=\"https://www.meetecho.com\">\n\t\t\t\t\t<img src=\"meetecho-logo.png\"/>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</div>\n"
  },
  {
    "path": "html/support.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Janus WebRTC Server (multistream): Support</title>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" ></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.min.js\"></script>\n<script>\n\t$(function() {\n\t\t$(\".fixed-top\").load(\"navbar.html\", function() {\n\t\t\t$(\".fixed-top a[href='support.html']\").addClass(\"active\");\n\t\t});\n\t\t$(\".footer\").load(\"footer.html\");\n\t});\n</script>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cerulean/bootstrap.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"css/demo.css\" type=\"text/css\"/>\n</head>\n<body>\n\n<a href=\"https://github.com/meetecho/janus-gateway\"><img style=\"position: absolute; top: 0; left: 0; border: 0; z-index: 2001;\" src=\"forkme_left_darkblue_121621.png\" alt=\"Fork me on GitHub\"></a>\n\n<div class=\"navbar navbar-expand-lg fixed-top navbar-dark bg-primary\">\n</div>\n\n<div class=\"container\">\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<div class=\"pb-2 mt-4 mb-2 border-bottom\">\n\t\t\t\t<h1>Support</h1>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<div class=\"row\">\n\t\t<div class=\"col-md-12\">\n\t\t\t<p>Janus is an open source project, which means that there's an active\n\t\t\t<a target=\"_blank\" href=\"https://janus.discourse.group/\">Community</a>\n\t\t\tyou can interact with (e.g., to discuss deployment or runtime issues,\n\t\t\tpotential new features, ideas and so on). Besides, our\n\t\t\t<a target=\"_blank\" href=\"https://github.com/meetecho/janus-gateway/blob/master/.github/CONTRIBUTING.md\">GitHub repo</a>\n\t\t\tis the place to go when you find issues you think need to be solved,\n\t\t\tor whenever you've implemented something and want to contribute\n\t\t\tit back to the project.</p>\n\t\t\t<p>That said, as a company we also provide additional services\n\t\t\trelated to Janus, and that you might be interested in. If you\n\t\t\tthink we can be of help, just <a href=\"https://www.meetecho.com/en/#contact\">contact us!</a></p>\n\t\t</div>\n\t</div>\n\t<div class=\"container card card-body bg-gray\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-md-6\">\n\t\t\t\t<h3>Commercial Support</h3>\n\t\t\t\t<p>We do offer premium support for Janus-based deployments.\n\t\t\t\tDifferent options are available for different kinds of commercial\n\t\t\t\tgrade support. <a href=\"https://www.meetecho.com/en/#contact\">Contact us</a>\n\t\t\t\tto learn more.</p>\n\t\t\t</div>\n\t\t\t<div class=\"col-md-6\">\n\t\t\t\t<h3>Commercial Licence</h3>\n\t\t\t\t<p>A commercial license is available in case, for one reason or another,\n\t\t\t\tyou're not comfortable with the Janus\n\t\t\t\t<a target=\"_blank\" href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\">GPLv3</a> license.\n\t\t\t\t<a href=\"https://www.meetecho.com/en/#contact\">Contact us</a>\n\t\t\t\tfor more details.</p>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col-md-6\">\n\t\t\t\t<h3>Training</h3>\n\t\t\t\t<p>We also offer training courses and tutoring on WebRTC, Janus,\n\t\t\t\tdeployments, troubleshooting and so on. If you're interested in\n\t\t\t\tlearning more, just <a href=\"https://www.meetecho.com/en/#contact\">get in touch</a>.</p>\n\t\t\t</div>\n\t\t\t<div class=\"col-md-6\">\n\t\t\t\t<h3>Consulting</h3>\n\t\t\t\t<p>Anything that doesn't fall in the above! We're quite flexible and can\n\t\t\t\teasily adapt to try and meet whatever your needs are. If you feel you\n\t\t\t\tneed help, don't hesitate and <a href=\"https://www.meetecho.com/en/#contact\">tell us</a>\n\t\t\t\twhat you'd like us to do for you.</p>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr>\n\t<div class=\"footer\">\n\t</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "janus-gateway.pc.in",
    "content": "prefix=@prefix@\nexec_prefix=@exec_prefix@\nlibdir=@libdir@\nincludedir=@includedir@\n\nName: janus-gateway\nDescription: Janus WebRTC Server\nVersion: @JANUS_VERSION_STRING@\nRequires: @JANUS_PACKAGES_PUBLIC@\nLibs:\nCflags: -I${includedir}\n"
  },
  {
    "path": "npm/.gitignore",
    "content": "bundles/\ndist/\nsrc/\nnode_modules/\nnpm-debug.log\n"
  },
  {
    "path": "npm/.npmignore",
    "content": ""
  },
  {
    "path": "npm/README.md",
    "content": "Janus WebRTC Server (janus.js)\n==============================\n\nThis is the npm version of `janus.js`, a JavaScript library to talk to the [Janus WebRTC Server](https://github.com/meetecho/janus-gateway). This is the only authorized version of `janus.js` on npm:\n\nhttps://www.npmjs.com/package/janus-gateway\n\nIf you encounter other versions, they were not made nor uploaded by [Meetecho](https://www.meetecho.com), and so should be considered not trusted.\n\nFor information on how to use the library, please refer to the [official documentation](https://janus.conf.meetecho.com/docs/JS), which includes information on how to use `janus.js` as a module.\n\n> **Note well**: please notice that `janus.js` is mostly meant as a convenience library for use in our Janus demos, and is not necessarily suited for production usages. For a more modern JavaScript and Node.js SDK to use with Janus, please refer to [Janode](https://www.npmjs.com/package/janode) instead.\n"
  },
  {
    "path": "npm/janus.d.ts",
    "content": "declare global {\n\tconst jQuery: any\n}\n\ndeclare namespace JanusJS {\n\tinterface Dependencies {\n\t\tadapter: any;\n\t\tWebSocket: (server: string, protocol: string) => WebSocket;\n\t\tisArray: (array: any) => array is Array<any>;\n\t\textension: ChromeExtension;\n\t\thttpAPICall: (url: string, options: HttpApiCallOption) => void;\n\t}\n\n\tinterface DefaultDependencies extends Dependencies {\n\t\tfetch: typeof fetch;\n\t\tPromise: PromiseConstructorLike;\n\t}\n\n\tinterface OldDependencies extends Dependencies {\n\t\tjQuery: typeof jQuery;\n\t}\n\n\tinterface DependenciesResult {\n\t\tadapter: any;\n\t\tnewWebSocket: (server: string, protocol: string) => WebSocket;\n\t\tisArray: (array: any) => array is Array<any>;\n\t\textension: ChromeExtension;\n\t\thttpAPICall: (url: string, options: HttpApiCallOption) => void;\n\t}\n\n\ttype ChromeExtension = {\n\t\tcache?: { [key in string]: GetScreenCallback },\n\t\textensionId: string;\n\t\tisInstalled: () => boolean;\n\t\tgetScreen: (callback: GetScreenCallback) => void;\n\t\tinit: () => void;\n\t}\n\n\ttype GetScreenCallback = (error:string, sourceId:string) => void\n\n\ttype HttpApiCallOption = {\n\t\tasync: boolean,\n\t\tverb: string,\n\t\tbody: JanusRequest,\n\t\ttimeout: number,\n\t\twithCredentials: boolean,\n\t\tsuccess: (result: unknown) => void,\n\t\terror: (error: string, reason?: unknown) => void,\n\t}\n\n\ttype JanusRequest = {\n\t\tplugin?: string,\n\t\ttoken?: string,\n\t\tapisecret?: string,\n\t\tsession_id?: number,\n\t\thandle_id?: number,\n\t\topaque_id?: string,\n\t\tloop_index?: number,\n\t\tjanus: string,\n\t\ttransaction: string,\n\t\tbody?: any,\n\t\tjsep?: JSEP,\n\t}\n\n\tenum DebugLevel {\n\t\tTrace = 'trace',\n\t\tvDebug = 'vdebug',\n\t\tDebug = 'debug',\n\t\tLog = 'log',\n\t\tWarning = 'warn',\n\t\tError = 'error',\n\t}\n\n\tinterface JSEP {\n\t\te2ee?: boolean;\n\t\tsdp?: string;\n\t\ttype?: string;\n\t\trid_order?: \"hml\" | \"lmh\";\n\t\tforce_relay?: boolean;\n\t}\n\n\tinterface InitOptions {\n\t\tdebug?: boolean | 'all' | DebugLevel[];\n\t\tcallback?: Function;\n\t\tdependencies?: DependenciesResult;\n\t}\n\n\tinterface ConstructorOptions {\n\t\tserver: string | string[];\n\t\ticeServers?: RTCIceServer[];\n\t\tipv6?: boolean;\n\t\twithCredentials?: boolean;\n\t\tmax_poll_events?: number;\n\t\tdestroyOnUnload?: boolean;\n\t\ttoken?: string;\n\t\tapisecret?: string;\n\t\tsuccess?: Function;\n\t\terror?: (error: any) => void;\n\t\tdestroyed?: Function;\n\t\ticeTransportPolicy?: RTCIceTransportPolicy;\n\t\tbundlePolicy?: RTCBundlePolicy;\n\t\tkeepAlivePeriod?: number;\n\t\tlongPollTimeout?: number;\n\t}\n\n\tinterface ReconnectOptions {\n\t\tsuccess?: Function;\n\t\terror?: (error: string) => void;\n\t}\n\n\tinterface DestroyOptions {\n\t\tcleanupHandles?: boolean;\n\t\tnotifyDestroyed?: boolean;\n\t\tunload?: boolean;\n\t\tsuccess?: () => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\tinterface GetInfoOptions {\n\t\tsuccess?: (info: any) => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\tinterface RemoteTrackMetadata {\n\t\treason: \"created\" | \"ended\" | \"mute\" | \"unmute\";\n\t}\n\n\tenum MessageType {\n\t\tRecording = 'recording',\n\t\tStarting = 'starting',\n\t\tStarted = 'started',\n\t\tStopped = 'stopped',\n\t\tSlowLink = 'slow_link',\n\t\tPreparing = 'preparing',\n\t\tRefreshing = 'refreshing'\n\t}\n\n\tinterface Message {\n\t\tresult?: {\n\t\t\tstatus: MessageType;\n\t\t\tid?: string;\n\t\t\tuplink?: number;\n\t\t};\n\t\terror?: string;\n\t\t[key: string]: any;\n\t}\n\n\tinterface PluginCallbacks {\n\t\tdataChannelOptions?: RTCDataChannelInit;\n\t\tsuccess?: (handle: PluginHandle) => void;\n\t\terror?: (error: string) => void;\n\t\tconsentDialog?: (on: boolean) => void;\n\t\twebrtcState?: (isConnected: boolean) => void;\n\t\ticeState?: (state: 'connected' | 'failed' | 'disconnected' | 'closed') => void;\n\t\tmediaState?: (medium: 'audio' | 'video', receiving: boolean, mid?: number) => void;\n\t\tslowLink?: (uplink: boolean, lost: number, mid: string) => void;\n\t\tonmessage?: (message: Message, jsep?: JSEP) => void;\n\t\tonlocaltrack?: (track: MediaStreamTrack, on: boolean) => void;\n\t\tonremotetrack?: (track: MediaStreamTrack, mid: string, on: boolean, metadata?: RemoteTrackMetadata) => void;\n\t\tondataopen?: Function;\n\t\tondata?: Function;\n\t\toncleanup?: Function;\n\t\tondetached?: Function;\n\t}\n\n\tinterface PluginOptions extends PluginCallbacks {\n\t\tplugin: string;\n\t\topaqueId?: string;\n\t\ttoken?: string;\n\t\tloopIndex?: number;\n\t}\n\n\tinterface OfferParams {\n\t\ttracks?: TrackOption[];\n\t\ttrickle?: boolean;\n\t\ticeRestart?: boolean;\n\t\texternalEncryption?: boolean;\n\t\tsuccess?: (jsep: JSEP) => void;\n\t\terror?: (error: Error) => void;\n\t\tcustomizeSdp?: (jsep: JSEP) => void;\n\n\t\t/** @deprecated use tracks instead */\n\t\tmedia?: {\n\t\t\taudioSend?: boolean;\n\t\t\taudioRecv?: boolean;\n\t\t\tvideoSend?: boolean;\n\t\t\tvideoRecv?: boolean;\n\t\t\taudio?: boolean | { deviceId: string };\n\t\t\tvideo?:\n\t\t\t\t| boolean\n\t\t\t\t| { deviceId: string }\n\t\t\t\t| 'lowres'\n\t\t\t\t| 'lowres-16:9'\n\t\t\t\t| 'stdres'\n\t\t\t\t| 'stdres-16:9'\n\t\t\t\t| 'hires'\n\t\t\t\t| 'hires-16:9';\n\t\t\tdata?: boolean;\n\t\t\tfailIfNoAudio?: boolean;\n\t\t\tfailIfNoVideo?: boolean;\n\t\t\tscreenshareFrameRate?: number;\n\t\t};\n\t}\n\n\tinterface PluginMessage {\n\t\tmessage: {\n\t\t\trequest: string;\n\t\t\t[otherProps: string]: any;\n\t\t};\n\t\tjsep?: JSEP;\n\t\tsuccess?: (data?: any) => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\tinterface WebRTCInfo {\n\t\tbitrate: {\n\t\t\tbsbefore: string | null;\n\t\t\tbsnow: string | null;\n\t\t\ttimer: string | null;\n\t\t\ttsbefore: string | null;\n\t\t\ttsnow: string | null;\n\t\t\tvalue: string | null;\n\t\t};\n\t\tdataChannel: { [key in string]: RTCDataChannel };\n\t\tdataChannelOptions: RTCDataChannelInit;\n\n\t\tdtmfSender: RTCDTMFSender\n\t\ticeDone: boolean;\n\t\tmediaConstraints: any;\n\t\tmySdp: {\n\t\t\tsdp: string;\n\t\t\ttype: string;\n\t\t};\n\t\tmyStream: MediaStream;\n\t\tpc: RTCPeerConnection;\n\t\treceiverTransforms: {\n\t\t\taudio: TransformStream;\n\t\t\tvideo: TransformStream;\n\t\t};\n\t\tremoteSdp: string;\n\t\tremoteStream: MediaStream;\n\t\tsenderTransforms: {\n\t\t\taudio: TransformStream;\n\t\t\tvideo: TransformStream;\n\t\t};\n\t\tstarted: boolean;\n\t\tstreamExternal: boolean;\n\t\ttrickle: boolean;\n\t\tvolume: {\n\t\t\tvalue: number;\n\t\t\ttimer: number;\n\t\t};\n\n\t\tsdpSent: boolean;\n\t\tinsertableStreams?: boolean;\n\t\texternalEncryption?: boolean;\n\t\tcandidates: RTCIceCandidateInit[];\n\t}\n\n\ttype PluginCreateAnswerParam = {\n\t\tjsep: JSEP;\n\t\ttracks?: TrackOption[];\n\t\texternalEncryption?: boolean;\n\n\t\t/** @deprecated use tracks instead */\n\t\tmedia?: { audioSend: any, videoSend: any };\n\t\tsuccess?: (data: JSEP) => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\ttype PluginHandleRemoteJsepParam = {\n\t\tjsep: JSEP;\n\t\tsuccess?: (data: JSEP) => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\ttype PluginReplaceTracksParam = {\n\t\ttracks: TrackOption[];\n\t\tsuccess?: (data: unknown) => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\ttype TrackOption = {\n\t\tadd?: boolean;\n\t\treplace?: boolean;\n\t\tremove?: boolean;\n\t\ttype: 'video' | 'screen' | 'audio' | 'data';\n\t\tmid?: string;\n\t\tcapture: boolean | MediaTrackConstraints | MediaStreamTrack;\n\t\trecv?: boolean;\n\t\tgroup?: 'default' | string;\n\t\tgumGroup?: TrackOption['group'];\n\t\tsimulcast?: boolean;\n\t\tsvc?: string;\n\t\tsimulcastMaxBitrates?: {\n\t\t\tlow: number;\n\t\t\tmedium: number;\n\t\t\thigh: number;\n\t\t};\n\t\tsendEncodings?: RTCRtpEncodingParameters[];\n\t\tframerate?: number;\n\t\tbitrate?: number;\n\t\tdontStop?: boolean;\n\t\ttransforms?: {\n\t\t\tsender: ReadableWritablePair;\n\t\t\treceiver: ReadableWritablePair;\n\t\t};\n\t}\n\n\ttype PluginDtmfParam = {\n\t\tdtmf: Dtmf;\n\t\tsuccess?: (data: unknown) => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\ttype Dtmf = {\n\t\ttones: string;\n\t\tduration: number;\n\t\tgap: number;\n\t}\n\n\ttype PluginDataParam = {\n\t\t/** @deprecated use data instead */\n\t\ttext?: string;\n\t\tdata?: any;\n\t\tlabel?: string;\n\t\tprotocol?: string;\n\t\tsuccess?: (data: unknown) => void;\n\t\terror?: (error: string) => void;\n\t}\n\n\ttype TrackDesc = {\n\t\tmid?: string\n\t\ttype?: string\n\t\tid?: string\n\t\tlabel?: string\n\t}\n\n\tinterface DetachOptions {\n\t\tsuccess?: () => void;\n\t\terror?: (error: string) => void;\n\t\tnoRequest?: boolean;\n\t}\n\n\tinterface PluginHandle {\n\t\tplugin: string;\n\t\tid: string;\n\t\ttoken?: string;\n\t\tdetached: boolean;\n\t\twebrtcStuff: WebRTCInfo;\n\t\tgetId(): string;\n\t\tgetPlugin(): string;\n\t\tgetVolume(mid: string, callback: (result: number) => void): void;\n\t\tgetRemoteVolume(mid: string, callback: (result: number) => void): void;\n\t\tgetLocalVolume(mid: string, callback: (result: number) => void): void;\n\t\tisAudioMuted(): boolean;\n\t\tmuteAudio(): void;\n\t\tunmuteAudio(): void;\n\t\tisVideoMuted(): boolean;\n\t\tmuteVideo(): void;\n\t\tunmuteVideo(): void;\n\t\tgetBitrate(mid? :string): string;\n\t\tsetMaxBitrate(bitrate: number): void;\n\t\tsend(message: PluginMessage): void;\n\t\tdata(params: PluginDataParam): void;\n\t\tdtmf(params: PluginDtmfParam): void;\n\t\tcreateOffer(params: OfferParams): void;\n\t\tcreateAnswer(params: PluginCreateAnswerParam): void;\n\t\thandleRemoteJsep(params: PluginHandleRemoteJsepParam): void;\n\t\treplaceTracks(params: PluginReplaceTracksParam): void;\n\t\tgetLocalTracks(): TrackDesc[];\n\t\tgetRemoteTracks(): TrackDesc[];\n\t\thangup(sendRequest?: boolean): void;\n\t\tdetach(params?: DetachOptions): void;\n\t}\n\n\tclass Janus {\n\t\tstatic webRTCAdapter: any;\n\t\tstatic safariVp8: boolean;\n\t\tstatic useDefaultDependencies(deps?: Partial<DefaultDependencies>): DependenciesResult;\n\t\tstatic useOldDependencies(deps?: Partial<OldDependencies>): DependenciesResult;\n\t\tstatic init(options: InitOptions): void;\n\t\tstatic isWebrtcSupported(): boolean;\n\t\tstatic debug(...args: any[]): void;\n\t\tstatic log(...args: any[]): void;\n\t\tstatic warn(...args: any[]): void;\n\t\tstatic error(...args: any[]): void;\n\t\tstatic randomString(length: number): string;\n\t\tstatic attachMediaStream(element: HTMLMediaElement, stream: MediaStream): void;\n\t\tstatic reattachMediaStream(to: HTMLMediaElement, from: HTMLMediaElement): void;\n\n\t\tstatic stopAllTracks(stream: MediaStream): void;\n\n\t\tconstructor(options: ConstructorOptions);\n\n\t\tattach(options: PluginOptions): void;\n\t\tgetServer(): string;\n\t\tisConnected(): boolean;\n\t\treconnect(callbacks: ReconnectOptions): void;\n\t\tgetSessionId(): number;\n\t\tgetInfo(callbacks: GetInfoOptions): void;\n\t\tdestroy(callbacks: DestroyOptions): void;\n\t}\n}\n\nexport default JanusJS.Janus;\nexport { JanusJS };\n"
  },
  {
    "path": "npm/module.js",
    "content": "to_remove: {\n/*\n * Module shim for rollup.js to work with.\n * Simply re-export Janus from janus.js, the real 'magic' is in the rollup config.\n */\n}\n\n@JANUS_CODE@\n\nexport default Janus;\n"
  },
  {
    "path": "npm/rollup.config.mjs",
    "content": "import strip from '@rollup/plugin-strip';\nimport replace from '@rollup/plugin-replace';\nimport { readFileSync } from 'fs';\n\nexport default {\n\tinput: 'npm/module.js',\n\toutput: {\n\t\tstrict: false,\n\t\tname: 'Janus',\n\t},\n\tplugins: [\n\t\treplace({\n\t\t\tJANUS_CODE: readFileSync('./html/demos/janus.js', 'utf-8'),\n\t\t\tdelimiters: ['@', '@'],\n\t\t\tincludes: 'module.js',\n\t\t\tpreventAssignment: true\n\t\t}),\n\t\tstrip({\n\t\t\tlabels: ['to_remove']\n\t\t})\n\t]\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"janus-gateway\",\n\t\"version\": \"1.4.1\",\n\t\"description\": \"A javascript library for interacting with the C based Janus WebRTC Server\",\n\t\"main\": \"npm/dist/janus.es.js\",\n\t\"types\": \"npm/janus.d.ts\",\n\t\"files\": [\n\t\t\"npm/dist\",\n\t\t\"npm/src\",\n\t\t\"npm/janus.d.ts\",\n\t\t\"npm/README.md\"\n\t],\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/meetecho/janus-gateway.git\",\n\t\t\"directory\": \"npm\"\n\t},\n\t\"author\": {\n\t\t\"name\": \"Lorenzo Miniero\",\n\t\t\"email\": \"lorenzo@meetecho.com\",\n\t\t\"url\": \"https://www.meetecho.com\"\n\t},\n\t\"license\": \"MIT\",\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/meetecho/janus-gateway/issues\"\n\t},\n\t\"homepage\": \"https://janus.conf.meetecho.com/docs/JS\",\n\t\"dependencies\": {\n\t\t\"webrtc-adapter\": \"9.0.3\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@rollup/plugin-replace\": \"^5.0.2\",\n\t\t\"@rollup/plugin-strip\": \"^3.0.2\",\n\t\t\"eslint\": \"^9.4.0\",\n\t\t\"eslint-plugin-html\": \"^8.1.1\",\n\t\t\"rollup\": \"^3.21.6\"\n\t},\n\t\"scripts\": {\n\t\t\"lint\": \"npx eslint --debug html\",\n\t\t\"rollup\": \"npx rollup -c npm/rollup.config.mjs\",\n\t\t\"prerelease\": \"npm run rollup -- --o npm/dist/janus.es.js --f es && node -e \\\"const fs=require('fs'); const data=fs.readFileSync('./html/demos/janus.js'); fs.mkdirSync('./npm/src', {recursive:true}); fs.writeFileSync('./npm/src/janus.js',data);\\\"\"\n\t}\n}\n"
  },
  {
    "path": "src/Makefile.am",
    "content": "EXTRA_DIST = $(NULL)\nCLEANFILES = $(NULL)\n\nbin_PROGRAMS = janus\n\nheaderdir = $(includedir)/janus\nheader_HEADERS = apierror.h config.h log.h debug.h mutex.h record.h \\\n\trtcp.h rtp.h rtpsrtp.h sdp-utils.h ip-utils.h utils.h refcount.h text2pcap.h\n\npluginsheaderdir = $(includedir)/janus/plugins\npluginsheader_HEADERS = plugins/plugin.h\n\ntransportsheaderdir = $(includedir)/janus/transports\ntransportsheader_HEADERS = transports/transport.h\n\neventsheaderdir = $(includedir)/janus/events\neventsheader_HEADERS = events/eventhandler.h\n\nloggersheaderdir = $(includedir)/janus/loggers\nloggersheader_HEADERS = loggers/logger.h\n\nconfdir = $(sysconfdir)/janus\nconf_DATA = $(NULL)\n\ndemosdir = $(datadir)/janus/html\n\nplugindir = $(libdir)/janus/plugins\nplugin_LTLIBRARIES = $(NULL)\n\ntransportdir = $(libdir)/janus/transports\ntransport_LTLIBRARIES = $(NULL)\n\neventdir = $(libdir)/janus/events\nevent_LTLIBRARIES = $(NULL)\n\nloggerdir = $(libdir)/janus/loggers\nlogger_LTLIBRARIES = $(NULL)\n\nstreamdir = $(datadir)/janus/streams\nstream_DATA = $(NULL)\n\nrecordingsdir = $(datadir)/janus/recordings\nrecordings_DATA = $(NULL)\n\nluadir = $(datadir)/janus/lua\nlua_DATA = $(NULL)\n\nduktapedir = $(datadir)/janus/duktape\nduktape_DATA = $(NULL)\n\n%.sample: %.sample.in\n\t$(MKDIR_P) $(@D)\n\t$(AM_V_GEN) sed -e \"\\\n\t    s|[@]confdir[@]|$(confdir)|;\\\n\t    s|[@]plugindir[@]|$(plugindir)|;\\\n\t    s|[@]transportdir[@]|$(transportdir)|;\\\n\t    s|[@]eventdir[@]|$(eventdir)|;\\\n\t    s|[@]loggerdir[@]|$(loggerdir)|;\\\n\t    s|[@]recordingsdir[@]|$(recordingsdir)|;\\\n\t    s|[@]demosdir[@]|$(demosdir)|;\\\n\t    s|[@]streamdir[@]|$(streamdir)|; \\\n\t    s|[@]luadir[@]|$(luadir)|; \\\n\t    s|[@]duktapedir[@]|$(duktapedir)|\" \\\n\t$< > $@ || rm $@\n\n##\n# Janus\n##\n\njanus_SOURCES = \\\n\tapierror.c \\\n\tapierror.h \\\n\tauth.c \\\n\tauth.h \\\n\tconfig.c \\\n\tconfig.h \\\n\tdebug.h \\\n\tdtls.c \\\n\tdtls.h \\\n\tdtls-bio.c \\\n\tdtls-bio.h \\\n\tevents.c \\\n\tevents.h \\\n\tice.c \\\n\tice.h \\\n\tjanus.c \\\n\tjanus.h \\\n\tlog.c \\\n\tlog.h \\\n\tmutex.h \\\n\toptions.c \\\n\toptions.h \\\n\trecord.c \\\n\trecord.h \\\n\trefcount.h \\\n\trtcp.c \\\n\trtcp.h \\\n\trtp.c \\\n\trtp.h \\\n\trtpfwd.c \\\n\trtpfwd.h \\\n\trtpsrtp.h \\\n\tsctp.c \\\n\tsctp.h \\\n\tsdp.c \\\n\tsdp.h \\\n\tsdp-utils.c \\\n\tsdp-utils.h \\\n\tip-utils.c \\\n\tip-utils.h \\\n\tturnrest.c \\\n\tturnrest.h \\\n\tutils.c \\\n\tutils.h \\\n\tversion.c \\\n\tversion.h \\\n\ttext2pcap.c \\\n\ttext2pcap.h \\\n\tplugins/plugin.c \\\n\tplugins/plugin.h \\\n\ttransports/transport.h \\\n\ttransports/transport.c \\\n\tevents/eventhandler.h \\\n\tloggers/logger.h \\\n\t$(NULL)\n\njanus_CFLAGS = \\\n\t$(AM_CFLAGS) \\\n\t$(JANUS_CFLAGS) \\\n\t$(LIBSRTP_CFLAGS) \\\n\t$(LIBCURL_CFLAGS) \\\n\t-DPLUGINDIR=\\\"$(plugindir)\\\" \\\n\t-DTRANSPORTDIR=\\\"$(transportdir)\\\" \\\n\t-DEVENTDIR=\\\"$(eventdir)\\\" \\\n\t-DLOGGERDIR=\\\"$(loggerdir)\\\" \\\n\t-DCONFDIR=\\\"$(confdir)\\\" \\\n\t$(BORINGSSL_CFLAGS) \\\n\t$(NULL)\n\njanus_LDADD = \\\n\t$(BORINGSSL_LIBS) \\\n\t$(JANUS_LIBS) \\\n\t$(JANUS_MANUAL_LIBS) \\\n\t$(LIBSRTP_LDFLAGS) $(LIBSRTP_LIBS) \\\n\t$(LIBCURL_LDFLAGS) $(LIBCURL_LIBS) \\\n\t$(NULL)\n\ndist_man1_MANS = janus.1\n\nbin_PROGRAMS += janus-cfgconv\n\njanus_cfgconv_SOURCES = \\\n\tjanus-cfgconv.c \\\n\tconfig.c \\\n\tlog.c \\\n\tutils.c \\\n\tversion.c \\\n\t$(NULL)\n\njanus_cfgconv_CFLAGS = \\\n\t$(AM_CFLAGS) \\\n\t$(JANUS_CFLAGS) \\\n\t$(NULL)\n\njanus_cfgconv_LDADD = \\\n\t$(JANUS_LIBS) \\\n\t$(JANUS_MANUAL_LIBS) \\\n\t$(NULL)\n\ndist_man1_MANS += janus-cfgconv.1\n\nBUILT_SOURCES = version.c\n\ndirectory = ../.git\ndir_target = $(directory)-$(wildcard $(directory))\ndir_present = $(directory)-$(directory)\ndir_absent = $(directory)-\n\nif WITH_SOURCE_DATE_EPOCH\nbuild_date = $(shell LC_ALL=C date --utc --date=\"@$(SOURCE_DATE_EPOCH)\")\nelse\nbuild_date = $(shell date)\nendif\n\nversion.c: FORCE | $(dir_target)\n\techo \"$(build_date)\" | awk 'BEGIN {} {print \"const char *janus_build_git_time = \\\"\"$$0\"\\\";\"} END {} ' >> version.c\n\techo \"$(JANUS_VERSION)\" | awk 'BEGIN {} {print \"int janus_version = \"$$0\";\"} END {} ' >> version.c\n\techo \"$(JANUS_VERSION_STRING)\" | awk 'BEGIN {} {print \"const char *janus_version_string = \\\"\"$$0\"\\\";\"} END {} ' >> version.c\n\tPKG_CONFIG_PATH=\"$(PKG_CONFIG_PATH)\" \"$(PKG_CONFIG)\" --modversion nice | awk 'BEGIN {} {print \"const char *libnice_version_string = \\\"\"$$0\"\\\";\"} END {} ' >> version.c\n\n$(dir_present):\n\t`which git` rev-parse HEAD | awk 'BEGIN {print \"#include \\\"version.h\\\"\"} {print \"const char *janus_build_git_sha = \\\"\" $$0\"\\\";\"} END {}' > version.c\n\n$(dir_absent):\n\techo \"not-a-git-repo\" | awk 'BEGIN {print \"#include \\\"version.h\\\"\"} {print \"const char *janus_build_git_sha = \\\"\" $$0\"\\\";\"} END {}' > version.c\n\nCLEANFILES += version.c\n\n##\n# Transports\n##\n\ntransports_cflags = \\\n\t$(AM_CFLAGS) \\\n\t$(TRANSPORTS_CFLAGS) \\\n\t$(NULL)\n\ntransports_ldflags = \\\n\t-version-info $(JANUS_VERSION_SO) \\\n\t$(NULL)\n\ntransports_libadd = \\\n\t$(TRANSPORTS_LIBS) \\\n\t$(NULL)\n\nif ENABLE_REST\ntransport_LTLIBRARIES += transports/libjanus_http.la\ntransports_libjanus_http_la_SOURCES = transports/janus_http.c\ntransports_libjanus_http_la_CFLAGS = $(transports_cflags) $(MHD_CFLAGS)\ntransports_libjanus_http_la_LDFLAGS = $(transports_ldflags) $(MHD_LDFLAGS) $(MHD_LIBS)\ntransports_libjanus_http_la_LIBADD = $(transports_libadd) $(MHD_LDFLAGS)\nconf_DATA += ../conf/janus.transport.http.jcfg.sample\nEXTRA_DIST += ../conf/janus.transport.http.jcfg.sample\nendif\n\nif ENABLE_WEBSOCKETS\ntransport_LTLIBRARIES += transports/libjanus_websockets.la\ntransports_libjanus_websockets_la_SOURCES = transports/janus_websockets.c\ntransports_libjanus_websockets_la_CFLAGS = $(transports_cflags)\ntransports_libjanus_websockets_la_LDFLAGS = $(transports_ldflags) $(WS_MANUAL_LIBS)\ntransports_libjanus_websockets_la_LIBADD = $(transports_libadd)\nconf_DATA += ../conf/janus.transport.websockets.jcfg.sample\nEXTRA_DIST += ../conf/janus.transport.websockets.jcfg.sample\nendif\n\nif ENABLE_RABBITMQ\ntransport_LTLIBRARIES += transports/libjanus_rabbitmq.la\ntransports_libjanus_rabbitmq_la_SOURCES = transports/janus_rabbitmq.c\ntransports_libjanus_rabbitmq_la_CFLAGS = $(transports_cflags)\ntransports_libjanus_rabbitmq_la_LDFLAGS = $(transports_ldflags) -lrabbitmq\ntransports_libjanus_rabbitmq_la_LIBADD = $(transports_libadd)\nconf_DATA += ../conf/janus.transport.rabbitmq.jcfg.sample\nEXTRA_DIST += ../conf/janus.transport.rabbitmq.jcfg.sample\nendif\n\nif ENABLE_MQTT\ntransport_LTLIBRARIES += transports/libjanus_mqtt.la\ntransports_libjanus_mqtt_la_SOURCES = transports/janus_mqtt.c\ntransports_libjanus_mqtt_la_CFLAGS = $(transports_cflags)\ntransports_libjanus_mqtt_la_LDFLAGS = $(transports_ldflags) -lpaho-mqtt3as\ntransports_libjanus_mqtt_la_LIBADD = $(transports_libadd)\nconf_DATA += ../conf/janus.transport.mqtt.jcfg.sample\nEXTRA_DIST += ../conf/janus.transport.mqtt.jcfg.sample\nendif\n\nif ENABLE_PFUNIX\ntransport_LTLIBRARIES += transports/libjanus_pfunix.la\ntransports_libjanus_pfunix_la_SOURCES = transports/janus_pfunix.c\ntransports_libjanus_pfunix_la_CFLAGS = $(transports_cflags)\ntransports_libjanus_pfunix_la_LDFLAGS = $(transports_ldflags) $(LIBSYSTEMD_LIBS)\ntransports_libjanus_pfunix_la_LIBADD = $(transports_libadd)\nconf_DATA += ../conf/janus.transport.pfunix.jcfg.sample\nEXTRA_DIST += ../conf/janus.transport.pfunix.jcfg.sample\nendif\n\nif ENABLE_NANOMSG\ntransport_LTLIBRARIES += transports/libjanus_nanomsg.la\ntransports_libjanus_nanomsg_la_SOURCES = transports/janus_nanomsg.c\ntransports_libjanus_nanomsg_la_CFLAGS = $(transports_cflags)\ntransports_libjanus_nanomsg_la_LDFLAGS = $(transports_ldflags) -lnanomsg\ntransports_libjanus_nanomsg_la_LIBADD = $(transports_libadd)\nconf_DATA += ../conf/janus.transport.nanomsg.jcfg.sample\nEXTRA_DIST += ../conf/janus.transport.nanomsg.jcfg.sample\nendif\n\n##\n# Event handlers\n##\n\nevents_cflags = \\\n\t$(AM_CFLAGS) \\\n\t$(EVENTS_CFLAGS) \\\n\t$(NULL)\n\nevents_ldflags = \\\n\t-version-info $(JANUS_VERSION_SO) \\\n\t$(NULL)\n\nevents_libadd = \\\n\t$(EVENTS_LIBS) \\\n\t$(NULL)\n\nif ENABLE_SAMPLEEVH\nevent_LTLIBRARIES += events/libjanus_sampleevh.la\nevents_libjanus_sampleevh_la_SOURCES = events/janus_sampleevh.c\nevents_libjanus_sampleevh_la_CFLAGS = $(events_cflags) $(LIBCURL_CFLAGS)\nevents_libjanus_sampleevh_la_LDFLAGS = $(events_ldflags) $(LIBCURL_LDFLAGS) $(LIBCURL_LIBS) -lm\nevents_libjanus_sampleevh_la_LIBADD = $(events_libadd) $(LIBCURL_LIBADD)\nconf_DATA += ../conf/janus.eventhandler.sampleevh.jcfg.sample\nEXTRA_DIST += ../conf/janus.eventhandler.sampleevh.jcfg.sample\nendif\n\nif ENABLE_WSEVH\nevent_LTLIBRARIES += events/libjanus_wsevh.la\nevents_libjanus_wsevh_la_SOURCES = events/janus_wsevh.c\nevents_libjanus_wsevh_la_CFLAGS = $(events_cflags)\nevents_libjanus_wsevh_la_LDFLAGS = $(events_ldflags) $(WS_MANUAL_LIBS)\nevents_libjanus_wsevh_la_LIBADD = $(events_libadd)\nconf_DATA += ../conf/janus.eventhandler.wsevh.jcfg.sample\nEXTRA_DIST += ../conf/janus.eventhandler.wsevh.jcfg.sample\nendif\n\nif ENABLE_RABBITMQEVH\nevent_LTLIBRARIES += events/libjanus_rabbitmqevh.la\nevents_libjanus_rabbitmqevh_la_SOURCES = events/janus_rabbitmqevh.c\nevents_libjanus_rabbitmqevh_la_CFLAGS = $(events_cflags)\nevents_libjanus_rabbitmqevh_la_LDFLAGS = $(events_ldflags) -lrabbitmq\nevents_libjanus_rabbitmqevh_la_LIBADD = $(events_libadd)\nconf_DATA += ../conf/janus.eventhandler.rabbitmqevh.jcfg.sample\nEXTRA_DIST += ../conf/janus.eventhandler.rabbitmqevh.jcfg.sample\nendif\n\nif ENABLE_MQTTEVH\nevent_LTLIBRARIES += events/libjanus_mqttevh.la\nevents_libjanus_mqttevh_la_SOURCES = events/janus_mqttevh.c\nevents_libjanus_mqttevh_la_CFLAGS = $(events_cflags)\nevents_libjanus_mqttevh_la_LDFLAGS = $(events_ldflags) -lpaho-mqtt3as\nevents_libjanus_mqttevh_la_LIBADD = $(events_libadd)\nconf_DATA += ../conf/janus.eventhandler.mqttevh.jcfg.sample\nEXTRA_DIST += ../conf/janus.eventhandler.mqttevh.jcfg.sample\nendif\n\nif ENABLE_NANOMSGEVH\nevent_LTLIBRARIES += events/libjanus_nanomsgevh.la\nevents_libjanus_nanomsgevh_la_SOURCES = events/janus_nanomsgevh.c\nevents_libjanus_nanomsgevh_la_CFLAGS = $(events_cflags)\nevents_libjanus_nanomsgevh_la_LDFLAGS = $(events_ldflags) -lnanomsg\nevents_libjanus_nanomsgevh_la_LIBADD = $(events_libadd)\nconf_DATA += ../conf/janus.eventhandler.nanomsgevh.jcfg.sample\nEXTRA_DIST += ../conf/janus.eventhandler.nanomsgevh.jcfg.sample\nendif\n\nif ENABLE_GELFEVH\nevent_LTLIBRARIES += events/libjanus_gelfevh.la\nevents_libjanus_gelfevh_la_SOURCES = events/janus_gelfevh.c\nevents_libjanus_gelfevh_la_CFLAGS = $(events_cflags)\nevents_libjanus_gelfevh_la_LDFLAGS = $(events_ldflags)\nevents_libjanus_gelfevh_la_LIBADD = $(events_libadd)\nconf_DATA += ../conf/janus.eventhandler.gelfevh.jcfg.sample\nEXTRA_DIST += ../conf/janus.eventhandler.gelfevh.jcfg.sample\nendif\n\n##\n# Loggers\n##\n\nloggers_cflags = \\\n\t$(AM_CFLAGS) \\\n\t$(LOGGERS_CFLAGS) \\\n\t$(NULL)\n\nloggers_ldflags = \\\n\t-version-info $(JANUS_VERSION_SO) \\\n\t$(NULL)\n\nloggers_libadd = \\\n\t$(LOGGERS_LIBS) \\\n\t$(NULL)\n\nif ENABLE_JSONLOGGER\nlogger_LTLIBRARIES += loggers/libjanus_jsonlog.la\nloggers_libjanus_jsonlog_la_SOURCES = loggers/janus_jsonlog.c\nloggers_libjanus_jsonlog_la_CFLAGS = $(loggers_cflags)\nloggers_libjanus_jsonlog_la_LDFLAGS = $(loggers_ldflags)\nloggers_libjanus_jsonlog_la_LIBADD = $(loggers_libadd)\nconf_DATA += ../conf/janus.logger.jsonlog.jcfg.sample\nEXTRA_DIST += ../conf/janus.logger.jsonlog.jcfg.sample\nendif\n\n##\n# Plugins\n##\n\nplugins_cflags = \\\n\t$(AM_CFLAGS) \\\n\t$(PLUGINS_CFLAGS) \\\n\t$(NULL)\n\nplugins_ldflags = \\\n\t-version-info $(JANUS_VERSION_SO) \\\n\t$(NULL)\n\nplugins_libadd = \\\n\t$(PLUGINS_LIBS) \\\n\t$(NULL)\n\nif ENABLE_PLUGIN_AUDIOBRIDGE\nplugin_LTLIBRARIES += plugins/libjanus_audiobridge.la\nplugins_libjanus_audiobridge_la_SOURCES = plugins/janus_audiobridge.c \\\n\tplugins/audiobridge-deps/jitter.c plugins/audiobridge-deps/resample.c plugins/audiobridge-deps/arch.h \\\n\tplugins/audiobridge-deps/os_support.h plugins/audiobridge-deps/speex/speex_jitter.h plugins/audiobridge-deps/speex/speex_resampler.h \\\n\tplugins/audiobridge-deps/speex/speexdsp_types.h plugins/audiobridge-deps/speex/speexdsp_config_types.h\nplugins_libjanus_audiobridge_la_CFLAGS = $(plugins_cflags) $(OPUS_CFLAGS) $(OGG_CFLAGS) $(RNNOISE_CFLAGS) $(LIBSRTP_CFLAGS)\nplugins_libjanus_audiobridge_la_LDFLAGS = $(plugins_ldflags) $(OPUS_LDFLAGS) $(OPUS_LIBS) $(OGG_LDFLAGS) $(OGG_LIBS) $(RNNOISE_LDFLAGS) $(RNNOISE_LIBS)\nplugins_libjanus_audiobridge_la_LIBADD = $(plugins_libadd) $(OPUS_LIBADD) $(OGG_LIBADD) $(RNNOISE_LIBADD)\nconf_DATA += ../conf/janus.plugin.audiobridge.jcfg.sample\nEXTRA_DIST += ../conf/janus.plugin.audiobridge.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_ECHOTEST\nplugin_LTLIBRARIES += plugins/libjanus_echotest.la\nplugins_libjanus_echotest_la_SOURCES = plugins/janus_echotest.c\nplugins_libjanus_echotest_la_CFLAGS = $(plugins_cflags)\nplugins_libjanus_echotest_la_LDFLAGS = $(plugins_ldflags)\nplugins_libjanus_echotest_la_LIBADD = $(plugins_libadd)\nconf_DATA += ../conf/janus.plugin.echotest.jcfg.sample\nEXTRA_DIST += ../conf/janus.plugin.echotest.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_RECORDPLAY\nplugin_LTLIBRARIES += plugins/libjanus_recordplay.la\nplugins_libjanus_recordplay_la_SOURCES = plugins/janus_recordplay.c\nplugins_libjanus_recordplay_la_CFLAGS = $(plugins_cflags)\nplugins_libjanus_recordplay_la_LDFLAGS = $(plugins_ldflags)\nplugins_libjanus_recordplay_la_LIBADD = $(plugins_libadd)\nconf_DATA += ../conf/janus.plugin.recordplay.jcfg.sample\nrecordings_DATA += \\\n\tplugins/recordings/1234.nfo \\\n\tplugins/recordings/rec-sample-audio.mjr \\\n\tplugins/recordings/rec-sample-video.mjr\nEXTRA_DIST += \\\n\t../conf/janus.plugin.recordplay.jcfg.sample.in \\\n\t$(recordings_DATA)\nCLEANFILES += ../conf/janus.plugin.recordplay.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_SIP\nplugin_LTLIBRARIES += plugins/libjanus_sip.la\nplugins_libjanus_sip_la_SOURCES = plugins/janus_sip.c\nplugins_libjanus_sip_la_CFLAGS = $(plugins_cflags) $(SOFIA_CFLAGS) $(LIBSRTP_CFLAGS)\nplugins_libjanus_sip_la_LDFLAGS = $(plugins_ldflags) $(SOFIA_LDFLAGS) $(SOFIA_LIBS)\nplugins_libjanus_sip_la_LIBADD = $(plugins_libadd) $(SOFIA_LIBADD)\nconf_DATA += ../conf/janus.plugin.sip.jcfg.sample\nEXTRA_DIST += ../conf/janus.plugin.sip.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_NOSIP\nplugin_LTLIBRARIES += plugins/libjanus_nosip.la\nplugins_libjanus_nosip_la_SOURCES = plugins/janus_nosip.c\nplugins_libjanus_nosip_la_CFLAGS = $(plugins_cflags) $(LIBSRTP_CFLAGS)\nplugins_libjanus_nosip_la_LDFLAGS = $(plugins_ldflags)\nplugins_libjanus_nosip_la_LIBADD = $(plugins_libadd)\nconf_DATA += ../conf/janus.plugin.nosip.jcfg.sample\nEXTRA_DIST += ../conf/janus.plugin.nosip.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_STREAMING\nplugin_LTLIBRARIES += plugins/libjanus_streaming.la\nplugins_libjanus_streaming_la_SOURCES = plugins/janus_streaming.c\nplugins_libjanus_streaming_la_CFLAGS = $(plugins_cflags) $(LIBCURL_CFLAGS) $(OGG_CFLAGS) $(LIBSRTP_CFLAGS)\nplugins_libjanus_streaming_la_LDFLAGS = $(plugins_ldflags) $(LIBCURL_LDFLAGS) $(LIBCURL_LIBS) $(OGG_LDFLAGS) $(OGG_LIBS)\nplugins_libjanus_streaming_la_LIBADD = $(plugins_libadd) $(LIBCURL_LIBADD) $(OGG_LIBADD)\nconf_DATA += ../conf/janus.plugin.streaming.jcfg.sample\nstream_DATA += \\\n\tplugins/streams/radio.alaw \\\n\tplugins/streams/remembrance.opus \\\n\tplugins/streams/test_gstreamer.sh \\\n\tplugins/streams/test_gstreamer1.sh \\\n\tplugins/streams/test_gstreamer_multistream.sh \\\n\tplugins/streams/test_gstreamer1_multistream.sh\nEXTRA_DIST += \\\n\t../conf/janus.plugin.streaming.jcfg.sample.in \\\n\t$(stream_DATA)\nCLEANFILES += ../conf/janus.plugin.streaming.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_VIDEOCALL\nplugin_LTLIBRARIES += plugins/libjanus_videocall.la\nplugins_libjanus_videocall_la_SOURCES = plugins/janus_videocall.c\nplugins_libjanus_videocall_la_CFLAGS = $(plugins_cflags)\nplugins_libjanus_videocall_la_LDFLAGS = $(plugins_ldflags)\nplugins_libjanus_videocall_la_LIBADD = $(plugins_libadd)\nconf_DATA += ../conf/janus.plugin.videocall.jcfg.sample\nEXTRA_DIST += ../conf/janus.plugin.videocall.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_VIDEOROOM\nplugin_LTLIBRARIES += plugins/libjanus_videoroom.la\nplugins_libjanus_videoroom_la_SOURCES = plugins/janus_videoroom.c\nplugins_libjanus_videoroom_la_CFLAGS = $(plugins_cflags) $(LIBSRTP_CFLAGS)\nplugins_libjanus_videoroom_la_LDFLAGS = $(plugins_ldflags)\nplugins_libjanus_videoroom_la_LIBADD = $(plugins_libadd)\nconf_DATA += ../conf/janus.plugin.videoroom.jcfg.sample\nEXTRA_DIST += ../conf/janus.plugin.videoroom.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_TEXTROOM\nplugin_LTLIBRARIES += plugins/libjanus_textroom.la\nplugins_libjanus_textroom_la_SOURCES = plugins/janus_textroom.c\nplugins_libjanus_textroom_la_CFLAGS = $(plugins_cflags) $(LIBCURL_CFLAGS)\nplugins_libjanus_textroom_la_LDFLAGS = $(plugins_ldflags) $(LIBCURL_LDFLAGS) $(LIBCURL_LIBS)\nplugins_libjanus_textroom_la_LIBADD = $(plugins_libadd)\nconf_DATA += ../conf/janus.plugin.textroom.jcfg.sample\nEXTRA_DIST += ../conf/janus.plugin.textroom.jcfg.sample\nendif\n\nif ENABLE_PLUGIN_LUA\nplugin_LTLIBRARIES += plugins/libjanus_lua.la\nplugins_libjanus_lua_la_SOURCES = plugins/janus_lua.c plugins/janus_lua_data.h plugins/janus_lua_extra.c plugins/janus_lua_extra.h\nplugins_libjanus_lua_la_CFLAGS = $(plugins_cflags) $(LUA_CFLAGS)\nplugins_libjanus_lua_la_LDFLAGS = $(plugins_ldflags) $(LUA_LDFLAGS) $(LUA_LIBS)\nplugins_libjanus_lua_la_LIBADD = $(plugins_libadd) $(LUA_LIBADD)\nconf_DATA += ../conf/janus.plugin.lua.jcfg.sample\nlua_DATA += \\\n\tplugins/lua/echotest.lua \\\n\tplugins/lua/videoroom.lua \\\n\tplugins/lua/janus-logger.lua \\\n\tplugins/lua/janus-sdp.lua\nEXTRA_DIST += ../conf/janus.plugin.lua.jcfg.sample.in\nendif\n\nif ENABLE_PLUGIN_DUKTAPE\nplugin_LTLIBRARIES += plugins/libjanus_duktape.la\nplugins_libjanus_duktape_la_SOURCES = plugins/janus_duktape.c \\\n\tplugins/janus_duktape_data.h plugins/janus_duktape_extra.c plugins/janus_duktape_extra.h \\\n\tplugins/duktape-deps/duk_module_duktape.c plugins/duktape-deps/duk_module_duktape.h \\\n\tplugins/duktape-deps/duk_console.c plugins/duktape-deps/duk_console.h\nplugins_libjanus_duktape_la_CFLAGS = $(plugins_cflags) $(DUKTAPE_CFLAGS)\nplugins_libjanus_duktape_la_LDFLAGS = $(plugins_ldflags) $(DUKTAPE_LDFLAGS) $(DUKTAPE_LIBS)\nplugins_libjanus_duktape_la_LIBADD = $(plugins_libadd) $(DUKTAPE_LIBADD)\nconf_DATA += ../conf/janus.plugin.duktape.jcfg.sample\nduktape_DATA += \\\n\tplugins/duktape/echotest.js \\\n\tplugins/duktape/janus-sdp.js\nEXTRA_DIST += ../conf/janus.plugin.duktape.jcfg.sample.in\nendif\n\n\n##\n# Post-processing\n##\n\nif ENABLE_POST_PROCESSING\nbin_PROGRAMS += janus-pp-rec\nbin_PROGRAMS += mjr2pcap\nif ENABLE_PCAP2MJR\nbin_PROGRAMS += pcap2mjr\nendif\n\njanus_pp_rec_SOURCES = \\\n\tpostprocessing/pp-g711.c \\\n\tpostprocessing/pp-g711.h \\\n\tpostprocessing/pp-g722.c \\\n\tpostprocessing/pp-g722.h \\\n\tpostprocessing/pp-l16.c \\\n\tpostprocessing/pp-l16.h \\\n\tpostprocessing/pp-h264.c \\\n\tpostprocessing/pp-h264.h \\\n\tpostprocessing/pp-av1.c \\\n\tpostprocessing/pp-av1.h \\\n\tpostprocessing/pp-avformat.c \\\n\tpostprocessing/pp-avformat.h \\\n\tpostprocessing/pp-h265.c \\\n\tpostprocessing/pp-h265.h \\\n\tpostprocessing/pp-opus.c \\\n\tpostprocessing/pp-opus.h \\\n\tpostprocessing/pp-opus-silence.h \\\n\tpostprocessing/pp-options.c \\\n\tpostprocessing/pp-options.h \\\n\tpostprocessing/pp-rtp.h \\\n\tpostprocessing/pp-srt.c \\\n\tpostprocessing/pp-srt.h \\\n\tpostprocessing/pp-binary.c \\\n\tpostprocessing/pp-binary.h \\\n\tpostprocessing/pp-webm.c \\\n\tpostprocessing/pp-webm.h \\\n\tpostprocessing/janus-pp-rec.c \\\n\tlog.c \\\n\tutils.c \\\n\tversion.c \\\n\t$(NULL)\n\njanus_pp_rec_CFLAGS = \\\n\t$(AM_CFLAGS) \\\n\t-I$(top_builddir)/src/postprocessing \\\n\t$(LIBCURL_CFLAGS) \\\n\t$(POST_PROCESSING_CFLAGS) \\\n\t$(BORINGSSL_CFLAGS) \\\n\t$(NULL)\n\njanus_pp_rec_LDADD = \\\n\t$(BORINGSSL_LIBS) \\\n\t$(POST_PROCESSING_LIBS) \\\n\t$(LIBCURL_LDFLAGS) $(LIBCURL_LIBS) \\\n\t$(NULL)\n\nmjr2pcap_SOURCES = \\\n\tpostprocessing/pp-rtp.h \\\n\tpostprocessing/mjr2pcap.c \\\n\tlog.c \\\n\tutils.c \\\n\tversion.c \\\n\t$(NULL)\n\nmjr2pcap_CFLAGS = \\\n\t$(AM_CFLAGS) \\\n\t$(POST_PROCESSING_CFLAGS) \\\n\t$(BORINGSSL_CFLAGS) \\\n\t$(NULL)\n\nmjr2pcap_LDADD = \\\n\t$(BORINGSSL_LIBS) \\\n\t$(POST_PROCESSING_LIBS) \\\n\t$(POST_PROCESSING_MANUAL_LIBS) \\\n\t$(NULL)\n\nif ENABLE_PCAP2MJR\npcap2mjr_SOURCES = \\\n\tpostprocessing/pp-rtp.h \\\n\tpostprocessing/pcap2mjr.c \\\n\tlog.c \\\n\tutils.c \\\n\tversion.c \\\n\t$(NULL)\n\npcap2mjr_CFLAGS = \\\n\t$(AM_CFLAGS) \\\n\t-I$(top_builddir)/src/postprocessing \\\n\t$(POST_PROCESSING_CFLAGS) \\\n\t$(PCAP_CFLAGS) \\\n\t$(BORINGSSL_CFLAGS) \\\n\t$(NULL)\n\npcap2mjr_LDADD = \\\n\t$(BORINGSSL_LIBS) \\\n\t$(POST_PROCESSING_LIBS) \\\n\t$(POST_PROCESSING_MANUAL_LIBS) \\\n\t$(PCAP_LIBS) \\\n\t$(NULL)\nendif\n\ndist_man1_MANS += postprocessing/janus-pp-rec.1\ndist_man1_MANS += postprocessing/mjr2pcap.1\nif ENABLE_PCAP2MJR\ndist_man1_MANS += postprocessing/pcap2mjr.1\nendif\n\nendif\n\n.PHONY: FORCE\nFORCE:\n\n##\n# Configuration\n##\n\nconfigs:\n\t$(MKDIR_P) $(DESTDIR)$(confdir)\n\t$(foreach config,$(conf_DATA),cp \"$(CURDIR)/$(config)\" \"$(DESTDIR)$(confdir)/$(notdir $(basename $(config) .sample))\";)\n"
  },
  {
    "path": "src/apierror.c",
    "content": "#include \"apierror.h\"\n\nconst char *janus_get_api_error(int error) {\n\tswitch(error) {\n\t\tcase JANUS_OK:\n\t\t\treturn \"Success\";\n\t\tcase JANUS_ERROR_UNAUTHORIZED:\n\t\t\treturn \"Unauthorized request (wrong or missing secret/token)\";\n\t\tcase JANUS_ERROR_UNAUTHORIZED_PLUGIN:\n\t\t\treturn \"Unauthorized access to plugin (token is not allowed to)\";\n\t\tcase JANUS_ERROR_UNKNOWN:\n\t\t\treturn \"Unknown error\";\n\t\tcase JANUS_ERROR_TRANSPORT_SPECIFIC:\n\t\t\treturn \"Transport specific error\";\n\t\tcase JANUS_ERROR_MISSING_REQUEST:\n\t\t\treturn \"Missing request\";\n\t\tcase JANUS_ERROR_UNKNOWN_REQUEST:\n\t\t\treturn \"Unknown request\";\n\t\tcase JANUS_ERROR_INVALID_JSON:\n\t\t\treturn \"Invalid JSON\";\n\t\tcase JANUS_ERROR_INVALID_JSON_OBJECT:\n\t\t\treturn \"Invalid JSON Object\";\n\t\tcase JANUS_ERROR_MISSING_MANDATORY_ELEMENT:\n\t\t\treturn \"Missing mandatory element\";\n\t\tcase JANUS_ERROR_INVALID_REQUEST_PATH:\n\t\t\treturn \"Invalid path for this request\";\n\t\tcase JANUS_ERROR_SESSION_NOT_FOUND:\n\t\t\treturn \"Session not found\";\n\t\tcase JANUS_ERROR_HANDLE_NOT_FOUND:\n\t\t\treturn \"Handle not found\";\n\t\tcase JANUS_ERROR_PLUGIN_NOT_FOUND:\n\t\t\treturn \"Plugin not found\";\n\t\tcase JANUS_ERROR_PLUGIN_ATTACH:\n\t\t\treturn \"Error attaching plugin\";\n\t\tcase JANUS_ERROR_PLUGIN_MESSAGE:\n\t\t\treturn \"Error sending message to plugin\";\n\t\tcase JANUS_ERROR_PLUGIN_DETACH:\n\t\t\treturn \"Error detaching from plugin\";\n\t\tcase JANUS_ERROR_JSEP_UNKNOWN_TYPE:\n\t\t\treturn \"Unsupported JSEP type\";\n\t\tcase JANUS_ERROR_JSEP_INVALID_SDP:\n\t\t\treturn \"Invalid SDP\";\n\t\tcase JANUS_ERROR_TRICKE_INVALID_STREAM:\n\t\t\treturn \"Invalid stream\";\n\t\tcase JANUS_ERROR_INVALID_ELEMENT_TYPE:\n\t\t\treturn \"Invalid element type\";\n\t\tcase JANUS_ERROR_SESSION_CONFLICT:\n\t\t\treturn \"Session ID already in use\";\n\t\tcase JANUS_ERROR_UNEXPECTED_ANSWER:\n\t\t\treturn \"Unexpected ANSWER (no OFFER)\";\n\t\tcase JANUS_ERROR_TOKEN_NOT_FOUND:\n\t\t\treturn \"Token not found\";\n\t\tcase JANUS_ERROR_WEBRTC_STATE:\n\t\t\treturn \"Wrong WebRTC state\";\n\t\tcase JANUS_ERROR_NOT_ACCEPTING_SESSIONS:\n\t\t\treturn \"Currently not accepting new sessions\";\n\t\tdefault:\n\t\t\treturn \"Unknown error\";\n\t}\n}\n"
  },
  {
    "path": "src/apierror.h",
    "content": "/*! \\file    apierror.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Janus API errors definition\n * \\details  Definition of all the API errors that may occur when invoking\n * the Janus web-based JSON API.\n * \\todo     This code still needs proper hooks in the JavaScript libraries that use the interface.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_API_ERROR_H\n#define JANUS_API_ERROR_H\n\n/*! \\brief Success (no error) */\n#define JANUS_OK\t\t\t\t\t\t\t\t0\n\n/*! \\brief Unauthorized (can only happen when using apisecret/auth token) */\n#define JANUS_ERROR_UNAUTHORIZED\t\t\t\t403\n/*! \\brief Unauthorized access to a plugin (can only happen when using auth token) */\n#define JANUS_ERROR_UNAUTHORIZED_PLUGIN\t\t\t405\n/*! \\brief Unknown/undocumented error */\n#define JANUS_ERROR_UNKNOWN\t\t\t\t\t\t490\n/*! \\brief Transport related error */\n#define JANUS_ERROR_TRANSPORT_SPECIFIC\t\t\t450\n/*! \\brief The request is missing in the message */\n#define JANUS_ERROR_MISSING_REQUEST\t\t\t\t452\n/*! \\brief The Janus core does not support this request */\n#define JANUS_ERROR_UNKNOWN_REQUEST\t\t\t\t453\n/*! \\brief The payload is not a valid JSON message */\n#define JANUS_ERROR_INVALID_JSON\t\t\t\t454\n/*! \\brief The object is not a valid JSON object as expected */\n#define JANUS_ERROR_INVALID_JSON_OBJECT\t\t\t455\n/*! \\brief A mandatory element is missing in the message */\n#define JANUS_ERROR_MISSING_MANDATORY_ELEMENT\t456\n/*! \\brief The request cannot be handled for this webserver path  */\n#define JANUS_ERROR_INVALID_REQUEST_PATH\t\t457\n/*! \\brief The session the request refers to doesn't exist */\n#define JANUS_ERROR_SESSION_NOT_FOUND\t\t\t458\n/*! \\brief The handle the request refers to doesn't exist */\n#define JANUS_ERROR_HANDLE_NOT_FOUND\t\t\t459\n/*! \\brief The plugin the request wants to talk to doesn't exist */\n#define JANUS_ERROR_PLUGIN_NOT_FOUND\t\t\t460\n/*! \\brief An error occurring when trying to attach to a plugin and create a handle  */\n#define JANUS_ERROR_PLUGIN_ATTACH\t\t\t\t461\n/*! \\brief An error occurring when trying to send a message/request to the plugin */\n#define JANUS_ERROR_PLUGIN_MESSAGE\t\t\t\t462\n/*! \\brief An error occurring when trying to detach from a plugin and destroy the related handle  */\n#define JANUS_ERROR_PLUGIN_DETACH\t\t\t\t463\n/*! \\brief The Janus core doesn't support this SDP type */\n#define JANUS_ERROR_JSEP_UNKNOWN_TYPE\t\t\t464\n/*! \\brief The Session Description provided by the peer is invalid */\n#define JANUS_ERROR_JSEP_INVALID_SDP\t\t\t465\n/*! \\brief The stream a trickle candidate for does not exist or is invalid */\n#define JANUS_ERROR_TRICKE_INVALID_STREAM\t\t466\n/*! \\brief A JSON element is of the wrong type (e.g., an integer instead of a string) */\n#define JANUS_ERROR_INVALID_ELEMENT_TYPE\t\t467\n/*! \\brief The ID provided to create a new session is already in use */\n#define JANUS_ERROR_SESSION_CONFLICT\t\t\t468\n/*! \\brief We got an ANSWER to an OFFER we never made */\n#define JANUS_ERROR_UNEXPECTED_ANSWER\t\t\t469\n/*! \\brief The auth token the request refers to doesn't exist */\n#define JANUS_ERROR_TOKEN_NOT_FOUND\t\t\t\t470\n/*! \\brief The current request cannot be handled because of not compatible WebRTC state */\n#define JANUS_ERROR_WEBRTC_STATE\t\t\t\t471\n/*! \\brief The server is currently configured not to accept new sessions */\n#define JANUS_ERROR_NOT_ACCEPTING_SESSIONS\t\t472\n\n\n/*! \\brief Helper method to get a string representation of an API error code\n * @param[in] error The API error code\n * @returns A string representation of the error code */\nconst char *janus_get_api_error(int error);\n\n#endif\n"
  },
  {
    "path": "src/auth.c",
    "content": "/*! \\file    auth.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Requests authentication\n * \\details  Implementation of simple mechanisms for authenticating\n * requests.\n *\n * If enabled (it's disabled by default), each request must contain a\n * valid token string, * or otherwise the request is rejected with an\n * error.\n *\n * When no \\c token_auth_secret is set, Stored-token mode is active.\n * In this mode the Janus admin API can be used to specify valid string\n * tokens. Whether tokens should be shared across users or not is\n * completely up to the controlling application: these tokens are\n * completely opaque to Janus, and treated as strings, which means\n * Janus will only check if the token exists or not when asked.\n *\n * However, if a secret is set, the Signed-token mode is used.\n * In this mode, no direct communication between the controlling\n * application and Janus is necessary. Instead, the application signs\n * tokens that Janus can verify using the secret key.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <string.h>\n#include <openssl/hmac.h>\n\n#include \"auth.h\"\n#include \"debug.h\"\n#include \"mutex.h\"\n#include \"utils.h\"\n\n/* Hash table to contain the tokens to match */\nstatic GHashTable *tokens = NULL, *allowed_plugins = NULL;\nstatic gboolean auth_enabled = FALSE;\nstatic janus_mutex mutex = JANUS_MUTEX_INITIALIZER;\nstatic char *auth_secret = NULL;\n\nstatic void janus_auth_free_token(char *token) {\n\tg_free(token);\n}\n\n/* Setup */\nvoid janus_auth_init(gboolean enabled, const char *secret) {\n\tif(enabled) {\n\t\tif(secret == NULL) {\n\t\t\tJANUS_LOG(LOG_INFO, \"Stored-Token based authentication enabled\\n\");\n\t\t\ttokens = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)janus_auth_free_token, NULL);\n\t\t\tallowed_plugins = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)janus_auth_free_token, NULL);\n\t\t\tauth_enabled = TRUE;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_INFO, \"Signed-Token based authentication enabled\\n\");\n\t\t\tauth_secret = g_strdup(secret);\n\t\t\tauth_enabled = TRUE;\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_INFO, \"Token based authentication disabled\\n\");\n\t}\n}\n\ngboolean janus_auth_is_enabled(void) {\n\treturn auth_enabled;\n}\n\ngboolean janus_auth_is_stored_mode(void) {\n\treturn auth_enabled && tokens != NULL;\n}\n\ngboolean janus_auth_is_signed_mode(void) {\n\treturn auth_enabled && auth_secret != NULL;\n}\n\nvoid janus_auth_deinit(void) {\n\tjanus_mutex_lock(&mutex);\n\tif(tokens != NULL)\n\t\tg_hash_table_destroy(tokens);\n\ttokens = NULL;\n\tif(allowed_plugins != NULL)\n\t\tg_hash_table_destroy(allowed_plugins);\n\tallowed_plugins = NULL;\n\tg_free(auth_secret);\n\tauth_secret = NULL;\n\tjanus_mutex_unlock(&mutex);\n}\n\ngboolean janus_auth_check_signature(const char *token, const char *realm) {\n\tif (!auth_enabled || auth_secret == NULL)\n\t\treturn FALSE;\n\tgchar **parts = g_strsplit(token, \":\", 2);\n\tgchar **data = NULL;\n\t/* Token should have exactly one data and one hash part */\n\tif(!parts[0] || !parts[1] || parts[2])\n\t\tgoto fail;\n\tdata = g_strsplit(parts[0], \",\", 3);\n\t/* Need at least an expiry timestamp and realm */\n\tif(!data[0] || !data[1])\n\t\tgoto fail;\n\t/* Verify timestamp */\n\tgint64 expiry_time = strtoll(data[0], NULL, 10);\n\tgint64 real_time = janus_get_real_time() / 1000000;\n\tif(expiry_time < 0 || real_time > expiry_time)\n\t\tgoto fail;\n\t/* Verify realm */\n\tif(strcmp(data[1], realm))\n\t\tgoto fail;\n\t/* Verify HMAC-SHA1 */\n\tunsigned char signature[EVP_MAX_MD_SIZE] = \"\";\n\tunsigned int len;\n\tHMAC(EVP_sha1(), auth_secret, strlen(auth_secret), (const unsigned char*)parts[0], strlen(parts[0]), signature, &len);\n\tgchar *base64 = g_base64_encode(signature, len);\n\tgboolean result = janus_strcmp_const_time(parts[1], base64);\n\tg_strfreev(data);\n\tg_strfreev(parts);\n\tg_free(base64);\n\treturn result;\n\nfail:\n\tg_strfreev(data);\n\tg_strfreev(parts);\n\treturn FALSE;\n}\n\ngboolean janus_auth_check_signature_contains(const char *token, const char *realm, const char *desc) {\n\tif (!auth_enabled || auth_secret == NULL) {\n\t\treturn TRUE;\n\t}\n\tif(token == NULL)\n\t\treturn FALSE;\n\tgchar **parts = g_strsplit(token, \":\", 2);\n\tgchar **data = NULL;\n\t/* Token should have exactly one data and one hash part */\n\tif(!parts[0] || !parts[1] || parts[2])\n\t\tgoto fail;\n\tdata = g_strsplit(parts[0], \",\", 0);\n\t/* Need at least an expiry timestamp and realm */\n\tif(!data[0] || !data[1])\n\t\tgoto fail;\n\t/* Verify timestamp */\n\tgint64 expiry_time = strtoll(data[0], NULL, 10);\n\tgint64 real_time = janus_get_real_time() / 1000000;\n\tif(expiry_time < 0 || real_time > expiry_time)\n\t\tgoto fail;\n\t/* Verify realm */\n\tif(strcmp(data[1], realm))\n\t\tgoto fail;\n\t/* Find descriptor */\n\tgboolean result = FALSE;\n\tint i = 2;\n\tfor(i = 2; data[i]; i++) {\n\t\tif (!strcmp(desc, data[i])) {\n\t\t\tresult = TRUE;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!result)\n\t\tgoto fail;\n\t/* Verify HMAC-SHA1 */\n\tunsigned char signature[EVP_MAX_MD_SIZE] = \"\";\n\tunsigned int len;\n\tHMAC(EVP_sha1(), auth_secret, strlen(auth_secret), (const unsigned char*)parts[0], strlen(parts[0]), signature, &len);\n\tgchar *base64 = g_base64_encode(signature, len);\n\tresult = janus_strcmp_const_time(parts[1], base64);\n\tg_strfreev(data);\n\tg_strfreev(parts);\n\tg_free(base64);\n\treturn result;\n\nfail:\n\tg_strfreev(data);\n\tg_strfreev(parts);\n\treturn FALSE;\n}\n\n/* Tokens manipulation */\ngboolean janus_auth_add_token(const char *token) {\n\tif(!auth_enabled || tokens == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add token, stored-authentication mechanism is disabled\\n\");\n\t\treturn FALSE;\n\t}\n\tif(token == NULL)\n\t\treturn FALSE;\n\tjanus_mutex_lock(&mutex);\n\tif(g_hash_table_lookup(tokens, token)) {\n\t\tJANUS_LOG(LOG_VERB, \"Token already validated\\n\");\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn TRUE;\n\t}\n\tchar *new_token = g_strdup(token);\n\tg_hash_table_insert(tokens, new_token, new_token);\n\tjanus_mutex_unlock(&mutex);\n\treturn TRUE;\n}\n\ngboolean janus_auth_check_token(const char *token) {\n\t/* Always TRUE if the mechanism is disabled, of course */\n\tif(!auth_enabled)\n\t\treturn TRUE;\n\tif (tokens == NULL)\n\t\treturn janus_auth_check_signature(token, \"janus\");\n\tjanus_mutex_lock(&mutex);\n\tif(token && g_hash_table_lookup(tokens, token)) {\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn TRUE;\n\t}\n\tjanus_mutex_unlock(&mutex);\n\treturn FALSE;\n}\n\nGList *janus_auth_list_tokens(void) {\n\t/* Always NULL if the mechanism is disabled, of course */\n\tif(!auth_enabled || tokens == NULL)\n\t\treturn NULL;\n\tjanus_mutex_lock(&mutex);\n\tGList *list = NULL;\n\tif(g_hash_table_size(tokens) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, tokens);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tconst char *token = value;\n\t\t\tlist = g_list_append(list, g_strdup(token));\n\t\t}\n\t}\n\tjanus_mutex_unlock(&mutex);\n\treturn list;\n}\n\ngboolean janus_auth_remove_token(const char *token) {\n\tif(!auth_enabled || tokens == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't remove token, stored-authentication mechanism is disabled\\n\");\n\t\treturn FALSE;\n\t}\n\tjanus_mutex_lock(&mutex);\n\tgboolean ok = token && g_hash_table_remove(tokens, token);\n\t/* Also clear the allowed plugins mapping */\n\tGList *list = g_hash_table_lookup(allowed_plugins, token);\n\tg_hash_table_remove(allowed_plugins, token);\n\tif(list != NULL)\n\t\tg_list_free(list);\n\t/* Done */\n\tjanus_mutex_unlock(&mutex);\n\treturn ok;\n}\n\n/* Plugins access */\ngboolean janus_auth_allow_plugin(const char *token, janus_plugin *plugin) {\n\tif(!auth_enabled || allowed_plugins == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't allow access to plugin, authentication mechanism is disabled\\n\");\n\t\treturn FALSE;\n\t}\n\tif(token == NULL || plugin == NULL)\n\t\treturn FALSE;\n\tjanus_mutex_lock(&mutex);\n\tif(!g_hash_table_lookup(tokens, token)) {\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn FALSE;\n\t}\n\tGList *list = g_hash_table_lookup(allowed_plugins, token);\n\tif(list == NULL) {\n\t\t/* Add the new permission now */\n\t\tlist = g_list_append(list, plugin);\n\t\tchar *new_token = g_strdup(token);\n\t\tg_hash_table_insert(allowed_plugins, new_token, list);\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn TRUE;\n\t}\n\t/* We already have a list, update it if needed */\n\tif(g_list_find(list, plugin) != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Plugin access already allowed for token\\n\");\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn TRUE;\n\t}\n\tlist = g_list_append(list, plugin);\n\tchar *new_token = g_strdup(token);\n\tg_hash_table_insert(allowed_plugins, new_token, list);\n\tjanus_mutex_unlock(&mutex);\n\treturn TRUE;\n}\n\ngboolean janus_auth_check_plugin(const char *token, janus_plugin *plugin) {\n\t/* Always TRUE if the mechanism is disabled, of course */\n\tif(!auth_enabled)\n\t\treturn TRUE;\n\tif (allowed_plugins == NULL)\n\t\treturn janus_auth_check_signature_contains(token, \"janus\", plugin->get_package());\n\tjanus_mutex_lock(&mutex);\n\tif(!g_hash_table_lookup(tokens, token)) {\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn FALSE;\n\t}\n\tGList *list = g_hash_table_lookup(allowed_plugins, token);\n\tif(g_list_find(list, plugin) == NULL) {\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn FALSE;\n\t}\n\tjanus_mutex_unlock(&mutex);\n\treturn TRUE;\n}\n\nGList *janus_auth_list_plugins(const char *token) {\n\t/* Always NULL if the mechanism is disabled, of course */\n\tif(!auth_enabled || allowed_plugins == NULL)\n\t\treturn NULL;\n\tjanus_mutex_lock(&mutex);\n\tif(!g_hash_table_lookup(tokens, token)) {\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn FALSE;\n\t}\n\tGList *list = NULL;\n\tGList *plugins_list = g_hash_table_lookup(allowed_plugins, token);\n\tif(plugins_list != NULL)\n\t\tlist = g_list_copy(plugins_list);\n\tjanus_mutex_unlock(&mutex);\n\treturn list;\n}\n\ngboolean janus_auth_disallow_plugin(const char *token, janus_plugin *plugin) {\n\tif(!auth_enabled || allowed_plugins == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't disallow access to plugin, authentication mechanism is disabled\\n\");\n\t\treturn FALSE;\n\t}\n\tjanus_mutex_lock(&mutex);\n\tif(!g_hash_table_lookup(tokens, token)) {\n\t\tjanus_mutex_unlock(&mutex);\n\t\treturn FALSE;\n\t}\n\tGList *list = g_hash_table_lookup(allowed_plugins, token);\n\tif(list != NULL) {\n\t\t/* Update the list */\n\t\tlist = g_list_remove_all(list, plugin);\n\t\tchar *new_token = g_strdup(token);\n\t\tg_hash_table_insert(allowed_plugins, new_token, list);\n\t}\n\tjanus_mutex_unlock(&mutex);\n\treturn TRUE;\n}\n"
  },
  {
    "path": "src/auth.h",
    "content": "/*! \\file    auth.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Requests authentication (headers)\n * \\details  Implementation of a simple mechanism for authenticating\n * requests. If enabled (it's disabled by default), the Janus admin API\n * can be used to specify valid tokens; each request must then contain\n * a valid token string, or otherwise the request is rejected with an\n * error. Whether tokens should be shared across users or not is\n * completely up to the controlling application: these tokens are\n * completely opaque to Janus, and treated as strings, which means\n * Janus will only check if the token exists or not when asked.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_AUTH_H\n#define JANUS_AUTH_H\n\n#include <glib.h>\n\n#include \"plugins/plugin.h\"\n\n/*! \\brief Method to initializing the token based authentication\n * @param[in] enabled Whether the authentication mechanism should be enabled or not\n * @param[in] secret the secret to validate signed tokens against, or NULL to use stored tokens */\nvoid janus_auth_init(gboolean enabled, const char *secret);\n/*! \\brief Method to check whether the mechanism is enabled or not */\ngboolean janus_auth_is_enabled(void);\n/*! \\brief Method to check whether the mechanism is in stored-token mode or not */\ngboolean janus_auth_is_stored_mode(void);\n/*! \\brief Method to check whether the mechanism is in signed-token mode or not */\ngboolean janus_auth_is_signed_mode(void);\n/*! \\brief Method to de-initialize the mechanism */\nvoid janus_auth_deinit(void);\n\n/*! \\brief Method to check whether a signed token is valid\n * @param[in] token The token to validate\n * @param[in] realm The token realm\n * @returns TRUE if the signature is valid and not expired, FALSE otherwise */\ngboolean janus_auth_check_signature(const char *token, const char *realm);\n/*! \\brief Method to verify a signed token contains a descriptor\n * @param[in] token The token to validate\n * @param[in] realm The token realm\n * @param[in] desc The descriptor to search for\n * @returns When the token based authentication is enabled: TRUE if  the token is valid, not expired and contains the descriptor, FALSE otherwise. When the token based authentication is disabled: always TRUE. */\ngboolean janus_auth_check_signature_contains(const char *token, const char *realm, const char *desc);\n\n/*! \\brief Method to add a new valid token for authenticating\n * @param[in] token The new valid token\n * @returns TRUE if the operation was successful, FALSE otherwise */\ngboolean janus_auth_add_token(const char *token);\n/*! \\brief Method to check whether a provided token is valid or not\n * \\note verifies both token signatures and against stored tokens\n * @param[in] token The token to validate\n * @returns TRUE if the token is valid, FALSE otherwise */\ngboolean janus_auth_check_token(const char *token);\n/*! \\brief Method to return a list of the tokens\n * \\note It's the caller responsibility to free the list and its values\n * @returns A pointer to a GList instance containing the tokens */\nGList *janus_auth_list_tokens(void);\n/*! \\brief Method to invalidate an existing token\n * @param[in] token The valid to invalidate\n * @returns TRUE if the operation was successful, FALSE otherwise */\ngboolean janus_auth_remove_token(const char *token);\n\n/*! \\brief Method to allow a token to use a plugin\n * @param[in] token The token that can now access this plugin\n * @param[in] plugin Opaque pointer to the janus_plugin instance this token can access\n * @returns TRUE if the operation was successful, FALSE otherwise */\ngboolean janus_auth_allow_plugin(const char *token, janus_plugin *plugin);\n/*! \\brief Method to check whether a provided token can access a specified plugin\n * \\note verifies both token signatures and against stored tokens\n * @param[in] token The token to check\n * @param[in] plugin The plugin to check as an opaque pointer to a janus_plugin instance\n * @returns TRUE if the token is allowed to access the plugin, FALSE otherwise */\ngboolean janus_auth_check_plugin(const char *token, janus_plugin *plugin);\n/*! \\brief Method to return a list of the plugins a specific token has access to\n * \\note It's the caller responsibility to free the list (but NOT the values)\n * @param[in] token The token to get the list for\n * @returns A pointer to a GList instance containing the liist */\nGList *janus_auth_list_plugins(const char *token);\n/*! \\brief Method to disallow a token to use a plugin\n * @param[in] token The token this operation refers to\n * @param[in] plugin Opaque pointer to the janus_plugin instance this token can not access anymore\n * @returns TRUE if the operation was successful, FALSE otherwise */\ngboolean janus_auth_disallow_plugin(const char *token, janus_plugin *plugin);\n\n#endif\n"
  },
  {
    "path": "src/config.c",
    "content": "/*! \\file    config.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Configuration files parsing\n * \\details  Implementation of a parser of INI and libconfig configuration files.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <ctype.h>\n#include <errno.h>\n#include <libgen.h>\n\n#include <libconfig.h>\n\n#include \"config.h\"\n#include \"debug.h\"\n#include \"utils.h\"\n\n\n/* Filename helper */\nstatic char *get_filename(char *path) {\n\treturn basename(path);\n}\n\nstatic gboolean is_jcfg_config(const char *path) {\n\tif(path == NULL)\n\t\treturn FALSE;\n\treturn strstr(path, \".jcfg\") != NULL;\n}\n\n/* Trimming helper */\nstatic char *ltrim(char *s) {\n\tif(strlen(s) == 0)\n\t\treturn s;\n\twhile(isspace(*s))\n\t\ts++;\n\treturn s;\n}\n\nstatic char *rtrim(char *s) {\n\tif(strlen(s) == 0)\n\t\treturn s;\n\tchar *back = s + strlen(s);\n\twhile(isspace(*--back));\n\t*(back+1) = '\\0';\n\treturn s;\n}\n\nstatic char *trim(char *s) {\n\tif(strlen(s) == 0)\n\t\treturn s;\n\treturn rtrim(ltrim(s));\n}\n\n\n/* Helper to recursively process a libconfig setting */\nstatic int janus_config_jcfg_parse(janus_config *config, janus_config_container *parent, config_setting_t *setting) {\n\tif(!config || !setting)\n\t\treturn -1;\n\tswitch(config_setting_type(setting)) {\n\t\tcase CONFIG_TYPE_INT: {\n\t\t\tconst char *name = config_setting_name(setting);\n\t\t\tint val = config_setting_get_int(setting);\n\t\t\tchar value[50];\n\t\t\tg_snprintf(value, sizeof(value), \"%d\", val);\n\t\t\tjanus_config_item *item = janus_config_item_create(name, value);\n\t\t\tif(janus_config_add(config, parent, item) < 0) {\n\t\t\t\tjanus_config_container_destroy(item);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase CONFIG_TYPE_INT64: {\n\t\t\tconst char *name = config_setting_name(setting);\n\t\t\tlong long val = config_setting_get_int64(setting);\n\t\t\tchar value[50];\n\t\t\tg_snprintf(value, sizeof(value), \"%lld\", val);\n\t\t\tjanus_config_item *item = janus_config_item_create(name, value);\n\t\t\tif(janus_config_add(config, parent, item) < 0) {\n\t\t\t\tjanus_config_container_destroy(item);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase CONFIG_TYPE_FLOAT: {\n\t\t\tconst char *name = config_setting_name(setting);\n\t\t\tdouble val = config_setting_get_float(setting);\n\t\t\tchar value[50];\n\t\t\tg_snprintf(value, sizeof(value), \"%f\", val);\n\t\t\tjanus_config_item *item = janus_config_item_create(name, value);\n\t\t\tif(janus_config_add(config, parent, item) < 0) {\n\t\t\t\tjanus_config_container_destroy(item);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase CONFIG_TYPE_STRING: {\n\t\t\tconst char *name = config_setting_name(setting);\n\t\t\tconst char *value = config_setting_get_string(setting);\n\t\t\tjanus_config_item *item = janus_config_item_create(name, value);\n\t\t\tif(janus_config_add(config, parent, item) < 0) {\n\t\t\t\tjanus_config_container_destroy(item);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase CONFIG_TYPE_BOOL: {\n\t\t\tconst char *name = config_setting_name(setting);\n\t\t\tgboolean val = config_setting_get_bool(setting);\n\t\t\tjanus_config_item *item = janus_config_item_create(name, val ? \"true\" : \"false\");\n\t\t\tif(janus_config_add(config, parent, item) < 0) {\n\t\t\t\tjanus_config_container_destroy(item);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase CONFIG_TYPE_ARRAY:\n\t\tcase CONFIG_TYPE_LIST:\n\t\tcase CONFIG_TYPE_GROUP: {\n\t\t\tint num = config_setting_length(setting);\n\t\t\tif(num > 0) {\n\t\t\t\tint i=0;\n\t\t\t\tfor(i=0; i<num; i++) {\n\t\t\t\t\tconfig_setting_t *elem = config_setting_get_elem(setting, i);\n\t\t\t\t\tif(elem == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't access element #%d of setting '%s'...\\n\", i, config_setting_name(setting));\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst char *name = config_setting_name(elem);\n\t\t\t\t\tif(config_setting_type(elem) == CONFIG_TYPE_ARRAY || config_setting_type(elem) == CONFIG_TYPE_LIST) {\n\t\t\t\t\t\t/* Create an array and parse it */\n\t\t\t\t\t\tjanus_config_category *cg = janus_config_array_create(name);\n\t\t\t\t\t\tif(janus_config_add(config, parent, cg) < 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding array %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\t\t\t\t\tjanus_config_container_destroy(cg);\n\t\t\t\t\t\t\treturn -1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tint res = janus_config_jcfg_parse(config, cg, elem);\n\t\t\t\t\t\tif(res < 0)\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t} else if(config_setting_type(elem) == CONFIG_TYPE_GROUP) {\n\t\t\t\t\t\t/* Create a category and parse it */\n\t\t\t\t\t\tjanus_config_category *cg = janus_config_category_create(name);\n\t\t\t\t\t\tif(janus_config_add(config, parent, cg) < 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding category %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\t\t\t\t\tjanus_config_container_destroy(cg);\n\t\t\t\t\t\t\treturn -1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tint res = janus_config_jcfg_parse(config, cg, elem);\n\t\t\t\t\t\tif(res < 0)\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tint res = janus_config_jcfg_parse(config, parent, elem);\n\t\t\t\t\t\tif(res < 0)\n\t\t\t\t\t\t\treturn res;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn 0;\n}\n\n/* Parse a configuration file */\njanus_config *janus_config_parse(const char *config_file) {\n\tif(config_file == NULL)\n\t\treturn NULL;\n\tchar *tmp_filename = g_strdup(config_file);\n\tchar *filename = get_filename(tmp_filename);\n\tif(filename == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid filename %s\\n\", config_file);\n\t\tg_free(tmp_filename);\n\t\treturn NULL;\n\t}\n\t/* Open file */\n\tFILE *file = fopen(config_file, \"rt\");\n\tif(!file) {\n\t\tJANUS_LOG(LOG_ERR, \"  -- Error reading configuration file '%s'... error %d (%s)\\n\", filename, errno, g_strerror(errno));\n\t\tg_free(tmp_filename);\n\t\treturn NULL;\n\t}\n\t/* Create configuration instance */\n\tjanus_config *jc = g_malloc0(sizeof(janus_config));\n\tjc->name = g_strdup(filename);\n\t/* Is this a libconfig or INI config file? */\n\tjc->is_jcfg = is_jcfg_config(jc->name);\n\tif(jc->is_jcfg) {\n\t\t/* Parse with libconfig and not manually */\n\t\tconfig_t config;\n\t\tconfig_init(&config);\n\t\tif(config_read(&config, file) == CONFIG_FALSE) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing config file at line %d: %s\\n\",\n\t\t\t\tconfig_error_line(&config), config_error_text(&config));\n\t\t\tconfig_destroy(&config);\n\t\t\tgoto error;\n\t\t}\n\t\t/* Traverse the document */\n\t\tconfig_setting_t *root = config_root_setting(&config);\n\t\tif(janus_config_jcfg_parse(jc, NULL, root) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing config file at line %d: %s\\n\",\n\t\t\t\tconfig_error_line(&config), config_error_text(&config));\n\t\t\tconfig_destroy(&config);\n\t\t\tgoto error;\n\t\t}\n\t\tconfig_destroy(&config);\n\t\t/* We're done */\n\t\tgoto done;\n\t}\n\t/* Not libconfig: assume INI, traverse manually and parse it */\n\tint line_number = 0;\n\tchar line_buffer[BUFSIZ];\n\tjanus_config_category *cg = NULL;\n\twhile(fgets(line_buffer, sizeof(line_buffer), file)) {\n\t\tline_number++;\n\t\tif(strlen(line_buffer) == 0)\n\t\t\tcontinue;\n\t\t/* Strip comments */\n\t\tchar *line = line_buffer, *sc = line, *c = NULL;\n\t\twhile((c = strchr(sc, ';')) != NULL) {\n\t\t\tif(c == line || *(c-1) != '\\\\') {\n\t\t\t\t/* Comment starts here */\n\t\t\t\t*c = '\\0';\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t/* Escaped semicolon, remove the slash */\n\t\t\tsc = c-1;\n\t\t\t/* length will be at least 2: ';' '\\0' */\n\t\t\tmemmove(sc, c, strlen(c)+1);\n\t\t\t/* Go on */\n\t\t\tsc++;\n\t\t}\n\t\t/* Trim (will remove newline characters too) */\n\t\tline = trim(line);\n\t\tif(strlen(line) == 0)\n\t\t\tcontinue;\n\t\t/* Parse */\n\t\tif(line[0] == '[') {\n\t\t\t/* Category */\n\t\t\tline++;\n\t\t\tchar *end = strchr(line, ']');\n\t\t\tif(end == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing category at line %d: syntax error (%s)\\n\", line_number, filename);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t*end = '\\0';\n\t\t\tline = trim(line);\n\t\t\tif(strlen(line) == 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing category at line %d: no name (%s)\\n\", line_number, filename);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tcg = janus_config_category_create(line);\n\t\t\tif(janus_config_add(jc, NULL, cg) < 0) {\n\t\t\t\tjanus_config_container_destroy(cg);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding category %s (%s)\\n\", line, filename);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t} else {\n\t\t\t/* Item */\n\t\t\tchar *name = line, *value = strchr(line, '=');\n\t\t\tif(value == NULL || value == line) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing item at line %d (%s)\\n\", line_number, filename);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t*value = '\\0';\n\t\t\tname = trim(name);\n\t\t\tif(strlen(name) == 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing item at line %d: no name (%s)\\n\", line_number, filename);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tvalue++;\n\t\t\tvalue = trim(value);\n\t\t\tif(strlen(value) > 0) {\n\t\t\t\tif(*value == '>') {\n\t\t\t\t\tvalue++;\n\t\t\t\t\tvalue = trim(value);\n\t\t\t\t\tif(strlen(value) == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing item at line %d: no value (%s)\\n\", line_number, filename);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_config_item *item = janus_config_item_create(name, value);\n\t\t\tif(janus_config_add(jc, cg, item) < 0) {\n\t\t\t\tjanus_config_container_destroy(item);\n\t\t\t\tif(cg == NULL)\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s (%s)\\n\", name, filename);\n\t\t\t\telse\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s to category %s (%s)\\n\", name, cg->name, filename);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t}\n\t}\ndone:\n\tg_free(tmp_filename);\n\tfclose(file);\n\treturn jc;\n\nerror:\n\tg_free(tmp_filename);\n\tfclose(file);\n\tjanus_config_destroy(jc);\n\treturn NULL;\n}\n\njanus_config *janus_config_create(const char *name) {\n\tif(name == NULL)\n\t\treturn NULL;\n\tjanus_config *jc = g_malloc0(sizeof(janus_config));\n\tjc->name = g_strdup(name);\n\t/* Is this a libconfig or INI config file? */\n\tjc->is_jcfg = is_jcfg_config(jc->name);\n\treturn jc;\n}\n\n\n/* Containers management */\njanus_config_item *janus_config_item_create(const char *name, const char *value) {\n\tif(name == NULL && value == NULL)\n\t\treturn NULL;\n\tjanus_config_item *item = g_malloc0(sizeof(janus_config_item));\n\titem->type = janus_config_type_item;\n\tif(name)\n\t\titem->name = g_strdup(name);\n\tif(value)\n\t\titem->value = g_strdup(value);\n\treturn item;\n}\n\njanus_config_category *janus_config_category_create(const char *name) {\n\tjanus_config_category *category = g_malloc0(sizeof(janus_config_category));\n\tcategory->type = janus_config_type_category;\n\tif(name != NULL)\n\t\tcategory->name = g_strdup(name);\n\treturn category;\n}\n\njanus_config_array *janus_config_array_create(const char *name) {\n\tjanus_config_array *array = g_malloc0(sizeof(janus_config_array));\n\tarray->type = janus_config_type_array;\n\tif(name != NULL)\n\t\tarray->name = g_strdup(name);\n\treturn array;\n}\n\nvoid janus_config_container_destroy(janus_config_container *container) {\n\tif(container) {\n\t\tif(container->name)\n\t\t\tg_free((gpointer)container->name);\n\t\tif(container->value)\n\t\t\tg_free((gpointer)container->value);\n\t\tif(container->list)\n\t\t\tg_list_free_full(container->list, (GDestroyNotify)janus_config_container_destroy);\n\t\tg_free(container);\n\t}\n}\n\nstatic janus_config_container *janus_config_get_internal(janus_config *config,\n\t\tjanus_config_container *parent, janus_config_type type, const char *name, gboolean create) {\n\tif(config == NULL || name == NULL)\n\t\treturn NULL;\n\tif(parent != NULL && parent->type != janus_config_type_category && parent->type != janus_config_type_array)\n\t\treturn NULL;\n\tjanus_config_container *c = NULL;\n\tGList *l = parent ? parent->list : config->list;\n\twhile(l) {\n\t\tc = (janus_config_container *)l->data;\n\t\tif(c && c->name && !strcasecmp(name, c->name) &&\n\t\t\t\t(type == janus_config_type_any || c->type == type))\n\t\t\treturn c;\n\t\tl = l->next;\n\t}\n\t/* If we got here, it doesn't exist, should we create it? */\n\tc = NULL;\n\tif(create) {\n\t\tif(type == janus_config_type_category) {\n\t\t\tc = janus_config_category_create(name);\n\t\t} else if(type == janus_config_type_array) {\n\t\t\tc = janus_config_array_create(name);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Not a category and not an array, not creating anything...\\n\");\n\t\t}\n\t\tif(c != NULL && janus_config_add(config, parent, c) < 0) {\n\t\t\tjanus_config_container_destroy(c);\n\t\t\tJANUS_LOG(LOG_ERR, \"Error adding item %s to %s\\n\", name, parent ? parent->name : \"root\");\n\t\t\treturn NULL;\n\t\t}\n\t}\n\treturn c;\n}\n\njanus_config_container *janus_config_get(janus_config *config,\n\t\tjanus_config_container *parent, janus_config_type type, const char *name) {\n\treturn janus_config_get_internal(config, parent, type, name, FALSE);\n}\n\njanus_config_container *janus_config_get_create(janus_config *config,\n\t\tjanus_config_container *parent, janus_config_type type, const char *name) {\n\treturn janus_config_get_internal(config, parent, type, name, TRUE);\n}\n\njanus_config_container *janus_config_search(janus_config *config, ...) {\n\tif(config == NULL)\n\t\treturn NULL;\n\tva_list args;\n\tva_start(args, config);\n\tchar *name = va_arg(args, char *);\n\tif(name == NULL) {\n\t\tva_end(args);\n\t\treturn NULL;\n\t}\n\t/* Get the full path we're looking for */\n\tGList *path = NULL;\n\twhile(name) {\n\t\tpath = g_list_append(path, name);\n\t\tname = va_arg(args, char *);\n\t}\n\tva_end(args);\n\t/* Start looking */\n\tjanus_config_container *parent = NULL, *c = NULL;\n\twhile(path) {\n\t\tname = (char *)path->data;\n\t\tc = janus_config_get(config, parent, janus_config_type_any, name);\n\t\tif(c == NULL) {\n\t\t\t/* Not found */\n\t\t\tbreak;\n\t\t}\n\t\tparent = c;\n\t\t/* Next parent */\n\t\tpath = path->next;\n\t}\n\treturn c;\n}\n\nint janus_config_add(janus_config *config, janus_config_container *container, janus_config_container *item) {\n\tif(config == NULL || item == NULL)\n\t\treturn -1;\n\tif(container != NULL && container->type != janus_config_type_category && container->type != janus_config_type_array)\n\t\treturn -2;\n\tif(item->name) {\n\t\t/* Remove any existing property with the same name in that container first, if any */\n\t\tjanus_config_remove(config, container, item->name);\n\t}\n\tif(container) {\n\t\t/* Add to parent */\n\t\tcontainer->list = g_list_append(container->list, item);\n\t} else {\n\t\t/* Add to root */\n\t\tconfig->list = g_list_append(config->list, item);\n\t}\n\treturn 0;\n}\n\nint janus_config_remove(janus_config *config, janus_config_container *container, const char *name) {\n\tif(config == NULL || name == NULL)\n\t\treturn -1;\n\tif(container != NULL && container->type != janus_config_type_category && container->type != janus_config_type_array)\n\t\treturn -2;\n\tjanus_config_container *item = janus_config_get(config, container, janus_config_type_any, name);\n\tif(item == NULL)\n\t\treturn -3;\n\tif(container) {\n\t\t/* Remove from parent */\n\t\tcontainer->list = g_list_remove(container->list, item);\n\t} else {\n\t\t/* Remove from root */\n\t\tconfig->list = g_list_remove(config->list, item);\n\t}\n\tjanus_config_container_destroy(item);\n\treturn 0;\n}\n\nGList *janus_config_get_items(janus_config *config, janus_config_container *parent) {\n\tif(config == NULL || (parent != NULL && parent->type != janus_config_type_category\n\t\t\t&& parent->type != janus_config_type_array))\n\t\treturn NULL;\n\tGList *list = NULL, *clist = parent ? parent->list : config->list;\n\twhile(clist) {\n\t\tjanus_config_container *c = (janus_config_container *)clist->data;\n\t\tif(c && c->type == janus_config_type_item)\n\t\t\tlist = g_list_append(list, c);\n\t\tclist = clist->next;\n\t}\n\treturn list;\n}\n\nGList *janus_config_get_categories(janus_config *config, janus_config_container *parent) {\n\tif(config == NULL || (parent != NULL && parent->type != janus_config_type_category\n\t\t\t&& parent->type != janus_config_type_array))\n\t\treturn NULL;\n\tGList *list = NULL, *clist = parent ? parent->list : config->list;\n\twhile(clist) {\n\t\tjanus_config_container *c = (janus_config_container *)clist->data;\n\t\tif(c && c->type == janus_config_type_category)\n\t\t\tlist = g_list_append(list, c);\n\t\tclist = clist->next;\n\t}\n\treturn list;\n}\n\nGList *janus_config_get_arrays(janus_config *config, janus_config_container *parent) {\n\tif(config == NULL || (parent != NULL && parent->type != janus_config_type_category\n\t\t\t&& parent->type != janus_config_type_array))\n\t\treturn NULL;\n\tGList *list = NULL, *clist = parent ? parent->list : config->list;\n\twhile(clist) {\n\t\tjanus_config_container *c = (janus_config_container *)clist->data;\n\t\tif(c && c->type == janus_config_type_array)\n\t\t\tlist = g_list_append(list, c);\n\t\tclist = clist->next;\n\t}\n\treturn list;\n}\n\n\n/* Printing utilities */\nvoid janus_config_print(janus_config *config) {\n\tjanus_config_print_as(config, LOG_VERB);\n}\n\n#define JANUS_CONFIG_INDENT 4\nstatic void janus_config_print_list(int level, GList *l, int indent) {\n\twhile(l) {\n\t\tjanus_config_container *c = (janus_config_container *)l->data;\n\t\tif(c->type == janus_config_type_item) {\n\t\t\tJANUS_LOG(level, \"%*s%s%s%s\\n\", indent, \"\",\n\t\t\t\tc->name ? c->name : \"\",\n\t\t\t\tc->name && c->value ? \": \" : \"\",\n\t\t\t\tc->value ? c->value : \"\");\n\t\t} else if(c->type == janus_config_type_category) {\n\t\t\tJANUS_LOG(level, \"%*s%s%s{\\n\", indent, \"\",\n\t\t\t\tc->name ? c->name : \"\",\n\t\t\t\tc->name ? \": \" : \"\");\n\t\t\tif(c->list)\n\t\t\t\tjanus_config_print_list(level, c->list, indent+JANUS_CONFIG_INDENT);\n\t\t\tJANUS_LOG(level, \"%*s}\\n\", indent, \"\");\n\t\t} else if(c->type == janus_config_type_array) {\n\t\t\tJANUS_LOG(level, \"%*s%s%s[\\n\", indent, \"\",\n\t\t\t\tc->name ? c->name : \"\",\n\t\t\t\tc->name ? \": \" : \"\");\n\t\t\tif(c->list)\n\t\t\t\tjanus_config_print_list(level, c->list, indent+JANUS_CONFIG_INDENT);\n\t\t\tJANUS_LOG(level, \"%*s]\\n\", indent, \"\");\n\t\t}\n\t\tl = l->next;\n\t}\n}\n\nvoid janus_config_print_as(janus_config *config, int level) {\n\tif(config == NULL)\n\t\treturn;\n\tJANUS_LOG(level, \"[%s]\\n\", config->name ? config->name : \"??\");\n\tif(config->list)\n\t\tjanus_config_print_list(level, config->list, JANUS_CONFIG_INDENT);\n}\n\nstatic void janus_config_save_list(janus_config *config, FILE *file, int level, gboolean array, GList *list, config_setting_t *lcfg) {\n\tif(config == NULL || file == NULL || list == NULL )\n\t\treturn;\n\tGList *l = list;\n\tconfig_setting_t *elem = NULL;\n\twhile(l) {\n\t\tjanus_config_container *c = (janus_config_container *)l->data;\n\t\tif(c->type == janus_config_type_item) {\n\t\t\tif(config->is_jcfg) {\n\t\t\t\telem = config_setting_add(lcfg, c->name, CONFIG_TYPE_STRING);\n\t\t\t\tif(elem == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error saving string '%s' to the config file...\\n\", c->name);\n\t\t\t\t\tl = l->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconfig_setting_set_string(elem, c->value);\n\t\t\t} else {\n\t\t\t\tfwrite(c->name, sizeof(char), strlen(c->name), file);\n\t\t\t\tfwrite(\" = \", sizeof(char), 3, file);\n\t\t\t\t/* If the value contains a semicolon, escape it */\n\t\t\t\tif(strchr(c->value, ';')) {\n\t\t\t\t\tchar *value = g_strdup(c->value);\n\t\t\t\t\tvalue = janus_string_replace((char *)value, \";\", \"\\\\;\");\n\t\t\t\t\tfwrite(value, sizeof(char), strlen(value), file);\n\t\t\t\t\tfwrite(\"\\n\", sizeof(char), 1, file);\n\t\t\t\t\tg_free(value);\n\t\t\t\t} else {\n\t\t\t\t\t/* No need to escape */\n\t\t\t\t\tfwrite(c->value, sizeof(char), strlen(c->value), file);\n\t\t\t\t\tfwrite(\"\\n\", sizeof(char), 1, file);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(c->type == janus_config_type_category) {\n\t\t\tif(config->is_jcfg) {\n\t\t\t\telem = config_setting_add(lcfg, c->name, CONFIG_TYPE_GROUP);\n\t\t\t\tif(elem == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error saving group '%s' to the config file...\\n\", c->name);\n\t\t\t\t\tl = l->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif(level > 0) {\n\t\t\t\t\t/* INI files don't support indented categories */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Dropping indented category %s (unsupported in INI files)\\n\", c->name);\n\t\t\t\t} else {\n\t\t\t\t\tfwrite(\"[\", sizeof(char), 1, file);\n\t\t\t\t\tfwrite(c->name, sizeof(char), strlen(c->name), file);\n\t\t\t\t\tfwrite(\"]\\n\", sizeof(char), 2, file);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(c->list != NULL) {\n\t\t\t\t/* Non-empty category */\n\t\t\t\tjanus_config_save_list(config, file, level+1, FALSE, c->list, elem);\n\t\t\t}\n\t\t\tif(!config->is_jcfg)\n\t\t\t\tfwrite(\"\\r\\n\", sizeof(char), 2, file);\n\t\t} else if(c->type == janus_config_type_array) {\n\t\t\tif(!config->is_jcfg) {\n\t\t\t\t/* INI files don't support arrays */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Dropping array %s (unsupported in INI files)\\n\", c->name);\n\t\t\t} else {\n\t\t\t\t/* FIXME We don't know in advance if all items will be of the\n\t\t\t\t * same kind, so we use list instead of array in libconfig */\n\t\t\t\telem = config_setting_add(lcfg, c->name, CONFIG_TYPE_LIST);\n\t\t\t\tif(elem == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error saving list '%s' to the config file...\\n\", c->name);\n\t\t\t\t\tl = l->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(c->list != NULL) {\n\t\t\t\t\t/* Non-empty array */\n\t\t\t\t\tjanus_config_save_list(config, file, level+1, FALSE, c->list, elem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tl = l->next;\n\t}\n}\n\ngboolean janus_config_save(janus_config *config, const char *folder, const char *filename) {\n\tif(config == NULL)\n\t\treturn -1;\n\t/* If this is a libconfig configuration, create an object for it */\n\tconfig_t lcfg;\n\tif(config->is_jcfg)\n\t\tconfig_init(&lcfg);\n\t/* Open the file */\n\tFILE *file = NULL;\n\tchar path[1024];\n\tif(folder != NULL) {\n\t\t/* Create folder, if needed */\n\t\tif(janus_mkdir(folder, 0755) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't save configuration file, error creating folder '%s'...\\n\", folder);\n\t\t\tif(config->is_jcfg)\n\t\t\t\tconfig_destroy(&lcfg);\n\t\t\treturn -2;\n\t\t}\n\t\tg_snprintf(path, 1024, \"%s/%s.%s\", folder, filename, config->is_jcfg ? \"jcfg\" : \"cfg\");\n\t} else {\n\t\tg_snprintf(path, 1024, \"%s.%s\", filename, config->is_jcfg ? \"jcfg\" : \"cfg\");\n\t}\n\tfile = fopen(path, \"wt\");\n\tif(file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't save configuration file, error opening file '%s'...\\n\", path);\n\t\tif(config->is_jcfg)\n\t\t\tconfig_destroy(&lcfg);\n\t\treturn -3;\n\t}\n\t/* Print a header/comment */\n\tchar date[64], header[256];\n\tstruct tm tmresult;\n\ttime_t ltime = time(NULL);\n\tlocaltime_r(&ltime, &tmresult);\n\tstrftime(date, sizeof(date), \"%a %b %e %T %Y\", &tmresult);\n\tchar comment = config->is_jcfg ? '#' : ';';\n\tg_snprintf(header, 256, \"%c\\n%c File automatically generated on %s\\n%c\\n\\n\",\n\t\tcomment, comment, date, comment);\n\tfwrite(header, sizeof(char), strlen(header), file);\n\t/* Go on with the configuration */\n\tif(config->list)\n\t\tjanus_config_save_list(config, file, 0, FALSE, config->list, config->is_jcfg ? config_root_setting(&lcfg) : NULL);\n\t/* If this is a libconfig output file, save and destroy the object */\n\tif(config->is_jcfg) {\n\t\tconfig_write(&lcfg, file);\n\t\tconfig_destroy(&lcfg);\n\t}\n\t/* Done */\n\tfclose(file);\n\treturn 0;\n}\n\nvoid janus_config_destroy(janus_config *config) {\n\tif(config == NULL)\n\t\treturn;\n\tif(config->list) {\n\t\tg_list_free_full(config->list, (GDestroyNotify)janus_config_container_destroy);\n\t\tconfig->list = NULL;\n\t}\n\tg_free((gpointer)config->name);\n\tg_free((gpointer)config);\n\tconfig = NULL;\n}\n"
  },
  {
    "path": "src/config.h",
    "content": "/*! \\file    config.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Configuration files parsing (headers)\n * \\details  Implementation of a parser of INI and libconfig configuration files.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_CONFIG_H\n#define JANUS_CONFIG_H\n\n#include <glib.h>\n\n/*! \\brief Configuration element type */\ntypedef enum janus_config_type {\n\t/*! \\brief Anything (just for searches) */\n\tjanus_config_type_any = 1,\n\t/*! \\brief Plain item */\n\tjanus_config_type_item,\n\t/*! \\brief Category */\n\tjanus_config_type_category,\n\t/*! \\brief Array */\n\tjanus_config_type_array,\n} janus_config_type;\n\n/*! \\brief Generic configuration container (can be an item, a category or an array) */\ntypedef struct janus_config_container {\n\t/*! \\brief Whether this is a category, an item or an array */\n\tjanus_config_type type;\n\t/*! \\brief Name of the item/category/array */\n\tconst char *name;\n\t/*! \\brief Value of the item (item only) */\n\tconst char *value;\n\t/*! \\brief Linked list of contained items/categories/arrays (category and array only) */\n\tGList *list;\n} janus_config_container;\n\n/*! \\brief Configuration item (defined for backwards compatibility) */\ntypedef struct janus_config_container janus_config_item;\n\n/*! \\brief Configuration category (defined for backwards compatibility) */\ntypedef struct janus_config_container janus_config_category;\n\n/*! \\brief Configuration array */\ntypedef struct janus_config_container janus_config_array;\n\n/*! \\brief Configuration container */\ntypedef struct janus_config {\n\t/*! \\brief Whether this is a libconfig (jcfg for us) or an INI config */\n\tgboolean is_jcfg;\n\t/*! \\brief Name of the configuration */\n\tconst char *name;\n\t/*! \\brief Linked list of items/categories/arrays */\n\tGList *list;\n} janus_config;\n\n\n/*! \\brief Method to parse an INI configuration file\n * @param[in] config_file Path to the configuration file\n * @returns A pointer to a valid janus_config instance if successful, NULL otherwise */\njanus_config *janus_config_parse(const char *config_file);\n/*! \\brief Method to create a new, empty, configuration\n * @param[in] name Name to give to the configuration\n * @returns A pointer to a valid janus_config instance if successful, NULL otherwise */\njanus_config *janus_config_create(const char *name);\n/*! \\brief Helper method to print a configuration on the standard output\n * @note This prints with LOG_VERB: if you need the configuration to be visible at\n * a different debugging level, use janus_config_print_as instead\n * @param[in] config The configuration to print */\nvoid janus_config_print(janus_config *config);\n/*! \\brief Helper method to print a configuration on the standard output\n * using a different logging level than LOG_VERB\n * @param[in] config The configuration to print\n * @param[in] level The debugging level to use */\nvoid janus_config_print_as(janus_config *config, int level);\n/*! \\brief Helper method to save a configuration to a file\n * @param[in] config The configuration to save\n * @param[in] folder The folder the file should be saved to\n * @param[in] filename The file name, extension included (should be .jcfg, or .cfg for legacy INI files)\n * @returns 0 if successful, a negative integer otherwise */\nint janus_config_save(janus_config *config, const char *folder, const char *filename);\n/*! \\brief Destroy a configuration container instance\n * @param[in] config The configuration to destroy */\nvoid janus_config_destroy(janus_config *config);\n\n/*! \\brief Method to create a new janus_config_item instance from name and value\n * @param[in] name Name to give to the item\n * @param[in] value Value of the item (optional)\n * @returns A valid janus_config_item instance if successful, NULL otherwise */\njanus_config_item *janus_config_item_create(const char *name, const char *value);\n/*! \\brief Method to create a new janus_config_category instance\n * @param[in] name Name to give to the category\n * @returns A pointer to a valid janus_config_category instance if successful, NULL otherwise */\njanus_config_category *janus_config_category_create(const char *name);\n/*! \\brief Method to create a new janus_config_array instance\n * @param[in] name Name to give to the array\n * @returns A valid janus_config_array instance if successful, NULL otherwise */\njanus_config_array *janus_config_array_create(const char *name);\n/*! \\brief Helper method to quickly destroy an item, category or array\n * @note This method also destroys anything it contains, if it's a category or\n * array, but will not unlink the object from its parent: this is up to the caller\n * @param[in] container The item/category/array to destroy */\nvoid janus_config_container_destroy(janus_config_container *container);\n\n/*! \\brief Helper method to quickly get an item, category, or array\n * @note If the parent container is NULL, the lookup is done at the root. If something is found\n * but type doesn't match (name is an array but we're looking for a category), NULL is returned.\n * @param[in] config The configuration instance\n * @param[in] parent The parent container (category or array), if any\n * @param[in] type The type of container to look for\n * @param[in] name The name of the item/category/array to look for\n * @returns A pointer to a valid janus_config_container instance if successful, NULL otherwise */\njanus_config_container *janus_config_get(janus_config *config,\n\tjanus_config_container *parent, janus_config_type type, const char *name);\n/*! \\brief Same as janus_config_get, but creates the element if it doesn't exist\n * @note Nothing is created if type is janus_config_type_any.\n * @param[in] config The configuration instance\n * @param[in] parent The parent container (category or array), if any\n * @param[in] type The type of container to look for\n * @param[in] name The name of the item/category/array to look for\n * @returns A pointer to a valid janus_config_container instance if successful, NULL otherwise */\njanus_config_container *janus_config_get_create(janus_config *config,\n\tjanus_config_container *parent, janus_config_type type, const char *name);\n/*! \\brief Helper method to quickly lookup an item, category, or array\n * @note If something is found but type doesn't match (name is an array\n * but we're looking for a category), NULL is returned.\n * @param[in] config The configuration instance\n * @returns A pointer to a valid janus_config_container instance if successful, NULL otherwise */\njanus_config_container *janus_config_search(janus_config *config, ...);\n\n/*! \\brief Add an item/category/array instance to a category or array\n * \\note If adding to a category and the item/category/array already exists, it is replaced;\n * it is appended if the target is an array instead, where duplicates are accepted.\n * @param[in] config The configuration instance\n * @param[in] parent The category or array to add the item to, if any\n * @param[in] item The item/category/array to add\n * @returns 0 if successful, a negative integer otherwise */\nint janus_config_add(janus_config *config, janus_config_container *parent, janus_config_container *item);\n/*! \\brief Remove an existing item with the specific name from a category/array\n * @param[in] config The configuration instance\n * @param[in] parent The category/array to remove the item from, if any\n * @param[in] name The name of the item/category/array to remove\n * @returns 0 if successful, a negative integer otherwise */\nint janus_config_remove(janus_config *config, janus_config_container *parent, const char *name);\n\n/*! \\brief Helper method to return the list of plain items, either in root or from a parent\n * @note The method returns a new GList: it's up to the caller to free it. The values\n * of the list data must NOT be freed, though, as it's just linked from the configuration.\n * @param[in] config The configuration instance\n * @param[in] parent The parent container (category or array), if any\n * @returns A pointer to the categories GLib linked list of items if successful, NULL otherwise */\nGList *janus_config_get_items(janus_config *config, janus_config_container *parent);\n/*! \\brief Helper method to return the list of categories, either in root or from a parent\n * @note The method returns a new GList: it's up to the caller to free it. The values\n * of the list data must NOT be freed, though, as it's just linked from the configuration.\n * @param[in] config The configuration instance\n * @param[in] parent The parent container (category or array), if any\n * @returns A pointer to the categories GLib linked list of categories if successful, NULL otherwise */\nGList *janus_config_get_categories(janus_config *config, janus_config_container *parent);\n/*! \\brief Helper method to return the list of arrays, either in root or from a parent\n * @note The method returns a new GList: it's up to the caller to free it. The values\n * of the list data must NOT be freed, though, as it's just linked from the configuration.\n * @param[in] config The configuration instance\n * @param[in] parent The parent container (category or array), if any\n * @returns A pointer to the categories GLib linked list of arrays if successful, NULL otherwise */\nGList *janus_config_get_arrays(janus_config *config, janus_config_container *parent);\n\n#endif\n"
  },
  {
    "path": "src/debug.h",
    "content": "/*! \\file    debug.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Logging and Debugging\n * \\details  Implementation of a wrapper on printf (or g_print) to either log or debug.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_DEBUG_H\n#define JANUS_DEBUG_H\n\n#include <glib.h>\n#include <glib/gprintf.h>\n#include \"log.h\"\n\nextern int janus_log_level;\nextern gboolean janus_log_timestamps;\nextern gboolean janus_log_colors;\nextern char *janus_log_global_prefix;\n\n/** @name Janus log colors\n */\n///@{\n#define ANSI_COLOR_RED     \"\\x1b[31m\"\n#define ANSI_COLOR_GREEN   \"\\x1b[32m\"\n#define ANSI_COLOR_YELLOW  \"\\x1b[33m\"\n#define ANSI_COLOR_BLUE    \"\\x1b[34m\"\n#define ANSI_COLOR_MAGENTA \"\\x1b[35m\"\n#define ANSI_COLOR_CYAN    \"\\x1b[36m\"\n#define ANSI_COLOR_RESET   \"\\x1b[0m\"\n///@}\n\n/** @name Janus log levels\n */\n///@{\n/*! \\brief No debugging */\n#define LOG_NONE     (0)\n/*! \\brief Fatal error */\n#define LOG_FATAL    (1)\n/*! \\brief Non-fatal error */\n#define LOG_ERR      (2)\n/*! \\brief Warning */\n#define LOG_WARN     (3)\n/*! \\brief Informational message */\n#define LOG_INFO     (4)\n/*! \\brief Verbose message */\n#define LOG_VERB     (5)\n/*! \\brief Overly verbose message */\n#define LOG_HUGE     (6)\n/*! \\brief Debug message (includes .c filename, function and line number) */\n#define LOG_DBG      (7)\n/*! \\brief Maximum level of debugging */\n#define LOG_MAX LOG_DBG\n\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wunused-variable\"\n/*! \\brief Coloured prefixes for errors and warnings logging. */\nstatic const char *janus_log_prefix[] = {\n/* no colors */\n\t\"\",\n\t\"[FATAL] \",\n\t\"[ERR] \",\n\t\"[WARN] \",\n\t\"\",\n\t\"\",\n\t\"\",\n\t\"\",\n/* with colors */\n\t\"\",\n\tANSI_COLOR_MAGENTA \"[FATAL]\" ANSI_COLOR_RESET \" \",\n\tANSI_COLOR_RED \"[ERR]\" ANSI_COLOR_RESET \" \",\n\tANSI_COLOR_YELLOW \"[WARN]\" ANSI_COLOR_RESET \" \",\n\t\"\",\n\t\"\",\n\t\"\",\n\t\"\"\n};\n///@}\n#pragma GCC diagnostic pop\n\n/** @name Janus log wrappers\n */\n///@{\n/*! \\brief Simple wrapper to g_print/printf */\n#define JANUS_PRINT janus_vprintf\n/*! \\brief Logger based on different levels, which can either be displayed\n * or not according to the configuration of the server.\n * The format must be a string literal. */\n#define JANUS_LOG(level, format, ...) \\\ndo { \\\n\tif (level > LOG_NONE && level <= LOG_MAX && level <= janus_log_level) { \\\n\t\tchar janus_log_ts[64] = \"\"; \\\n\t\tchar janus_log_src[128] = \"\"; \\\n\t\tif (janus_log_timestamps) { \\\n\t\t\tstruct tm janustmresult; \\\n\t\t\ttime_t janusltime = time(NULL); \\\n\t\t\tlocaltime_r(&janusltime, &janustmresult); \\\n\t\t\tstrftime(janus_log_ts, sizeof(janus_log_ts), \\\n\t\t\t         \"[%a %b %e %T %Y] \", &janustmresult); \\\n\t\t} \\\n\t\tif (level == LOG_FATAL || level == LOG_ERR || level == LOG_DBG) { \\\n\t\t\tsnprintf(janus_log_src, sizeof(janus_log_src), \\\n\t\t\t         \"[%s:%s:%d] \", __FILE__, __FUNCTION__, __LINE__); \\\n\t\t} \\\n\t\tJANUS_PRINT(\"%s%s%s%s\" format, \\\n\t\t\tjanus_log_global_prefix ? janus_log_global_prefix : \"\", \\\n\t\t\tjanus_log_ts, \\\n\t\t\tjanus_log_prefix[level | ((int)janus_log_colors << 3)], \\\n\t\t\tjanus_log_src, \\\n\t\t\t##__VA_ARGS__); \\\n\t} \\\n} while (0)\n///@}\n\n#endif\n"
  },
  {
    "path": "src/dtls-bio.c",
    "content": "/*! \\file    dtls-bio.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    OpenSSL BIO agent writer\n * \\details  OpenSSL BIO that writes packets to a libnice agent.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#include <glib.h>\n\n#include \"dtls-bio.h\"\n#include \"debug.h\"\n#include \"ice.h\"\n#include \"mutex.h\"\n\n/* Starting MTU value for the DTLS BIO agent writer */\nstatic int mtu = 1200;\nvoid janus_dtls_bio_agent_set_mtu(int start_mtu) {\n\tif(start_mtu < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid MTU...\\n\");\n\t\treturn;\n\t}\n\tmtu = start_mtu;\n\tJANUS_LOG(LOG_VERB, \"Setting starting MTU in the DTLS BIO writer: %d\\n\", mtu);\n}\nint janus_dtls_bio_agent_get_mtu(void) {\n\treturn mtu;\n}\n\n/* BIO implementation */\nstatic int janus_dtls_bio_agent_write(BIO *h, const char *buf, int num);\nstatic long janus_dtls_bio_agent_ctrl(BIO *h, int cmd, long arg1, void *arg2);\nstatic int janus_dtls_bio_agent_new(BIO *h);\nstatic int janus_dtls_bio_agent_free(BIO *data);\n\n/* BIO initialization */\n#if JANUS_USE_OPENSSL_PRE_1_1_API\nstatic BIO_METHOD janus_dtls_bio_agent_methods = {\n\tBIO_TYPE_BIO,\n\t\"janus agent writer\",\n\tjanus_dtls_bio_agent_write,\n\tNULL,\n\tNULL,\n\tNULL,\n\tjanus_dtls_bio_agent_ctrl,\n\tjanus_dtls_bio_agent_new,\n\tjanus_dtls_bio_agent_free,\n\tNULL\n};\n#else\nstatic BIO_METHOD *janus_dtls_bio_agent_methods = NULL;\n#endif\nint janus_dtls_bio_agent_init(void) {\n#if JANUS_USE_OPENSSL_PRE_1_1_API\n\t/* No initialization needed for OpenSSL pre-1.1.0 */\n#else\n\tjanus_dtls_bio_agent_methods = BIO_meth_new(BIO_TYPE_BIO, \"janus agent writer\");\n\tif(!janus_dtls_bio_agent_methods) {\n\t\treturn -1;\n\t}\n\tBIO_meth_set_write(janus_dtls_bio_agent_methods, janus_dtls_bio_agent_write);\n\tBIO_meth_set_ctrl(janus_dtls_bio_agent_methods, janus_dtls_bio_agent_ctrl);\n\tBIO_meth_set_create(janus_dtls_bio_agent_methods, janus_dtls_bio_agent_new);\n\tBIO_meth_set_destroy(janus_dtls_bio_agent_methods, janus_dtls_bio_agent_free);\n#endif\n\treturn 0;\n}\n\nstatic BIO_METHOD *BIO_janus_dtls_agent_method(void) {\n#if JANUS_USE_OPENSSL_PRE_1_1_API\n\treturn(&janus_dtls_bio_agent_methods);\n#else\n\treturn janus_dtls_bio_agent_methods;\n#endif\n}\n\nBIO *BIO_janus_dtls_agent_new(void *dtls) {\n\tBIO* bio = BIO_new(BIO_janus_dtls_agent_method());\n\tif(bio == NULL) {\n\t\treturn NULL;\n\t}\n#if JANUS_USE_OPENSSL_PRE_1_1_API\n\tbio->ptr = dtls;\n#else\n\tBIO_set_data(bio, dtls);\n#endif\n\treturn bio;\n}\n\nstatic int janus_dtls_bio_agent_new(BIO *bio) {\n#if JANUS_USE_OPENSSL_PRE_1_1_API\n\tbio->init = 1;\n\tbio->ptr = NULL;\n\tbio->flags = 0;\n#else\n\tBIO_set_init(bio, 1);\n\tBIO_set_data(bio, NULL);\n\tBIO_set_shutdown(bio, 0);\n#endif\n\treturn 1;\n}\n\nstatic int janus_dtls_bio_agent_free(BIO *bio) {\n\tif(bio == NULL) {\n\t\treturn 0;\n\t}\n#if JANUS_USE_OPENSSL_PRE_1_1_API\n\tbio->ptr = NULL;\n#else\n\tBIO_set_data(bio, NULL);\n#endif\n\treturn 1;\n}\n\nstatic int janus_dtls_bio_agent_write(BIO *bio, const char *in, int inl) {\n\tJANUS_LOG(LOG_HUGE, \"janus_dtls_bio_agent_write: %p, %d\\n\", in, inl);\n\t/* Forward data to the write BIO */\n\tif(inl <= 0) {\n\t\t/* ... unless the size is negative or zero */\n\t\tJANUS_LOG(LOG_WARN, \"janus_dtls_bio_agent_write failed: negative size (%d)\\n\", inl);\n\t\treturn inl;\n\t}\n\tjanus_dtls_srtp *dtls;\n#if JANUS_USE_OPENSSL_PRE_1_1_API\n\tdtls = (janus_dtls_srtp *)bio->ptr;\n#else\n\tdtls = (janus_dtls_srtp *)BIO_get_data(bio);\n#endif\n\tif(dtls == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No DTLS-SRTP stack, no DTLS bridge...\\n\");\n\t\treturn -1;\n\t}\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)dtls->pc;\n\tif(pc == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No WebRTC PeerConnection, no DTLS bridge...\\n\");\n\t\treturn -1;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle || !handle->agent || !dtls->write_bio) {\n\t\tJANUS_LOG(LOG_ERR, \"No handle/agent/bio, no DTLS bridge...\\n\");\n\t\treturn -1;\n\t}\n\n\tif(inl > 1500) {\n\t\t/* FIXME Just a warning for now, this will need to be solved with proper fragmentation */\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] The DTLS stack is trying to send a packet of %d bytes, this may be larger than the MTU and get dropped!\\n\", handle->handle_id, inl);\n\t}\n\tint bytes = nice_agent_send(handle->agent, pc->stream_id, pc->component_id, inl, in);\n\tif(bytes < inl) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error sending DTLS message on component %d of stream %d (%d)\\n\", handle->handle_id, pc->component_id, pc->stream_id, bytes);\n\t} else {\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] >> >> ... and sent %d of those bytes on the socket\\n\", handle->handle_id, bytes);\n\t}\n\t/* Update stats (TODO Do the same for the last second window as well)\n\t * FIXME: the Data stats includes the bytes used for the handshake */\n\tif(bytes > 0) {\n\t\tpc->dtls_out_stats.info[0].packets++;\n\t\tpc->dtls_out_stats.info[0].bytes += bytes;\n\t\t/* If there's a datachannel medium, update the stats there too */\n\t\tjanus_ice_peerconnection_medium *medium = g_hash_table_lookup(pc->media_bytype, GINT_TO_POINTER(JANUS_MEDIA_DATA));\n\t\tif(medium) {\n\t\t\tmedium->in_stats.info[0].packets++;\n\t\t\tmedium->in_stats.info[0].bytes += bytes;\n\t\t}\n\t}\n\treturn bytes;\n}\n\nstatic long janus_dtls_bio_agent_ctrl(BIO *bio, int cmd, long num, void *ptr) {\n\tswitch(cmd) {\n\t\tcase BIO_CTRL_FLUSH:\n\t\t\t/* The OpenSSL library needs this */\n\t\t\treturn 1;\n\t\tcase BIO_CTRL_DGRAM_QUERY_MTU:\n\t\t\t/* Let's force the MTU that was configured */\n\t\t\tJANUS_LOG(LOG_HUGE, \"Advertizing MTU: %d\\n\", mtu);\n\t\t\treturn mtu;\n\t\tcase BIO_CTRL_WPENDING:\n\t\tcase BIO_CTRL_PENDING:\n\t\t\treturn 0L;\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_HUGE, \"janus_dtls_bio_agent_ctrl: %d\\n\", cmd);\n\t}\n\treturn 0;\n}\n"
  },
  {
    "path": "src/dtls-bio.h",
    "content": "/*! \\file    dtls-bio.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    OpenSSL BIO agent writer\n * \\details  OpenSSL BIO that writes packets to a libnice agent.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_DTLS_BIO_H\n#define JANUS_DTLS_BIO_H\n\n#include <openssl/opensslv.h>\n#include <openssl/err.h>\n#include <openssl/ssl.h>\n\n/*! \\brief OpenSSL BIO agent writer initialization */\nint janus_dtls_bio_agent_init(void);\n\n/*! \\brief OpenSSL BIO agent writer constructor */\nBIO *BIO_janus_dtls_agent_new(void *dtls);\n\n/*! \\brief Set the MTU for the BIO agent writer\n * \\note The default starting MTU is 1472, in case fragmentation is needed\n * the OpenSSL DTLS stack automatically decreases it. That said, if\n * you know for sure the MTU in the network Janus is deployed in is\n * smaller than that, it makes sense to configure an according value to\n * start from\n * @param start_mtu The MTU to start from (1200 by default)\n */\nvoid janus_dtls_bio_agent_set_mtu(int start_mtu);\n/*! \\brief Return which MTU was configured for the BIO agent writer\n * @returns The MTU the stack will start from for each session */\nint janus_dtls_bio_agent_get_mtu(void);\n\n#if defined(LIBRESSL_VERSION_NUMBER)\n#define JANUS_USE_OPENSSL_PRE_1_1_API (LIBRESSL_VERSION_NUMBER < 0x30500000L)\n#else\n#define JANUS_USE_OPENSSL_PRE_1_1_API (OPENSSL_VERSION_NUMBER < 0x10100000L)\n#endif\n\n#endif\n"
  },
  {
    "path": "src/dtls.c",
    "content": "/*! \\file    dtls.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    DTLS/SRTP processing\n * \\details  Implementation (based on OpenSSL and libsrtp) of the DTLS/SRTP\n * transport. The code takes care of the DTLS handshake between peers and\n * the server, and sets the proper SRTP and SRTCP context up accordingly.\n * A DTLS alert from a peer is notified to the plugin handling him/her\n * by means of the hangup_media callback.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#include \"janus.h\"\n#include \"debug.h\"\n#include \"dtls.h\"\n#include \"rtcp.h\"\n#include \"events.h\"\n\n#include <openssl/err.h>\n#include <openssl/bn.h>\n#include <openssl/evp.h>\n#include <openssl/rsa.h>\n#include <openssl/asn1.h>\n\n\nconst gchar *janus_get_dtls_srtp_state(janus_dtls_state state) {\n\tswitch(state) {\n\t\tcase JANUS_DTLS_STATE_CREATED:\n\t\t\treturn \"created\";\n\t\tcase JANUS_DTLS_STATE_TRYING:\n\t\t\treturn \"trying\";\n\t\tcase JANUS_DTLS_STATE_CONNECTED:\n\t\t\treturn \"connected\";\n\t\tcase JANUS_DTLS_STATE_FAILED:\n\t\t\treturn \"failed\";\n\t\tdefault:\n\t\t\treturn NULL;\n\t}\n\treturn NULL;\n}\n\nconst gchar *janus_get_dtls_srtp_role(janus_dtls_role role) {\n\tswitch(role) {\n\t\tcase JANUS_DTLS_ROLE_ACTPASS:\n\t\t\treturn \"actpass\";\n\t\tcase JANUS_DTLS_ROLE_SERVER:\n\t\t\treturn \"passive\";\n\t\tcase JANUS_DTLS_ROLE_CLIENT:\n\t\t\treturn \"active\";\n\t\tdefault:\n\t\t\treturn NULL;\n\t}\n\treturn NULL;\n}\n\nconst gchar *janus_get_dtls_srtp_profile(int profile) {\n\tswitch(profile) {\n\t\tcase SRTP_AES128_CM_SHA1_80:\n\t\t\treturn \"SRTP_AES128_CM_SHA1_80\";\n\t\tcase SRTP_AES128_CM_SHA1_32:\n\t\t\treturn \"SRTP_AES128_CM_SHA1_32\";\n#ifdef HAVE_SRTP_AESGCM\n\t\tcase SRTP_AEAD_AES_256_GCM:\n\t\t\treturn \"SRTP_AEAD_AES_256_GCM\";\n\t\tcase SRTP_AEAD_AES_128_GCM:\n\t\t\treturn \"SRTP_AEAD_AES_128_GCM\";\n#endif\n\t\tdefault:\n\t\t\treturn NULL;\n\t}\n\treturn NULL;\n}\n\n/* Helper to notify DTLS state changes to the event handlers */\nstatic void janus_dtls_notify_state_change(janus_dtls_srtp *dtls) {\n\tif(!janus_events_is_enabled())\n\t\treturn;\n\tif(dtls == NULL)\n\t\treturn;\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)dtls->pc;\n\tif(pc == NULL)\n\t\treturn;\n\tjanus_ice_handle *handle = pc->handle;\n\tif(handle == NULL)\n\t\treturn;\n\tjanus_session *session = (janus_session *)handle->session;\n\tif(session == NULL)\n\t\treturn;\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"dtls\", json_string(janus_get_dtls_srtp_state(dtls->dtls_state)));\n\tjson_object_set_new(info, \"stream_id\", json_integer(pc->stream_id));\n\tjson_object_set_new(info, \"component_id\", json_integer(pc->component_id));\n\tjson_object_set_new(info, \"retransmissions\", json_integer(dtls->retransmissions));\n\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_WEBRTC, JANUS_EVENT_SUBTYPE_WEBRTC_DTLS,\n\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n}\n\ngboolean janus_is_dtls(char *buf) {\n\treturn ((*buf >= 20) && (*buf <= 64));\n}\n\n/* DTLS stuff */\n#define DTLS_DEFAULT_CIPHERS\t\"DEFAULT:!NULL:!aNULL:!SHA256:!SHA384:!aECDH:!AESGCM+AES256:!aPSK\"\nstatic const char *dtls_ciphers = DTLS_DEFAULT_CIPHERS;\n/* Duration for the self-generated certs: 1 year */\n#define DTLS_AUTOCERT_DURATION\t60*60*24*365\n/* NIST P-256 elliptic curve used for private key generation */\n#define DTLS_ELLIPTIC_CURVE NID_X9_62_prime256v1\n\n/* By default we always accept self-signed certificates (that's what almost\n * all of the existing WebRTC implementations do today), but if told so we\n * can be configured to reject them, and validate them instead */\nstatic gboolean dtls_selfsigned_certs_ok = TRUE;\ngboolean janus_dtls_are_selfsigned_certs_ok(void) {\n\treturn dtls_selfsigned_certs_ok;\n}\n\n/* DTLS timeout base to enforce: notice that this can currently only be\n * modified if you're using BoringSSL, as OpenSSL uses 1s (1000ms) and\n * that value cannot be modified (it will in OpenSSL v1.1.1) */\nstatic guint16 dtls_timeout_base = 1000;\n\nstatic SSL_CTX *ssl_ctx = NULL;\nstatic X509 *ssl_cert = NULL;\nstatic EVP_PKEY *ssl_key = NULL;\n\nstatic gchar local_fingerprint[160];\ngchar *janus_dtls_get_local_fingerprint(void) {\n\treturn (gchar *)local_fingerprint;\n}\n\n\n#if JANUS_USE_OPENSSL_PRE_1_1_API && !defined(HAVE_BORINGSSL)\n/*\n * DTLS locking stuff to make OpenSSL thread safe (not needed for 1.1.0)\n *\n * Note: this is an attempt to fix the infamous issue #316:\n * \t\thttps://github.com/meetecho/janus-gateway/issues/316\n * that is the \"tlsv1 alert decrypt error\" randomly happening when\n * doing handshakes that force Janus to be restarted (issue affecting\n * OpenSSL but NOT BoringSSL, apparently). The cause might be related\n * to race conditions, and in fact OpenSSL docs state that:\n *\n * \t\t\"OpenSSL can safely be used in multi-threaded applications\n * \t\tprovided that at least two callback functions are set,\n * \t\tlocking_function and threadid_func.\"\n *\n * See here for the whole docs:\n * \t\thttps://www.openssl.org/docs/manmaster/crypto/threads.html\n *\n * The fix proposed here is heavily derived from a discussion related to\n * RTPEngine:\n * \t\thttp://lists.sip-router.org/pipermail/sr-dev/2015-January/026860.html\n * where it was mentioned the issue was fixed in this commit:\n * \t\thttps://github.com/sipwise/rtpengine/commit/935487b66363c9932684d8085f47450d65a8c37e\n * which does indeed implement the callbacks the OpenSSL docs suggest.\n *\n */\nstatic pthread_mutex_t *janus_dtls_locks;\n\nstatic void janus_dtls_cb_openssl_threadid(CRYPTO_THREADID *tid) {\n\t/* FIXME Assuming pthread, which is fine as GLib wraps pthread and\n\t * so that's what we use anyway? */\n\tpthread_t me = pthread_self();\n\n\tif(sizeof(me) == sizeof(void *)) {\n\t\tCRYPTO_THREADID_set_pointer(tid, (void *) me);\n\t} else {\n\t\tCRYPTO_THREADID_set_numeric(tid, (unsigned long) me);\n\t}\n}\n\nstatic void janus_dtls_cb_openssl_lock(int mode, int type, const char *file, int line) {\n\tif((mode & CRYPTO_LOCK)) {\n\t\tpthread_mutex_lock(&janus_dtls_locks[type]);\n\t} else {\n\t\tpthread_mutex_unlock(&janus_dtls_locks[type]);\n\t}\n}\n#endif\n\n\nstatic int janus_dtls_generate_keys(X509 **certificate, EVP_PKEY **private_key, gboolean rsa_private_key) {\n\tstatic const int num_bits = 2048;\n/* OPENSSL_VERSION_MAJOR is defined only in OpenSSL >= 3 */\n#ifndef OPENSSL_VERSION_MAJOR\n\tBIGNUM *bne = NULL;\n\tRSA *rsa_key = NULL;\n\tEC_KEY *ecc_key = NULL;\n#endif\n\tX509_NAME *cert_name = NULL;\n\n\tJANUS_LOG(LOG_VERB, \"Generating DTLS key / cert\\n\");\n\n\tif(rsa_private_key) {\n#ifndef OPENSSL_VERSION_MAJOR\n\t\t/* Create a private key object (needed to hold the RSA key). */\n\t\t*private_key = EVP_PKEY_new();\n\t\tif(!*private_key) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EVP_PKEY_new() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Create a big number object. */\n\t\tbne = BN_new();\n\t\tif(!bne) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"BN_new() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\tif(!BN_set_word(bne, RSA_F4)) {  /* RSA_F4 == 65537 */\n\t\t\tJANUS_LOG(LOG_FATAL, \"BN_set_word() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Generate a RSA key. */\n\t\trsa_key = RSA_new();\n\t\tif(!rsa_key) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"RSA_new() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* This takes some time. */\n\t\tif(!RSA_generate_key_ex(rsa_key, num_bits, bne, NULL)) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"RSA_generate_key_ex() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\tif(!EVP_PKEY_assign_RSA(*private_key, rsa_key)) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EVP_PKEY_assign_RSA() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* The RSA key now belongs to the private key, so don't clean it up separately. */\n\t\trsa_key = NULL;\n#else\n\t\t*private_key = EVP_RSA_gen(num_bits);\n\t\tif(!*private_key) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EVP_RSA_gen() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n#endif\n\t} else {\n\t\t/* Create key with curve dictated by DTLS_ELLIPTIC_CURVE */\n#ifndef OPENSSL_VERSION_MAJOR\n\t\t*private_key = EVP_PKEY_new();\n\t\tif(!*private_key) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EVP_PKEY_new() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\tif((ecc_key = EC_KEY_new_by_curve_name(DTLS_ELLIPTIC_CURVE)) == NULL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EC_KEY_new_by_curve_name() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\tEC_KEY_set_asn1_flag(ecc_key, OPENSSL_EC_NAMED_CURVE);\n\n\t\t/* This takes some time. */\n\t\tif(EC_KEY_generate_key(ecc_key) == 0) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EC_KEY_generate_key() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\tif(EVP_PKEY_assign_EC_KEY(*private_key, ecc_key) == 0) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EVP_PKEY_assign_EC_KEY() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* The EC key now belongs to the private key, so don't clean it up separately. */\n\t\tecc_key = NULL;\n#else\n\t\t*private_key = EVP_EC_gen(\"prime256v1\");\n\t\tif(!*private_key) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"EVP_EC_gen() failed\\n\");\n\t\t\tgoto error;\n\t\t}\n#endif\n\t}\n\n\t/* Create the X509 certificate. */\n\t*certificate = X509_new();\n\tif(!*certificate) {\n\t\tJANUS_LOG(LOG_FATAL, \"X509_new() failed (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\tgoto error;\n\t}\n\n\t/* Set version 3 (note that 0 means version 1). */\n\tX509_set_version(*certificate, 2);\n\n\t/* Set serial number. */\n\tASN1_INTEGER_set(X509_get_serialNumber(*certificate), (long)g_random_int());\n\n\t/* Set valid period. */\n\tX509_gmtime_adj(X509_get_notBefore(*certificate), -1 * DTLS_AUTOCERT_DURATION);  /* -1 year */\n\tX509_gmtime_adj(X509_get_notAfter(*certificate), DTLS_AUTOCERT_DURATION);  /* 1 year */\n\n\t/* Set the public key for the certificate using the key. */\n\tif(!X509_set_pubkey(*certificate, *private_key)) {\n\t\tJANUS_LOG(LOG_FATAL, \"X509_set_pubkey() failed (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\tgoto error;\n\t}\n\n\t/* Set certificate fields. */\n\tcert_name = X509_get_subject_name(*certificate);\n\tif(!cert_name) {\n\t\tJANUS_LOG(LOG_FATAL, \"X509_get_subject_name() failed (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\tgoto error;\n\t}\n\tX509_NAME_add_entry_by_txt(cert_name, \"O\", MBSTRING_ASC, (const unsigned char*)\"Janus\", -1, -1, 0);\n\tX509_NAME_add_entry_by_txt(cert_name, \"CN\", MBSTRING_ASC, (const unsigned char*)\"Janus\", -1, -1, 0);\n\n\t/* It is self-signed so set the issuer name to be the same as the subject. */\n\tif(!X509_set_issuer_name(*certificate, cert_name)) {\n\t\tJANUS_LOG(LOG_FATAL, \"X509_set_issuer_name() failed (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\tgoto error;\n\t}\n\n\t/* Sign the certificate with the private key. */\n\tif(!X509_sign(*certificate, *private_key, EVP_sha256())) {\n\t\tJANUS_LOG(LOG_FATAL, \"X509_sign() failed (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\tgoto error;\n\t}\n\n\t/* Free stuff and resurn. */\n#ifndef OPENSSL_VERSION_MAJOR\n\tBN_free(bne);\n#endif\n\treturn 0;\n\nerror:\n#ifndef OPENSSL_VERSION_MAJOR\n\tif(bne)\n\t\tBN_free(bne);\n\tif(rsa_key && !*private_key)\n\t\tRSA_free(rsa_key);\n\tif(ecc_key && !*private_key)\n\t\tEC_KEY_free(ecc_key);\n#endif\n\tif(*private_key)\n\t\tEVP_PKEY_free(*private_key);  /* This also frees the RSA key. */\n\tif(*certificate)\n\t\tX509_free(*certificate);\n\treturn -1;\n}\n\n\nstatic int janus_dtls_load_keys(const char *server_pem, const char *server_key, const char *password,\n\t\tX509 **certificate, EVP_PKEY **private_key) {\n\tFILE *f = NULL;\n\n\tf = fopen(server_pem, \"r\");\n\tif(!f) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error opening certificate file (%s)\\n\", g_strerror(errno));\n\t\tgoto error;\n\t}\n\t*certificate = PEM_read_X509(f, NULL, NULL, NULL);\n\tif(!*certificate) {\n\t\tJANUS_LOG(LOG_FATAL, \"PEM_read_X509 failed\\n\");\n\t\tgoto error;\n\t}\n\tfclose(f);\n\n\tf = fopen(server_key, \"r\");\n\tif(!f) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error opening key file (%s)\\n\", g_strerror(errno));\n\t\tgoto error;\n\t}\n\t*private_key = PEM_read_PrivateKey(f, NULL, NULL, (void *)password);\n\tif(!*private_key) {\n\t\tJANUS_LOG(LOG_FATAL, \"PEM_read_PrivateKey failed\\n\");\n\t\tgoto error;\n\t}\n\tfclose(f);\n\n\treturn 0;\n\nerror:\n\tif(*certificate) {\n\t\tX509_free(*certificate);\n\t\t*certificate = NULL;\n\t}\n\tif(*private_key) {\n\t\tEVP_PKEY_free(*private_key);\n\t\t*private_key = NULL;\n\t}\n\treturn -1;\n}\n\n/* Versioning info ( */\nconst char *janus_get_ssl_version(void) {\n\treturn OPENSSL_VERSION_TEXT;\n}\n\n/* DTLS-SRTP initialization */\ngint janus_dtls_srtp_init(const char *server_pem, const char *server_key, const char *password,\n\t\tconst char *ciphers, guint16 timeout, gboolean rsa_private_key, gboolean accept_selfsigned) {\n\tconst char *crypto_lib = NULL;\n#if JANUS_USE_OPENSSL_PRE_1_1_API && !defined(HAVE_BORINGSSL)\n#if defined(LIBRESSL_VERSION_NUMBER)\n\tcrypto_lib = \"LibreSSL\";\n#else\n\tcrypto_lib = \"OpenSSL pre-1.1.0\";\n#endif\n\t/* First of all make OpenSSL thread safe (see note above on issue #316) */\n\tjanus_dtls_locks = g_malloc0(sizeof(*janus_dtls_locks) * CRYPTO_num_locks());\n\tint l=0;\n\tfor(l = 0; l < CRYPTO_num_locks(); l++) {\n\t\tpthread_mutex_init(&janus_dtls_locks[l], NULL);\n\t}\n\tCRYPTO_THREADID_set_callback(janus_dtls_cb_openssl_threadid);\n\tCRYPTO_set_locking_callback(janus_dtls_cb_openssl_lock);\n#else\n\tcrypto_lib = \"OpenSSL >= 1.1.0\";\n#endif\n#ifdef HAVE_BORINGSSL\n\tcrypto_lib = \"BoringSSL\";\n#endif\n\tJANUS_LOG(LOG_INFO, \"Crypto: %s\\n\", crypto_lib);\n#ifndef HAVE_SRTP_AESGCM\n\tJANUS_LOG(LOG_WARN, \"The libsrtp installation does not support AES-GCM profiles\\n\");\n#endif\n\n\t/* Go on and create the DTLS context */\n#if JANUS_USE_OPENSSL_PRE_1_1_API && !defined(HAVE_BORINGSSL)\n#if defined(LIBRESSL_VERSION_NUMBER)\n\tssl_ctx = SSL_CTX_new(DTLSv1_method());\n#else\n\tssl_ctx = SSL_CTX_new(DTLSv1_2_method());\n#endif\n#else\n\tssl_ctx = SSL_CTX_new(DTLS_method());\n#endif\n\tif(!ssl_ctx) {\n\t\tJANUS_LOG(LOG_FATAL, \"Ops, error creating DTLS context?\\n\");\n\t\treturn -1;\n\t}\n\tSSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, janus_dtls_verify_callback);\n\tSSL_CTX_set_tlsext_use_srtp(ssl_ctx,\n#ifdef HAVE_SRTP_AESGCM\n\t\t\"SRTP_AEAD_AES_256_GCM:SRTP_AEAD_AES_128_GCM:SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32\");\n#else\n\t\t\"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32\");\n#endif\n\n\tif(!server_pem && !server_key) {\n\t\tJANUS_LOG(LOG_INFO, \"No cert/key specified, autogenerating some...\\n\");\n\t\tif(janus_dtls_generate_keys(&ssl_cert, &ssl_key, rsa_private_key) != 0) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Error generating DTLS key/certificate\\n\");\n\t\t\treturn -2;\n\t\t}\n\t} else if(!server_pem || !server_key) {\n\t\tJANUS_LOG(LOG_FATAL, \"DTLS certificate and key must be specified\\n\");\n\t\treturn -2;\n\t} else if(janus_dtls_load_keys(server_pem, server_key, password, &ssl_cert, &ssl_key) != 0) {\n\t\treturn -3;\n\t}\n\n\tif(!SSL_CTX_use_certificate(ssl_ctx, ssl_cert)) {\n\t\tJANUS_LOG(LOG_FATAL, \"Certificate error (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\treturn -4;\n\t}\n\tif(!SSL_CTX_use_PrivateKey(ssl_ctx, ssl_key)) {\n\t\tJANUS_LOG(LOG_FATAL, \"Certificate key error (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\treturn -5;\n\t}\n\tif(!SSL_CTX_check_private_key(ssl_ctx)) {\n\t\tJANUS_LOG(LOG_FATAL, \"Certificate check error (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\treturn -6;\n\t}\n\tSSL_CTX_set_options(ssl_ctx, SSL_OP_NO_TICKET);\n\n\tunsigned int size;\n\tunsigned char fingerprint[EVP_MAX_MD_SIZE];\n\tif(X509_digest(ssl_cert, EVP_sha256(), (unsigned char *)fingerprint, &size) == 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error converting X509 structure (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\treturn -7;\n\t}\n\tchar *lfp = (char *)&local_fingerprint;\n\tunsigned int i = 0;\n\tfor(i = 0; i < size; i++) {\n\t\tg_snprintf(lfp, 4, \"%.2X:\", fingerprint[i]);\n\t\tlfp += 3;\n\t}\n\t*(lfp-1) = 0;\n\tJANUS_LOG(LOG_INFO, \"Fingerprint of our certificate: %s\\n\", local_fingerprint);\n\tif(ciphers)\n\t\tdtls_ciphers = ciphers;\n\tif(SSL_CTX_set_cipher_list(ssl_ctx, dtls_ciphers) == 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error setting cipher list (%s)\\n\", ERR_reason_error_string(ERR_get_error()));\n\t\treturn -8;\n\t}\n\n\tif(janus_dtls_bio_agent_init() < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error initializing BIO agent\\n\");\n\t\treturn -9;\n\t}\n\n\tdtls_timeout_base = timeout;\n#ifndef HAVE_BORINGSSL\n\tif(dtls_timeout_base != 1000) {\n\t\tJANUS_LOG(LOG_WARN, \"DTLS timeout set to %u ms, but not using BoringSSL: ignoring\\n\", timeout);\n\t}\n#endif\n\n\t/* Initialize libsrtp */\n\tif(srtp_init() != srtp_err_status_ok) {\n\t\tJANUS_LOG(LOG_FATAL, \"Ops, error setting up libsrtp?\\n\");\n\t\treturn -10;\n\t}\n\n\t/* Finally, let's set our policy with respect to DTLS self signed certificates */\n\tdtls_selfsigned_certs_ok = accept_selfsigned;\n\tif(!dtls_selfsigned_certs_ok) {\n\t\tJANUS_LOG(LOG_WARN, \"WebRTC PeerConnections with self-signed certificates will NOT be accepted\\n\");\n\t}\n\n\treturn 0;\n}\n\nstatic void janus_dtls_srtp_free(const janus_refcount *dtls_ref) {\n\tjanus_dtls_srtp *dtls = janus_refcount_containerof(dtls_ref, janus_dtls_srtp, ref);\n\t/* This stack can be destroyed, free all the resources */\n\tdtls->pc = NULL;\n\tif(dtls->ssl != NULL) {\n\t\tSSL_free(dtls->ssl);\n\t\tdtls->ssl = NULL;\n\t}\n\t/* BIOs are destroyed by SSL_free */\n\tdtls->read_bio = NULL;\n\tdtls->write_bio = NULL;\n\tif(dtls->srtp_valid) {\n\t\tif(dtls->srtp_in) {\n\t\t\tsrtp_dealloc(dtls->srtp_in);\n\t\t\tdtls->srtp_in = NULL;\n\t\t}\n\t\tif(dtls->srtp_out) {\n\t\t\tsrtp_dealloc(dtls->srtp_out);\n\t\t\tdtls->srtp_out = NULL;\n\t\t}\n\t\t/* FIXME What about dtls->remote_policy and dtls->local_policy? */\n\t}\n\tg_free(dtls);\n\tdtls = NULL;\n}\n\nvoid janus_dtls_srtp_cleanup(void) {\n\tif(ssl_cert != NULL) {\n\t\tX509_free(ssl_cert);\n\t\tssl_cert = NULL;\n\t}\n\tif(ssl_key != NULL) {\n\t\tEVP_PKEY_free(ssl_key);\n\t\tssl_key = NULL;\n\t}\n\tif(ssl_ctx != NULL) {\n\t\tSSL_CTX_free(ssl_ctx);\n\t\tssl_ctx = NULL;\n\t}\n#if JANUS_USE_OPENSSL_PRE_1_1_API && !defined(HAVE_BORINGSSL)\n\tfor(int l = 0; l < CRYPTO_num_locks(); l++) {\n\t\tpthread_mutex_destroy(&janus_dtls_locks[l]);\n\t}\n\tg_free(janus_dtls_locks);\n#endif\n}\n\n\njanus_dtls_srtp *janus_dtls_srtp_create(void *ice_pc, janus_dtls_role role) {\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)ice_pc;\n\tif(pc == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No WebRTC PeerConnection, no DTLS...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle || !handle->agent) {\n\t\tJANUS_LOG(LOG_ERR, \"No handle/agent, no DTLS...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_dtls_srtp *dtls = g_malloc0(sizeof(janus_dtls_srtp));\n\tg_atomic_int_set(&dtls->destroyed, 0);\n\tjanus_refcount_init(&dtls->ref, janus_dtls_srtp_free);\n\t/* Create SSL context, at last */\n\tdtls->srtp_valid = 0;\n\tdtls->ssl = SSL_new(ssl_ctx);\n\tif(!dtls->ssl) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     Error creating DTLS session! (%s)\\n\",\n\t\t\thandle->handle_id, ERR_reason_error_string(ERR_get_error()));\n\t\tjanus_refcount_decrease(&dtls->ref);\n\t\treturn NULL;\n\t}\n\tSSL_set_ex_data(dtls->ssl, 0, dtls);\n\tSSL_set_info_callback(dtls->ssl, janus_dtls_callback);\n\tdtls->read_bio = BIO_new(BIO_s_mem());\n\tif(!dtls->read_bio) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]   Error creating read BIO! (%s)\\n\",\n\t\t\thandle->handle_id, ERR_reason_error_string(ERR_get_error()));\n\t\tjanus_refcount_decrease(&dtls->ref);\n\t\treturn NULL;\n\t}\n\tBIO_set_mem_eof_return(dtls->read_bio, -1);\n\tdtls->write_bio = BIO_janus_dtls_agent_new(dtls);\n\tif(!dtls->write_bio) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]   Error creating write BIO! (%s)\\n\",\n\t\t\thandle->handle_id, ERR_reason_error_string(ERR_get_error()));\n\t\tjanus_refcount_decrease(&dtls->ref);\n\t\treturn NULL;\n\t}\n\tSSL_set_bio(dtls->ssl, dtls->read_bio, dtls->write_bio);\n\t/* The role may change later, depending on the negotiation */\n\tdtls->dtls_role = role;\n\t/* https://code.google.com/p/chromium/issues/detail?id=406458\n\t * Specify an ECDH group for ECDHE ciphers, otherwise they cannot be\n\t * negotiated when acting as the server. Use NIST's P-256 which is\n\t * commonly supported.\n\t */\n#ifndef OPENSSL_VERSION_MAJOR\n\tEC_KEY *ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);\n\tif(ecdh == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]   Error creating ECDH group! (%s)\\n\",\n\t\t\thandle->handle_id, ERR_reason_error_string(ERR_get_error()));\n\t\tjanus_refcount_decrease(&dtls->ref);\n\t\treturn NULL;\n\t}\n\tSSL_set_tmp_ecdh(dtls->ssl, ecdh);\n\tEC_KEY_free(ecdh);\n#else\n\tint grp_list[1] = { NID_X9_62_prime256v1 };\n\tSSL_set1_groups(dtls->ssl, grp_list, 1);\n#endif\n\tconst long flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | SSL_OP_SINGLE_ECDH_USE;\n\tSSL_set_options(dtls->ssl, flags);\n#ifdef HAVE_DTLS_SETTIMEOUT\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Setting DTLS initial timeout: %\"SCNu16\"ms\\n\", handle->handle_id, dtls_timeout_base);\n\tDTLSv1_set_initial_timeout_duration(dtls->ssl, dtls_timeout_base);\n#endif\n\tdtls->ready = 0;\n\tdtls->retransmissions = 0;\n#ifdef HAVE_SCTP\n\tdtls->sctp = NULL;\n#endif\n\t/* Done */\n\tdtls->dtls_connected = 0;\n\tdtls->pc = pc;\n\treturn dtls;\n}\n\nvoid janus_dtls_srtp_handshake(janus_dtls_srtp *dtls) {\n\tif(dtls == NULL || dtls->ssl == NULL)\n\t\treturn;\n\tif(dtls->dtls_state == JANUS_DTLS_STATE_CREATED) {\n\t\t/* Starting the handshake now: enforce the role */\n\t\tdtls->dtls_started = janus_get_monotonic_time();\n\t\tif(dtls->dtls_role == JANUS_DTLS_ROLE_CLIENT) {\n\t\t\tSSL_set_connect_state(dtls->ssl);\n\t\t} else {\n\t\t\tSSL_set_accept_state(dtls->ssl);\n\t\t}\n\t\tdtls->dtls_state = JANUS_DTLS_STATE_TRYING;\n\t}\n\tSSL_do_handshake(dtls->ssl);\n\n\t/* Notify event handlers */\n\tjanus_dtls_notify_state_change(dtls);\n}\n\nint janus_dtls_srtp_create_sctp(janus_dtls_srtp *dtls) {\n#ifdef HAVE_SCTP\n\tif(dtls == NULL)\n\t\treturn -1;\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)dtls->pc;\n\tif(pc == NULL)\n\t\treturn -2;\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle || !handle->agent)\n\t\treturn -4;\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn -5;\n\tdtls->sctp = janus_sctp_association_create(dtls, handle, 5000);\n\tif(dtls->sctp == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error creating SCTP association...\\n\", handle->handle_id);\n\t\treturn -6;\n\t}\n\treturn 0;\n#else\n\t/* Support for datachannels hasn't been built in */\n\treturn -1;\n#endif\n}\n\nvoid janus_dtls_srtp_incoming_msg(janus_dtls_srtp *dtls, char *buf, uint16_t len) {\n\tif(dtls == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No DTLS-SRTP stack, no incoming message...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)dtls->pc;\n\tif(pc == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No WebRTC PeerConnection, no DTLS...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle || !handle->agent) {\n\t\tJANUS_LOG(LOG_ERR, \"No handle/agent, no DTLS...\\n\");\n\t\treturn;\n\t}\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)) {\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Alert already triggered, clearing up...\\n\", handle->handle_id);\n\t\treturn;\n\t}\n\tif(!dtls->ssl || !dtls->read_bio) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No DTLS stuff for component %d in stream %d??\\n\", handle->handle_id, pc->component_id, pc->stream_id);\n\t\treturn;\n\t}\n\tif(dtls->dtls_started == 0) {\n\t\t/* Handshake not started yet: maybe we're still waiting for the answer and the DTLS role? */\n\t\treturn;\n\t}\n\tint written = BIO_write(dtls->read_bio, buf, len);\n\tif(written != len) {\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]     Only written %d/%d of those bytes on the read BIO...\\n\", handle->handle_id, written, len);\n\t} else {\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]     Written %d bytes on the read BIO...\\n\", handle->handle_id, written);\n\t}\n\t/* Try to read data */\n\tchar data[1500];\t/* FIXME */\n\tint read = SSL_read(dtls->ssl, &data, 1500);\n\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]     ... and read %d of them from SSL...\\n\", handle->handle_id, read);\n\tif(read < 0) {\n\t\tunsigned long err = SSL_get_error(dtls->ssl, read);\n\t\tif(err == SSL_ERROR_SSL) {\n\t\t\t/* Ops, something went wrong with the DTLS handshake */\n\t\t\tchar error[200];\n\t\t\tERR_error_string_n(ERR_get_error(), error, 200);\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Handshake error: %s\\n\", handle->handle_id, error);\n\t\t\treturn;\n\t\t}\n\t}\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) || janus_is_stopping()) {\n\t\t/* DTLS alert triggered, we should end it here */\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Forced to stop it here...\\n\", handle->handle_id);\n\t\treturn;\n\t}\n\tif(!SSL_is_init_finished(dtls->ssl)) {\n\t\t/* Nothing else to do for now */\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Initialization not finished yet...\\n\", handle->handle_id);\n\t\treturn;\n\t}\n\tif(dtls->ready) {\n\t\t/* There's data to be read? */\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Any data available?\\n\", handle->handle_id);\n#ifdef HAVE_SCTP\n\t\tif(dtls->sctp != NULL && read > 0) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Sending data (%d bytes) to the SCTP stack...\\n\", handle->handle_id, read);\n\t\t\tjanus_sctp_data_from_dtls(dtls->sctp, data, read);\n\t\t}\n#else\n\t\tif(read > 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Data available but Data Channels support disabled...\\n\", handle->handle_id);\n\t\t}\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] DTLS established, yay!\\n\", handle->handle_id);\n\t\t/* Check the remote fingerprint */\n\t\tX509 *rcert = SSL_get_peer_certificate(dtls->ssl);\n\t\tif(!rcert) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No remote certificate?? (%s)\\n\",\n\t\t\t\thandle->handle_id, ERR_reason_error_string(ERR_get_error()));\n\t\t} else {\n\t\t\tunsigned int rsize;\n\t\t\tunsigned char rfingerprint[EVP_MAX_MD_SIZE];\n\t\t\tchar remote_fingerprint[160];\n\t\t\tchar *rfp = (char *)&remote_fingerprint;\n\t\t\tif(pc->remote_hashing && !strcasecmp(pc->remote_hashing, \"sha-1\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Computing sha-1 fingerprint of remote certificate...\\n\", handle->handle_id);\n\t\t\t\tX509_digest(rcert, EVP_sha1(), (unsigned char *)rfingerprint, &rsize);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Computing sha-256 fingerprint of remote certificate...\\n\", handle->handle_id);\n\t\t\t\tX509_digest(rcert, EVP_sha256(), (unsigned char *)rfingerprint, &rsize);\n\t\t\t}\n\t\t\tX509_free(rcert);\n\t\t\trcert = NULL;\n\t\t\tunsigned int i = 0;\n\t\t\tfor(i = 0; i < rsize; i++) {\n\t\t\t\tg_snprintf(rfp, 4, \"%.2X:\", rfingerprint[i]);\n\t\t\t\trfp += 3;\n\t\t\t}\n\t\t\t*(rfp-1) = 0;\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Remote fingerprint (%s) of the client is %s\\n\",\n\t\t\t\thandle->handle_id, pc->remote_hashing ? pc->remote_hashing : \"sha-256\", remote_fingerprint);\n\t\t\tif(!strcasecmp(remote_fingerprint, pc->remote_fingerprint ? pc->remote_fingerprint : \"(none)\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  Fingerprint is a match!\\n\", handle->handle_id);\n\t\t\t\tdtls->dtls_state = JANUS_DTLS_STATE_CONNECTED;\n\t\t\t\tdtls->dtls_connected = janus_get_monotonic_time();\n\t\t\t\t/* Notify event handlers */\n\t\t\t\tjanus_dtls_notify_state_change(dtls);\n\t\t\t} else {\n\t\t\t\t/* FIXME NOT a match! MITM? */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]  Fingerprint is NOT a match! got %s, expected %s\\n\", handle->handle_id, remote_fingerprint, pc->remote_fingerprint);\n\t\t\t\tdtls->dtls_state = JANUS_DTLS_STATE_FAILED;\n\t\t\t\t/* Notify event handlers */\n\t\t\t\tjanus_dtls_notify_state_change(dtls);\n\t\t\t\tgoto done;\n\t\t\t}\n\t\t\tif(dtls->dtls_state == JANUS_DTLS_STATE_CONNECTED) {\n\t\t\t\t/* Which SRTP profile is being negotiated? */\n\t\t\t\tconst SRTP_PROTECTION_PROFILE *srtp_profile = SSL_get_selected_srtp_profile(dtls->ssl);\n\t\t\t\tif(srtp_profile == NULL) {\n\t\t\t\t\t/* Should never happen, but just in case... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No SRTP profile selected...\\n\", handle->handle_id);\n\t\t\t\t\tdtls->dtls_state = JANUS_DTLS_STATE_FAILED;\n\t\t\t\t\t/* Notify event handlers */\n\t\t\t\t\tjanus_dtls_notify_state_change(dtls);\n\t\t\t\t\tgoto done;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] %s\\n\", handle->handle_id, srtp_profile->name);\n\t\t\t\tint key_length = 0, salt_length = 0, master_length = 0;\n\t\t\t\tswitch(srtp_profile->id) {\n\t\t\t\t\tcase SRTP_AES128_CM_SHA1_80:\n\t\t\t\t\tcase SRTP_AES128_CM_SHA1_32:\n\t\t\t\t\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\t\t\t\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\t\t\t\t\tmaster_length = SRTP_MASTER_LENGTH;\n\t\t\t\t\t\tbreak;\n#ifdef HAVE_SRTP_AESGCM\n\t\t\t\t\tcase SRTP_AEAD_AES_256_GCM:\n\t\t\t\t\t\tkey_length = SRTP_AESGCM256_MASTER_KEY_LENGTH;\n\t\t\t\t\t\tsalt_length = SRTP_AESGCM256_MASTER_SALT_LENGTH;\n\t\t\t\t\t\tmaster_length = SRTP_AESGCM256_MASTER_LENGTH;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase SRTP_AEAD_AES_128_GCM:\n\t\t\t\t\t\tkey_length = SRTP_AESGCM128_MASTER_KEY_LENGTH;\n\t\t\t\t\t\tsalt_length = SRTP_AESGCM128_MASTER_SALT_LENGTH;\n\t\t\t\t\t\tmaster_length = SRTP_AESGCM128_MASTER_LENGTH;\n\t\t\t\t\t\tbreak;\n#endif\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t/* Will never happen? */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unsupported SRTP profile %lu\\n\", handle->handle_id, srtp_profile->id);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Key/Salt/Master: %d/%d/%d\\n\",\n\t\t\t\t\thandle->handle_id, master_length, key_length, salt_length);\n\t\t\t\t/* Complete with SRTP setup */\n\t\t\t\tunsigned char material[master_length*2];\n\t\t\t\tunsigned char *local_key, *local_salt, *remote_key, *remote_salt;\n\t\t\t\t/* Export keying material for SRTP */\n\t\t\t\tif(!SSL_export_keying_material(dtls->ssl, material, master_length*2, \"EXTRACTOR-dtls_srtp\", 19, NULL, 0, 0)) {\n\t\t\t\t\t/* Oops... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Oops, couldn't extract SRTP keying material for component %d in stream %d?? (%s)\\n\",\n\t\t\t\t\t\thandle->handle_id, pc->component_id, pc->stream_id, ERR_reason_error_string(ERR_get_error()));\n\t\t\t\t\tgoto done;\n\t\t\t\t}\n\t\t\t\t/* Key derivation (http://tools.ietf.org/html/rfc5764#section-4.2) */\n\t\t\t\tif(dtls->dtls_role == JANUS_DTLS_ROLE_CLIENT) {\n\t\t\t\t\tlocal_key = material;\n\t\t\t\t\tremote_key = local_key + key_length;\n\t\t\t\t\tlocal_salt = remote_key + key_length;\n\t\t\t\t\tremote_salt = local_salt + salt_length;\n\t\t\t\t} else {\n\t\t\t\t\tremote_key = material;\n\t\t\t\t\tlocal_key = remote_key + key_length;\n\t\t\t\t\tremote_salt = local_key + key_length;\n\t\t\t\t\tlocal_salt = remote_salt + salt_length;\n\t\t\t\t}\n\t\t\t\t/* Build master keys and set SRTP policies */\n\t\t\t\t\t/* Remote (inbound) */\n\t\t\t\tswitch(srtp_profile->id) {\n\t\t\t\t\tcase SRTP_AES128_CM_SHA1_80:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->remote_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->remote_policy.rtcp));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase SRTP_AES128_CM_SHA1_32:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(dtls->remote_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->remote_policy.rtcp));\n\t\t\t\t\t\tbreak;\n#ifdef HAVE_SRTP_AESGCM\n\t\t\t\t\tcase SRTP_AEAD_AES_256_GCM:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->remote_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->remote_policy.rtcp));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase SRTP_AEAD_AES_128_GCM:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->remote_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->remote_policy.rtcp));\n\t\t\t\t\t\tbreak;\n#endif\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t/* Will never happen? */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unsupported SRTP profile %s\\n\", handle->handle_id, srtp_profile->name);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tdtls->remote_policy.ssrc.type = ssrc_any_inbound;\n\t\t\t\tunsigned char remote_policy_key[master_length];\n\t\t\t\tdtls->remote_policy.key = (unsigned char *)&remote_policy_key;\n\t\t\t\tmemcpy(dtls->remote_policy.key, remote_key, key_length);\n\t\t\t\tmemcpy(dtls->remote_policy.key + key_length, remote_salt, salt_length);\n#if HAS_DTLS_WINDOW_SIZE\n\t\t\t\tdtls->remote_policy.window_size = 128;\n\t\t\t\tdtls->remote_policy.allow_repeat_tx = 0;\n#endif\n\t\t\t\tdtls->remote_policy.next = NULL;\n\t\t\t\t\t/* Local (outbound) */\n\t\t\t\tswitch(srtp_profile->id) {\n\t\t\t\t\tcase SRTP_AES128_CM_SHA1_80:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->local_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->local_policy.rtcp));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase SRTP_AES128_CM_SHA1_32:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(dtls->local_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->local_policy.rtcp));\n\t\t\t\t\t\tbreak;\n#ifdef HAVE_SRTP_AESGCM\n\t\t\t\t\tcase SRTP_AEAD_AES_256_GCM:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->local_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->local_policy.rtcp));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase SRTP_AEAD_AES_128_GCM:\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->local_policy.rtp));\n\t\t\t\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->local_policy.rtcp));\n\t\t\t\t\t\tbreak;\n#endif\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t/* Will never happen? */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unsupported SRTP profile %s\\n\", handle->handle_id, srtp_profile->name);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tdtls->local_policy.ssrc.type = ssrc_any_outbound;\n\t\t\t\tunsigned char local_policy_key[master_length];\n\t\t\t\tdtls->local_policy.key = (unsigned char *)&local_policy_key;\n\t\t\t\tmemcpy(dtls->local_policy.key, local_key, key_length);\n\t\t\t\tmemcpy(dtls->local_policy.key + key_length, local_salt, salt_length);\n#if HAS_DTLS_WINDOW_SIZE\n\t\t\t\tdtls->local_policy.window_size = 128;\n\t\t\t\tdtls->local_policy.allow_repeat_tx = 0;\n#endif\n\t\t\t\tdtls->local_policy.next = NULL;\n\t\t\t\t/* Create SRTP sessions */\n\t\t\t\tsrtp_err_status_t res = srtp_create(&(dtls->srtp_in), &(dtls->remote_policy));\n\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t/* Something went wrong... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Oops, error creating inbound SRTP session for component %d in stream %d??\\n\", handle->handle_id, pc->component_id, pc->stream_id);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]  -- %d (%s)\\n\", handle->handle_id, res, janus_srtp_error_str(res));\n\t\t\t\t\tgoto done;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Created inbound SRTP session for component %d in stream %d\\n\", handle->handle_id, pc->component_id, pc->stream_id);\n\t\t\t\tres = srtp_create(&(dtls->srtp_out), &(dtls->local_policy));\n\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t/* Something went wrong... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Oops, error creating outbound SRTP session for component %d in stream %d??\\n\", handle->handle_id, pc->component_id, pc->stream_id);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]  -- %d (%s)\\n\", handle->handle_id, res, janus_srtp_error_str(res));\n\t\t\t\t\tgoto done;\n\t\t\t\t}\n\t\t\t\tdtls->srtp_profile = srtp_profile->id;\n\t\t\t\tdtls->srtp_valid = 1;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Created outbound SRTP session for component %d in stream %d\\n\", handle->handle_id, pc->component_id, pc->stream_id);\n#ifdef HAVE_SCTP\n\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)) {\n\t\t\t\t\t/* Create SCTP association as well */\n\t\t\t\t\tjanus_dtls_srtp_create_sctp(dtls);\n\t\t\t\t}\n#endif\n\t\t\t\tdtls->ready = 1;\n\t\t\t}\ndone:\n\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && dtls->srtp_valid) {\n\t\t\t\t/* Handshake successfully completed */\n\t\t\t\tjanus_ice_dtls_handshake_done(handle);\n\t\t\t} else {\n\t\t\t\t/* Something went wrong in either DTLS or SRTP... tell the plugin about it */\n\t\t\t\tjanus_dtls_callback(dtls->ssl, SSL_CB_ALERT, 0);\n\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid janus_dtls_srtp_send_alert(janus_dtls_srtp *dtls) {\n\tif(!dtls)\n\t\treturn;\n\t/* Send alert */\n\tjanus_refcount_increase(&dtls->ref);\n\tif(dtls != NULL && dtls->ssl != NULL) {\n\t\tSSL_shutdown(dtls->ssl);\n\t}\n\tjanus_refcount_decrease(&dtls->ref);\n}\n\nvoid janus_dtls_srtp_destroy(janus_dtls_srtp *dtls) {\n\tif(!dtls || !g_atomic_int_compare_and_exchange(&dtls->destroyed, 0, 1))\n\t\treturn;\n\tdtls->ready = 0;\n\tdtls->retransmissions = 0;\n#ifdef HAVE_SCTP\n\t/* Destroy the SCTP association if this is a DataChannel */\n\tif(dtls->sctp != NULL) {\n\t\tjanus_sctp_association_destroy(dtls->sctp);\n\t\tdtls->sctp = NULL;\n\t}\n#endif\n\tjanus_refcount_decrease(&dtls->ref);\n}\n\n/* DTLS alert callback */\nvoid janus_dtls_callback(const SSL *ssl, int where, int ret) {\n\t/* We only care about alerts */\n\tif(!(where & SSL_CB_ALERT)) {\n\t\treturn;\n\t}\n\tjanus_dtls_srtp *dtls = SSL_get_ex_data(ssl, 0);\n\tif(!dtls) {\n\t\tJANUS_LOG(LOG_ERR, \"No DTLS session related to this alert...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_peerconnection *pc = dtls->pc;\n\tif(pc == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No WebRTC PeerConnection related to this alert...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle) {\n\t\tJANUS_LOG(LOG_ERR, \"No ICE handle related to this alert...\\n\");\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] DTLS alert triggered on stream %u (component %u), closing...\\n\", handle->handle_id, pc->stream_id, pc->component_id);\n\tjanus_ice_webrtc_hangup(handle, \"DTLS alert\");\n}\n\n/* DTLS certificate verification callback */\nint janus_dtls_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {\n\t/* We just use the verify_callback to request a certificate from the client */\n\tint err = X509_STORE_CTX_get_error(ctx);\n\tif(err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT || err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) {\n\t\t/* Self signed certificate: by default we always accept it */\n\t\tif(!dtls_selfsigned_certs_ok) {\n\t\t\t/* ... unless we're enforcing validation */\n\t\t\treturn 0;\n\t\t}\n\t}\n\t/* We always reject expired certificates, even when self-signed */\n\tif(err == X509_V_ERR_CERT_HAS_EXPIRED)\n\t\treturn 0;\n\t/* Return a success if we're ok with self-signed, the result of the validation otherwise */\n\treturn dtls_selfsigned_certs_ok ? 1 : (err == X509_V_OK);\n}\n\n#ifdef HAVE_SCTP\nvoid janus_dtls_sctp_data_ready(janus_dtls_srtp *dtls) {\n\tif(dtls == NULL)\n\t\treturn;\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)dtls->pc;\n\tif(pc == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No WebRTC PeerConnection...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle || !handle->agent || !dtls->write_bio) {\n\t\tJANUS_LOG(LOG_ERR, \"No handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_notify_data_ready(handle);\n}\n\nvoid janus_dtls_wrap_sctp_data(janus_dtls_srtp *dtls, char *label, char *protocol, gboolean textdata, char *buf, int len) {\n\tif(dtls == NULL || !dtls->ready || dtls->sctp == NULL || buf == NULL || len < 1)\n\t\treturn;\n\tjanus_refcount_increase(&dtls->sctp->ref);\n\tjanus_sctp_send_data(dtls->sctp, label, protocol, textdata, buf, len);\n\tjanus_refcount_decrease(&dtls->sctp->ref);\n}\n\nint janus_dtls_send_sctp_data(janus_dtls_srtp *dtls, char *buf, int len) {\n\tif(dtls == NULL || !dtls->ready || buf == NULL || len < 1)\n\t\treturn -1;\n\tint res = SSL_write(dtls->ssl, buf, len);\n\tif(res <= 0) {\n\t\tunsigned long err = SSL_get_error(dtls->ssl, res);\n\t\tJANUS_LOG(LOG_ERR, \"Error sending data: %s\\n\", ERR_reason_error_string(err));\n\t}\n\treturn res;\n}\n\nvoid janus_dtls_notify_sctp_data(janus_dtls_srtp *dtls, char *label, char *protocol, gboolean textdata, char *buf, int len) {\n\tif(dtls == NULL || buf == NULL || len < 1)\n\t\treturn;\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)dtls->pc;\n\tif(pc == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No WebRTC PeerConnection...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle || !handle->agent || !dtls->write_bio) {\n\t\tJANUS_LOG(LOG_ERR, \"No handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_incoming_data(handle, label, protocol, textdata, buf, len);\n}\n#endif\n\ngboolean janus_dtls_retry(gpointer stack) {\n\tjanus_dtls_srtp *dtls = (janus_dtls_srtp *)stack;\n\tif(dtls == NULL)\n\t\treturn FALSE;\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)dtls->pc;\n\tif(pc == NULL)\n\t\treturn FALSE;\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle)\n\t\tgoto stoptimer;\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP))\n\t\tgoto stoptimer;\n\tif(dtls->dtls_state == JANUS_DTLS_STATE_CONNECTED) {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] DTLS already set up, disabling retransmission timer!\\n\", handle->handle_id);\n\t\tgoto stoptimer;\n\t}\n\tif(janus_get_monotonic_time() - dtls->dtls_started >= 20*G_USEC_PER_SEC) {\n\t\t/* FIXME Should we really give up after 20 seconds waiting for DTLS? */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] DTLS taking too much time for component %d in stream %d...\\n\",\n\t\t\thandle->handle_id, pc->component_id, pc->stream_id);\n\t\tjanus_ice_webrtc_hangup(handle, \"DTLS timeout\");\n\t\tgoto stoptimer;\n\t}\n\tstruct timeval timeout = {0};\n\tif(DTLSv1_get_timeout(dtls->ssl, &timeout) == 0) {\n\t\t/* failed to get timeout. try again on next iter */\n\t\treturn TRUE;\n\t}\n\tguint64 timeout_value = timeout.tv_sec*1000 + timeout.tv_usec/1000;\n\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] DTLSv1_get_timeout: %\"SCNu64\"\\n\", handle->handle_id, timeout_value);\n\tif(timeout_value == 0) {\n\t\tdtls->retransmissions++;\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] DTLS timeout on component %d of stream %d, retransmitting\\n\", handle->handle_id, pc->component_id, pc->stream_id);\n\t\t/* Notify event handlers */\n\t\tjanus_dtls_notify_state_change(dtls);\n\t\t/* Retransmit the packet */\n\t\tint res = DTLSv1_handle_timeout(dtls->ssl);\n\t\tif(res == -1 && SSL_get_error(dtls->ssl, res) != SSL_ERROR_WANT_WRITE) {\n\t\t\t/* DTLSv1_handle_timeout returned an unrecoverable error, fail right away\n\t\t\t * Ref.: https://webrtc-review.googlesource.com/c/src/+/260100 */\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] DTLSv1_handle_timeout failed...\\n\", handle->handle_id);\n\t\t\tjanus_ice_webrtc_hangup(handle, \"DTLS error\");\n\t\t\tgoto stoptimer;\n\t\t}\n\t}\n\treturn TRUE;\n\nstoptimer:\n\tif(pc->dtlsrt_source != NULL) {\n\t\tg_source_destroy(pc->dtlsrt_source);\n\t\tg_source_unref(pc->dtlsrt_source);\n\t\tpc->dtlsrt_source = NULL;\n\t}\n\treturn FALSE;\n}\n"
  },
  {
    "path": "src/dtls.h",
    "content": "/*! \\file    dtls.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    DTLS/SRTP processing (headers)\n * \\details  Implementation (based on OpenSSL and libsrtp) of the DTLS/SRTP\n * transport. The code takes care of the DTLS handshake between peers and\n * the server, and sets the proper SRTP and SRTCP context up accordingly.\n * A DTLS alert from a peer is notified to the plugin handling him/her\n * by means of the hangup_media callback.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_DTLS_H\n#define JANUS_DTLS_H\n\n#include <inttypes.h>\n#include <glib.h>\n\n#include \"rtp.h\"\n#include \"rtpsrtp.h\"\n#include \"sctp.h\"\n#include \"refcount.h\"\n#include \"dtls-bio.h\"\n\n/*! \\brief Helper method to return info on the crypto library and its version\n * @returns A pointer to a static string with the version */\nconst char *janus_get_ssl_version(void);\n\n/*! \\brief DTLS stuff initialization\n * @param[in] server_pem Path to the certificate to use\n * @param[in] server_key Path to the key to use\n * @param[in] password Password needed to use the key, if any\n * @param[in] ciphers DTLS ciphers to use (will use hardcoded defaults, if NULL)\n * @param[in] timeout DTLS timeout base, in ms, to use for retransmissions (ignored if not using BoringSSL)\n * @param[in] rsa_private_key Whether RSA certificates should be generated, instead of NIST P-256\n * @param[in] accept_selfsigned Whether to accept self-signed certificates (default) or enforce validation\n * @returns 0 in case of success, a negative integer on errors */\ngint janus_dtls_srtp_init(const char *server_pem, const char *server_key, const char *password,\n\tconst char *ciphers, guint16 timeout, gboolean rsa_private_key, gboolean accept_selfsigned);\n/*! \\brief Method to cleanup DTLS stuff before exiting */\nvoid janus_dtls_srtp_cleanup(void);\n/*! \\brief Method to return a string representation (SHA-256) of the certificate fingerprint */\ngchar *janus_dtls_get_local_fingerprint(void);\n/*! \\brief Method to check whether DTLS self-signed certificates are ok (default) or not */\ngboolean janus_dtls_are_selfsigned_certs_ok(void);\n\n\n/*! \\brief DTLS roles */\ntypedef enum janus_dtls_role {\n\tJANUS_DTLS_ROLE_ACTPASS = -1,\n\tJANUS_DTLS_ROLE_SERVER,\n\tJANUS_DTLS_ROLE_CLIENT,\n} janus_dtls_role;\n\n/*! \\brief DTLS state */\ntypedef enum janus_dtls_state {\n\tJANUS_DTLS_STATE_FAILED = -1,\n\tJANUS_DTLS_STATE_CREATED,\n\tJANUS_DTLS_STATE_TRYING,\n\tJANUS_DTLS_STATE_CONNECTED,\n} janus_dtls_state;\n\n/*! \\brief Janus DTLS-SRTP handle */\ntypedef struct janus_dtls_srtp {\n\t/*! \\brief Opaque pointer to the WebRTC PeerConnection this DTLS-SRTP context belongs to */\n\tvoid *pc;\n\t/*! \\brief DTLS role of the server for this stream: 1=client, 0=server */\n\tjanus_dtls_role dtls_role;\n\t/*! \\brief DTLS state of this component: -1=failed, 0=nothing, 1=trying, 2=connected */\n\tjanus_dtls_state dtls_state;\n\t/*! \\brief Monotonic time of when the DTLS handshake has started */\n\tgint64 dtls_started;\n\t/*! \\brief Monotonic time of when the DTLS state has switched to connected */\n\tgint64 dtls_connected;\n\t/*! \\brief SSL context used for DTLS for this component */\n\tSSL *ssl;\n\t/*! \\brief Read BIO (incoming DTLS data) */\n\tBIO *read_bio;\n\t/*! \\brief Write BIO (outgoing DTLS data) */\n\tBIO *write_bio;\n\t/*! \\brief Whether SRTP has been correctly set up for this component or not */\n\tgint srtp_valid;\n\t/*! \\brief The SRTP profile currently in use */\n\tgint srtp_profile;\n\t/*! \\brief libsrtp context for incoming SRTP packets */\n\tsrtp_t srtp_in;\n\t/*! \\brief libsrtp context for outgoing SRTP packets */\n\tsrtp_t srtp_out;\n\t/*! \\brief libsrtp policy for incoming SRTP packets */\n\tsrtp_policy_t remote_policy;\n\t/*! \\brief libsrtp policy for outgoing SRTP packets */\n\tsrtp_policy_t local_policy;\n\t/*! \\brief Whether this DTLS stack is now ready to be used for messages as well (e.g., SCTP encapsulation) */\n\tint ready;\n\t/*! \\brief The number of retransmissions that have occurred for this DTLS instance so far */\n\tint retransmissions;\n#ifdef HAVE_SCTP\n\t/*! \\brief SCTP association, if DataChannels are involved */\n\tjanus_sctp_association *sctp;\n#endif\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_dtls_srtp;\n\n\n/*! \\brief Create a janus_dtls_srtp instance\n * @param[in] pc Opaque pointer to the WebRTC PeerConnection owning the stack\n * @param[in] role The role of the DTLS stack (client/server)\n * @returns A new janus_dtls_srtp instance if successful, NULL otherwise */\njanus_dtls_srtp *janus_dtls_srtp_create(void *pc, janus_dtls_role role);\n/*! \\brief Start a DTLS handshake\n * @param[in] dtls The janus_dtls_srtp instance to start the handshake on */\nvoid janus_dtls_srtp_handshake(janus_dtls_srtp *dtls);\n/*! \\brief Create an SCTP association, for data channels\n * \\note This is a separate method as, with renegotiations, it might happen\n * that data channels are not created right away, right after the DTLS\n * handshake has been completed, but only later, when DTLS is already up\n * @param[in] dtls The janus_dtls_srtp instance to setup SCTP on\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_dtls_srtp_create_sctp(janus_dtls_srtp *dtls);\n/*! \\brief Handle an incoming DTLS message\n * @param[in] dtls The janus_dtls_srtp instance to start the handshake on\n * @param[in] buf The DTLS message data\n * @param[in] len The DTLS message data length */\nvoid janus_dtls_srtp_incoming_msg(janus_dtls_srtp *dtls, char *buf, uint16_t len);\n/*! \\brief Send an alert on a janus_dtls_srtp instance\n * @param[in] dtls The janus_dtls_srtp instance to send the alert on */\nvoid janus_dtls_srtp_send_alert(janus_dtls_srtp *dtls);\n/*! \\brief Destroy a janus_dtls_srtp instance\n * @param[in] dtls The janus_dtls_srtp instance to destroy */\nvoid janus_dtls_srtp_destroy(janus_dtls_srtp *dtls);\n\n/*! \\brief DTLS alert callback (http://www.openssl.org/docs/ssl/SSL_CTX_set_info_callback.html)\n * @param[in] ssl SSL instance where the alert occurred\n * @param[in] where The context where the event occurred\n * @param[in] ret The error code */\nvoid janus_dtls_callback(const SSL *ssl, int where, int ret);\n\n/*! \\brief DTLS certificate verification callback (http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html)\n * \\details This method always returns 1 (true), in order not to fail when a certificate verification is requested. This is especially needed because all certificates used for DTLS in WebRTC are self signed, and as such a formal verification would fail.\n * @param[in] preverify_ok Whether the verification of the certificate was passed\n * @param[in] ctx context used for the certificate verification */\nint janus_dtls_verify_callback(int preverify_ok, X509_STORE_CTX *ctx);\n\n#ifdef HAVE_SCTP\n/*! \\brief Callback (called from the SCTP stack) that SCTP data (DataChannel) can be sent\n * @param[in] dtls The janus_dtls_srtp instance to use */\nvoid janus_dtls_sctp_data_ready(janus_dtls_srtp *dtls);\n\n/*! \\brief Callback (called from the ICE handle) to encapsulate in DTLS outgoing SCTP data (DataChannel)\n * @param[in] dtls The janus_dtls_srtp instance to use\n * @param[in] label The label of the data channel to use\n * @param[in] protocol The protocol of the data channel to use\n * @param[in] textdata Whether the buffer is text (domstring) or binary data\n * @param[in] buf The data buffer to encapsulate\n * @param[in] len The data length */\nvoid janus_dtls_wrap_sctp_data(janus_dtls_srtp *dtls, char *label, char *protocol, gboolean textdata, char *buf, int len);\n\n/*! \\brief Callback (called from the SCTP stack) to encapsulate in DTLS outgoing SCTP data (DataChannel)\n * @param[in] dtls The janus_dtls_srtp instance to use\n * @param[in] buf The data buffer to encapsulate\n * @param[in] len The data length\n * @returns The number of sent bytes in case of success, 0 or a negative integer otherwise */\nint janus_dtls_send_sctp_data(janus_dtls_srtp *dtls, char *buf, int len);\n\n/*! \\brief Callback to be notified about incoming SCTP data (DataChannel) to forward to the handle\n * @param[in] dtls The janus_dtls_srtp instance to use\n * @param[in] label The label of the data channel the message is from\n * @param[in] protocol The protocol of the data channel the message is from\n * @param[in] textdata Whether the buffer is text (domstring) or binary data\n * @param[in] buf The data buffer\n * @param[in] len The data length */\nvoid janus_dtls_notify_sctp_data(janus_dtls_srtp *dtls, char *label, char *protocol, gboolean textdata, char *buf, int len);\n#endif\n\n/*! \\brief DTLS retransmission timer\n * \\details As libnice is going to actually send and receive data, OpenSSL cannot handle retransmissions by itself: this timed callback (g_source_set_callback) deals with this.\n * @param[in] stack Opaque pointer to the janus_dtls_srtp instance to use\n * @returns true if a retransmission is still needed, false otherwise */\ngboolean janus_dtls_retry(gpointer stack);\n\n/*! \\brief Helper method to get a string representation of a Janus DTLS state\n * @param[in] state The Janus DTLS state\n * @returns A string representation of the state */\nconst gchar *janus_get_dtls_srtp_state(janus_dtls_state state);\n\n/*! \\brief Helper method to get a string representation of a DTLS role\n * @param[in] role The DTLS role\n * @returns A string representation of the role */\nconst gchar *janus_get_dtls_srtp_role(janus_dtls_role role);\n\n/*! \\brief Helper method to get a string representation of an SRTP profile\n * @param[in] profile The SRTP profile as exported by a DTLS-SRTP handshake\n * @returns A string representation of the profile */\nconst gchar *janus_get_dtls_srtp_profile(int profile);\n\n/*! \\brief Helper method to demultiplex DTLS from other protocols\n * @param[in] buf Buffer to inspect */\ngboolean janus_is_dtls(char *buf);\n\n#endif\n"
  },
  {
    "path": "src/events/eventhandler.h",
    "content": "/*! \\file   eventhandler.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Modular Janus event handlers (headers)\n * \\details  This header contains the definition of the callbacks all\n * the event handlers need to implement to interact with the Janus core.\n * In fact, an event handler is basically a module that receives\n * notifications from the Janus core and plugins about things happening,\n * together with more or less detailed information that may be relevant.\n * This may include WebRTC related events (e.g., a PeerConnection going\n * up or down, media stopping or resuming, etc.), events related to media,\n * or custom events plugins may originate on their own (e.g., a participant\n * publishing their media in a conference, or a SIP call starting). What\n * to do with these events is then up to the handler: it may choose to store\n * them somewhere (e.g., a database), analyse and process them, or simply\n * send them to an external tool for statistics purposes or troubleshooting.\n * Whatever the aim, the structures to make the interaction between core\n * and event handlers possible are defined here.\n *\n * An event handler plugin that wants to register at the Janus core needs to\n * implement the \\c janus_eventhandler interface. This includes callbacks\n * the Janus core can use to pass and request information, and a mask of\n * the events the plugin is interested in subscribing to. Besides, as an\n * event handler plugin is a shared object, and as such external to the\n * core itself, in order to be dynamically loaded at startup it needs\n * to implement the \\c create_e() hook as well, that should return a\n * pointer to the plugin instance. This is an example of such a step:\n *\n\\verbatim\nstatic janus_eventhandler myhandler = {\n\t[..]\n};\n\njanus_eventhandler *create(void) {\n\tJANUS_LOG(LOG_VERB, , \"%s created!\\n\", MY_HANDLER_NAME);\n\treturn &myhandler;\n}\n\\endverbatim\n *\n * This will make sure that your event handler plugin is loaded at startup\n * by the Janus core, if it is deployed in the proper folder.\n *\n * As anticipated and described in the above example, an event handler plugin\n * must basically be an instance of the \\c janus_eventhandler type. As such,\n * it must implement the following methods and callbacks for the core:\n *\n * - \\c init(): this is called by the Janus core as soon as your event handler\n * plugin is started; this is where you should setup your event handler plugin\n * (e.g., static stuff and reading the configuration file);\n * - \\c destroy(): on the other hand, this is called by the core when it\n * is shutting down, and your event handler plugin should too;\n * - \\c get_api_compatibility(): this method MUST return JANUS_EVENTHANDLER_API_VERSION;\n * - \\c get_version(): this method should return a numeric version identifier (e.g., 3);\n * - \\c get_version_string(): this method should return a verbose version identifier (e.g., \"v1.0.1\");\n * - \\c get_description(): this method should return a verbose description of your event handler plugin (e.g., \"This is an event handler that saves some events on a database\");\n * - \\c get_name(): this method should return a short display name for your event handler plugin (e.g., \"My Amazing Event Handler\");\n * - \\c get_package(): this method should return a unique package identifier for your event handler plugin (e.g., \"janus.eventhandler.myeventhandler\");\n * - \\c incoming_event(): this callback informs the event handler that an event is available for consumption.\n *\n * All the above methods and callbacks are mandatory: the Janus core will\n * reject an event handler plugin that doesn't implement any of the\n * mandatory callbacks.\n *\n * Additionally, a \\c janus_eventhandler instance must also include a\n * mask of the events it is interested in, a \\c events_mask janus_flag\n * object that must refer to the available types defined in this header.\n * The core, in fact, will refer to that mask to check whether your event\n * handler is interested in a specific event or not.\n *\n * Unlike other kind of modules (transports, plugins), the \\c init() method\n * here only passes the path to the configurations files folder, as event\n * handlers never need to contact the Janus core themselves. This path can be used to read and\n * parse a configuration file for the event handler plugin: the event handler\n * plugins we made available out of the box use the package name as a\n * name for the file (e.g., \\c janus.eventhandler.fake.jcfg for the sample\n * event handler plugin), but you're free to use a different one, as long\n * as it doesn't collide with existing ones. Besides, the existing eventhandler\n * plugins use the same libconfig format for configuration files the core\n * uses (relying on the \\c janus_config helpers for the purpose) but\n * again, if you prefer a different format (XML, JSON, etc.) that's up to you.\n *\n * \\ingroup eventhandlerapi\n * \\ref eventhandlerapi\n */\n\n#ifndef JANUS_EVENTHANDLER_H\n#define JANUS_EVENTHANDLER_H\n\n#include <stdlib.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n#include <ctype.h>\n#include <unistd.h>\n#include <inttypes.h>\n\n#include <glib.h>\n#include <jansson.h>\n\n#include \"../utils.h\"\n\n\n/*! \\brief Version of the API, to match the one event handler plugins were compiled against */\n#define JANUS_EVENTHANDLER_API_VERSION\t3\n\n/*! \\brief Initialization of all event handler plugin properties to NULL\n *\n * \\note All event handler plugins MUST add this as the FIRST line when initializing\n * their event handler plugin structure, e.g.:\n *\n\\verbatim\nstatic janus_eventhandler janus_fake_eventhandler_plugin =\n\t{\n\t\tJANUS_EVENTHANDLER_INIT,\n\n\t\t.init = janus_fake_init,\n\t\t[..]\n\\endverbatim\n */\n\n/** @name Type of events Janus could notify, and the handler subscribe to\n * @details This mask makes it easy to subscribe to, and unsubscribe from,\n * specific events, as all you need to do is to use janus_flags_set and\n * janus_flags_clear on the \\c events_mask property of the handler instance,\n * and the core will know whether you care about something or not.\n */\n///@{\n/*! \\brief No event */\n#define JANUS_EVENT_TYPE_NONE\t\t\t(0)\n/*! \\brief Session related events (e.g., session created/destroyed, etc.) */\n#define JANUS_EVENT_TYPE_SESSION\t\t(1 << 0)\n/*! \\brief Handle related events (e.g., handle attached/detached, etc.) */\n#define JANUS_EVENT_TYPE_HANDLE\t\t\t(1 << 1)\n/*! \\brief External events originated via Admin API (e.g., custom events coming from external scripts) */\n#define JANUS_EVENT_TYPE_EXTERNAL\t\t(1 << 2)\n/*! \\brief JSEP related events (e.g., got/sent offer/answer) */\n#define JANUS_EVENT_TYPE_JSEP\t\t\t(1 << 3)\n/*! \\brief WebRTC related events (e.g., PeerConnection up/down, ICE updates, DTLS updates, etc.) */\n#define JANUS_EVENT_TYPE_WEBRTC\t\t\t(1 << 4)\n/*! \\brief Media related events (e.g., media started/stopped flowing, stats on packets/bytes, etc.) */\n#define JANUS_EVENT_TYPE_MEDIA\t\t\t(1 << 5)\n/*! \\brief Events originated by plugins (at the moment, all of them, no way to pick) */\n#define JANUS_EVENT_TYPE_PLUGIN\t\t\t(1 << 6)\n/*! \\brief Events originated by transports (at the moment, all of them, no way to pick) */\n#define JANUS_EVENT_TYPE_TRANSPORT\t\t(1 << 7)\n/*! \\brief Events originated by the core for its own events (e.g., Janus starting/shutting down) */\n#define JANUS_EVENT_TYPE_CORE\t\t\t(1 << 8)\n\t/* TODO Others? */\n/*! \\brief Mask with all events enabled (shortcut when you want to subscribe to everything) */\n#define JANUS_EVENT_TYPE_ALL\t\t(0xffffffff)\n///@}\n\n/** @name Subtype of event types Janus could notify\n * @details Some events, like JANUS_EVENT_TYPE_WEBRTC, don't have a uniform syntax:\n * an event related to a new local candidate looks very different from an event\n * related to, e.g., a selected pair or a DTLS state, all of which belong to the\n * same category of \\c webrtc type events. In order to simplify the management of\n * events in strongly typed languages, events can contain a \\c subtype property\n * as well: this property is optional, because not all event types need this finer\n * grain of detail. At the time of writing, subtypes are only available for\n * JANUS_EVENT_TYPE_CORE (\"core\"), JANUS_EVENT_TYPE_WEBRTC (\"webrtc\") and\n * JANUS_EVENT_TYPE_MEDIA (\"media\") types.\n * @note Unlike the type, subtypes are not a mask: as a consequence, you cannot\n * filter subtypes using the Event Handler plugin API, only types. Besides,\n * there can be overlaps between subtypes related to\n */\n///@{\n/*! \\brief No subtype */\n#define JANUS_EVENT_SUBTYPE_NONE\t\t\t0\n/*! \\brief Core event subtypes: startup */\n#define JANUS_EVENT_SUBTYPE_CORE_STARTUP\t1\n/*! \\brief Core event subtypes: shutdown */\n#define JANUS_EVENT_SUBTYPE_CORE_SHUTDOWN\t2\n/*! \\brief WebRTC event subtypes: ICE state */\n#define JANUS_EVENT_SUBTYPE_WEBRTC_ICE\t\t1\n/*! \\brief WebRTC event subtypes: local candidate */\n#define JANUS_EVENT_SUBTYPE_WEBRTC_LCAND\t2\n/*! \\brief WebRTC event subtypes: remote candidate */\n#define JANUS_EVENT_SUBTYPE_WEBRTC_RCAND\t3\n/*! \\brief WebRTC event subtypes: selected pair */\n#define JANUS_EVENT_SUBTYPE_WEBRTC_PAIR\t\t4\n/*! \\brief WebRTC event subtypes: DTLS state */\n#define JANUS_EVENT_SUBTYPE_WEBRTC_DTLS\t\t5\n/*! \\brief WebRTC event subtypes: PeerConnection state */\n#define JANUS_EVENT_SUBTYPE_WEBRTC_STATE\t6\n/*! \\brief Media event subtypes: media state */\n#define JANUS_EVENT_SUBTYPE_MEDIA_STATE\t\t1\n/*! \\brief Media event subtypes: slow link */\n#define JANUS_EVENT_SUBTYPE_MEDIA_SLOWLINK\t2\n/*! \\brief Media event subtypes: stats */\n#define JANUS_EVENT_SUBTYPE_MEDIA_STATS\t\t3\n///@}\n\n#define JANUS_EVENTHANDLER_INIT(...) {\t\t\t\\\n\t\t.init = NULL,\t\t\t\t\t\t\t\\\n\t\t.destroy = NULL,\t\t\t\t\t\t\\\n\t\t.get_api_compatibility = NULL,\t\t\t\\\n\t\t.get_version = NULL,\t\t\t\t\t\\\n\t\t.get_version_string = NULL,\t\t\t\t\\\n\t\t.get_description = NULL,\t\t\t\t\\\n\t\t.get_name = NULL,\t\t\t\t\t\t\\\n\t\t.get_author = NULL,\t\t\t\t\t\t\\\n\t\t.get_package = NULL,\t\t\t\t\t\\\n\t\t.incoming_event = NULL,\t\t\t\t\t\\\n\t\t.events_mask = JANUS_EVENT_TYPE_NONE,\t\\\n\t\t## __VA_ARGS__ }\n\n\n/*! \\brief The event handler plugin session and callbacks interface */\ntypedef struct janus_eventhandler janus_eventhandler;\n\n\n/*! \\brief The event handler plugin session and callbacks interface */\nstruct janus_eventhandler {\n\t/*! \\brief Event handler plugin initialization/constructor\n\t * @param[in] config_path Path of the folder where the configuration for this event handler plugin can be found\n\t * @returns 0 in case of success, a negative integer in case of error */\n\tint (* const init)(const char *config_path);\n\t/*! \\brief Event handler plugin deinitialization/destructor */\n\tvoid (* const destroy)(void);\n\n\t/*! \\brief Informative method to request the API version this event handler plugin was compiled against\n\t *  \\note All event handler plugins MUST implement this method and return JANUS_EVENTHANDLER_API_VERSION\n\t * to make this work, or they will be rejected by the core. */\n\tint (* const get_api_compatibility)(void);\n\t/*! \\brief Informative method to request the numeric version of the event handler plugin */\n\tint (* const get_version)(void);\n\t/*! \\brief Informative method to request the string version of the event handler plugin */\n\tconst char *(* const get_version_string)(void);\n\t/*! \\brief Informative method to request a description of the event handler plugin */\n\tconst char *(* const get_description)(void);\n\t/*! \\brief Informative method to request the name of the event handler plugin */\n\tconst char *(* const get_name)(void);\n\t/*! \\brief Informative method to request the author of the event handler plugin */\n\tconst char *(* const get_author)(void);\n\t/*! \\brief Informative method to request the package name of the event handler plugin (what will be used in web applications to refer to it) */\n\tconst char *(* const get_package)(void);\n\n\t/*! \\brief Method to notify the event handler plugin that a new event is available\n\t * \\details All events are notified as a Jansson json_t object, and the syntax of\n\t * the associated JSON document is as follows:\n\t * \\verbatim\n\t{\n\t\t\"type\" : <numeric event type identifier>,\n\t\t\"timestamp\" : <monotonic time of when the event was generated>,\n\t\t\"session_id\" : <unique session identifier>,\n\t\t\"handle_id\" : <unique handle identifier, if provided/available>,\n\t\t\"event\" : {\n\t\t\t<event body, custom depending on event type>\n\t\t}\n\t}\n\t * \\endverbatim\n\t * \\note Do NOT handle the event directly in this method. Janus sends events from its\n\t * working threads, and so you'd most likely end up slowing it down. Just take note of it\n\t * and handle it somewhere else. It's your responsibility to \\c json_decref the event\n\t * object once you're done with it: a failure to do so will result in memory leaks.\n\t * @param[in] event Jansson object containing the event details */\n\tvoid (* const incoming_event)(json_t *event);\n\n\t/*! \\brief Method to send a request to this specific event handler plugin\n\t * \\details The method takes a Jansson json_t, that contains all the info related\n\t * to the request. This object will come from an Admin API request, and is\n\t * meant to represent a synchronous request. Since each handler can have\n\t * its own bells and whistles, there's no constraint on what this object should\n\t * contain, which is entirely handler specific. A json_t object needs to be\n\t * returned as a response, which will be sent in response to the Admin API call.\n\t * This can be useful to tweak settings in real-time, or to probe the internals\n\t * of the handler plugin for monitoring purposes.\n\t * @param[in] request Jansson object containing the request\n\t * @returns A Jansson object containing the response for the client */\n\tjson_t *(* const handle_request)(json_t *request);\n\n\t/*! \\brief Mask of events this handler is interested in, as a janus_flags object */\n\tjanus_flags events_mask;\n};\n\n/*! \\brief The hook that event handler plugins need to implement to be created from the Janus core */\ntypedef janus_eventhandler* create_e(void);\n\n#endif\n"
  },
  {
    "path": "src/events/janus_gelfevh.c",
    "content": "/*! \\file   janus_gelfevh.c\n * \\author Mirko Brankovic <mirkobrankovic@gmail.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus GelfEventHandler plugin\n * \\details  This is a GELF event handler plugin for Janus, which is supposed\n * to send json events to GELF\n * (Graylog logger https://docs.graylog.org/en/3.2/pages/gelf.html).\n * Necessary headers are prepended.\n * For sending, you can use TCP which is not recommended in case there will be\n * a lot of messages. There is also UDP support, but you need to limit the payload\n * size with max_message_len and remember to leave room for 12 bytes for special\n * headers. UDP messages will be chunked automatically.\n * There is also compression available for UDP protocol, to save network bandwidth\n * while using a bit more CPU. This is not available for TCP due to GELF limitations\n *\n * \\ingroup eventhandlers\n * \\ref eventhandlers\n */\n\n#include \"eventhandler.h\"\n\n#include <math.h>\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n#include \"../events.h\"\n#include <netdb.h>\n#include <errno.h>\n#include <arpa/inet.h>\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include \"../ip-utils.h\"\n\n/* Plugin information */\n#define JANUS_GELFEVH_VERSION\t\t\t1\n#define JANUS_GELFEVH_VERSION_STRING \t\"0.0.1\"\n#define JANUS_GELFEVH_DESCRIPTION \t\t\"This is event handler plugin for Janus, which forwards events via TCP/UDP to GELF server.\"\n#define JANUS_GELFEVH_NAME \t\t\t\t\"JANUS GelfEventHandler plugin\"\n#define JANUS_GELFEVH_AUTHOR \t\t\t\"Mirko Brankovic <mirkobrankovic@gmail.com>\"\n#define JANUS_GELFEVH_PACKAGE\t\t\t\"janus.eventhandler.gelfevh\"\n\n#define MAX_GELF_CHUNKS \t\t\t\t128\n\n/* Plugin methods */\njanus_eventhandler *create(void);\nint janus_gelfevh_init(const char *config_path);\nvoid janus_gelfevh_destroy(void);\nint janus_gelfevh_get_api_compatibility(void);\nint janus_gelfevh_get_version(void);\nconst char *janus_gelfevh_get_version_string(void);\nconst char *janus_gelfevh_get_description(void);\nconst char *janus_gelfevh_get_name(void);\nconst char *janus_gelfevh_get_author(void);\nconst char *janus_gelfevh_get_package(void);\nvoid janus_gelfevh_incoming_event(json_t *event);\njson_t *janus_gelfevh_handle_request(json_t *request);\n\n/* Event handler setup */\nstatic janus_eventhandler janus_gelfevh =\n\tJANUS_EVENTHANDLER_INIT (\n\t\t.init = janus_gelfevh_init,\n\t\t.destroy = janus_gelfevh_destroy,\n\n\t\t.get_api_compatibility = janus_gelfevh_get_api_compatibility,\n\t\t.get_version = janus_gelfevh_get_version,\n\t\t.get_version_string = janus_gelfevh_get_version_string,\n\t\t.get_description = janus_gelfevh_get_description,\n\t\t.get_name = janus_gelfevh_get_name,\n\t\t.get_author = janus_gelfevh_get_author,\n\t\t.get_package = janus_gelfevh_get_package,\n\n\t\t.incoming_event = janus_gelfevh_incoming_event,\n\t\t.handle_request = janus_gelfevh_handle_request,\n\n\t\t.events_mask = JANUS_EVENT_TYPE_NONE\n\t);\n\n/* Plugin creator */\njanus_eventhandler *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_GELFEVH_NAME);\n\treturn &janus_gelfevh;\n}\n\n/* Compression, if any */\nstatic gboolean compress = FALSE;\nstatic int compression = 6; /* Z_DEFAULT_COMPRESSION */\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic GThread *handler_thread;\nstatic void *janus_gelfevh_handler(void *data);\nstatic janus_mutex evh_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Queue of events to handle */\nstatic GAsyncQueue *events = NULL;\nstatic json_t exit_event;\nstatic void janus_gelfevh_event_free(json_t *event) {\n\tif(!event || event == &exit_event)\n\t\treturn;\n\tjson_decref(event);\n}\n\n/* GELF backend to send the events to */\nstatic char *backend = NULL;\nstatic char *port = NULL;\n\ntypedef enum janus_gelfevh_socket_type {\n\tJANUS_GELFEVH_SOCKET_TYPE_TCP = 1,\n\tJANUS_GELFEVH_SOCKET_TYPE_UDP = 2\n} janus_gelfevh_socket_type;\n\nstatic int max_gelf_msg_len = 500;\nstatic int sockfd;\n/* Set TCP as Default transport */\nstatic janus_gelfevh_socket_type transport = JANUS_GELFEVH_SOCKET_TYPE_UDP;\n\n/* Parameter validation (for tweaking via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}};\nstatic struct janus_json_parameter tweak_parameters[] = {\n\t{\"events\", JSON_STRING, 0},\n\t{\"backend\", JSON_STRING, 0},\n\t{\"port\", JSON_STRING, 0},\n\t{\"max_gelf_msg_len\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"janus_gelfevh_socket_type\", JSON_STRING, 0}\n};\n/* Error codes (for the tweaking via Admin API */\n#define JANUS_GELFEVH_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_GELFEVH_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_GELFEVH_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_GELFEVH_ERROR_UNKNOWN_ERROR\t\t499\n\n/* Plugin implementation */\nstatic char *randstring(size_t length) {\n\tstatic char charset[] = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n\tchar *randomString = NULL;\n\tint n;\n\tif(length) {\n\t\trandomString = g_malloc(sizeof(char) * (length + 1));\n\t\tif(randomString) {\n\t\t\tfor(n = 0; n < (int)length; n++) {\n\t\t\t\tint key = rand() % (int)(sizeof(charset) - 1);\n\t\t\t\trandomString[n] = charset[key];\n\t\t\t}\n\t\t\trandomString[length] = '\\0';\n\t\t}\n\t}\n\treturn randomString;\n}\n\nstatic int janus_gelfevh_connect(void) {\n\tstruct addrinfo *res = NULL;\n\tjanus_network_address addr;\n\tjanus_network_address_string_buffer addr_buf;\n\tstruct sockaddr_in servaddr;\n\n\tif(getaddrinfo(backend, NULL, NULL, &res) != 0 ||\n\t\t\t\tjanus_network_address_from_sockaddr(res->ai_addr, &addr) != 0 ||\n\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) != 0) {\n\t\tif(res)\n\t\t\tfreeaddrinfo(res);\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s): %d (%s)\\n\", backend, errno, g_strerror(errno));\n\t\treturn -1;\n\t}\n\tchar *host = g_strdup(janus_network_address_string_from_buffer(&addr_buf));\n\tfreeaddrinfo(res);\n\n\tif((sockfd = socket(AF_INET, transport, 0)) < 0 ) {\n\t\tJANUS_LOG(LOG_ERR, \"Socket creation failed: %d (%s)\\n\", errno, g_strerror(errno));\n\t\tg_free(host);\n\t\treturn -1;\n\t}\n\n\tmemset(&servaddr, 0, sizeof(servaddr));\n\n\tservaddr.sin_family = AF_INET;\n\tservaddr.sin_port = htons(atoi(port));\n\tservaddr.sin_addr.s_addr = inet_addr(host);\n\n\tif(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Connect to GELF host failed\\n\");\n\t\tg_free(host);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"Connected to GELF backend: [%s:%s]\\n\", host, port);\n\tg_free(host);\n\treturn 0;\n}\n\nstatic char compressed_text[8192];\nstatic int janus_gelfevh_send(char *message) {\n\tif(!message) {\n\t\tJANUS_LOG(LOG_WARN, \"Message is NULL, not sending to GELF!\\n\");\n\t\treturn -1;\n\t}\n\tif(transport == JANUS_GELFEVH_SOCKET_TYPE_TCP) {\n\t\t/* TCP */\n\t\tint out_bytes = 0;\n\t\tint length = strlen(message) + 1;\n\t\tchar *buffer = message;\n\t\twhile(length > 0) {\n\t\t\tout_bytes = send(sockfd, buffer, length, 0);\n\t\t\tif(out_bytes <= 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Sending TCP message failed, dropping event: %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\tclose(sockfd);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tbuffer += out_bytes;\n\t\t\tlength -= out_bytes;\n\t\t}\n\t} else {\n\t\t/* UDP chunking with headers. Check if we need to compress the data */\n\t\tint len = strlen(message);\n\t\tchar *buf = message;\n\t\tif(compress) {\n\t\t\tsize_t compressed_len = 0;\n\t\t\tcompressed_len = janus_gzip_compress(compression,\n\t\t\t\tmessage, strlen(message),\n\t\t\t\tcompressed_text, sizeof(compressed_text));\n\t\t\tif(compressed_len == 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Failed to compress event (%zu bytes), sending message uncompressed\\n\", strlen(message));\n\t\t\t\t/* Sending message uncompressed */\n\t\t\t} else {\n\t\t\t\tlen = compressed_len;\n\t\t\t\tbuf = compressed_text;\n\t\t\t}\n\t\t}\n\n\t\tint total = len / max_gelf_msg_len + 1;\n\t\tif(total > MAX_GELF_CHUNKS) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Event not sent! GELF allows %d number of chunks, try increasing max_gelf_msg_len\\n\", MAX_GELF_CHUNKS);\n\t\t\treturn -1;\n\t\t}\n\t\t/* Do we need to chunk the message */\n\t\tif(total == 1) {\n\t\t\tint n = send(sockfd, buf, len, 0);\n\t\t\tif(n < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Sending UDP message failed, dropping event: %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\treturn 0;\n\t\t} else {\n\t\t\tint offset = 0;\n\t\t\tchar *rnd = randstring(8);\n\t\t\tint i;\n\t\t\tfor(i = 0; i < total; i++) {\n\t\t\t\tint bytesToSend = ((offset + max_gelf_msg_len) < len) ? max_gelf_msg_len : (len - offset);\n\t\t\t\t/* Prepend the necessary headers (imitate TCP) */\n\t\t\t\tchar chunk[bytesToSend + 12];\n\t\t\t\tchunk[0] = 0x1e;\n\t\t\t\tchunk[1] = 0x0f;\n\t\t\t\tmemcpy(chunk + 2, rnd, 8);\n\t\t\t\tchunk[10] = (char)i;\n\t\t\t\tchunk[11] = (char)total;\n\t\t\t\tchar *head = chunk;\n\t\t\t\tmemcpy(head+12, buf, bytesToSend);\n\t\t\t\tbuf += bytesToSend;\n\t\t\t\tint n = send(sockfd, head, bytesToSend + 12, 0);\n\t\t\t\tif(n < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Sending UDP message failed: %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\t\tg_free(rnd);\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t\toffset += bytesToSend;\n\t\t\t\tmemset(chunk, 0, sizeof chunk);\n\t\t\t}\n\t\t\tg_free(rnd);\n\t\t}\n\t}\n\treturn 0;\n}\n\nint janus_gelfevh_init(const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tgboolean enabled = FALSE;\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_GELFEVH_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_GELFEVH_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_GELFEVH_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\t/* Handle configuration */\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\n\t\t/* Setup the sample event handler, if required */\n\t\tjanus_config_item *item;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"GELF event handler disabled (Janus API)\\n\");\n\t\t\tgoto done;\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"backend\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Missing or invalid backend\\n\");\n\t\t\tgoto done;\n\t\t}\n\t\tbackend = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"port\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Missing or invalid port\\n\");\n\t\t\tgoto done;\n\t\t}\n\t\tport = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"protocol\");\n\t\tif(item && item->value) {\n\t\t\tif(strcasecmp(item->value, \"udp\") == 0) {\n\t\t\t\ttransport = JANUS_GELFEVH_SOCKET_TYPE_UDP;\n\t\t\t} else if(strcasecmp(item->value, \"tcp\") == 0) {\n\t\t\t\ttransport = JANUS_GELFEVH_SOCKET_TYPE_TCP;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing or invalid transport, using default: UDP\\n\");\n\t\t\t}\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"max_message_len\");\n\t\tif(item && item->value) {\n\t\t\tif(atoi(item->value) == 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing or invalid max_message_len, using default: %d\\n\", max_gelf_msg_len);\n\t\t\t} else {\n\t\t\t\tmax_gelf_msg_len = atoi(item->value);\n\t\t\t}\n\t\t}\n\t\t/* Which events should we subscribe to? */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(item && item->value)\n\t\t\tjanus_events_edit_events_mask(item->value, &janus_gelfevh.events_mask);\n\t\t/* Compact, so no spaces between separators */\n\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\n\t\t/* Check if we need any compression */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"compress\");\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tif(transport == JANUS_GELFEVH_SOCKET_TYPE_TCP) {\n\t\t\t\tcompress = FALSE;\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Compression on TCP Gelf transport not allowed, disabling...\\n\");\n\t\t\t} else {\n\t\t\t\tcompress = TRUE;\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"compression\");\n\t\t\t\tif(item && item->value) {\n\t\t\t\t\tint c = atoi(item->value);\n\t\t\t\t\tif(c < 0 || c > 9) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid compression factor '%d', falling back to '%d'...\\n\", c, compression);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcompression = c;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Done */\n\t\tenabled = TRUE;\n\t}\ndone:\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\tif(!enabled) {\n\t\tJANUS_LOG(LOG_FATAL, \"GELF event handler not enabled/needed, giving up...\\n\");\n\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t}\n\tJANUS_LOG(LOG_VERB, \"GELF event handler configured: %s:%s\\n\", backend, port);\n\n\t/* Check if connection failed. Error is logged in janus_gelfevh_connect function */\n\tif(janus_gelfevh_connect() < 0 ) {\n\t\treturn -1;\n\t}\n\n\t/* Initialize the events queue */\n\tevents = g_async_queue_new_full((GDestroyNotify) janus_gelfevh_event_free);\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming events */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"janus gelfevh handler\", janus_gelfevh_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the GelfEventHandler handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_GELFEVH_NAME);\n\treturn 0;\n}\n\nvoid janus_gelfevh_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(events, &exit_event);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\tg_async_queue_unref(events);\n\tevents = NULL;\n\n\tg_free(backend);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\n\tclose(sockfd);\n\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_GELFEVH_NAME);\n}\n\nint janus_gelfevh_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_EVENTHANDLER_API_VERSION;\n}\n\nint janus_gelfevh_get_version(void) {\n\treturn JANUS_GELFEVH_VERSION;\n}\n\nconst char *janus_gelfevh_get_version_string(void) {\n\treturn JANUS_GELFEVH_VERSION_STRING;\n}\n\nconst char *janus_gelfevh_get_description(void) {\n\treturn JANUS_GELFEVH_DESCRIPTION;\n}\n\nconst char *janus_gelfevh_get_name(void) {\n\treturn JANUS_GELFEVH_NAME;\n}\n\nconst char *janus_gelfevh_get_author(void) {\n\treturn JANUS_GELFEVH_AUTHOR;\n}\n\nconst char *janus_gelfevh_get_package(void) {\n\treturn JANUS_GELFEVH_PACKAGE;\n}\n\nvoid janus_gelfevh_incoming_event(json_t *event) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t/* Janus is closing or the plugin is */\n\t\treturn;\n\t}\n\n\t/* Do NOT handle the event here in this callback! Since Janus notifies you right\n\t * away when something happens, these events are triggered from working threads and\n\t * not some sort of message bus. As such, performing I/O or network operations in\n\t * here could dangerously slow Janus down. Let's just reference and enqueue the event,\n\t * and handle it in our own thread: the event contains a monotonic time indicator of\n\t * when the event actually happened on this machine, so that, if relevant, we can compute\n\t * any delay in the actual event processing ourselves. */\n\tjson_incref(event);\n\tg_async_queue_push(events, event);\n\n}\n\njson_t *janus_gelfevh_handle_request(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this requests to apply tweaks to the logic */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_GELFEVH_ERROR_MISSING_ELEMENT, JANUS_GELFEVH_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"tweak\")) {\n\t\t/* We only support a request to tweak the current settings */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, tweak_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_GELFEVH_ERROR_MISSING_ELEMENT, JANUS_GELFEVH_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\t/* Parameters we can change */\n\t\tconst char *req_events = NULL, *req_backend = NULL, *req_port = NULL;\n\t\tgboolean req_compress = -1, req_compression = -1;\n\t\t/* Events */\n\t\tif(json_object_get(request, \"events\"))\n\t\t\treq_events = json_string_value(json_object_get(request, \"events\"));\n\t\t/* Compression */\n\t\tif(json_object_get(request, \"compress\"))\n\t\t\treq_compress = json_is_true(json_object_get(request, \"compress\"));\n\t\tif(json_object_get(request, \"compression\"))\n\t\t\treq_compression = json_integer_value(json_object_get(request, \"compression\"));\n\t\t/* Backend stuff */\n\t\tif(json_object_get(request, \"backend\"))\n\t\t\treq_backend = json_string_value(json_object_get(request, \"backend\"));\n\t\tif(json_object_get(request, \"port\"))\n\t\t\treq_port = json_string_value(json_object_get(request, \"port\"));\n\t\tif(json_object_get(request, \"max_message_len\"))\n\t\t\tmax_gelf_msg_len = json_integer_value(json_object_get(request, \"max_message_len\"));\n\t\tif(strcasecmp(json_string_value(json_object_get(request, \"protocol\")), \"tcp\") == 0) {\n\t\t\ttransport = JANUS_GELFEVH_SOCKET_TYPE_TCP;\n\t\t} else if(strcasecmp(json_string_value(json_object_get(request, \"protocol\")), \"udp\") == 0) {\n\t\t\ttransport = JANUS_GELFEVH_SOCKET_TYPE_UDP;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Missing or invalid transport, using default: UDP\\n\");\n\t\t\ttransport = JANUS_GELFEVH_SOCKET_TYPE_UDP;\n\t\t}\n\t\tif(!req_backend || !req_port) {\n\t\t\t/* Invalid backend address or port */\n\t\t\terror_code = JANUS_GELFEVH_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Invalid backend URI '%s:%s'\", req_backend, req_port);\n\t\t\tgoto plugin_response;\n\t\t}\n\t\t/* If we got here, we can enforce */\n\t\tjanus_mutex_lock(&evh_mutex);\n\t\tif(req_events)\n\t\t\tjanus_events_edit_events_mask(req_events, &janus_gelfevh.events_mask);\n\t\tif(req_compress > -1) {\n\t\t\tif(req_compress && transport == JANUS_GELFEVH_SOCKET_TYPE_TCP) {\n\t\t\t\tcompress = FALSE;\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Compression on TCP Gelf transport not allowed, disabling...\\n\");\n\t\t\t} else {\n\t\t\t\tcompress = req_compress ? TRUE : FALSE;\n\t\t\t}\n\t\t}\n\t\tif(req_compression > -1 && req_compression < 10)\n\t\t\tcompression = req_compression;\n\t\tif(req_backend && req_port) {\n\t\t\tg_free(backend);\n\t\t\tg_free(port);\n\t\t\tbackend = g_strdup(req_backend);\n\t\t\tport = g_strdup(req_port);\n\t\t}\n\t\tjanus_mutex_unlock(&evh_mutex);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_GELFEVH_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tjson_t *response = json_object();\n\t\t\tif(error_code == 0) {\n\t\t\t\t/* Return a success */\n\t\t\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\t\t} else {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n/* Thread to handle incoming events */\nstatic void *janus_gelfevh_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining GelfEventHandler handler thread\\n\");\n\tjson_t *event = NULL;\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tevent = g_async_queue_pop(events);\n\t\tif(event == NULL)\n\t\t\tcontinue;\n\t\tif(event == &exit_event)\n\t\t\tbreak;\n\n\t\t/* Handle event */\n\t\twhile(TRUE) {\n\t\t\t/* Add custom fields */\n\t\t\tjson_t *output = json_object();\n\n\t\t\tint type = json_integer_value(json_object_get(event, \"type\"));\n\t\t\tconst char *short_message = janus_events_type_to_name(type);\n\t\t\tjson_t *microtimestamp = json_object_get(event, \"timestamp\");\n\t\t\tif(microtimestamp && json_is_integer(microtimestamp)) {\n\t\t\t\tdouble created_timestamp = (double)json_integer_value(microtimestamp) / 1000000;\n\t\t\t\tjson_object_set_new(output, \"timestamp\", json_real(created_timestamp));\n\t\t\t} else {\n\t\t\t\tjson_object_set_new(output, \"timestamp\", json_real(janus_get_real_time()));\n\t\t\t}\n\t\t\tjson_object_set(output, \"host\", json_object_get(event, \"emitter\"));\n\t\t\tjson_object_set_new(output, \"version\", json_string(\"1.1\"));\n\t\t\tjson_object_set(output, \"level\", json_object_get(event, \"type\"));\n\t\t\tjson_object_set_new(output, \"short_message\", json_string(short_message));\n\t\t\tjson_object_set(output, \"full_message\", event);\n\n\t\t\tchar *message = json_dumps(output, json_format);\n\t\t\tif(janus_gelfevh_send(message) < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't send event to GELF, reconnect?, or event was null: %s\\n\", message);\n\t\t\t}\n\t\t\tjson_decref(output);\n\t\t\tfree(message);\n\t\t\toutput = NULL;\n\n\t\t\tbreak;\n\t\t}\n\t\tjson_decref(event);\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving GELF Event handler thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/events/janus_mqttevh.c",
    "content": "/*! \\file   janus_mqttevh.c\n * \\author Olle E. Johansson <oej@edvina.net>\n * \\copyright GNU General Public License v3\n * \\brief  Janus MQTTEventHandler plugin\n * \\details  This is an MQTT event handler plugin for Janus. It is a\n * refactoring of the original effort contributed by Olle E. Johansson\n * (see https://github.com/meetecho/janus-gateway/pull/1185), which was\n * based on the MQTT transport by Andrei Nesterov and the RabbitMQ event\n * plugin by Piter Konstantinov.\n *\n * \\ingroup eventhandlers\n * \\ref eventhandlers\n */\n\n#include \"eventhandler.h\"\n\n#include <MQTTAsync.h>\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../utils.h\"\n#include \"../events.h\"\n\n/* Plugin information */\n#define JANUS_MQTTEVH_VERSION\t\t\t\t1\n#define JANUS_MQTTEVH_VERSION_STRING\t\t\"0.1.0\"\n#define JANUS_MQTTEVH_DESCRIPTION\t\t\t\"An MQTT event handler plugin for Janus.\"\n#define JANUS_MQTTEVH_NAME\t\t\t\t\t\"JANUS MQTTEventHandler plugin\"\n#define JANUS_MQTTEVH_AUTHOR\t\t\t\t\"Olle E. Johansson, Edvina AB\"\n#define JANUS_MQTTEVH_PACKAGE\t\t\t\t\"janus.eventhandler.mqttevh\"\n\n/* Plugin methods */\njanus_eventhandler *create(void);\nstatic int janus_mqttevh_init(const char *config_path);\nstatic void janus_mqttevh_destroy(void);\nstatic int janus_mqttevh_get_api_compatibility(void);\nstatic int janus_mqttevh_get_version(void);\nstatic const char *janus_mqttevh_get_version_string(void);\nstatic const char *janus_mqttevh_get_description(void);\nstatic const char *janus_mqttevh_get_name(void);\nstatic const char *janus_mqttevh_get_author(void);\nstatic const char *janus_mqttevh_get_package(void);\nstatic void janus_mqttevh_incoming_event(json_t *event);\njson_t *janus_mqttevh_handle_request(json_t *request);\n\nstatic int janus_mqttevh_send_message(void *context, const char *topic, json_t *message);\nstatic void *janus_mqttevh_handler(void *data);\n\n/* Event handler setup */\nstatic janus_eventhandler janus_mqttevh =\n\tJANUS_EVENTHANDLER_INIT (\n\t\t.init = janus_mqttevh_init,\n\t\t.destroy = janus_mqttevh_destroy,\n\n\t\t.get_api_compatibility = janus_mqttevh_get_api_compatibility,\n\t\t.get_version = janus_mqttevh_get_version,\n\t\t.get_version_string = janus_mqttevh_get_version_string,\n\t\t.get_description = janus_mqttevh_get_description,\n\t\t.get_name = janus_mqttevh_get_name,\n\t\t.get_author = janus_mqttevh_get_author,\n\t\t.get_package = janus_mqttevh_get_package,\n\n\t\t.incoming_event = janus_mqttevh_incoming_event,\n\t\t.handle_request = janus_mqttevh_handle_request,\n\n\t\t.events_mask = JANUS_EVENT_TYPE_NONE\n\t);\n\n/* Fix an exit event */\nstatic json_t exit_event;\n\n/* Destruction of events */\nstatic void janus_mqttevh_event_free(json_t *event) {\n\tif(!event || event == &exit_event)\n\t\treturn;\n\tjson_decref(event);\n}\n\n/* Queue of events to handle */\nstatic GAsyncQueue *events = NULL;\n\n/* Plugin creator */\njanus_eventhandler *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_MQTTEVH_NAME);\n\treturn &janus_mqttevh;\n};\n\n/* API flags */\nstatic gboolean janus_mqtt_evh_enabled = FALSE;\nstatic GThread *handler_thread;\nstatic volatile gint initialized = 0, stopping = 0;\n\n/* JSON serialization options */\n#define JANUS_MQTTEVH_DEFAULT_ADDPLUGIN\t\t\t\t1\n#define\tJANUS_MQTTEVH_DEFAULT_ADDEVENT\t\t\t\t1\n#define\tJANUS_MQTTEVH_DEFAULT_KEEPALIVE\t\t\t\t30\n#define\tJANUS_MQTTEVH_DEFAULT_CLEANSESSION\t\t\t0\t/* Off */\n#define JANUS_MQTTEVH_DEFAULT_MAX_INFLIGHT\t\t\t10\n#define JANUS_MQTTEVH_DEFAULT_MAX_BUFFERED\t\t\t100\n#define JANUS_MQTTEVH_DEFAULT_TIMEOUT\t\t\t\t30\n#define JANUS_MQTTEVH_DEFAULT_DISCONNECT_TIMEOUT\t100\n#define JANUS_MQTTEVH_DEFAULT_QOS\t\t\t\t\t0\n#define JANUS_MQTTEVH_DEFAULT_RETAIN\t\t\t\t0\n#define JANUS_MQTTEVH_DEFAULT_CONNECT_STATUS\t\t\"{\\\"event\\\": \\\"connected\\\", \\\"eventhandler\\\": \\\"\"JANUS_MQTTEVH_PACKAGE\"\\\"}\"\n#define JANUS_MQTTEVH_DEFAULT_DISCONNECT_STATUS\t\t\"{\\\"event\\\": \\\"disconnected\\\"}\"\n#define JANUS_MQTTEVH_DEFAULT_WILL_RETAIN\t\t\t1\n#define JANUS_MQTTEVH_DEFAULT_WILL_QOS\t \t\t\t0\n#define JANUS_MQTTEVH_DEFAULT_BASETOPIC\t \t\t\t\"/janus/events\"\n#define JANUS_MQTTEVH_DEFAULT_MQTTURL\t\t\t\t\"tcp://localhost:1883\"\n#define JANUS_MQTTEVH_DEFAULT_JSON_FORMAT\t\t\tJSON_INDENT(3) | JSON_PRESERVE_ORDER\n#define JANUS_MQTTEVH_DEFAULT_TLS_ENABLE\t\t\tFALSE\n#define JANUS_MQTTEVH_DEFAULT_TLS_VERIFY_PEER\t\tFALSE\n#define JANUS_MQTTEVH_DEFAULT_TLS_VERIFY_HOST\t\tFALSE\n\n#define JANUS_MQTTEVH_VERSION_3_1\t\t\"3.1\"\n#define JANUS_MQTTEVH_VERSION_3_1_1\t\t\"3.1.1\"\n#define JANUS_MQTTEVH_VERSION_5\t\t\t\"5\"\n#define JANUS_MQTTEVH_VERSION_DEFAULT JANUS_MQTTEVH_VERSION_3_1_1\n\nstatic size_t json_format = JANUS_MQTTEVH_DEFAULT_JSON_FORMAT;\n\n\n/* Parameter validation (for tweaking via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter tweak_parameters[] = {\n\t{\"events\", JSON_STRING, 0}\n};\n/* Error codes (for the tweaking via Admin API */\n#define JANUS_MQTTEVH_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_MQTTEVH_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_MQTTEVH_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_MQTTEVH_ERROR_UNKNOWN_ERROR\t\t499\n\n/* Special topics postfix */\n#define JANUS_MQTTEVH_STATUS_TOPIC \"status\"\n\n\n/* MQTT client context */\ntypedef struct janus_mqttevh_context {\n\t/* THe Paho MQTT client data structure */\n\tMQTTAsync client;\n\n\tint addplugin;\n\tint addevent;\n\n\t/* Connection data - authentication and url */\n\tstruct {\n\t\tint mqtt_version;\n\t\tint keep_alive_interval;\n\t\tint cleansession;\n\t\tint max_inflight;\n\t\tint max_buffered;\n\t\tchar *client_id;\n\t\tchar *username;\n\t\tchar *password;\n\t\tchar *url;\n\t} connect;\n\n\tstruct {\n\t\tint timeout;\n\t} disconnect;\n\n\t/* Data for publishing events */\n\tstruct {\n\t\tchar *topic;\n\t\tchar *connect_status;\n\t\tchar *disconnect_status;\n\t\tint qos;\n\t\tint retain;\n#ifdef MQTTVERSION_5\n\t\tGArray *add_user_properties;\n#endif\n\t} publish;\n\n\t/* If we loose connection, the will is our last publish */\n\tstruct {\n\t\tgboolean enabled;\n\t\tchar *topic;\n\t\tint qos;\n\t\tint retain;\n\t} will;\n\n\t/* TLS connection data */\n\tstruct {\n\t\tgboolean enable;\n\t\tchar *cacert_file;\n\t\tchar *cert_file;\n\t\tchar *key_file;\n\t\tgboolean verify_peer;\n\t\tgboolean verify_host;\n\t} tls;\n} janus_mqttevh_context;\n\n#ifdef MQTTVERSION_5\n/* MQTT 5 specific types */\ntypedef struct janus_mqttevh_set_add_user_property_user_data {\n\tGArray *acc;\n\tjanus_config *config;\n} janus_mqttevh_set_add_user_property_user_data;\n#endif\n\n/* Event handler methods */\nstatic void janus_mqttevh_client_connection_lost(void *context, char *cause);\nstatic int janus_mqttevh_client_connect(janus_mqttevh_context *ctx);\nstatic int janus_mqttevh_client_disconnect(janus_mqttevh_context *ctx);\nstatic void janus_mqttevh_client_destroy_context(janus_mqttevh_context **ctx);\n/* MQTT v3.x interface callbacks */\nstatic void janus_mqttevh_client_connect_success(void *context, MQTTAsync_successData *response);\nstatic void janus_mqttevh_client_connect_failure(void *context, MQTTAsync_failureData *response);\nstatic void janus_mqttevh_client_disconnect_success(void *context, MQTTAsync_successData *response);\nstatic void janus_mqttevh_client_disconnect_failure(void *context, MQTTAsync_failureData *response);\nstatic void janus_mqttevh_client_publish_message_success(void *context, MQTTAsync_successData *response);\nstatic void janus_mqttevh_client_publish_message_failure(void *context, MQTTAsync_failureData *response);\nstatic int janus_mqttevh_client_publish_message(janus_mqttevh_context *ctx, const char *topic, int retain, char *payload);\nint janus_mqttevh_client_get_response_code(MQTTAsync_failureData *response);\n#ifdef MQTTVERSION_5\n/* MQTT v5 interface callbacks */\nstatic void janus_mqttevh_client_connect_success5(void *context, MQTTAsync_successData5 *response);\nstatic void janus_mqttevh_client_connect_failure5(void *context, MQTTAsync_failureData5 *response);\nstatic void janus_mqttevh_client_disconnect_success5(void *context, MQTTAsync_successData5 *response);\nstatic void janus_mqttevh_client_disconnect_failure5(void *context, MQTTAsync_failureData5 *response);\nstatic void janus_mqttevh_client_publish_message_success5(void *context, MQTTAsync_successData5 *response);\nstatic void janus_mqttevh_client_publish_message_failure5(void *context, MQTTAsync_failureData5 *response);\nstatic int janus_mqttevh_client_publish_message5(janus_mqttevh_context *ctx, const char *topic, int retain, char *payload, MQTTProperties *properties);\nint janus_mqttevh_client_get_response_code5(MQTTAsync_failureData5 *response);\n#endif\n/* MQTT version independent callback implementations */\nstatic void janus_mqttevh_client_connect_success_impl(void *context);\nstatic void janus_mqttevh_client_connect_failure_impl(void *context, int rc);\nstatic void janus_mqttevh_client_disconnect_success_impl(void *context);\nstatic void janus_mqttevh_client_disconnect_failure_impl(void *context, int rc);\nstatic void janus_mqttevh_client_publish_message_success_impl(void *context);\nstatic void janus_mqttevh_client_publish_message_failure_impl(void *context, int rc);\nint janus_mqttevh_client_publish_message_wrap(void *context, const char *topic, int retain, char *payload);\n\n#ifdef MQTTVERSION_5\n/* MQTT 5 specific functions */\nvoid janus_mqttevh_add_properties(GArray *user_properties, MQTTProperties *properties);\nvoid janus_mqttevh_set_add_user_property(gpointer item_ptr, gpointer user_data_ptr);\n#endif\n\n/* We only handle a single connection */\nstatic janus_mqttevh_context *context = NULL;\n\n\n/* Janus API methods */\nstatic int janus_mqttevh_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_EVENTHANDLER_API_VERSION;\n}\n\nstatic int janus_mqttevh_get_version(void) {\n\treturn JANUS_MQTTEVH_VERSION;\n}\n\nstatic const char *janus_mqttevh_get_version_string(void) {\n\treturn JANUS_MQTTEVH_VERSION_STRING;\n}\n\nstatic const char *janus_mqttevh_get_description(void) {\n\treturn JANUS_MQTTEVH_DESCRIPTION;\n}\n\nstatic const char *janus_mqttevh_get_name(void) {\n\treturn JANUS_MQTTEVH_NAME;\n}\n\nstatic const char *janus_mqttevh_get_author(void) {\n\treturn JANUS_MQTTEVH_AUTHOR;\n}\n\nstatic const char *janus_mqttevh_get_package(void) {\n\treturn JANUS_MQTTEVH_PACKAGE;\n}\n\n/* Send an JSON message to a MQTT topic */\nstatic int janus_mqttevh_send_message(void *context, const char *topic, json_t *message) {\n\tchar *payload = NULL;\n\tint rc = 0;\n\tjanus_mqttevh_context *ctx;\n\n\tif(message == NULL) {\n\t\treturn -1;\n\t}\n\tif(context == NULL) {\n\t\t/* We have no context, so skip and move on */\n\t\tjson_decref(message);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_HUGE, \"About to send message to %s\\n\", topic);\n\n#ifdef SKREP\n\tif(payload != NULL) {\n\t\t/* Free previous message */\n\t\tfree(payload);\n\t\tpayload = NULL;\n\t}\n#endif\n\tctx = (janus_mqttevh_context *)context;\n\n\tpayload = json_dumps(message, json_format);\n\tif(payload == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't convert message to string format\\n\");\n\t\tjson_decref(message);\n\t\treturn 0;\n\t}\n\tJANUS_LOG(LOG_HUGE, \"Converted message to JSON for %s\\n\", topic);\n\t/* Ok, lets' get rid of the message */\n\tjson_decref(message);\n\n\trc = janus_mqttevh_client_publish_message_wrap(context, topic, ctx->publish.retain, payload);\n\tif(rc != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_WARN, \"Can't publish to MQTT topic: %s, return code: %d\\n\", ctx->publish.topic, rc);\n\t}\n\n\tfree(payload);\n\n\tJANUS_LOG(LOG_HUGE, \"Done with message to JSON for %s\\n\", topic);\n\n\treturn 0;\n}\n\nint janus_mqttevh_client_publish_message_wrap(void *context, const char *topic, int retain, char *payload) {\n\tint rc = 0;\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\tMQTTProperties properties = MQTTProperties_initializer;\n\t\tjanus_mqttevh_add_properties(ctx->publish.add_user_properties, &properties);\n\t\trc = janus_mqttevh_client_publish_message5(ctx, topic, retain, payload, &properties);\n\t\tMQTTProperties_free(&properties);\n\t} else {\n\t\trc = janus_mqttevh_client_publish_message(ctx, topic, retain, payload);\n\t}\n#else\n\trc = janus_mqttevh_client_publish_message(ctx, topic, retain, payload);\n#endif\n\n\treturn rc;\n}\n\nstatic void janus_mqttevh_client_connection_lost(void *context, char *cause) {\n\n\t/* Notify handlers about this transport being gone */\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\n\tJANUS_LOG(LOG_WARN, \"MQTT EVH connection %s lost cause of %s. Reconnecting...\\n\", ctx->connect.url, cause);\n}\n\n/* Set up connection to MQTT broker */\nstatic int janus_mqttevh_client_connect(janus_mqttevh_context *ctx) {\n\tMQTTAsync_connectOptions options = MQTTAsync_connectOptions_initializer;\n\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\tMQTTAsync_connectOptions options5 = MQTTAsync_connectOptions_initializer5;\n\t\toptions = options5;\n\t\toptions.cleanstart = ctx->connect.cleansession;\n\t\toptions.onSuccess5 = janus_mqttevh_client_connect_success5;\n\t\toptions.onFailure5 = janus_mqttevh_client_connect_failure5;\n\t} else {\n\t\toptions.cleansession = ctx->connect.cleansession;\n\t\toptions.onSuccess = janus_mqttevh_client_connect_success;\n\t\toptions.onFailure = janus_mqttevh_client_connect_failure;\n\t}\n#else\n\toptions.cleansession = ctx->connect.cleansession;\n\toptions.onSuccess = janus_mqttevh_client_connect_success;\n\toptions.onFailure = janus_mqttevh_client_connect_failure;\n#endif\n\n\toptions.MQTTVersion = ctx->connect.mqtt_version;\n\toptions.username = ctx->connect.username;\n\toptions.password = ctx->connect.password;\n\toptions.automaticReconnect = TRUE;\n\toptions.keepAliveInterval = ctx->connect.keep_alive_interval;\n\toptions.maxInflight = ctx->connect.max_inflight;\n\n\tMQTTAsync_SSLOptions ssl_opts = MQTTAsync_SSLOptions_initializer;\n\tif(ctx->tls.enable) {\n\t\tssl_opts.trustStore = ctx->tls.cacert_file;\n\t\tssl_opts.keyStore = ctx->tls.cert_file;\n\t\tssl_opts.privateKey = ctx->tls.key_file;\n\t\tssl_opts.enableServerCertAuth = ctx->tls.verify_peer;\n\t\toptions.ssl = &ssl_opts;\n\t}\n\n\tMQTTAsync_willOptions willOptions = MQTTAsync_willOptions_initializer;\n\tif(ctx->will.enabled) {\n\t\twillOptions.topicName = ctx->will.topic;\n\t\twillOptions.message = ctx->publish.disconnect_status;\n\t\twillOptions.retained = ctx->will.retain;\n\t\twillOptions.qos = ctx->will.qos;\n\t\toptions.will = &willOptions;\n\t}\n\n\toptions.context = ctx;\n\treturn MQTTAsync_connect(ctx->client, &options);\n}\n\n/* Callback for successful connection to MQTT broker */\nstatic void janus_mqttevh_client_connect_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqttevh_client_connect_success_impl(context);\n}\n\n#ifdef MQTTVERSION_5\nstatic void janus_mqttevh_client_connect_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqttevh_client_connect_success_impl(context);\n}\n#endif\n\nstatic void janus_mqttevh_client_connect_success_impl(void *context) {\n\tJANUS_LOG(LOG_INFO, \"MQTT EVH client has been successfully connected to the broker\\n\");\n\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\n\tchar topicbuf[512];\n\tsnprintf(topicbuf, sizeof(topicbuf), \"%s/%s\", ctx->publish.topic, JANUS_MQTTEVH_STATUS_TOPIC);\n\n\t/* Using LWT's retain for initial status message because\n\t * we need to ensure we overwrite LWT if it's retained.\n\t */\n\tint rc = janus_mqttevh_client_publish_message_wrap(context, topicbuf, ctx->will.retain, ctx->publish.connect_status);\n\n\tif(rc != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_WARN, \"Can't publish to MQTT topic: %s, return code: %d\\n\", topicbuf, rc);\n\t}\n}\n\n/* Callback for MQTT broker connection failure */\nstatic void janus_mqttevh_client_connect_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqttevh_client_get_response_code(response);\n\tjanus_mqttevh_client_connect_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nstatic void janus_mqttevh_client_connect_failure5(void *context, MQTTAsync_failureData5 *response) {\n\t\tint rc = janus_mqttevh_client_get_response_code5(response);\n\t\tjanus_mqttevh_client_connect_failure_impl(context, rc);\n}\n#endif\n\nstatic void janus_mqttevh_client_connect_failure_impl(void *contexts, int rc) {\n\t/* Notify handlers about this transport failure */\n\tJANUS_LOG(LOG_ERR, \"MQTT EVH client has failed connecting to the broker, return code: %d. Reconnecting...\\n\", rc);\n}\n\n/* Disconnect from MQTT broker */\nstatic int janus_mqttevh_client_disconnect(janus_mqttevh_context *ctx) {\n\tchar topicbuf[512];\n\tsnprintf(topicbuf, sizeof(topicbuf), \"%s/%s\", ctx->publish.topic, JANUS_MQTTEVH_STATUS_TOPIC);\n\n\t/* Using LWT's retain for disconnect status message because\n\t * we need to ensure we overwrite LWT if it's retained.\n\t */\n\tint rc = janus_mqttevh_client_publish_message_wrap(context, topicbuf, 1, ctx->publish.disconnect_status);\n\n\tif(rc != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_WARN, \"Can't publish to MQTT topic: %s, return code: %d\\n\", topicbuf, rc);\n\t}\n\n\tMQTTAsync_disconnectOptions options = MQTTAsync_disconnectOptions_initializer;\n\toptions.context = ctx;\n\toptions.timeout = ctx->disconnect.timeout;\n\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\toptions.onSuccess5 = janus_mqttevh_client_disconnect_success5;\n\t\toptions.onFailure5 = janus_mqttevh_client_disconnect_failure5;\n\t} else {\n\t\toptions.onSuccess = janus_mqttevh_client_disconnect_success;\n\t\toptions.onFailure = janus_mqttevh_client_disconnect_failure;\n\t}\n#else\n\toptions.onSuccess = janus_mqttevh_client_disconnect_success;\n\toptions.onFailure = janus_mqttevh_client_disconnect_failure;\n#endif\n\n\treturn MQTTAsync_disconnect(ctx->client, &options);\n}\n\n/* Callback for successful MQTT disconnect */\nstatic void janus_mqttevh_client_disconnect_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqttevh_client_disconnect_success_impl(context);\n}\n\n#ifdef MQTTVERSION_5\nstatic void janus_mqttevh_client_disconnect_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqttevh_client_disconnect_success_impl(context);\n}\n#endif\n\nstatic void janus_mqttevh_client_disconnect_success_impl(void *context) {\n\t/* Notify handlers about this transport being gone */\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\n\tJANUS_LOG(LOG_INFO, \"MQTT EVH client has been successfully disconnected from %s. Destroying the client...\\n\", ctx->connect.url);\n\tjanus_mqttevh_client_destroy_context(&ctx);\n}\n\n/* Callback for MQTT disconnect failure */\nvoid janus_mqttevh_client_disconnect_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqttevh_client_get_response_code(response);\n\tjanus_mqttevh_client_disconnect_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqttevh_client_disconnect_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqttevh_client_get_response_code5(response);\n\tjanus_mqttevh_client_disconnect_failure_impl(context, rc);\n}\n#endif\n\nvoid janus_mqttevh_client_disconnect_failure_impl(void *context, int rc) {\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\tJANUS_LOG(LOG_ERR, \"Can't disconnect from MQTT EVH broker %s, return code: %d\\n\", ctx->connect.url, rc);\n\tjanus_mqttevh_client_destroy_context(&ctx);\n}\n\n\n/* Publish mqtt message using paho\n * Payload is a string. JSON objects should be stringified before calling this function.\n */\nstatic int janus_mqttevh_client_publish_message(janus_mqttevh_context *ctx, const char *topic, int retain, char *payload) {\n\tint rc;\n\n\tMQTTAsync_message msg = MQTTAsync_message_initializer;\n\tmsg.payload = payload;\n\tmsg.payloadlen = strlen(payload);\n\tmsg.qos = ctx->publish.qos;\n\tmsg.retained = retain;\n\n\t/* TODO: The payload if generated by json_dumps needs to be freed\n\tfree(payload);\n\tpayload = (char *)NULL;\n\t*/\n\n\tMQTTAsync_responseOptions options = MQTTAsync_responseOptions_initializer;\n\toptions.context = ctx;\n\toptions.onSuccess = janus_mqttevh_client_publish_message_success;\n\toptions.onFailure = janus_mqttevh_client_publish_message_failure;\n\n\trc = MQTTAsync_sendMessage(ctx->client, topic, &msg, &options);\n\tswitch(rc) {\n\t\tcase MQTTASYNC_SUCCESS:\n\t\t\tJANUS_LOG(LOG_HUGE, \"MQTT EVH message sent to topic %s on %s. Result %d\\n\", topic, ctx->connect.url, rc);\n\t\t\tbreak;\n\t\tcase MQTTASYNC_OPERATION_INCOMPLETE:\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_WARN, \"FAILURE: MQTT EVH message probably not sent to topic %s on %s. Result %d\\n\", topic, ctx->connect.url, rc);\n\t}\n\n\treturn rc;\n}\n\n#ifdef MQTTVERSION_5\nstatic int janus_mqttevh_client_publish_message5(janus_mqttevh_context *ctx, const char *topic, int retain, char *payload, MQTTProperties *properties) {\n\tint rc;\n\n\tMQTTAsync_message msg = MQTTAsync_message_initializer;\n\tmsg.payload = payload;\n\tmsg.payloadlen = strlen(payload);\n\tmsg.qos = ctx->publish.qos;\n\tmsg.retained = retain;\n\tmsg.properties = MQTTProperties_copy(properties);\n\n\tMQTTAsync_responseOptions options = MQTTAsync_responseOptions_initializer;\n\toptions.context = ctx;\n\toptions.onSuccess5 = janus_mqttevh_client_publish_message_success5;\n\toptions.onFailure5 = janus_mqttevh_client_publish_message_failure5;\n\n\trc = MQTTAsync_sendMessage(ctx->client, topic, &msg, &options);\n\tswitch(rc) {\n\t\tcase MQTTASYNC_SUCCESS:\n\t\t\tJANUS_LOG(LOG_HUGE, \"MQTT EVH message sent to topic %s on %s. Result %d\\n\", topic, ctx->connect.url, rc);\n\t\t\tbreak;\n\t\tcase MQTTASYNC_OPERATION_INCOMPLETE:\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_WARN, \"FAILURE: MQTT EVH message probably not sent to topic %s on %s. Result %d\\n\", topic, ctx->connect.url, rc);\n\t}\n\n\treturn rc;\n}\n#endif\n\n/* Callback for successful MQTT publish */\nstatic void janus_mqttevh_client_publish_message_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqttevh_client_publish_message_success_impl(context);\n}\n\n#ifdef MQTTVERSION_5\nstatic void janus_mqttevh_client_publish_message_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqttevh_client_publish_message_success_impl(context);\n}\n#endif\n\nstatic void janus_mqttevh_client_publish_message_success_impl(void *context) {\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\tJANUS_LOG(LOG_HUGE, \"MQTT EVH client has successfully published to MQTT base topic: %s\\n\", ctx->publish.topic);\n}\n\n/* Callback for MQTT publish failure\n * \tShould we bring message into queue? Right now, we just drop it.\n */\nstatic void janus_mqttevh_client_publish_message_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqttevh_client_get_response_code(response);\n\tjanus_mqttevh_client_publish_message_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nstatic void janus_mqttevh_client_publish_message_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqttevh_client_get_response_code5(response);\n\tjanus_mqttevh_client_publish_message_failure_impl(context, rc);\n}\n#endif\n\nstatic void janus_mqttevh_client_publish_message_failure_impl(void *context, int rc) {\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\tswitch(rc) {\n\t\tcase MQTTASYNC_OPERATION_INCOMPLETE:\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_ERR, \"MQTT EVH client has failed publishing to MQTT topic: %s, return code: %d\\n\", ctx->publish.topic, rc);\n\t}\n}\n\n/* Destroy Janus MQTT event handler session context */\nstatic void janus_mqttevh_client_destroy_context(janus_mqttevh_context **ptr) {\n\tJANUS_LOG(LOG_INFO, \"About to destroy MQTT EVH context...\\n\");\n\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)*ptr;\n\n\tif(ctx) {\n\t\tMQTTAsync_destroy(&ctx->client);\n\t\tg_free(ctx->publish.topic);\n\t\tg_free(ctx->connect.username);\n\t\tg_free(ctx->connect.password);\n\t\tg_free(ctx);\n\t\t*ptr = NULL;\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_MQTTEVH_NAME);\n}\n\n/* This is not used here, but required by the api (even though docs says no) */\nstatic int janus_mqttevh_client_message_arrived(void *context, char *topicName, int topicLen, MQTTAsync_message *message) {\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)context;\n\tgchar *topic = g_strndup(topicName, topicLen);\n\t//~ const gboolean janus = janus_mqtt_evh_enabled && !strcasecmp(topic, ctx->subscribe.topic);\n\tconst gboolean janus = janus_mqtt_evh_enabled ;\n\tg_free(topic);\n\n\tif(janus && message->payloadlen) {\n\t\tJANUS_LOG(LOG_HUGE, \"MQTT %s: Receiving %s EVH message over MQTT: %s\\n\",\n\t\t\tctx->connect.url, \"Janus\", (char *)message->payload);\n\t}\n\n\tMQTTAsync_freeMessage(&message);\n\tMQTTAsync_free(topicName);\n\treturn TRUE;\n}\n\nstatic void janus_mqttevh_client_delivery_complete(void *context, MQTTAsync_token token) {\n\t/* If you send with QoS, you get confirmation here */\n}\n\n/* Plugin implementation */\nstatic int janus_mqttevh_init(const char *config_path) {\n\tint res = 0;\n\tjanus_config_item *url_item;\n\tjanus_config_item *username_item, *password_item, *topic_item, *addevent_item;\n\tjanus_config_item *keep_alive_interval_item, *cleansession_item, *max_inflight_item, *max_buffered_item, *disconnect_timeout_item, *qos_item, *retain_item, *connect_status_item, *disconnect_status_item;\n\tjanus_config_item *will_retain_item, *will_qos_item, *will_enabled_item;\n\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, sizeof(filename), \"%s/%s.jcfg\", config_path, JANUS_MQTTEVH_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_MQTTEVH_PACKAGE);\n\t\tg_snprintf(filename, sizeof(filename), \"%s/%s.cfg\", config_path, JANUS_MQTTEVH_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t\tif(config == NULL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Couldn't find .cfg configuration file (%s), giving up\\n\", JANUS_MQTTEVH_PACKAGE);\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tjanus_config_print(config);\n\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\n\t/* Initializing context */\n\tjanus_mqttevh_context *ctx = g_malloc0(sizeof(struct janus_mqttevh_context));\n\tcontext = ctx;\n\n\t/* Set default values */\n\t/* Strings are set to default values later */\n\tctx->addplugin = JANUS_MQTTEVH_DEFAULT_ADDPLUGIN;\n\tctx->addevent = JANUS_MQTTEVH_DEFAULT_ADDEVENT;\n\tctx->publish.qos = JANUS_MQTTEVH_DEFAULT_QOS;\n\tctx->publish.retain = JANUS_MQTTEVH_DEFAULT_RETAIN;\n\tctx->connect.username = NULL;\n\tctx->connect.password = NULL;\n\tctx->disconnect.timeout = JANUS_MQTTEVH_DEFAULT_TIMEOUT;\n\n\tctx->will.enabled = FALSE;\n\tctx->will.qos = JANUS_MQTTEVH_DEFAULT_WILL_QOS;\n\tctx->will.retain = JANUS_MQTTEVH_DEFAULT_WILL_RETAIN;\n\n\tctx->tls.enable = JANUS_MQTTEVH_DEFAULT_TLS_ENABLE;\n\tctx->tls.verify_peer = JANUS_MQTTEVH_DEFAULT_TLS_VERIFY_PEER;\n\tctx->tls.verify_host = JANUS_MQTTEVH_DEFAULT_TLS_VERIFY_HOST;\n\n\t/* Setup the event handler, if required */\n\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_WARN, \"MQTT event handler disabled\\n\");\n\t\tgoto error;\n\t}\n\tjanus_mqtt_evh_enabled = TRUE;\n\n\t/* MQTT URL */\n\turl_item = janus_config_get(config, config_general, janus_config_type_item, \"url\");\n\tctx->connect.url= g_strdup((url_item && url_item->value) ? url_item->value : JANUS_MQTTEVH_DEFAULT_MQTTURL);\n\n\tjanus_config_item *mqtt_version = janus_config_get(config, config_general, janus_config_type_item, \"mqtt_version\");\n\tconst char *mqtt_version_str = (mqtt_version && mqtt_version->value) ? mqtt_version->value : JANUS_MQTTEVH_VERSION_DEFAULT;\n\n\tif(strcmp(mqtt_version_str, JANUS_MQTTEVH_VERSION_3_1) == 0) {\n\t\tctx->connect.mqtt_version = MQTTVERSION_3_1;\n\t} else if(strcmp(mqtt_version_str, JANUS_MQTTEVH_VERSION_3_1_1) == 0) {\n\t\tctx->connect.mqtt_version = MQTTVERSION_3_1_1;\n\t} else if(strcmp(mqtt_version_str, JANUS_MQTTEVH_VERSION_5) == 0) {\n#ifdef MQTTVERSION_5\n\t\tctx->connect.mqtt_version = MQTTVERSION_5;\n#else\n\t\tJANUS_LOG(LOG_FATAL, \"Using MQTT v5 requires compilation with Paho >= 1.3.0\\n\");\n\t\tgoto error;\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_FATAL, \"Unknown MQTT version\\n\");\n\t\tgoto error;\n\t}\n\n\tjanus_config_item *client_id_item = janus_config_get(config, config_general, janus_config_type_item, \"client_id\");\n\n\tctx->connect.client_id = g_strdup((client_id_item && client_id_item->value) ? client_id_item->value : \"guest\");\n\n\tusername_item = janus_config_get(config, config_general, janus_config_type_item, \"username\");\n\tif(username_item && username_item->value) {\n\t\tctx->connect.username = g_strdup(username_item->value);\n\t} else {\n\t\tctx->connect.username = NULL;\n\t}\n\n\tpassword_item = janus_config_get(config, config_general, janus_config_type_item, \"password\");\n\tif(password_item && password_item->value) {\n\t\tctx->connect.password = g_strdup(password_item->value);\n\t} else {\n\t\tctx->connect.password = NULL;\n\t}\n\n\tjanus_config_item *json_item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\tif(json_item && json_item->value) {\n\t\t/* Check how we need to format/serialize the JSON output */\n\t\tif(!strcasecmp(json_item->value, \"indented\")) {\n\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(json_item->value, \"plain\")) {\n\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(json_item->value, \"compact\")) {\n\t\t\t/* Compact, so no spaces between separators */\n\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", json_item->value);\n\t\t\tjson_format = JANUS_MQTTEVH_DEFAULT_JSON_FORMAT;\n\t\t}\n\t}\n\n\t/* Which events should we subscribe to? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\tif(item && item->value)\n\t\tjanus_events_edit_events_mask(item->value, &janus_mqttevh.events_mask);\n\n\t/* Connect configuration */\n\tkeep_alive_interval_item = janus_config_get(config, config_general, janus_config_type_item, \"keep_alive_interval\");\n\tctx->connect.keep_alive_interval = (keep_alive_interval_item && keep_alive_interval_item->value) ?\n\t\tatoi(keep_alive_interval_item->value) : JANUS_MQTTEVH_DEFAULT_KEEPALIVE;\n\tif(ctx->connect.keep_alive_interval < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid keep-alive value: %s (falling back to default)\\n\", keep_alive_interval_item->value);\n\t\tctx->connect.keep_alive_interval = JANUS_MQTTEVH_DEFAULT_KEEPALIVE;\n\t}\n\n\tcleansession_item = janus_config_get(config, config_general, janus_config_type_item, \"cleansession\");\n\tctx->connect.cleansession = (cleansession_item && cleansession_item->value) ?\n\t\tatoi(cleansession_item->value) : JANUS_MQTTEVH_DEFAULT_CLEANSESSION;\n\tif(ctx->connect.cleansession < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid clean-session value: %s (falling back to default)\\n\", cleansession_item->value);\n\t\tctx->connect.cleansession = JANUS_MQTTEVH_DEFAULT_CLEANSESSION;\n\t}\n\n\tmax_inflight_item = janus_config_get(config, config_general, janus_config_type_item, \"max_inflight\");\n\tctx->connect.max_inflight = (max_inflight_item && max_inflight_item->value) ?\n\t\tatoi(max_inflight_item->value) : JANUS_MQTTEVH_DEFAULT_MAX_INFLIGHT;\n\tif(ctx->connect.max_inflight < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid max-inflight value: %s (falling back to default)\\n\", max_inflight_item->value);\n\t\tctx->connect.max_inflight = JANUS_MQTTEVH_DEFAULT_MAX_INFLIGHT;\n\t}\n\n\tmax_buffered_item = janus_config_get(config, config_general, janus_config_type_item, \"max_buffered\");\n\tctx->connect.max_buffered = (max_buffered_item && max_buffered_item->value) ?\n\t\tatoi(max_buffered_item->value) : JANUS_MQTTEVH_DEFAULT_MAX_BUFFERED;\n\tif(ctx->connect.max_buffered < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid max-buffered value: %s (falling back to default)\\n\", max_buffered_item->value);\n\t\tctx->connect.max_buffered = JANUS_MQTTEVH_DEFAULT_MAX_BUFFERED;\n\t}\n\n\t/* Disconnect configuration */\n\tdisconnect_timeout_item = janus_config_get(config, config_general, janus_config_type_item, \"disconnect_timeout\");\n\tctx->disconnect.timeout = (disconnect_timeout_item && disconnect_timeout_item->value) ?\n\t\tatoi(disconnect_timeout_item->value) : JANUS_MQTTEVH_DEFAULT_DISCONNECT_TIMEOUT;\n\tif(ctx->disconnect.timeout < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid disconnect-timeout value: %s (falling back to default)\\n\", disconnect_timeout_item->value);\n\t\tctx->disconnect.timeout = JANUS_MQTTEVH_DEFAULT_DISCONNECT_TIMEOUT;\n\t}\n\n\ttopic_item = janus_config_get(config, config_general, janus_config_type_item, \"topic\");\n\tif(!topic_item || !topic_item->value) {\n\t\tctx->publish.topic = g_strdup(JANUS_MQTTEVH_DEFAULT_BASETOPIC);\n\t} else {\n\t\tctx->publish.topic = g_strdup(topic_item->value);\n\t}\n\taddevent_item = janus_config_get(config, config_general, janus_config_type_item, \"addevent\");\n\tif(addevent_item && addevent_item->value && janus_is_true(addevent_item->value)) {\n\t\tctx->addevent = TRUE;\n\t}\n\tretain_item = janus_config_get(config, config_general, janus_config_type_item, \"retain\");\n\tif(retain_item && retain_item->value && janus_is_true(retain_item->value)) {\n\t\tctx->publish.retain = atoi(retain_item->value);;\n\t\tif(ctx->publish.retain < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid publish-retain value: %s (disabling)\\n\", retain_item->value);\n\t\t\tctx->publish.retain = 0;\n\t\t}\n\t}\n\n\tqos_item = janus_config_get(config, config_general, janus_config_type_item, \"qos\");\n\tctx->publish.qos = (qos_item && qos_item->value) ? atoi(qos_item->value) : 1;\n\tif(ctx->publish.qos < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid publish-qos value: %s (falling back to default)\\n\", qos_item->value);\n\t\tctx->publish.qos = 1;\n\t}\n\n\tconnect_status_item = janus_config_get(config, config_general, janus_config_type_item, \"connect_status\");\n\tif(connect_status_item && connect_status_item->value) {\n\t\tctx->publish.connect_status = g_strdup(connect_status_item->value);\n\t} else {\n\t\tctx->publish.connect_status = g_strdup(JANUS_MQTTEVH_DEFAULT_CONNECT_STATUS);\n\t}\n\n\tdisconnect_status_item = janus_config_get(config, config_general, janus_config_type_item, \"disconnect_status\");\n\tif(disconnect_status_item && disconnect_status_item->value) {\n\t\tctx->publish.disconnect_status = g_strdup(disconnect_status_item->value);\n\t} else {\n\t\tctx->publish.disconnect_status = g_strdup(JANUS_MQTTEVH_DEFAULT_DISCONNECT_STATUS);\n\t}\n\n\t/* LWT config */\n\twill_enabled_item = janus_config_get(config, config_general, janus_config_type_item, \"will_enabled\");\n\tif(will_enabled_item && will_enabled_item->value && janus_is_true(will_enabled_item->value)) {\n\t\tctx->will.enabled = TRUE;\n\n\t\twill_retain_item = janus_config_get(config, config_general, janus_config_type_item, \"will_retain\");\n\t\tif(will_retain_item && will_retain_item->value && janus_is_true(will_retain_item->value)) {\n\t\t\tctx->will.retain = 1;\n\t\t}\n\n\t\twill_qos_item = janus_config_get(config, config_general, janus_config_type_item, \"will_qos\");\n\t\tif(will_qos_item && will_qos_item->value) {\n\t\t\tctx->will.qos = atoi(will_qos_item->value);\n\t\t\tif(ctx->will.qos < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid will-qos value: %s (setting to 0)\\n\", will_qos_item->value);\n\t\t\t\tctx->will.qos = 0;\n\t\t\t}\n\t\t}\n\n\t\t/* Using the topic for LWT as configured for publish and suffixed with JANUS_MQTTEVH_STATUS_TOPIC. */\n\t\tchar will_topic_buf[512];\n\t\tsnprintf(will_topic_buf, sizeof(will_topic_buf), \"%s/%s\", ctx->publish.topic, JANUS_MQTTEVH_STATUS_TOPIC);\n\t\tctx->will.topic = g_strdup(will_topic_buf);\n\t}\n\n\t/* TLS config*/\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"tls_enable\");\n\t/* for old people */\n\tif(!item) {\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_enable\");\n\t}\n\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_INFO, \"MQTTEventHandler: MQTT TLS support disabled\\n\");\n\t} else {\n\t\tctx->tls.enable = TRUE;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"tls_cacert\");\n\t\tif(!item) {\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_cacert\");\n\t\t}\n\t\tif(item && item->value) {\n\t\t\tctx->tls.cacert_file = janus_make_absolute_path(config_path, item->value);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"tls_client_cert\");\n\t\tif(!item) {\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_client_cert\");\n\t\t}\n\t\tif(item && item->value) {\n\t\t\tctx->tls.cert_file = janus_make_absolute_path(config_path, item->value);\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"tls_client_key\");\n\t\tif(!item) {\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_client_key\");\n\t\t}\n\t\tif(item && item->value) {\n\t\t\tctx->tls.key_file = janus_make_absolute_path(config_path, item->value);\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"tls_verify_peer\");\n\t\tif(!item) {\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_verify_peer\");\n\t\t}\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tctx->tls.verify_peer = TRUE;\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"tls_verify_hostname\");\n\t\tif(!item) {\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_verify_hostname\");\n\t\t}\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tctx->tls.verify_host = TRUE;\n\t\t}\n\t}\n\n#ifdef MQTTVERSION_5\n\tif (ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\t/* MQTT 5 specific configuration */\n\t\tjanus_config_array *add_user_properties_array = janus_config_get(config, config_general, janus_config_type_array, \"add_user_properties\");\n\t\tif(add_user_properties_array) {\n\t\t\tGList *add_user_properties_array_items = janus_config_get_arrays(config, add_user_properties_array);\n\t\t\tif(add_user_properties_array_items != NULL) {\n\t\t\t\tint add_user_properties_array_len = g_list_length(add_user_properties_array_items);\n\t\t\t\tif(add_user_properties_array_len > 0) {\n\t\t\t\t\tctx->publish.add_user_properties = g_array_sized_new(FALSE, FALSE, sizeof(MQTTProperty), add_user_properties_array_len);\n\n\t\t\t\t\tjanus_mqttevh_set_add_user_property_user_data user_data = {\n\t\t\t\t\t\tctx->publish.add_user_properties,\n\t\t\t\t\t\tconfig\n\t\t\t\t\t};\n\n\t\t\t\t\tg_list_foreach(\n\t\t\t\t\t\tadd_user_properties_array_items,\n\t\t\t\t\t\t(GFunc)janus_mqttevh_set_add_user_property,\n\t\t\t\t\t\t(gpointer)&user_data\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n#endif\n\n\tif(!janus_mqtt_evh_enabled) {\n\t\tJANUS_LOG(LOG_WARN, \"MQTT event handler support disabled, giving up\\n\");\n\t\tgoto error;\n\t}\n\n\t/* Create a MQTT client */\n\tMQTTAsync_createOptions create_options = MQTTAsync_createOptions_initializer;\n\n#ifdef MQTTVERSION_5\n\tif (ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\tcreate_options.MQTTVersion = MQTTVERSION_5;\n\t}\n#endif\n\n\tcreate_options.maxBufferedMessages = ctx->connect.max_buffered;\n\n\tcreate_options.sendWhileDisconnected = TRUE;\n\tres = MQTTAsync_createWithOptions(\n\t\t&ctx->client,\n\t\tctx->connect.url,\n\t\tctx->connect.client_id,\n\t\tMQTTCLIENT_PERSISTENCE_NONE,\n\t\tNULL,\n\t\t&create_options\n\t);\n\n\t if(res != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't setup library for connection to MQTT broker %s: error %d creating client...\\n\",\n\t\t\tctx->connect.url, res);\n\t\tgoto error;\n\t}\n\n\t/* Set callbacks. We should not really subscribe to anything but nevertheless */\n\tres = MQTTAsync_setCallbacks(ctx->client,\n\t\t\tctx,\n\t\t\tjanus_mqttevh_client_connection_lost,\n\t\t\tjanus_mqttevh_client_message_arrived,\t//Needed\n\t\t\tjanus_mqttevh_client_delivery_complete);\n\n\tif(res != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Event handler: Can't setup MQTT broker %s: error %d setting up callbacks...\\n\",\n\t\t\tctx->connect.url, res);\n\t\tgoto error;\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"Event handler: About to connect to MQTT broker %s: ...\\n\",\n\t\tctx->connect.url);\n\n\t/* Connecting to the broker */\n\tint rc = janus_mqttevh_client_connect(ctx);\n\tif(rc != MQTTASYNC_SUCCESS) {\n\t\tconst char *error;\n\t\tswitch(rc) {\n\t\t\tcase 1: error = \"Connection refused - protocol version\";\n\t\t\t\tbreak;\n\t\t\tcase 2: error = \"Connection refused - identifier rejected\";\n\t\t\t\tbreak;\n\t\t\tcase 3: error = \"Connection refused - server unavailable\";\n\t\t\t\tbreak;\n\t\t\tcase 4: error = \"Connection refused - bad credentials\";\n\t\t\t\tbreak;\n\t\t\tcase 5: error = \"Connection refused - not authroized\";\n\t\t\t\tbreak;\n\t\t\tdefault: error = \"Connection refused - unknown error\";\n\t\t\t\tbreak;\n\t\t}\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker, return code: %d (%s)\\n\", rc, error);\n\t\tgoto error;\n\t}\n\n\t/* Initialize the events queue */\n\tevents = g_async_queue_new_full((GDestroyNotify)janus_mqttevh_event_free);\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Create the event handler thread */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"janus mqttevh handler\", janus_mqttevh_handler, ctx, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to launch the MQTT EventHandler handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tgoto error;\n\t}\n\n\t/* Done */\n\tif(config) {\n\t\tjanus_config_destroy(config);\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_MQTTEVH_NAME);\n\treturn 0;\n\nerror:\n\t/* If we got here, something went wrong */\n\tjanus_mqttevh_client_destroy_context(&ctx);\n\n\tif(config) {\n\t\tjanus_config_destroy(config);\n\t}\n\treturn -1;\n}\n\nstatic void janus_mqttevh_destroy(void) {\n\n\tif(!g_atomic_int_get(&initialized)) {\n\t\t/* We never started, so just quit */\n\t\treturn;\n\t}\n\tg_atomic_int_set(&stopping, 1);\n\n\t/* Put the exit event on the queue to stop the other thread */\n\tg_async_queue_push(events, &exit_event);\n\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\tg_async_queue_unref(events);\n\tevents = NULL;\n\n\t/* Shut down the MQTT connection now */\n\tjanus_mqttevh_client_disconnect(context);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_MQTTEVH_NAME);\n}\n\nstatic void janus_mqttevh_incoming_event(json_t *event) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t/* Janus is closing or the plugin is */\n\t\treturn;\n\t}\n\tjson_incref(event);\n\tg_async_queue_push(events, event);\n}\n\njson_t *janus_mqttevh_handle_request(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) return NULL;\n\n\t/* We can use this requests to apply tweaks to the logic */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_MQTTEVH_ERROR_MISSING_ELEMENT, JANUS_MQTTEVH_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"tweak\")) {\n\t\t/* We only support a request to tweak the current settings */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, tweak_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_MQTTEVH_ERROR_MISSING_ELEMENT, JANUS_MQTTEVH_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\t/* Events */\n\t\tif(json_object_get(request, \"events\"))\n\t\t\tjanus_events_edit_events_mask(json_string_value(json_object_get(request, \"events\")), &janus_mqttevh.events_mask);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_MQTTEVH_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tjson_t *response = json_object();\n\t\t\tif(error_code == 0) {\n\t\t\t\t/* Return a success */\n\t\t\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\t\t} else {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n\n/* Thread to handle incoming events and push them out on the MQTT. We\n * will publish events on multiple topics, depending on the event type.\n * If the base topic is configured to \"/janus/events\", then a handle\n * event will be published to \"/janus/events/handle\" */\nstatic void *janus_mqttevh_handler(void *data) {\n\tjanus_mqttevh_context *ctx = (janus_mqttevh_context *)data;\n\tjson_t *event = NULL;\n\tchar topicbuf[512];\n\ttopicbuf[0] = '\\0';\n\n\tJANUS_LOG(LOG_VERB, \"Joining MqttEventHandler handler thread\\n\");\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\t/* Get event from queue */\n\t\tevent = g_async_queue_pop(events);\n\t\tif(event == &exit_event) break;\n\n\t\t/* Handle event: just for fun, let's see how long it took for us to take care of this */\n\t\tjson_t *created = json_object_get(event, \"timestamp\");\n\t\tif(created && json_is_integer(created)) {\n\t\t\tgint64 then = json_integer_value(created);\n\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\tJANUS_LOG(LOG_DBG, \"Handled event after %\"SCNu64\" us\\n\", now-then);\n\t\t}\n\n\t\tint type = json_integer_value(json_object_get(event, \"type\"));\n\t\tconst char *elabel = janus_events_type_to_label(type);\n\t\tconst char *ename = janus_events_type_to_name(type);\n\n\t\t/* Hack to test new functions */\n\t\tif(elabel && ename) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"Event label %s, name %s\\n\", elabel, ename);\n\t\t\tjson_object_set_new(event, \"eventtype\", json_string(ename));\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Can't get event label or name\\n\");\n\t\t}\n\n\t\tif(!g_atomic_int_get(&stopping)) {\n\t\t\t/* Convert event to string */\n\t\t\tif(ctx->addevent) {\n\t\t\t\tg_snprintf(topicbuf, sizeof(topicbuf), \"%s/%s\", ctx->publish.topic, janus_events_type_to_label(type));\n\t\t\t\tJANUS_LOG(LOG_DBG, \"Debug: MQTT Publish event on %s\\n\", topicbuf);\n\t\t\t\tjanus_mqttevh_send_message(ctx, topicbuf, event);\n\t\t\t} else {\n\t\t\t\tjanus_mqttevh_send_message(ctx, ctx->publish.topic, event);\n\t\t\t}\n\t\t}\n\n\t\tJANUS_LOG(LOG_VERB, \"Debug: Thread done publishing MQTT Publish event on %s\\n\", topicbuf);\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving MQTTEventHandler handler thread\\n\");\n\treturn NULL;\n}\n\nint janus_mqttevh_client_get_response_code(MQTTAsync_failureData *response) {\n\treturn response ? response->code : 0;\n}\n\n#ifdef MQTTVERSION_5\nint janus_mqttevh_client_get_response_code5(MQTTAsync_failureData5 *response) {\n\treturn response ? response->code : 0;\n}\n\nvoid janus_mqttevh_add_properties(GArray *user_properties, MQTTProperties *properties) {\n\tif(user_properties == NULL || user_properties->len == 0) return;\n\n\tuint i = 0;\n\tfor(i = 0; i < user_properties->len; i++) {\n\t\tMQTTProperty *property = &g_array_index(user_properties, MQTTProperty, i);\n\t\tint rc = MQTTProperties_add(properties, property);\n\t\tif(rc != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to user properties to MQTT response\\n\");\n\t\t}\n\t}\n}\n\nvoid janus_mqttevh_set_add_user_property(gpointer item_ptr, gpointer user_data_ptr) {\n\tjanus_config_item *item = (janus_config_item*)item_ptr;\n\tif(item->value != NULL) return;\n\n\tjanus_mqttevh_set_add_user_property_user_data *user_data = (janus_mqttevh_set_add_user_property_user_data*)user_data_ptr;\n\tGList *key_value = janus_config_get_items(user_data->config, item);\n\tif(key_value == NULL || g_list_length(key_value) != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Expected a key-value pair\\n\");\n\t\treturn;\n\t}\n\n\tjanus_config_item *key_item = (janus_config_item*)g_list_first(key_value)->data;\n\tjanus_config_item *value_item = (janus_config_item*)g_list_last(key_value)->data;\n\n\tif(key_item->value == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Expected key item to have a value\\n\");\n\t} else if(value_item->value == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Expected value item to have a value\\n\");\n\t} else {\n\t\tMQTTProperty property;\n\t\tproperty.identifier = MQTTPROPERTY_CODE_USER_PROPERTY;\n\t\tproperty.value.data.data = g_strdup(key_item->value);\n\t\tproperty.value.data.len = strlen(key_item->value);\n\t\tproperty.value.value.data = g_strdup(value_item->value);\n\t\tproperty.value.value.len = strlen(value_item->value);\n\t\tg_array_append_val(user_data->acc, property);\n\t}\n}\n#endif\n"
  },
  {
    "path": "src/events/janus_nanomsgevh.c",
    "content": "/*! \\file   janus_nanomsgevh.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus NanomsgEventHandler plugin\n * \\details  This is a trivial Nanomsg event handler plugin for Janus\n *\n * \\ingroup eventhandlers\n * \\ref eventhandlers\n */\n\n#include \"eventhandler.h\"\n\n#include <math.h>\n\n#include <nanomsg/nn.h>\n#include <nanomsg/pubsub.h>\n#include <nanomsg/inproc.h>\n#include <nanomsg/ipc.h>\n#include <nanomsg/pipeline.h>\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../utils.h\"\n#include \"../events.h\"\n\n\n/* Plugin information */\n#define JANUS_NANOMSGEVH_VERSION\t\t\t1\n#define JANUS_NANOMSGEVH_VERSION_STRING\t\t\"0.0.1\"\n#define JANUS_NANOMSGEVH_DESCRIPTION\t\t\"This is a trivial Nanomsg event handler plugin for Janus.\"\n#define JANUS_NANOMSGEVH_NAME\t\t\t\t\"JANUS NanomsgEventHandler plugin\"\n#define JANUS_NANOMSGEVH_AUTHOR\t\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_NANOMSGEVH_PACKAGE\t\t\t\"janus.eventhandler.nanomsgevh\"\n\n/* Plugin methods */\njanus_eventhandler *create(void);\nint janus_nanomsgevh_init(const char *config_path);\nvoid janus_nanomsgevh_destroy(void);\nint janus_nanomsgevh_get_api_compatibility(void);\nint janus_nanomsgevh_get_version(void);\nconst char *janus_nanomsgevh_get_version_string(void);\nconst char *janus_nanomsgevh_get_description(void);\nconst char *janus_nanomsgevh_get_name(void);\nconst char *janus_nanomsgevh_get_author(void);\nconst char *janus_nanomsgevh_get_package(void);\nvoid janus_nanomsgevh_incoming_event(json_t *event);\njson_t *janus_nanomsgevh_handle_request(json_t *request);\n\n/* Event handler setup */\nstatic janus_eventhandler janus_nanomsgevh =\n\tJANUS_EVENTHANDLER_INIT (\n\t\t.init = janus_nanomsgevh_init,\n\t\t.destroy = janus_nanomsgevh_destroy,\n\n\t\t.get_api_compatibility = janus_nanomsgevh_get_api_compatibility,\n\t\t.get_version = janus_nanomsgevh_get_version,\n\t\t.get_version_string = janus_nanomsgevh_get_version_string,\n\t\t.get_description = janus_nanomsgevh_get_description,\n\t\t.get_name = janus_nanomsgevh_get_name,\n\t\t.get_author = janus_nanomsgevh_get_author,\n\t\t.get_package = janus_nanomsgevh_get_package,\n\n\t\t.incoming_event = janus_nanomsgevh_incoming_event,\n\t\t.handle_request = janus_nanomsgevh_handle_request,\n\n\t\t.events_mask = JANUS_EVENT_TYPE_NONE\n\t);\n\n/* Plugin creator */\njanus_eventhandler *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_NANOMSGEVH_NAME);\n\treturn &janus_nanomsgevh;\n}\n\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic GThread *pub_thread, *handler_thread;\nstatic void *janus_nanomsgevh_thread(void *data);\nstatic void *janus_nanomsgevh_handler(void *data);\n\n/* Queue of events to handle */\nstatic GAsyncQueue *events = NULL, *nfd_queue = NULL;\nstatic gboolean group_events = TRUE;\nstatic json_t exit_event;\nstatic void janus_nanomsgevh_event_free(json_t *event) {\n\tif(!event || event == &exit_event)\n\t\treturn;\n\tjson_decref(event);\n}\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Nanomsg stuff */\nstatic int nfd = -1, nfd_addr = -1, write_nfd[2];\n\n\n/* Parameter validation (for tweaking via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter tweak_parameters[] = {\n\t{\"events\", JSON_STRING, 0},\n\t{\"grouping\", JANUS_JSON_BOOL, 0}\n};\n/* Error codes (for the tweaking via Admin API */\n#define JANUS_NANOMSGEVH_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_NANOMSGEVH_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_NANOMSGEVH_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_NANOMSGEVH_ERROR_UNKNOWN_ERROR\t\t499\n\n\n/* Plugin implementation */\nint janus_nanomsgevh_init(const char *config_path) {\n\tgboolean success = TRUE;\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_NANOMSGEVH_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_NANOMSGEVH_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_NANOMSGEVH_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\n\t/* Setup the event handler, if required */\n\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_WARN, \"Nanomsg event handler disabled\\n\");\n\t\tgoto error;\n\t}\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\tif(item && item->value) {\n\t\t/* Check how we need to format/serialize the JSON output */\n\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t/* Compact, so no spaces between separators */\n\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t}\n\t}\n\n\t/* Which events should we subscribe to? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\tif(item && item->value)\n\t\tjanus_events_edit_events_mask(item->value, &janus_nanomsgevh.events_mask);\n\n\t/* Is grouping of events ok? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"grouping\");\n\tif(item && item->value)\n\t\tgroup_events = janus_is_true(item->value);\n\n\t/* First of all, initialize the pipeline for writeable notifications */\n\twrite_nfd[0] = nn_socket(AF_SP, NN_PULL);\n\twrite_nfd[1] = nn_socket(AF_SP, NN_PUSH);\n\tif(nn_bind(write_nfd[0], \"inproc://janusevh\") < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error configuring internal Nanomsg pipeline... %d (%s)\\n\", errno, nn_strerror(errno));\n\t\tgoto error;\n\t}\n\tif(nn_connect(write_nfd[1], \"inproc://janusevh\") < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error configuring internal Nanomsg pipeline...%d (%s)\\n\", errno, nn_strerror(errno));\n\t\tgoto error;\n\t}\n\t/* Handle the Nanomsg configuration */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"address\");\n\tconst char *address = item && item->value ? item->value : NULL;\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"mode\");\n\tconst char *mode = item && item->value ? item->value : NULL;\n\tif(mode == NULL)\n\t\tmode = \"connect\";\n\tnfd = nn_socket(AF_SP, NN_PUB);\n\tif(nfd < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error creating Nanomsg event handler socket: %d (%s)\\n\", errno, nn_strerror(errno));\n\t\tgoto error;\n\t}\n\tif(!strcasecmp(mode, \"bind\")) {\n\t\t/* Bind to this address */\n\t\tnfd_addr = nn_bind(nfd, address);\n\t\tif(nfd_addr < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error binding Nanomsg event handler socket to address '%s': %d (%s)\\n\",\n\t\t\t\taddress, errno, nn_strerror(errno));\n\t\t\tgoto error;\n\t\t}\n\t} else if(!strcasecmp(mode, \"connect\")) {\n\t\t/* Connect to this address */\n\t\tnfd_addr = nn_connect(nfd, address);\n\t\tif(nfd_addr < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error connecting Nanomsg event handler socket to address '%s': %d (%s)\\n\",\n\t\t\t\taddress, errno, nn_strerror(errno));\n\t\t\tgoto error;\n\t\t}\n\t} else {\n\t\t/* Unsupported mode */\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported mode '%s'\\n\", mode);\n\t\tgoto error;\n\t}\n\n\t/* Initialize the events queue */\n\tevents = g_async_queue_new_full((GDestroyNotify) janus_nanomsgevh_event_free);\n\tnfd_queue = g_async_queue_new_full((GDestroyNotify) g_free);\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Start the Nanomsg and event handler threads */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"janus nanomsgevh thread\", janus_nanomsgevh_thread, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to launch the NanomsgEventHandler loop thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tgoto error;\n\t}\n\terror = NULL;\n\thandler_thread = g_thread_try_new(\"janus nanomsgevh handler\", janus_nanomsgevh_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to launch the NanomsgEventHandler handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tgoto error;\n\t}\n\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"Setup of Nanomsg event handler completed\\n\");\n\tgoto done;\n\nerror:\n\t/* If we got here, something went wrong */\n\tsuccess = FALSE;\n\tif(write_nfd[0] > -1)\n\t\tnn_close(write_nfd[0]);\n\tif(write_nfd[1] > -1)\n\t\tnn_close(write_nfd[1]);\n\tif(nfd > -1) {\n\t\tnn_shutdown(nfd, nfd_addr);\n\t\tnn_close(nfd);\n\t}\n\t/* Fall through */\ndone:\n\tif(config)\n\t\tjanus_config_destroy(config);\n\tif(!success) {\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_NANOMSGEVH_NAME);\n\treturn 0;\n}\n\nvoid janus_nanomsgevh_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(events, &exit_event);\n\t(void)nn_send(write_nfd[1], \"x\", 1, 0);\n\tif(pub_thread != NULL) {\n\t\tg_thread_join(pub_thread);\n\t\tpub_thread = NULL;\n\t}\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\tg_async_queue_unref(events);\n\tevents = NULL;\n\tg_async_queue_unref(nfd_queue);\n\tnfd_queue = NULL;\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_NANOMSGEVH_NAME);\n}\n\nint janus_nanomsgevh_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_EVENTHANDLER_API_VERSION;\n}\n\nint janus_nanomsgevh_get_version(void) {\n\treturn JANUS_NANOMSGEVH_VERSION;\n}\n\nconst char *janus_nanomsgevh_get_version_string(void) {\n\treturn JANUS_NANOMSGEVH_VERSION_STRING;\n}\n\nconst char *janus_nanomsgevh_get_description(void) {\n\treturn JANUS_NANOMSGEVH_DESCRIPTION;\n}\n\nconst char *janus_nanomsgevh_get_name(void) {\n\treturn JANUS_NANOMSGEVH_NAME;\n}\n\nconst char *janus_nanomsgevh_get_author(void) {\n\treturn JANUS_NANOMSGEVH_AUTHOR;\n}\n\nconst char *janus_nanomsgevh_get_package(void) {\n\treturn JANUS_NANOMSGEVH_PACKAGE;\n}\n\nvoid janus_nanomsgevh_incoming_event(json_t *event) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t/* Janus is closing or the plugin is */\n\t\treturn;\n\t}\n\n\t/* Do NOT handle the event here in this callback! Since Janus notifies you right\n\t * away when something happens, these events are triggered from working threads and\n\t * not some sort of message bus. As such, performing I/O or network operations in\n\t * here could dangerously slow Janus down. Let's just reference and enqueue the event,\n\t * and handle it in our own thread: the event contains a monotonic time indicator of\n\t * when the event actually happened on this machine, so that, if relevant, we can compute\n\t * any delay in the actual event processing ourselves. */\n\tjson_incref(event);\n\tg_async_queue_push(events, event);\n}\n\njson_t *janus_nanomsgevh_handle_request(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this requests to apply tweaks to the logic */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_NANOMSGEVH_ERROR_MISSING_ELEMENT, JANUS_NANOMSGEVH_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"tweak\")) {\n\t\t/* We only support a request to tweak the current settings */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, tweak_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_NANOMSGEVH_ERROR_MISSING_ELEMENT, JANUS_NANOMSGEVH_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\t/* Events */\n\t\tif(json_object_get(request, \"events\"))\n\t\t\tjanus_events_edit_events_mask(json_string_value(json_object_get(request, \"events\")), &janus_nanomsgevh.events_mask);\n\t\t/* Grouping */\n\t\tif(json_object_get(request, \"grouping\"))\n\t\t\tgroup_events = json_is_true(json_object_get(request, \"grouping\"));\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_NANOMSGEVH_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tjson_t *response = json_object();\n\t\t\tif(error_code == 0) {\n\t\t\t\t/* Return a success */\n\t\t\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\t\t} else {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n/* Thread to handle incoming events */\nstatic void *janus_nanomsgevh_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining NanomsgEventHandler handler thread\\n\");\n\tjson_t *event = NULL, *output = NULL;\n\tchar *event_text = NULL;\n\tint count = 0, max = group_events ? 100 : 1;\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\n\t\tevent = g_async_queue_pop(events);\n\t\tif(event == &exit_event)\n\t\t\tbreak;\n\t\tcount = 0;\n\t\toutput = NULL;\n\n\t\twhile(TRUE) {\n\t\t\t/* Handle event: just for fun, let's see how long it took for us to take care of this */\n\t\t\tjson_t *created = json_object_get(event, \"timestamp\");\n\t\t\tif(created && json_is_integer(created)) {\n\t\t\t\tgint64 then = json_integer_value(created);\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\tJANUS_LOG(LOG_DBG, \"Handled event after %\"SCNu64\" us\\n\", now-then);\n\t\t\t}\n\t\t\tif(!group_events) {\n\t\t\t\t/* We're done here, we just need a single event */\n\t\t\t\toutput = event;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t/* If we got here, we're grouping */\n\t\t\tif(output == NULL)\n\t\t\t\toutput = json_array();\n\t\t\tjson_array_append_new(output, event);\n\t\t\t/* Never group more than a maximum number of events, though, or we might stay here forever */\n\t\t\tcount++;\n\t\t\tif(count == max)\n\t\t\t\tbreak;\n\t\t\tevent = g_async_queue_try_pop(events);\n\t\t\tif(event == NULL || event == &exit_event)\n\t\t\t\tbreak;\n\t\t}\n\n\t\tif(!g_atomic_int_get(&stopping)) {\n\t\t\t/* Since this a simple plugin, it does the same for all events: so just convert to string... */\n\t\t\tevent_text = json_dumps(output, json_format);\n\t\t\tif(event_text == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Failed to stringify event, event lost...\\n\");\n\t\t\t\t/* Nothing we can do... get rid of the event */\n\t\t\t\tjson_decref(output);\n\t\t\t\toutput = NULL;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tg_async_queue_push(nfd_queue, event_text);\n\t\t\t(void)nn_send(write_nfd[1], \"x\", 1, 0);\n\t\t}\n\n\t\t/* Done, let's unref the event */\n\t\tjson_decref(output);\n\t\toutput = NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving NanomsgEventHandler handler thread\\n\");\n\treturn NULL;\n}\n\n/* Thread */\nvoid *janus_nanomsgevh_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining NanomsgEventHandler loop thread\\n\");\n\n\tint fds = 0;\n\tstruct nn_pollfd poll_nfds[2];\n\tchar buffer[1];\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\t/* Prepare poll list of file descriptors */\n\t\tfds = 0;\n\t\t/* Writeable monitor */\n\t\tpoll_nfds[fds].fd = write_nfd[0];\n\t\tpoll_nfds[fds].events = NN_POLLIN;\n\t\tfds++;\n\t\t/* Publisher socket */\n\t\tif(nfd > -1 && g_async_queue_length(nfd_queue) > 0) {\n\t\t\tpoll_nfds[fds].fd = nfd;\n\t\t\tpoll_nfds[fds].events |= NN_POLLOUT;\n\t\t\tfds++;\n\t\t}\n\t\t/* Start polling */\n\t\tint res = nn_poll(poll_nfds, fds, -1);\n\t\tif(res == 0)\n\t\t\tcontinue;\n\t\tif(res < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got an EINTR (%s) polling the Nanomsg descriptors, ignoring...\\n\", nn_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"poll() failed: %d (%s)\\n\", errno, nn_strerror(errno));\n\t\t\tbreak;\n\t\t}\n\t\tint i = 0;\n\t\tfor(i=0; i<fds; i++) {\n\t\t\t/* FIXME Is there a Nanomsg equivalent of POLLERR? */\n\t\t\tif(poll_nfds[i].revents & NN_POLLOUT) {\n\t\t\t\t/* Find the client from its file descriptor */\n\t\t\t\tif(poll_nfds[i].fd == nfd) {\n\t\t\t\t\tchar *payload = NULL;\n\t\t\t\t\twhile((payload = g_async_queue_try_pop(nfd_queue)) != NULL) {\n\t\t\t\t\t\tint res = nn_send(poll_nfds[i].fd, payload, strlen(payload), 0);\n\t\t\t\t\t\t/* FIXME Should we check if sent everything? */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Written %d/%zu bytes on %d\\n\", res, strlen(payload), poll_nfds[i].fd);\n\t\t\t\t\t\tg_free(payload);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(poll_nfds[i].revents & NN_POLLIN) {\n\t\t\t\tif(poll_nfds[i].fd == write_nfd[0]) {\n\t\t\t\t\t/* Read and ignore: we use this to unlock the poll if there's data to write */\n\t\t\t\t\t(void)nn_recv(poll_nfds[i].fd, buffer, sizeof(buffer), 0);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tnn_close(write_nfd[0]);\n\tnn_close(write_nfd[1]);\n\tif(nfd > -1) {\n\t\tnn_shutdown(nfd, nfd_addr);\n\t\tnn_close(nfd);\n\t}\n\n\t/* Done */\n\tJANUS_LOG(LOG_VERB, \"Leaving NanomsgEventHandler loop thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/events/janus_rabbitmqevh.c",
    "content": "/*! \\file   janus_rabbitmqevh.c\n * \\author Piter Konstantinov <pit.here@gmail.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus RabbitMQEventHandler plugin\n * \\details  This is a trivial RabbitMQ event handler plugin for Janus\n *\n * \\ingroup eventhandlers\n * \\ref eventhandlers\n */\n\n#include \"eventhandler.h\"\n\n#include <math.h>\n\n/* Latest RabbitMQ-C library changes the library paths from 0.12.0.0 onwards */\n#ifdef HAVE_RABBITMQ_C_AMQP_H\n#include <rabbitmq-c/amqp.h>\n#include <rabbitmq-c/framing.h>\n#include <rabbitmq-c/tcp_socket.h>\n#include <rabbitmq-c/ssl_socket.h>\n#else\n#include <amqp.h>\n#include <amqp_framing.h>\n#include <amqp_tcp_socket.h>\n#include <amqp_ssl_socket.h>\n#endif\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n#include \"../events.h\"\n\n\n/* Plugin information */\n#define JANUS_RABBITMQEVH_VERSION\t\t\t1\n#define JANUS_RABBITMQEVH_VERSION_STRING\t\"0.0.1\"\n#define JANUS_RABBITMQEVH_DESCRIPTION\t\t\"This is a trivial RabbitMQ event handler plugin for Janus.\"\n#define JANUS_RABBITMQEVH_NAME\t\t\t\t\"JANUS RabbitMQEventHandler plugin\"\n#define JANUS_RABBITMQEVH_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_RABBITMQEVH_PACKAGE\t\t\t\"janus.eventhandler.rabbitmqevh\"\n\n/* Plugin methods */\njanus_eventhandler *create(void);\nint janus_rabbitmqevh_init(const char *config_path);\nvoid janus_rabbitmqevh_destroy(void);\nint janus_rabbitmqevh_get_api_compatibility(void);\nint janus_rabbitmqevh_get_version(void);\nconst char *janus_rabbitmqevh_get_version_string(void);\nconst char *janus_rabbitmqevh_get_description(void);\nconst char *janus_rabbitmqevh_get_name(void);\nconst char *janus_rabbitmqevh_get_author(void);\nconst char *janus_rabbitmqevh_get_package(void);\nvoid janus_rabbitmqevh_incoming_event(json_t *event);\njson_t *janus_rabbitmqevh_handle_request(json_t *request);\n\n/* Event handler setup */\nstatic janus_eventhandler janus_rabbitmqevh =\n\tJANUS_EVENTHANDLER_INIT (\n\t\t.init = janus_rabbitmqevh_init,\n\t\t.destroy = janus_rabbitmqevh_destroy,\n\n\t\t.get_api_compatibility = janus_rabbitmqevh_get_api_compatibility,\n\t\t.get_version = janus_rabbitmqevh_get_version,\n\t\t.get_version_string = janus_rabbitmqevh_get_version_string,\n\t\t.get_description = janus_rabbitmqevh_get_description,\n\t\t.get_name = janus_rabbitmqevh_get_name,\n\t\t.get_author = janus_rabbitmqevh_get_author,\n\t\t.get_package = janus_rabbitmqevh_get_package,\n\n\t\t.incoming_event = janus_rabbitmqevh_incoming_event,\n\t\t.handle_request = janus_rabbitmqevh_handle_request,\n\n\t\t.events_mask = JANUS_EVENT_TYPE_NONE\n\t);\n\n/* Plugin creator */\njanus_eventhandler *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_RABBITMQEVH_NAME);\n\treturn &janus_rabbitmqevh;\n}\n\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic GThread *handler_thread;\nstatic GThread *in_thread;\nstatic void *jns_rmqevh_hdlr(void *data);\nstatic void *jns_rmqevh_hrtbt(void *data);\nint janus_rabbitmqevh_connect(void);\n\n/* Queue of events to handle */\nstatic GAsyncQueue *events = NULL;\nstatic gboolean group_events = TRUE;\nstatic json_t exit_event;\nstatic void janus_rabbitmqevh_event_free(json_t *event) {\n\tif(!event || event == &exit_event)\n\t\treturn;\n\tjson_decref(event);\n}\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n#define JANUS_RABBITMQEVH_EXCHANGE_TYPE \"fanout\"\n\n/* RabbitMQ session */\nstatic amqp_connection_state_t rmq_conn;\nstatic amqp_channel_t rmq_channel = 0;\nstatic amqp_bytes_t rmq_exchange;\n\nstatic janus_mutex mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic char *rmqhost = NULL;\nstatic char *vhost = NULL, *username = NULL, *password = NULL;\nstatic char *ssl_cacert_file = NULL;\nstatic char *ssl_cert_file = NULL;\nstatic char *ssl_key_file = NULL;\nstatic gboolean ssl_enable = FALSE;\nstatic gboolean ssl_verify_peer = FALSE;\nstatic gboolean ssl_verify_hostname = FALSE;\nstatic char *route_key = NULL, *exchange = NULL, *exchange_type = NULL ;\nstatic uint16_t heartbeat = 0;\nstatic uint16_t rmqport = AMQP_PROTOCOL_PORT;\nstatic gboolean declare_outgoing_queue = TRUE;\n\n/* Parameter validation (for tweaking via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter tweak_parameters[] = {\n\t{\"events\", JSON_STRING, 0},\n\t{\"grouping\", JANUS_JSON_BOOL, 0}\n};\n/* Error codes (for the tweaking via Admin API */\n#define JANUS_RABBITMQEVH_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_RABBITMQEVH_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_RABBITMQEVH_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_RABBITMQEVH_ERROR_UNKNOWN_ERROR\t\t\t499\n\n\n/* Plugin implementation */\nint janus_rabbitmqevh_init(const char *config_path) {\n\tgboolean success = TRUE;\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_RABBITMQEVH_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_RABBITMQEVH_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_RABBITMQEVH_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\n\t/* Setup the event handler, if required */\n\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_WARN, \"RabbitMQ event handler disabled\\n\");\n\t\tgoto error;\n\t}\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\tif(item && item->value) {\n\t\t/* Check how we need to format/serialize the JSON output */\n\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t/* Compact, so no spaces between separators */\n\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"RabbitMQEventHandler: Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t}\n\t}\n\n\t/* Which events should we subscribe to? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\tif(item && item->value)\n\t\tjanus_events_edit_events_mask(item->value, &janus_rabbitmqevh.events_mask);\n\n\t/* Is grouping of events ok? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"grouping\");\n\tif(item && item->value)\n\t\tgroup_events = janus_is_true(item->value);\n\n\t/* Handle configuration, starting from the server details */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"host\");\n\tif(item && item->value)\n\t\trmqhost = g_strdup(item->value);\n\telse\n\t\trmqhost = g_strdup(\"localhost\");\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"port\");\n\tif(item && item->value && janus_string_to_uint16(item->value, &rmqport) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s), falling back to default\\n\", item->value);\n\t\trmqport = AMQP_PROTOCOL_PORT;\n\t}\n\n\t/* Credentials and Virtual Host */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"vhost\");\n\tif(item && item->value)\n\t\tvhost = g_strdup(item->value);\n\telse\n\t\tvhost = g_strdup(\"/\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"username\");\n\tif(item && item->value)\n\t\tusername = g_strdup(item->value);\n\telse\n\t\tusername = g_strdup(\"guest\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"password\");\n\tif(item && item->value)\n\t\tpassword = g_strdup(item->value);\n\telse\n\t\tpassword = g_strdup(\"guest\");\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"heartbeat\");\n\tif(item && item->value && janus_string_to_uint16(item->value, &heartbeat) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"RabbitMQEventHandler: Invalid heartbeat timeout (%s), falling back to default (0, disabling heartbeat)\\n\", item->value);\n\t\theartbeat = 0;\n\t}\n\n\t/* SSL config*/\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_enable\");\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_INFO, \"RabbitMQEventHandler: RabbitMQ SSL support disabled\\n\");\n\t} else {\n\t\tssl_enable = TRUE;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_cacert\");\n\t\tif(item && item->value)\n\t\t\tssl_cacert_file = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_cert\");\n\t\tif(item && item->value)\n\t\t\tssl_cert_file = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_key\");\n\t\tif(item && item->value)\n\t\t\tssl_key_file = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_verify_peer\");\n\t\tif(item && item->value && janus_is_true(item->value))\n\t\t\tssl_verify_peer = TRUE;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_verify_hostname\");\n\t\tif(item && item->value && janus_is_true(item->value))\n\t\t\tssl_verify_hostname = TRUE;\n\t}\n\n\t/* Parse configuration */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"route_key\");\n\tif(!item || !item->value) {\n\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Missing name of outgoing route_key for RabbitMQ...\\n\");\n\t\tgoto error;\n\t}\n\troute_key = g_strdup(item->value);\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"exchange_type\");\n\tif(!item || !item->value) {\n\t\texchange_type = (char *)JANUS_RABBITMQEVH_EXCHANGE_TYPE;\n\t} else {\n\t\texchange_type = g_strdup(item->value);\n\t}\n\n\t/* By default we *DO* declare the outgoing queue */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"declare_outgoing_queue\");\n\tif(item && item->value && !janus_is_true(item->value)) {\n\t\tdeclare_outgoing_queue = FALSE;\n\t}\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"exchange\");\n\tif(!item || !item->value) {\n\t\tJANUS_LOG(LOG_INFO, \"RabbitMQEventHandler: Missing name of outgoing exchange for RabbitMQ, using default\\n\");\n\t} else {\n\t\texchange = g_strdup(item->value);\n\t}\n\tif (exchange == NULL) {\n\t\tJANUS_LOG(LOG_INFO, \"RabbitMQEventHandler: enabled, %s:%d (%s) exchange_type:%s\\n\", rmqhost, rmqport, route_key,exchange_type);\n\t} else {\n\t\tJANUS_LOG(LOG_INFO, \"RabbitMQEventHandler: enabled, %s:%d (%s) exch: (%s) exchange_type:%s\\n\", rmqhost, rmqport, route_key, exchange,exchange_type);\n\t}\n\n\t/* Connect */\n\tint result = janus_rabbitmqevh_connect();\n\tif(result < 0) {\n\t\tgoto error;\n\t}\n\n\t/* Initialize the events queue */\n\tevents = g_async_queue_new_full((GDestroyNotify) janus_rabbitmqevh_event_free);\n\tg_atomic_int_set(&initialized, 1);\n\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"janus rabbitmqevh handler\", jns_rmqevh_hdlr, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Got error %d (%s) trying to launch the RabbitMQEventHandler handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tgoto error;\n\t}\n\tif(heartbeat > 0) {\n\t\tin_thread = g_thread_try_new(\"janus rabbitmqevh heartbeat handler\", jns_rmqevh_hrtbt, NULL, &error);\n\t\tif(error != NULL) {\n\t\t\tg_atomic_int_set(&initialized, 0);\n\t\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Got error %d (%s) trying to launch the RabbitMQEventHandler heartbeat thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tgoto error;\n\t\t}\n\t}\n\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"Setup of RabbitMQ event handler completed\\n\");\n\tgoto done;\n\nerror:\n\t/* If we got here, something went wrong */\n\tsuccess = FALSE;\n\tg_free(route_key);\n\tg_free(exchange);\n\t/* Fall through */\ndone:\n\tif(config)\n\t\tjanus_config_destroy(config);\n\n\tif(!success) {\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_RABBITMQEVH_NAME);\n\treturn 0;\n}\n\nint janus_rabbitmqevh_connect(void) {\n\trmq_conn = amqp_new_connection();\n\tamqp_socket_t *socket = NULL;\n\tint status = AMQP_STATUS_OK;\n\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Creating RabbitMQ socket...\\n\");\n\tif (ssl_enable) {\n\t\tsocket = amqp_ssl_socket_new(rmq_conn);\n\t\tif(socket == NULL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error creating socket...\\n\");\n\t\t\treturn -1;\n\t\t}\n\n\t\tamqp_ssl_socket_set_verify_peer(socket, ssl_verify_peer);\n\t\tamqp_ssl_socket_set_verify_hostname(socket, ssl_verify_hostname);\n\n\t\tif(ssl_cacert_file) {\n\t\t\tstatus = amqp_ssl_socket_set_cacert(socket, ssl_cacert_file);\n\t\t\tif(status != AMQP_STATUS_OK) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error setting CA certificate... (%s)\\n\", amqp_error_string2(status));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t\tif(ssl_cert_file && ssl_key_file) {\n\t\t\tamqp_ssl_socket_set_key(socket, ssl_cert_file, ssl_key_file);\n\t\t\tif(status != AMQP_STATUS_OK) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error setting key... (%s)\\n\", amqp_error_string2(status));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tsocket = amqp_tcp_socket_new(rmq_conn);\n\t\tif(socket == NULL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error creating socket...\\n\");\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Connecting to RabbitMQ server...\\n\");\n\tstatus = amqp_socket_open(socket, rmqhost, rmqport);\n\tif(status != AMQP_STATUS_OK) {\n\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error opening socket... (%s)\\n\", amqp_error_string2(status));\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Logging in...\\n\");\n\tamqp_rpc_reply_t result = amqp_login(rmq_conn, vhost, 0, 131072, heartbeat, AMQP_SASL_METHOD_PLAIN, username, password);\n\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error logging in... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\treturn -1;\n\t}\n\n\trmq_channel = 1;\n\tJANUS_LOG(LOG_VERB, \"Opening channel...\\n\");\n\tamqp_channel_open(rmq_conn, rmq_channel);\n\tresult = amqp_get_rpc_reply(rmq_conn);\n\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error opening channel... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\treturn -1;\n\t}\n\trmq_exchange = amqp_empty_bytes;\n\tif(exchange != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Declaring exchange...\\n\");\n\t\trmq_exchange = amqp_cstring_bytes(exchange);\n\t\tamqp_exchange_declare(rmq_conn, rmq_channel, rmq_exchange, amqp_cstring_bytes(exchange_type), 0, 0, 0, 0, amqp_empty_table);\n\t\tresult = amqp_get_rpc_reply(rmq_conn);\n\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error declaring exchange... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tif (declare_outgoing_queue) {\n\t\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Declaring outgoing queue... (%s)\\n\", route_key);\n\t\tamqp_queue_declare(rmq_conn, rmq_channel, amqp_cstring_bytes(route_key), 0, 0, 0, 0, amqp_empty_table);\n\t\tresult = amqp_get_rpc_reply(rmq_conn);\n\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"RabbitMQEventHandler: Can't connect to RabbitMQ server: error declaring queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"RabbitMQEventHandler: Connected successfully\\n\");\n\n\treturn 0;\n}\n\nvoid janus_rabbitmqevh_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(events, &exit_event);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\tif(in_thread != NULL) {\n\t\tg_thread_join(in_thread);\n\t\tin_thread = NULL;\n\t}\n\n\tg_async_queue_unref(events);\n\tevents = NULL;\n\n\tif(rmq_conn) {\n\t\tamqp_destroy_connection(rmq_conn);\n\t}\n\tg_free(rmq_exchange.bytes);\n\tg_free(rmqhost);\n\tg_free(vhost);\n\tg_free(username);\n\tg_free(password);\n\tg_free(ssl_cacert_file);\n\tg_free(ssl_cert_file);\n\tg_free(ssl_key_file);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_RABBITMQEVH_NAME);\n}\n\nint janus_rabbitmqevh_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_EVENTHANDLER_API_VERSION;\n}\n\nint janus_rabbitmqevh_get_version(void) {\n\treturn JANUS_RABBITMQEVH_VERSION;\n}\n\nconst char *janus_rabbitmqevh_get_version_string(void) {\n\treturn JANUS_RABBITMQEVH_VERSION_STRING;\n}\n\nconst char *janus_rabbitmqevh_get_description(void) {\n\treturn JANUS_RABBITMQEVH_DESCRIPTION;\n}\n\nconst char *janus_rabbitmqevh_get_name(void) {\n\treturn JANUS_RABBITMQEVH_NAME;\n}\n\nconst char *janus_rabbitmqevh_get_author(void) {\n\treturn JANUS_RABBITMQEVH_AUTHOR;\n}\n\nconst char *janus_rabbitmqevh_get_package(void) {\n\treturn JANUS_RABBITMQEVH_PACKAGE;\n}\n\nvoid janus_rabbitmqevh_incoming_event(json_t *event) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t/* Janus is closing or the plugin is */\n\t\treturn;\n\t}\n\n\t/* Do NOT handle the event here in this callback! Since Janus notifies you right\n\t * away when something happens, these events are triggered from working threads and\n\t * not some sort of message bus. As such, performing I/O or network operations in\n\t * here could dangerously slow Janus down. Let's just reference and enqueue the event,\n\t * and handle it in our own thread: the event contains a monotonic time indicator of\n\t * when the event actually happened on this machine, so that, if relevant, we can compute\n\t * any delay in the actual event processing ourselves. */\n\tjson_incref(event);\n\tg_async_queue_push(events, event);\n}\n\njson_t *janus_rabbitmqevh_handle_request(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this requests to apply tweaks to the logic */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_RABBITMQEVH_ERROR_MISSING_ELEMENT, JANUS_RABBITMQEVH_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"tweak\")) {\n\t\t/* We only support a request to tweak the current settings */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, tweak_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_RABBITMQEVH_ERROR_MISSING_ELEMENT, JANUS_RABBITMQEVH_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\t/* Events */\n\t\tif(json_object_get(request, \"events\"))\n\t\t\tjanus_events_edit_events_mask(json_string_value(json_object_get(request, \"events\")), &janus_rabbitmqevh.events_mask);\n\t\t/* Grouping */\n\t\tif(json_object_get(request, \"grouping\"))\n\t\t\tgroup_events = json_is_true(json_object_get(request, \"grouping\"));\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_RABBITMQEVH_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tjson_t *response = json_object();\n\t\t\tif(error_code == 0) {\n\t\t\t\t/* Return a success */\n\t\t\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\t\t} else {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n/* Thread to handle incoming events */\nstatic void *jns_rmqevh_hdlr(void *data) {\n\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: joining handler thread\\n\");\n\tjson_t *event = NULL, *output = NULL;\n\tchar *event_text = NULL;\n\tint count = 0, max = group_events ? 100 : 1;\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\n\t\tevent = g_async_queue_pop(events);\n\t\tif(event == &exit_event)\n\t\t\tbreak;\n\t\tcount = 0;\n\t\toutput = NULL;\n\n\t\twhile(TRUE) {\n\t\t\t/* Handle event: just for fun, let's see how long it took for us to take care of this */\n\t\t\tjson_t *created = json_object_get(event, \"timestamp\");\n\t\t\tif(created && json_is_integer(created)) {\n\t\t\t\tgint64 then = json_integer_value(created);\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\tJANUS_LOG(LOG_DBG, \"RabbitMQEventHandler: Handled event after %\"SCNu64\" us\\n\", now-then);\n\t\t\t}\n\t\t\tif(!group_events) {\n\t\t\t\t/* We're done here, we just need a single event */\n\t\t\t\toutput = event;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t/* If we got here, we're grouping */\n\t\t\tif(output == NULL)\n\t\t\t\toutput = json_array();\n\t\t\tjson_array_append_new(output, event);\n\t\t\t/* Never group more than a maximum number of events, though, or we might stay here forever */\n\t\t\tcount++;\n\t\t\tif(count == max)\n\t\t\t\tbreak;\n\t\t\tevent = g_async_queue_try_pop(events);\n\t\t\tif(event == NULL || event == &exit_event)\n\t\t\t\tbreak;\n\t\t}\n\n\t\tif(!g_atomic_int_get(&stopping)) {\n\t\t\t/* Since this a simple plugin, it does the same for all events: so just convert to string... */\n\t\t\tevent_text = json_dumps(output, json_format);\n\t\t\tif(event_text == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"RabbitMQEventHandler: Failed to stringify event, event lost...\\n\");\n\t\t\t\t/* Nothing we can do... get rid of the event */\n\t\t\t\tjson_decref(output);\n\t\t\t\toutput = NULL;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tamqp_basic_properties_t props;\n\t\t\tprops._flags = 0;\n\t\t\tprops._flags |= AMQP_BASIC_CONTENT_TYPE_FLAG;\n\t\t\tprops.content_type = amqp_cstring_bytes(\"application/json\");\n\t\t\tamqp_bytes_t message = amqp_cstring_bytes(event_text);\n\t\t\tjanus_mutex_lock(&mutex);\n\t\t\tint status = amqp_basic_publish(rmq_conn, rmq_channel, rmq_exchange, amqp_cstring_bytes(route_key), 0, 0, &props, message);\n\t\t\tif(status != AMQP_STATUS_OK) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"RabbitMQEventHandler: Error publishing... %d, %s\\n\", status, amqp_error_string2(status));\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&mutex);\n\t\t\tfree(event_text);\n\t\t\tevent_text = NULL;\n\t\t}\n\n\t\t/* Done, let's unref the event */\n\t\tjson_decref(output);\n\t\toutput = NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: leaving handler thread\\n\");\n\treturn NULL;\n}\n\n\n/* Thread to handle heartbeats */\nstatic void *jns_rmqevh_hrtbt(void *data) {\n\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Monitoring RabbitMQ Heartbeat\\n\");\n\tint waiting_usec = (heartbeat/2) * 1000000;\n\tstruct timeval timeout;\n\ttimeout.tv_sec = 0;\n\ttimeout.tv_usec = 0;\n\tamqp_frame_t frame;\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tjanus_mutex_lock(&mutex);\n\t\tamqp_maybe_release_buffers(rmq_conn);\n\t\t/* Wait for a frame */\n\t\tint res = amqp_simple_wait_frame_noblock(rmq_conn, &frame, &timeout);\n\t\tjanus_mutex_unlock(&mutex);\n\t\tif(res != AMQP_STATUS_OK) {\n\t\t\t/* No data */\n\t\t\tif(res == AMQP_STATUS_TIMEOUT || res == AMQP_STATUS_SSL_ERROR) {\n\t\t\t\t/* Wait half of heartbeat before test again*/\n\t\t\t\tg_usleep(waiting_usec);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Error on amqp_simple_wait_frame_noblock: %d (%s)\\n\", res, amqp_error_string2(res));\n\n\t\t\tif(rmq_conn) {\n\t\t\t\tamqp_destroy_connection(rmq_conn);\n\t\t\t}\n\t\t\tif(!g_atomic_int_get(&stopping)) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Trying to reconnect\\n\");\n\t\t\t\tint result = janus_rabbitmqevh_connect();\n\t\t\t\tif(result < 0) {\n\t\t\t\t\tg_usleep(5000000);\n\t\t\t\t} else {\n\t\t\t\t\tg_usleep(waiting_usec);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t/* Wait half of heartbeat before test again*/\n\t\t\tg_usleep(waiting_usec);\n\t\t}\n\t}\n\n\tJANUS_LOG(LOG_VERB, \"RabbitMQEventHandler: Leaving HeartBeat thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/events/janus_sampleevh.c",
    "content": "/*! \\file   janus_sampleevh.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus SampleEventHandler plugin\n * \\details  This is a trivial event handler plugin for Janus, which is only\n * there to showcase how you can handle an event coming from the Janus core\n * or one of the plugins. This specific plugin forwards every event it receives\n * to a web server via an HTTP POST request, using libcurl.\n *\n * \\ingroup eventhandlers\n * \\ref eventhandlers\n */\n\n#include \"eventhandler.h\"\n\n#include <math.h>\n#include <curl/curl.h>\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n#include \"../events.h\"\n\n\n/* Plugin information */\n#define JANUS_SAMPLEEVH_VERSION\t\t\t1\n#define JANUS_SAMPLEEVH_VERSION_STRING\t\"0.0.1\"\n#define JANUS_SAMPLEEVH_DESCRIPTION\t\t\"This is a trivial sample event handler plugin for Janus, which forwards events via HTTP POST.\"\n#define JANUS_SAMPLEEVH_NAME\t\t\t\"JANUS SampleEventHandler plugin\"\n#define JANUS_SAMPLEEVH_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_SAMPLEEVH_PACKAGE\t\t\t\"janus.eventhandler.sampleevh\"\n\n/* Plugin methods */\njanus_eventhandler *create(void);\nint janus_sampleevh_init(const char *config_path);\nvoid janus_sampleevh_destroy(void);\nint janus_sampleevh_get_api_compatibility(void);\nint janus_sampleevh_get_version(void);\nconst char *janus_sampleevh_get_version_string(void);\nconst char *janus_sampleevh_get_description(void);\nconst char *janus_sampleevh_get_name(void);\nconst char *janus_sampleevh_get_author(void);\nconst char *janus_sampleevh_get_package(void);\nvoid janus_sampleevh_incoming_event(json_t *event);\njson_t *janus_sampleevh_handle_request(json_t *request);\n\n/* Event handler setup */\nstatic janus_eventhandler janus_sampleevh =\n\tJANUS_EVENTHANDLER_INIT (\n\t\t.init = janus_sampleevh_init,\n\t\t.destroy = janus_sampleevh_destroy,\n\n\t\t.get_api_compatibility = janus_sampleevh_get_api_compatibility,\n\t\t.get_version = janus_sampleevh_get_version,\n\t\t.get_version_string = janus_sampleevh_get_version_string,\n\t\t.get_description = janus_sampleevh_get_description,\n\t\t.get_name = janus_sampleevh_get_name,\n\t\t.get_author = janus_sampleevh_get_author,\n\t\t.get_package = janus_sampleevh_get_package,\n\n\t\t.incoming_event = janus_sampleevh_incoming_event,\n\t\t.handle_request = janus_sampleevh_handle_request,\n\n\t\t.events_mask = JANUS_EVENT_TYPE_NONE\n\t);\n\n/* Plugin creator */\njanus_eventhandler *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_SAMPLEEVH_NAME);\n\treturn &janus_sampleevh;\n}\n\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic GThread *handler_thread;\nstatic void *janus_sampleevh_handler(void *data);\nstatic janus_mutex evh_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Compression, if any */\nstatic gboolean compress = FALSE;\nstatic int compression = 6;\t\t/* Z_DEFAULT_COMPRESSION */\n\n/* Queue of events to handle */\nstatic GAsyncQueue *events = NULL;\nstatic gboolean group_events = TRUE;\nstatic json_t exit_event;\nstatic void janus_sampleevh_event_free(json_t *event) {\n\tif(!event || event == &exit_event)\n\t\treturn;\n\tjson_decref(event);\n}\n\n/* Retransmission management */\nstatic int max_retransmissions = 5;\nstatic int retransmissions_backoff = 100;\n\n/* Web backend to send the events to */\nstatic char *backend = NULL;\nstatic char *backend_user = NULL, *backend_pwd = NULL;\nstatic size_t janus_sampleehv_write_data(void *buffer, size_t size, size_t nmemb, void *userp) {\n\treturn size*nmemb;\n}\n\n\n/* Parameter validation (for tweaking via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter tweak_parameters[] = {\n\t{\"events\", JSON_STRING, 0},\n\t{\"grouping\", JANUS_JSON_BOOL, 0},\n\t{\"backend\", JSON_STRING, 0},\n\t{\"backend_user\", JSON_STRING, 0},\n\t{\"backend_pwd\", JSON_STRING, 0},\n\t{\"max_retransmissions\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"retransmissions_backoff\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\n/* Error codes (for the tweaking via Admin API */\n#define JANUS_SAMPLEEVH_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_SAMPLEEVH_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_SAMPLEEVH_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_SAMPLEEVH_ERROR_UNKNOWN_ERROR\t\t\t499\n\n\n/* Plugin implementation */\nint janus_sampleevh_init(const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tgboolean enabled = FALSE;\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_SAMPLEEVH_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_SAMPLEEVH_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_SAMPLEEVH_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\t/* Handle configuration */\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\n\t\t/* Setup the sample event handler, if required */\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Sample event handler disabled (Janus API)\\n\");\n\t\t} else {\n\t\t\t/* Backend to send events to */\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"backend\");\n\t\t\tif(!item || !item->value || strstr(item->value, \"http\") != item->value) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing or invalid backend\\n\");\n\t\t\t} else {\n\t\t\t\tbackend = g_strdup(item->value);\n\t\t\t\t/* Any credentials needed? */\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"backend_user\");\n\t\t\t\tbackend_user = (item && item->value) ? g_strdup(item->value) : NULL;\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"backend_pwd\");\n\t\t\t\tbackend_pwd = (item && item->value) ? g_strdup(item->value) : NULL;\n\t\t\t\t/* Any specific setting for retransmissions? */\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"max_retransmissions\");\n\t\t\t\tif(item && item->value) {\n\t\t\t\t\tint mr = atoi(item->value);\n\t\t\t\t\tif(mr < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid negative value for 'max_retransmissions', using default (%d)\\n\", max_retransmissions);\n\t\t\t\t\t} else if(mr == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Retransmissions disabled (max_retransmissions=0)\\n\");\n\t\t\t\t\t\tmax_retransmissions = 0;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmax_retransmissions = mr;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"retransmissions_backoff\");\n\t\t\t\tif(item && item->value) {\n\t\t\t\t\tint rb = atoi(item->value);\n\t\t\t\t\tif(rb <= 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid negative or null value for 'retransmissions_backoff', using default (%d)\\n\", retransmissions_backoff);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tretransmissions_backoff = rb;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Which events should we subscribe to? */\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\t\t\tif(item && item->value)\n\t\t\t\t\tjanus_events_edit_events_mask(item->value, &janus_sampleevh.events_mask);\n\t\t\t\t/* Is grouping of events ok? */\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"grouping\");\n\t\t\t\tif(item && item->value)\n\t\t\t\t\tgroup_events = janus_is_true(item->value);\n\t\t\t\t/* Check the JSON indentation */\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\t\t\t\tif(item && item->value) {\n\t\t\t\t\t/* Check how we need to format/serialize the JSON output */\n\t\t\t\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Check if we need any compression */\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"compress\");\n\t\t\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\t\t\tcompress = TRUE;\n\t\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"compression\");\n\t\t\t\t\tif(item && item->value) {\n\t\t\t\t\t\tint c = atoi(item->value);\n\t\t\t\t\t\tif(c < 0 || c > 9) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid compression factor '%d', falling back to '%d'...\\n\", c, compression);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcompression = c;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Done */\n\t\t\t\tenabled = TRUE;\n\t\t\t}\n\t\t}\n\t}\n\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\tif(!enabled) {\n\t\tJANUS_LOG(LOG_FATAL, \"Sample event handler not enabled/needed, giving up...\\n\");\n\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t}\n\tJANUS_LOG(LOG_VERB, \"Sample event handler configured: %s\\n\", backend);\n\n\t/* Initialize libcurl, needed for forwarding events via HTTP POST */\n\tcurl_global_init(CURL_GLOBAL_ALL);\n\n\t/* Initialize the events queue */\n\tevents = g_async_queue_new_full((GDestroyNotify) janus_sampleevh_event_free);\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming events */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"janus sampleevh handler\", janus_sampleevh_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the SampleEventHandler handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_SAMPLEEVH_NAME);\n\treturn 0;\n}\n\nvoid janus_sampleevh_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(events, &exit_event);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\tg_async_queue_unref(events);\n\tevents = NULL;\n\n\tg_free(backend);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_SAMPLEEVH_NAME);\n}\n\nint janus_sampleevh_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_EVENTHANDLER_API_VERSION;\n}\n\nint janus_sampleevh_get_version(void) {\n\treturn JANUS_SAMPLEEVH_VERSION;\n}\n\nconst char *janus_sampleevh_get_version_string(void) {\n\treturn JANUS_SAMPLEEVH_VERSION_STRING;\n}\n\nconst char *janus_sampleevh_get_description(void) {\n\treturn JANUS_SAMPLEEVH_DESCRIPTION;\n}\n\nconst char *janus_sampleevh_get_name(void) {\n\treturn JANUS_SAMPLEEVH_NAME;\n}\n\nconst char *janus_sampleevh_get_author(void) {\n\treturn JANUS_SAMPLEEVH_AUTHOR;\n}\n\nconst char *janus_sampleevh_get_package(void) {\n\treturn JANUS_SAMPLEEVH_PACKAGE;\n}\n\nvoid janus_sampleevh_incoming_event(json_t *event) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t/* Janus is closing or the plugin is */\n\t\treturn;\n\t}\n\n\t/* Do NOT handle the event here in this callback! Since Janus notifies you right\n\t * away when something happens, these events are triggered from working threads and\n\t * not some sort of message bus. As such, performing I/O or network operations in\n\t * here could dangerously slow Janus down. Let's just reference and enqueue the event,\n\t * and handle it in our own thread: the event contains a monotonic time indicator of\n\t * when the event actually happened on this machine, so that, if relevant, we can compute\n\t * any delay in the actual event processing ourselves. */\n\tjson_incref(event);\n\tg_async_queue_push(events, event);\n\n}\n\njson_t *janus_sampleevh_handle_request(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this requests to apply tweaks to the logic */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_SAMPLEEVH_ERROR_MISSING_ELEMENT, JANUS_SAMPLEEVH_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"tweak\")) {\n\t\t/* We only support a request to tweak the current settings */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, tweak_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_SAMPLEEVH_ERROR_MISSING_ELEMENT, JANUS_SAMPLEEVH_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\t/* Parameters we can change */\n\t\tconst char *req_events = NULL, *req_backend = NULL,\n\t\t\t*req_backend_user = NULL, *req_backend_pwd = NULL;\n\t\tint req_grouping = -1, req_maxretr = -1, req_backoff = -1,\n\t\t\treq_compress = -1, req_compression = -1;\n\t\t/* Events */\n\t\tif(json_object_get(request, \"events\"))\n\t\t\treq_events = json_string_value(json_object_get(request, \"events\"));\n\t\t/* Grouping */\n\t\tif(json_object_get(request, \"grouping\"))\n\t\t\treq_grouping = json_is_true(json_object_get(request, \"grouping\"));\n\t\t/* Compression */\n\t\tif(json_object_get(request, \"compress\"))\n\t\t\treq_compress = json_is_true(json_object_get(request, \"compress\"));\n\t\tif(json_object_get(request, \"compression\"))\n\t\t\treq_compression = json_integer_value(json_object_get(request, \"compression\"));\n\t\t/* Backend stuff */\n\t\tif(json_object_get(request, \"backend\"))\n\t\t\treq_backend = json_string_value(json_object_get(request, \"backend\"));\n\t\tif(req_backend && strstr(req_backend, \"http\") != req_backend) {\n\t\t\t/* Not an HTTP address */\n\t\t\terror_code = JANUS_SAMPLEEVH_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Invalid HTTP URI '%s'\", req_backend);\n\t\t\tgoto plugin_response;\n\t\t}\n\t\tif(json_object_get(request, \"backend_user\"))\n\t\t\treq_backend_user = json_string_value(json_object_get(request, \"backend_user\"));\n\t\tif(json_object_get(request, \"backend_pwd\"))\n\t\t\treq_backend_pwd = json_string_value(json_object_get(request, \"backend_pwd\"));\n\t\t/* Retransmissions stuff */\n\t\tif(json_object_get(request, \"max_retransmissions\"))\n\t\t\treq_maxretr = json_integer_value(json_object_get(request, \"max_retransmissions\"));\n\t\tif(json_object_get(request, \"retransmissions_backoff\"))\n\t\t\treq_backoff = json_integer_value(json_object_get(request, \"retransmissions_backoff\"));\n\t\t/* If we got here, we can enforce */\n\t\tjanus_mutex_lock(&evh_mutex);\n\t\tif(req_events)\n\t\t\tjanus_events_edit_events_mask(req_events, &janus_sampleevh.events_mask);\n\t\tif(req_grouping > -1)\n\t\t\tgroup_events = req_grouping ? TRUE : FALSE;\n\t\tif(req_compress > -1)\n\t\t\tcompress = req_compress ? TRUE : FALSE;\n\t\tif(req_compression > -1 && req_compression < 10)\n\t\t\tcompression = req_compression;\n\t\tif(req_backend || req_backend_user || req_backend_pwd) {\n\t\t\tif(req_backend) {\n\t\t\t\tg_free(backend);\n\t\t\t\tbackend = g_strdup(req_backend);\n\t\t\t}\n\t\t\tif(req_backend_user) {\n\t\t\t\tg_free(backend_user);\n\t\t\t\tbackend_user = g_strdup(req_backend_user);\n\t\t\t}\n\t\t\tif(req_backend_pwd) {\n\t\t\t\tg_free(backend_pwd);\n\t\t\t\tbackend_pwd = g_strdup(req_backend_pwd);\n\t\t\t}\n\t\t}\n\t\tif(req_maxretr > -1)\n\t\t\tmax_retransmissions = req_maxretr;\n\t\tif(req_backoff > -1)\n\t\t\tretransmissions_backoff = req_backoff;\n\t\tjanus_mutex_unlock(&evh_mutex);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_SAMPLEEVH_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tjson_t *response = json_object();\n\t\t\tif(error_code == 0) {\n\t\t\t\t/* Return a success */\n\t\t\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\t\t} else {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n/* Thread to handle incoming events */\nstatic void *janus_sampleevh_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining SampleEventHandler handler thread\\n\");\n\tjson_t *event = NULL, *output = NULL;\n\tchar *event_text = NULL;\n\tchar compressed_text[8192];\n\tsize_t compressed_len = 0;\n\tint count = 0, max = group_events ? 100 : 1;\n\tint retransmit = 0;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tif(!retransmit) {\n\t\t\tevent = g_async_queue_pop(events);\n\t\t\tif(event == &exit_event)\n\t\t\t\tbreak;\n\t\t\tcount = 0;\n\t\t\toutput = NULL;\n\n\t\t\twhile(TRUE) {\n\t\t\t\t/* Handle event: just for fun, let's see how long it took for us to take care of this */\n\t\t\t\tjson_t *created = json_object_get(event, \"timestamp\");\n\t\t\t\tif(created && json_is_integer(created)) {\n\t\t\t\t\tgint64 then = json_integer_value(created);\n\t\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\t\tJANUS_LOG(LOG_DBG, \"Handled event after %\"SCNu64\" us\\n\", now-then);\n\t\t\t\t}\n\n\t\t\t\t/* Let's check what kind of event this is: we don't really do anything\n\t\t\t\t * with it in this plugin, it's just to show how you can handle\n\t\t\t\t * different types of events in an event handler. */\n\t\t\t\tint type = json_integer_value(json_object_get(event, \"type\"));\n\t\t\t\tswitch(type) {\n\t\t\t\t\tcase JANUS_EVENT_TYPE_SESSION:\n\t\t\t\t\t\t/* This is a session related event. The only info that is\n\t\t\t\t\t\t * required is a name for the event itself: a \"created\"\n\t\t\t\t\t\t * event may also contain transport info, in the form of\n\t\t\t\t\t\t * the transport module that originated the session\n\t\t\t\t\t\t * (e.g., \"janus.transport.http\") and an internal unique\n\t\t\t\t\t\t * ID for the transport instance (which may be associated\n\t\t\t\t\t\t * to a connection or anything else within the specifics\n\t\t\t\t\t\t * of the transport module itself). Here's an example of\n\t\t\t\t\t\t * a new session being created:\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 1,\n\t\t\t\t\t\t\t   \"timestamp\": 3583879627,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"name\": \"created\"\n\t\t\t\t\t\t\t   },\n\t\t\t\t\t\t\t   \"transport\": {\n\t\t\t\t\t\t\t      \"transport\": \"janus.transport.http\",\n\t\t\t\t\t\t\t      \"id\": \"0x7fcb100008c0\"\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_HANDLE:\n\t\t\t\t\t\t/* This is a handle related event. The only info that is provided\n\t\t\t\t\t\t * are the name for the event itself and the package name of the\n\t\t\t\t\t\t * plugin this handle refers to (e.g., \"janus.plugin.echotest\").\n\t\t\t\t\t\t * Here's an example of a new handled being attached in a session\n\t\t\t\t\t\t * to the EchoTest plugin:\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 2,\n\t\t\t\t\t\t\t   \"timestamp\": 3570304977,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"name\": \"attached\",\n\t\t\t\t\t\t\t\t  \"plugin: \"janus.plugin.echotest\"\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_JSEP:\n\t\t\t\t\t\t/* This is a JSEP/SDP related event. It provides information\n\t\t\t\t\t\t * about an ongoing WebRTC negotiation, and so tells you\n\t\t\t\t\t\t * about the SDP being sent/received, and who's sending it\n\t\t\t\t\t\t * (\"local\" means Janus, \"remote\" means the user). Here's an\n\t\t\t\t\t\t * example, where the user originated an offer towards Janus:\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 8,\n\t\t\t\t\t\t\t   \"timestamp\": 3570400208,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"owner\": \"remote\",\n\t\t\t\t\t\t\t\t  \"jsep\": {\n\t\t\t\t\t\t\t\t\t \"type\": \"offer\",\n\t\t\t\t\t\t\t\t\t \"sdp\": \"v=0[..]\\r\\n\"\n\t\t\t\t\t\t\t\t  }\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_WEBRTC:\n\t\t\t\t\t\t/* This is a WebRTC related event, and so the content of\n\t\t\t\t\t\t * the event may vary quite a bit. In fact, you may be notified\n\t\t\t\t\t\t * about ICE or DTLS states, or when a WebRTC PeerConnection\n\t\t\t\t\t\t * goes up or down. Here are some examples, in no particular order:\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 16,\n\t\t\t\t\t\t\t   \"timestamp\": 3570416659,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"ice\": \"connecting\",\n\t\t\t\t\t\t\t\t  \"stream_id\": 1,\n\t\t\t\t\t\t\t\t  \"component_id\": 1\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t *\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 16,\n\t\t\t\t\t\t\t   \"timestamp\": 3570637554,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"selected-pair\": \"[..]\",\n\t\t\t\t\t\t\t\t  \"stream_id\": 1,\n\t\t\t\t\t\t\t\t  \"component_id\": 1\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t *\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 16,\n\t\t\t\t\t\t\t   \"timestamp\": 3570656112,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"dtls\": \"connected\",\n\t\t\t\t\t\t\t\t  \"stream_id\": 1,\n\t\t\t\t\t\t\t\t  \"component_id\": 1\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t *\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 16,\n\t\t\t\t\t\t\t   \"timestamp\": 3570657237,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"connection\": \"webrtcup\"\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_MEDIA:\n\t\t\t\t\t\t/* This is a media related event. This can contain different\n\t\t\t\t\t\t * information about the health of a media session, or about\n\t\t\t\t\t\t * what's going on in general (e.g., when Janus started/stopped\n\t\t\t\t\t\t * receiving media of a certain type, or (TODO) when some media related\n\t\t\t\t\t\t * statistics are available). Here's an example of Janus getting\n\t\t\t\t\t\t * video from the peer for the first time, or after a second\n\t\t\t\t\t\t * of no video at all (which would have triggered a \"receiving\": false):\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 32,\n\t\t\t\t\t\t\t   \"timestamp\": 3571078797,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"media\": \"video\",\n\t\t\t\t\t\t\t\t  \"receiving\": \"true\"\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_PLUGIN:\n\t\t\t\t\t\t/* This is a plugin related event. Since each plugin may\n\t\t\t\t\t\t * provide info in a very custom way, the format of this event\n\t\t\t\t\t\t * is in general very dynamic. You'll always find, though,\n\t\t\t\t\t\t * an \"event\" object containing the package name of the\n\t\t\t\t\t\t * plugin (e.g., \"janus.plugin.echotest\") and a \"data\"\n\t\t\t\t\t\t * object that contains whatever the plugin decided to\n\t\t\t\t\t\t * notify you about, that will always vary from plugin to\n\t\t\t\t\t\t * plugin. Besides, notice that \"session_id\" and \"handle_id\"\n\t\t\t\t\t\t * may or may not be present: when they are, you'll know\n\t\t\t\t\t\t * the event has been triggered within the context of a\n\t\t\t\t\t\t * specific handle session with the plugin; when they're\n\t\t\t\t\t\t * not, the plugin sent an event out of context of a\n\t\t\t\t\t\t * specific session it is handling. Here's an example:\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 64,\n\t\t\t\t\t\t\t   \"timestamp\": 3570336031,\n\t\t\t\t\t\t\t   \"session_id\": 2004798115,\n\t\t\t\t\t\t\t   \"handle_id\": 3708519405,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"plugin\": \"janus.plugin.echotest\",\n\t\t\t\t\t\t\t\t  \"data\": {\n\t\t\t\t\t\t\t\t\t \"audio_active\": \"true\",\n\t\t\t\t\t\t\t\t\t \"video_active\": \"true\",\n\t\t\t\t\t\t\t\t\t \"bitrate\": 0\n\t\t\t\t\t\t\t\t  }\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_TRANSPORT:\n\t\t\t\t\t\t/* This is a transport related event (TODO). The syntax of\n\t\t\t\t\t\t * the common format (transport specific data aside) is\n\t\t\t\t\t\t * exactly the same as that of the plugin related events\n\t\t\t\t\t\t * above, with a \"transport\" property instead of \"plugin\"\n\t\t\t\t\t\t * to contain the transport package name. */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_CORE:\n\t\t\t\t\t\t/* This is a core related event. This can contain different\n\t\t\t\t\t\t * information about the health of the Janus instance, or\n\t\t\t\t\t\t * more generically on some events in the Janus life cycle\n\t\t\t\t\t\t * (e.g., when it's just been started or when a shutdown\n\t\t\t\t\t\t * has been requested). Considering the heterogeneous nature\n\t\t\t\t\t\t * of the information being reported, the content is always\n\t\t\t\t\t\t * a JSON object (event). Core events are the only ones\n\t\t\t\t\t\t * missing a session_id. Here's an example:\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 256,\n\t\t\t\t\t\t\t   \"timestamp\": 28381185382,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"status\": \"started\"\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_EVENT_TYPE_EXTERNAL:\n\t\t\t\t\t\t/* This is an external event, not originated by Janus itself\n\t\t\t\t\t\t * or any of its plugins, but from an ad-hoc Admin API request\n\t\t\t\t\t\t * instead. As such, the content of the event is not bound to\n\t\t\t\t\t\t * any rules (apart from the fact that it needs to be a JSON\n\t\t\t\t\t\t * object), but can be whatever the external source thought\n\t\t\t\t\t\t * appropriate. In order to facilitare life to recipients, all\n\t\t\t\t\t\t * external events must contain a \"schema\" property, which anyway\n\t\t\t\t\t\t * is not bound to any rules either. As an example:\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t   \"type\": 4,\n\t\t\t\t\t\t\t   \"timestamp\": 28381185382,\n\t\t\t\t\t\t\t   \"event\": {\n\t\t\t\t\t\t\t\t  \"schema\": \"my.custom.source\",\n\t\t\t\t\t\t\t\t  \"data\": {\n\t\t\t\t\t\t\t\t     \"whatever\": \"youwant\"\n\t\t\t\t\t\t\t\t  }\n\t\t\t\t\t\t\t   }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unknown type of event '%d'\\n\", type);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif(!group_events) {\n\t\t\t\t\t/* We're done here, we just need a single event */\n\t\t\t\t\toutput = event;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* If we got here, we're grouping */\n\t\t\t\tif(output == NULL)\n\t\t\t\t\toutput = json_array();\n\t\t\t\tjson_array_append_new(output, event);\n\t\t\t\t/* Never group more than a maximum number of events, though, or we might stay here forever */\n\t\t\t\tcount++;\n\t\t\t\tif(count == max)\n\t\t\t\t\tbreak;\n\t\t\t\tevent = g_async_queue_try_pop(events);\n\t\t\t\tif(event == NULL || event == &exit_event)\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t/* Since this a simple plugin, it does the same for all events: so just convert to string... */\n\t\t\tevent_text = json_dumps(output, json_format);\n\t\t\tif(event_text == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Failed to stringify event, event lost...\\n\");\n\t\t\t\t/* Nothing we can do... get rid of the event */\n\t\t\t\tjson_decref(output);\n\t\t\t\toutput = NULL;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\t/* Whether we just prepared the event or this is a retransmission, send it via HTTP POST */\n\t\tCURLcode res;\n\t\tstruct curl_slist *headers = NULL;\n\t\tCURL *curl = curl_easy_init();\n\t\tif(curl == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error initializing CURL context\\n\");\n\t\t\tgoto done;\n\t\t}\n\t\tjanus_mutex_lock(&evh_mutex);\n\t\tcurl_easy_setopt(curl, CURLOPT_URL, backend);\n\t\t/* Any credentials? */\n\t\tif(backend_user != NULL && backend_pwd != NULL) {\n\t\t\tcurl_easy_setopt(curl, CURLOPT_USERNAME, backend_user);\n\t\t\tcurl_easy_setopt(curl, CURLOPT_PASSWORD, backend_pwd);\n\t\t}\n\t\tjanus_mutex_unlock(&evh_mutex);\n\t\theaders = curl_slist_append(headers, \"Content-Type: application/json\");\n\t\t/* Check if we need to compress the data */\n\t\tif(compress) {\n\t\t\tcompressed_len = janus_gzip_compress(compression,\n\t\t\t\tevent_text, strlen(event_text),\n\t\t\t\tcompressed_text, sizeof(compressed_text));\n\t\t\tif(compressed_len == 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to compress event (%zu bytes)...\\n\", strlen(event_text));\n\t\t\t\t/* Nothing we can do... get rid of the event */\n\t\t\t\tif(curl)\n\t\t\t\t\tcurl_easy_cleanup(curl);\n\t\t\t\tfree(event_text);\n\t\t\t\tjson_decref(output);\n\t\t\t\toutput = NULL;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\theaders = curl_slist_append(headers, \"Content-Encoding: gzip\");\n\t\t}\n\t\tcurl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n\t\tcurl_easy_setopt(curl, CURLOPT_POSTFIELDS, compress ? compressed_text : event_text);\n\t\tcurl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, compress ? compressed_len : strlen(event_text));\n\t\tcurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, janus_sampleehv_write_data);\n\t\t/* Don't wait forever (let's say, 10 seconds) */\n\t\tcurl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);\n\t\t/* Send the request */\n\t\tres = curl_easy_perform(curl);\n\t\tif(res != CURLE_OK) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't relay event to the backend: %s\\n\", curl_easy_strerror(res));\n\t\t\tif(max_retransmissions > 0) {\n\t\t\t\t/* Retransmissions enabled, let's try again */\n\t\t\t\tif(retransmit == max_retransmissions) {\n\t\t\t\t\tretransmit = 0;\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Maximum number of retransmissions reached (%d), event lost...\\n\", max_retransmissions);\n\t\t\t\t} else {\n\t\t\t\t\tint next = retransmissions_backoff * (pow(2, retransmit));\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Retransmitting event in %d ms...\\n\", next);\n\t\t\t\t\tg_usleep(next*1000);\n\t\t\t\t\tretransmit++;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Retransmissions disabled, event lost...\\n\");\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_DBG, \"Event sent!\\n\");\n\t\t\tretransmit = 0;\n\t\t}\ndone:\n\t\t/* Cleanup */\n\t\tif(curl)\n\t\t\tcurl_easy_cleanup(curl);\n\t\tif(headers)\n\t\t\tcurl_slist_free_all(headers);\n\t\tif(!retransmit)\n\t\t\tfree(event_text);\n\n\t\t/* Done, let's unref the event */\n\t\tjson_decref(output);\n\t\toutput = NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving SampleEventHandler handler thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/events/janus_wsevh.c",
    "content": "/*! \\file   janus_wsevh.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus WebSockets EventHandler plugin\n * \\details  This is a trivial WebSockets event handler plugin for Janus\n *\n * \\ingroup eventhandlers\n * \\ref eventhandlers\n */\n\n#include \"eventhandler.h\"\n\n#include <math.h>\n\n#include <libwebsockets.h>\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n#include \"../events.h\"\n\n\n/* Plugin information */\n#define JANUS_WSEVH_VERSION\t\t\t1\n#define JANUS_WSEVH_VERSION_STRING\t\"0.0.1\"\n#define JANUS_WSEVH_DESCRIPTION\t\t\"This is a trivial WebSockets event handler plugin for Janus.\"\n#define JANUS_WSEVH_NAME\t\t\t\"JANUS WebSocketsEventHandler plugin\"\n#define JANUS_WSEVH_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_WSEVH_PACKAGE\t\t\t\"janus.eventhandler.wsevh\"\n\n/* Plugin methods */\njanus_eventhandler *create(void);\nint janus_wsevh_init(const char *config_path);\nvoid janus_wsevh_destroy(void);\nint janus_wsevh_get_api_compatibility(void);\nint janus_wsevh_get_version(void);\nconst char *janus_wsevh_get_version_string(void);\nconst char *janus_wsevh_get_description(void);\nconst char *janus_wsevh_get_name(void);\nconst char *janus_wsevh_get_author(void);\nconst char *janus_wsevh_get_package(void);\nvoid janus_wsevh_incoming_event(json_t *event);\njson_t *janus_wsevh_handle_request(json_t *request);\n\n#define WS_LIST_TERM 0, NULL, 0\n\n/* Event handler setup */\nstatic janus_eventhandler janus_wsevh =\n\tJANUS_EVENTHANDLER_INIT (\n\t\t.init = janus_wsevh_init,\n\t\t.destroy = janus_wsevh_destroy,\n\n\t\t.get_api_compatibility = janus_wsevh_get_api_compatibility,\n\t\t.get_version = janus_wsevh_get_version,\n\t\t.get_version_string = janus_wsevh_get_version_string,\n\t\t.get_description = janus_wsevh_get_description,\n\t\t.get_name = janus_wsevh_get_name,\n\t\t.get_author = janus_wsevh_get_author,\n\t\t.get_package = janus_wsevh_get_package,\n\n\t\t.incoming_event = janus_wsevh_incoming_event,\n\t\t.handle_request = janus_wsevh_handle_request,\n\n\t\t.events_mask = JANUS_EVENT_TYPE_NONE\n\t);\n\n/* Plugin creator */\njanus_eventhandler *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_WSEVH_NAME);\n\treturn &janus_wsevh;\n}\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic GThread *ws_thread;\nstatic void *janus_wsevh_thread(void *data);\n\n/* Connection related helper methods */\nstatic void janus_wsevh_schedule_connect_attempt(void);\nstatic void janus_wsevh_calculate_reconnect_delay_on_fail(void);\n/* lws_sorted_usec_list_t is defined starting with lws 3.2 */\n#if !(((LWS_LIBRARY_VERSION_MAJOR == 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || LWS_LIBRARY_VERSION_MAJOR >= 4))\n\t#define lws_sorted_usec_list_t void\n#endif\nstatic void janus_wsevh_connect_attempt(lws_sorted_usec_list_t *sul);\n\n/* Queue of events to handle */\nstatic GQueue *events = NULL;\nstatic janus_mutex events_mutex = JANUS_MUTEX_INITIALIZER;\nstatic gboolean group_events = TRUE;\nstatic volatile gint events_cap_on_reconnect = 0, dropped = 0;\nstatic void janus_wsevh_event_free(json_t *event) {\n\tjson_decref(event);\n}\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n\n/* Parameter validation (for tweaking via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter tweak_parameters[] = {\n\t{\"events\", JSON_STRING, 0},\n\t{\"grouping\", JANUS_JSON_BOOL, 0},\n\t{\"events_cap_on_reconnect\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\n/* Error codes (for the tweaking via Admin API */\n#define JANUS_WSEVH_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_WSEVH_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_WSEVH_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_WSEVH_ERROR_UNKNOWN_ERROR\t\t\t499\n\n/* Logging */\nstatic int wsevh_log_level = 0;\nstatic const char *janus_wsevh_get_level_str(int level) {\n\tswitch(level) {\n\t\tcase LLL_ERR:\n\t\t\treturn \"ERR\";\n\t\tcase LLL_WARN:\n\t\t\treturn \"WARN\";\n\t\tcase LLL_NOTICE:\n\t\t\treturn \"NOTICE\";\n\t\tcase LLL_INFO:\n\t\t\treturn \"INFO\";\n\t\tcase LLL_DEBUG:\n\t\t\treturn \"DEBUG\";\n\t\tcase LLL_PARSER:\n\t\t\treturn \"PARSER\";\n\t\tcase LLL_HEADER:\n\t\t\treturn \"HEADER\";\n\t\tcase LLL_EXT:\n\t\t\treturn \"EXT\";\n\t\tcase LLL_CLIENT:\n\t\t\treturn \"CLIENT\";\n\t\tcase LLL_LATENCY:\n\t\t\treturn \"LATENCY\";\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\tcase LLL_USER:\n\t\t\treturn \"USER\";\n#endif\n\t\tcase LLL_COUNT:\n\t\t\treturn \"COUNT\";\n\t\tdefault:\n\t\t\treturn NULL;\n\t}\n}\nstatic void janus_wsevh_log_emit_function(int level, const char *line) {\n\t/* FIXME Do we want to use different Janus debug levels according to the level here? */\n\tJANUS_LOG(LOG_INFO, \"[libwebsockets][wsevh][%s] %s\", janus_wsevh_get_level_str(level), line);\n}\n\n\n/* WebSockets properties */\nstatic char *backend = NULL;\nstatic const char *protocol = NULL, *address = NULL;\nstatic char path[256];\nstatic int port = 0;\nstatic struct lws_context *context = NULL;\n#if ((LWS_LIBRARY_VERSION_MAJOR == 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || LWS_LIBRARY_VERSION_MAJOR >= 4)\nstatic lws_sorted_usec_list_t sul_stagger = { 0 };\n#endif\nstatic gint64 disconnected = 0;\nstatic volatile gint reconnect = 0;\nstatic int reconnect_delay = 0;\n#define JANUS_WSEVH_MAX_RETRY_SECS\t8\n\ntypedef struct janus_wsevh_client {\n\tstruct lws *wsi;\t\t/* The libwebsockets client instance */\n\tunsigned char *buffer;\t/* Buffer containing the message to send */\n\tint buflen;\t\t\t\t/* Length of the buffer (may be resized after re-allocations) */\n\tint bufpending;\t\t\t/* Data an interrupted previous write couldn't send */\n\tint bufoffset;\t\t\t/* Offset from where the interrupted previous write should resume */\n\tjanus_mutex mutex;\t\t/* Mutex to lock/unlock this instance */\n} janus_wsevh_client;\nstatic janus_wsevh_client *ws_client = NULL;\nstatic struct lws *wsi = NULL;\n#if (LWS_LIBRARY_VERSION_MAJOR < 3)\nstatic janus_mutex writable_mutex = JANUS_MUTEX_INITIALIZER;\n#endif\n\nstatic int janus_wsevh_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);\nstatic struct lws_protocols protocols[] = {\n\t{ \"janus-event-handlers\", janus_wsevh_callback, sizeof(janus_wsevh_client), 0, WS_LIST_TERM },\t/* Subprotocol will be configurable */\n\t{ NULL, NULL, 0, 0, WS_LIST_TERM }\n};\nstatic const struct lws_extension exts[] = {\n#ifndef LWS_WITHOUT_EXTENSIONS\n\t{ \"permessage-deflate\", lws_extension_callback_pm_deflate, \"permessage-deflate; client_max_window_bits\" },\n\t{ \"deflate-frame\", lws_extension_callback_pm_deflate, \"deflate_frame\" },\n#endif\n\t{ NULL, NULL, NULL }\n};\n\n/* WebSockets error management */\n#define CASE_STR(name) case name: return #name\nstatic const char *janus_wsevh_reason_string(enum lws_callback_reasons reason) {\n\tswitch(reason) {\n\t\tCASE_STR(LWS_CALLBACK_ESTABLISHED);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_CONNECTION_ERROR);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_ESTABLISHED);\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_CLOSED);\n#endif\n\t\tCASE_STR(LWS_CALLBACK_CLOSED);\n\t\tCASE_STR(LWS_CALLBACK_CLOSED_HTTP);\n\t\tCASE_STR(LWS_CALLBACK_RECEIVE);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_RECEIVE);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_RECEIVE_PONG);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_WRITEABLE);\n\t\tCASE_STR(LWS_CALLBACK_SERVER_WRITEABLE);\n\t\tCASE_STR(LWS_CALLBACK_HTTP);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_BODY);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_BODY_COMPLETION);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_FILE_COMPLETION);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_WRITEABLE);\n\t\tCASE_STR(LWS_CALLBACK_FILTER_NETWORK_CONNECTION);\n\t\tCASE_STR(LWS_CALLBACK_FILTER_HTTP_CONNECTION);\n\t\tCASE_STR(LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED);\n\t\tCASE_STR(LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION);\n\t\tCASE_STR(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS);\n\t\tCASE_STR(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS);\n\t\tCASE_STR(LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER);\n\t\tCASE_STR(LWS_CALLBACK_CONFIRM_EXTENSION_OKAY);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED);\n\t\tCASE_STR(LWS_CALLBACK_PROTOCOL_INIT);\n\t\tCASE_STR(LWS_CALLBACK_PROTOCOL_DESTROY);\n\t\tCASE_STR(LWS_CALLBACK_WSI_CREATE);\n\t\tCASE_STR(LWS_CALLBACK_WSI_DESTROY);\n\t\tCASE_STR(LWS_CALLBACK_GET_THREAD_ID);\n\t\tCASE_STR(LWS_CALLBACK_ADD_POLL_FD);\n\t\tCASE_STR(LWS_CALLBACK_DEL_POLL_FD);\n\t\tCASE_STR(LWS_CALLBACK_CHANGE_MODE_POLL_FD);\n\t\tCASE_STR(LWS_CALLBACK_LOCK_POLL);\n\t\tCASE_STR(LWS_CALLBACK_UNLOCK_POLL);\n\t\tCASE_STR(LWS_CALLBACK_USER);\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\n\n/* Plugin implementation */\nint janus_wsevh_init(const char *config_path) {\n\tgboolean success = TRUE;\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_WSEVH_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_WSEVH_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_WSEVH_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\n\t/* Setup the event handler, if required */\n\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_WARN, \"WebSockets event handler disabled\\n\");\n\t\tgoto error;\n\t}\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\tif(item && item->value) {\n\t\t/* Check how we need to format/serialize the JSON output */\n\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t/* Compact, so no spaces between separators */\n\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t}\n\t}\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"ws_logging\");\n\tif(item && item->value) {\n\t\t/* libwebsockets uses a mask to set log levels, as documented here:\n\t\t * https://libwebsockets.org/lws-api-doc-master/html/group__log.html */\n\t\tif(strstr(item->value, \"none\")) {\n\t\t\t/* Disable libwebsockets logging completely (the default) */\n\t\t} else if(strstr(item->value, \"all\")) {\n\t\t\t/* Enable all libwebsockets logging */\n\t\t\twsevh_log_level = LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO |\n\t\t\t\tLLL_DEBUG | LLL_PARSER | LLL_HEADER | LLL_EXT |\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\t\tLLL_CLIENT | LLL_LATENCY | LLL_USER | LLL_COUNT;\n#else\n\t\t\t\tLLL_CLIENT | LLL_LATENCY | LLL_COUNT;\n#endif\n\t\t} else {\n\t\t\t/* Only enable some of the properties */\n\t\t\tif(strstr(item->value, \"err\"))\n\t\t\t\twsevh_log_level |= LLL_ERR;\n\t\t\tif(strstr(item->value, \"warn\"))\n\t\t\t\twsevh_log_level |= LLL_WARN;\n\t\t\tif(strstr(item->value, \"notice\"))\n\t\t\t\twsevh_log_level |= LLL_NOTICE;\n\t\t\tif(strstr(item->value, \"info\"))\n\t\t\t\twsevh_log_level |= LLL_INFO;\n\t\t\tif(strstr(item->value, \"debug\"))\n\t\t\t\twsevh_log_level |= LLL_DEBUG;\n\t\t\tif(strstr(item->value, \"parser\"))\n\t\t\t\twsevh_log_level |= LLL_PARSER;\n\t\t\tif(strstr(item->value, \"header\"))\n\t\t\t\twsevh_log_level |= LLL_HEADER;\n\t\t\tif(strstr(item->value, \"ext\"))\n\t\t\t\twsevh_log_level |= LLL_EXT;\n\t\t\tif(strstr(item->value, \"client\"))\n\t\t\t\twsevh_log_level |= LLL_CLIENT;\n\t\t\tif(strstr(item->value, \"latency\"))\n\t\t\t\twsevh_log_level |= LLL_LATENCY;\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\tif(strstr(item->value, \"user\"))\n\t\t\t\twsevh_log_level |= LLL_USER;\n#endif\n\t\t\tif(strstr(item->value, \"count\"))\n\t\t\t\twsevh_log_level |= LLL_COUNT;\n\t\t}\n\t}\n\tif(wsevh_log_level > 0)\n\t\tJANUS_LOG(LOG_INFO, \"WebSockets event handler libwebsockets logging: %d\\n\", wsevh_log_level);\n\tlws_set_log_level(wsevh_log_level, janus_wsevh_log_emit_function);\n\n\t/* Which events should we subscribe to? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\tif(item && item->value)\n\t\tjanus_events_edit_events_mask(item->value, &janus_wsevh.events_mask);\n\n\t/* Is grouping of events ok? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"grouping\");\n\tif(item && item->value)\n\t\tgroup_events = janus_is_true(item->value);\n\n\t/* Do we need to cap the number of queued events when reconnecting */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"events_cap_on_reconnect\");\n\tif(item && item->value)\n\t\tevents_cap_on_reconnect = atoi(item->value);\n\tif(events_cap_on_reconnect < 0)\n\t\tevents_cap_on_reconnect = 0;\n\n\t/* Handle the rest of the configuration, starting from the server details */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"backend\");\n\tif(item && item->value)\n\t\tbackend = g_strdup(item->value);\n\tif(backend == NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Missing WebSockets backend\\n\");\n\t\tgoto error;\n\t}\n\tconst char *p = NULL;\n\tif(lws_parse_uri(backend, &protocol, &address, &port, &p)) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error parsing address\\n\");\n\t\tgoto error;\n\t}\n\tif((strcasecmp(protocol, \"ws\") && strcasecmp(protocol, \"wss\")) || !strlen(address)) {\n\t\tJANUS_LOG(LOG_FATAL, \"Invalid address (only ws:// and wss:// addresses are supported)\\n\");\n\t\tJANUS_LOG(LOG_FATAL, \"  -- Protocol: %s\\n\", protocol);\n\t\tJANUS_LOG(LOG_FATAL, \"  -- Address:  %s\\n\", address);\n\t\tJANUS_LOG(LOG_FATAL, \"  -- Path:     %s\\n\", p);\n\t\tgoto error;\n\t}\n\tpath[0] = '/';\n\tif(strnlen(p, 1 + 1) > 1)\n\t\tg_strlcpy(path + 1, p, sizeof(path)-2);\n\t/* Before connecting, let's check if the server expects a subprotocol */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"subprotocol\");\n\tif(item && item->value)\n\t\tprotocols[0].name = g_strdup(item->value);\n\n\t/* Connect */\n\tgboolean secure = !strcasecmp(protocol, \"wss\");\n\tstruct lws_context_creation_info info = { 0 };\n\tinfo.port = CONTEXT_PORT_NO_LISTEN;\n\tinfo.protocols = protocols;\n\tinfo.gid = -1;\n\tinfo.uid = -1;\n#if ((LWS_LIBRARY_VERSION_MAJOR == 4 && LWS_LIBRARY_VERSION_MINOR >= 1) || LWS_LIBRARY_VERSION_MAJOR >= 5)\n\tinfo.connect_timeout_secs = 5;\n#endif\n\tif(secure)\n\t\tinfo.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;\n\tcontext = lws_create_context(&info);\n\tif(context == NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Creating libwebsocket context failed\\n\");\n\t\tgoto error;\n\t}\n\tjanus_wsevh_connect_attempt(NULL);\n\tif(wsi == NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error initializing WebSocket connection\\n\");\n\t\tgoto error;\n\t}\n\n\t/* Initialize the events queue */\n\tevents = g_queue_new();\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Start a thread to handle the WebSockets event loop */\n\tGError *error = NULL;\n\tws_thread = g_thread_try_new(\"janus wsevh client\", janus_wsevh_thread, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to launch the WebSocketsEventHandler client thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tgoto error;\n\t}\n\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"Setup of WebSockets event handler completed\\n\");\n\tgoto done;\n\nerror:\n\t/* If we got here, something went wrong */\n\tsuccess = FALSE;\n\t/* Fall through */\ndone:\n\tif(config)\n\t\tjanus_config_destroy(config);\n\tif(!success) {\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_WSEVH_NAME);\n\treturn 0;\n}\n\nvoid janus_wsevh_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n#if ((LWS_LIBRARY_VERSION_MAJOR == 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || LWS_LIBRARY_VERSION_MAJOR >= 4)\n\tlws_cancel_service(context);\n#endif\n\n\tif(ws_thread != NULL) {\n\t\tg_thread_join(ws_thread);\n\t\tws_thread = NULL;\n\t}\n\n\tg_queue_free_full(events, (GDestroyNotify)janus_wsevh_event_free);\n\tevents = NULL;\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_WSEVH_NAME);\n}\n\nint janus_wsevh_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_EVENTHANDLER_API_VERSION;\n}\n\nint janus_wsevh_get_version(void) {\n\treturn JANUS_WSEVH_VERSION;\n}\n\nconst char *janus_wsevh_get_version_string(void) {\n\treturn JANUS_WSEVH_VERSION_STRING;\n}\n\nconst char *janus_wsevh_get_description(void) {\n\treturn JANUS_WSEVH_DESCRIPTION;\n}\n\nconst char *janus_wsevh_get_name(void) {\n\treturn JANUS_WSEVH_NAME;\n}\n\nconst char *janus_wsevh_get_author(void) {\n\treturn JANUS_WSEVH_AUTHOR;\n}\n\nconst char *janus_wsevh_get_package(void) {\n\treturn JANUS_WSEVH_PACKAGE;\n}\n\nvoid janus_wsevh_incoming_event(json_t *event) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t/* Janus is closing or the plugin is */\n\t\treturn;\n\t}\n\n\t/* Do NOT handle the event here in this callback! Since Janus notifies you right\n\t * away when something happens, these events are triggered from working threads and\n\t * not some sort of message bus. As such, performing I/O or network operations in\n\t * here could dangerously slow Janus down. Let's just reference and enqueue the event,\n\t * and notify the websocket thread: the event contains a monotonic time indicator of\n\t * when the event actually happened on this machine, so that, if relevant, we can compute\n\t * any delay in the actual event processing ourselves. */\n\tjson_incref(event);\n\tjanus_mutex_lock(&events_mutex);\n\tg_queue_push_tail(events, event);\n\tif(g_atomic_int_get(&reconnect)) {\n\t\t/* We're reconnecting: check if there's a cap to how many events to keep in the buffer */\n\t\tguint cap = g_atomic_int_get(&events_cap_on_reconnect);\n\t\tif(cap > 0 && g_queue_get_length(events) > cap) {\n\t\t\t/* Get rid of older events, we won't need them anymore */\n\t\t\tjson_t *drop = NULL;\n\t\t\twhile(g_queue_get_length(events) > cap) {\n\t\t\t\tdrop = g_queue_pop_head(events);\n\t\t\t\tjson_decref(drop);\n\t\t\t\tg_atomic_int_inc(&dropped);\n\t\t\t}\n\t\t}\n\t}\n\tjanus_mutex_unlock(&events_mutex);\n\t/* We notify the websocket thread so that it can be handled */\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\tif(context != NULL)\n\t\t\tlws_cancel_service(context);\n#else\n\t\t/* On libwebsockets < 3.x we use lws_callback_on_writable */\n\t\tjanus_mutex_lock(&writable_mutex);\n\t\tif(wsi != NULL)\n\t\t\tlws_callback_on_writable(wsi);\n\t\tjanus_mutex_unlock(&writable_mutex);\n#endif\n}\n\njson_t *janus_wsevh_handle_request(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this requests to apply tweaks to the logic */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_WSEVH_ERROR_MISSING_ELEMENT, JANUS_WSEVH_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"tweak\")) {\n\t\t/* We only support a request to tweak the current settings */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, tweak_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_WSEVH_ERROR_MISSING_ELEMENT, JANUS_WSEVH_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\t/* Events */\n\t\tif(json_object_get(request, \"events\"))\n\t\t\tjanus_events_edit_events_mask(json_string_value(json_object_get(request, \"events\")), &janus_wsevh.events_mask);\n\t\t/* Grouping */\n\t\tif(json_object_get(request, \"grouping\"))\n\t\t\tgroup_events = json_is_true(json_object_get(request, \"grouping\"));\n\t\t/* Whether we should put a cap on queued events when reconnecting */\n\t\tif(json_object_get(request, \"events_cap_on_reconnect\"))\n\t\t\tg_atomic_int_set(&events_cap_on_reconnect, json_integer_value(json_object_get(request, \"events_cap_on_reconnect\")));\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_WSEVH_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tjson_t *response = json_object();\n\t\t\tif(error_code == 0) {\n\t\t\t\t/* Return a success */\n\t\t\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\t\t} else {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 4)\n/* Websocket thread loop for websocket library newer than 3.2\n * The reconnect is handled in a dedicated lws scheduler janus_wsevh_schedule_connect_attempt */\nstatic void *janus_wsevh_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining WebSocketsEventHandler (lws>=3.2) client thread\\n\");\n\tint nLast = 0;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tint n = lws_service(context, 0);\n\t\tif((n < 0 || nLast < 0) && nLast != n) {\n\t\t\tJANUS_LOG(LOG_ERR, \"lws_service returned %d\\n\", n);\n\t\t\tnLast = n;\n\t\t}\n\t}\n\tlws_context_destroy(context);\n\tJANUS_LOG(LOG_VERB, \"Leaving WebSocketsEventHandler (lws>=3.2) client thread\\n\");\n\treturn NULL;\n}\n#else\n/* Websocket thread loop for websocket library prior to (less than) 3.2\n * The reconnect is handled in the loop for lws < 3.2 */\nstatic void *janus_wsevh_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining WebSocketsEventHandler (lws<3.2) client thread\\n\");\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\t/* Loop until we have to stop */\n\t\tif(!g_atomic_int_get(&reconnect)) {\n\t\t\tlws_service(context, 50);\n\t\t} else {\n\t\t\t/* We should reconnect, get rid of the previous context */\n\t\t\tif(reconnect_delay > 0) {\n\t\t\t\t/* Wait a few seconds before retrying */\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\tif((now-disconnected) < (gint64)reconnect_delay*G_USEC_PER_SEC) {\n\t\t\t\t\t/* Try again later */\n\t\t\t\t\tg_usleep(100000);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tws_client = NULL;\n\t\t\tjanus_wsevh_connect_attempt(NULL);\n\t\t\tif(!wsi) {\n\t\t\t\tjanus_wsevh_calculate_reconnect_delay_on_fail();\n\t\t\t\tJANUS_LOG(LOG_WARN, \"WebSocketsEventHandler: Error attempting connection... (next retry in %ds)\\n\", reconnect_delay);\n\t\t\t}\n\t\t}\n\t}\n\tlws_context_destroy(context);\n\tJANUS_LOG(LOG_VERB, \"Leaving WebSocketsEventHandler (lws<3.2) client thread\\n\");\n\treturn NULL;\n}\n#endif\n\n/* Helper function to pop events from the queue and turn them to string for delivery */\nstatic char *janus_wsevh_stringify_events(void) {\n\tif(!g_atomic_int_get(&initialized) || g_atomic_int_get(&stopping))\n\t\treturn NULL;\n\tjson_t *event = NULL, *output = NULL;\n\tchar *event_text = NULL;\n\tint count = 0, max = group_events ? 100 : 1;\n\n\t/* Pop the first queued event */\n\tjanus_mutex_lock(&events_mutex);\n\tevent = g_queue_peek_head(events);\n\tif(event != NULL)\n\t\t(void)g_queue_pop_head(events);\n\tjanus_mutex_unlock(&events_mutex);\n\tif(event == NULL)\n\t\treturn NULL;\n\n\t/* Start with the stringification, grouping if required */\n\tcount = 0;\n\toutput = NULL;\n\twhile(TRUE) {\n\t\t/* Handle event: just for fun, let's see how long it took for us to take care of this */\n\t\tjson_t *created = json_object_get(event, \"timestamp\");\n\t\tif(created && json_is_integer(created)) {\n\t\t\tgint64 then = json_integer_value(created);\n\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\tJANUS_LOG(LOG_DBG, \"Handled event after %\"SCNu64\" us\\n\", now-then);\n\t\t}\n\t\tif(!group_events) {\n\t\t\t/* We're done here, we just need a single event */\n\t\t\toutput = event;\n\t\t\tbreak;\n\t\t}\n\t\t/* If we got here, we're grouping */\n\t\tif(output == NULL)\n\t\t\toutput = json_array();\n\t\tjson_array_append_new(output, event);\n\t\t/* Never group more than a maximum number of events, though, or we might stay here forever */\n\t\tcount++;\n\t\tif(count == max)\n\t\t\tbreak;\n\t\tjanus_mutex_lock(&events_mutex);\n\t\tevent = g_queue_peek_head(events);\n\t\tif(event != NULL)\n\t\t\t(void)g_queue_pop_head(events);\n\t\tjanus_mutex_unlock(&events_mutex);\n\t\tif(event == NULL)\n\t\t\tbreak;\n\t}\n\n\tif(!g_atomic_int_get(&stopping)) {\n\t\t/* Since this a simple plugin, it does the same for all events: so just convert to string... */\n\t\tevent_text = json_dumps(output, json_format);\n\t\tif(event_text == NULL) {\n\t\t\t/* Nothing we can do... get rid of the event */\n\t\t\tjson_decref(output);\n\t\t\toutput = NULL;\n\t\t\treturn NULL;\n\t\t}\n\t}\n\n\t/* Done, let's unref the event */\n\tjson_decref(output);\n\toutput = NULL;\n\treturn event_text;\n}\n\nstatic int janus_wsevh_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {\n\tswitch(reason) {\n\t\tcase LWS_CALLBACK_CLIENT_ESTABLISHED: {\n\t\t\t/* Prepare the session */\n\t\t\tif(ws_client == NULL)\n\t\t\t\tws_client = (janus_wsevh_client *)user;\n\t\t\tws_client->wsi = wsi;\n\t\t\tws_client->buffer = NULL;\n\t\t\tws_client->buflen = 0;\n\t\t\tws_client->bufpending = 0;\n\t\t\tws_client->bufoffset = 0;\n\t\t\treconnect_delay = 0;\n\t\t\tg_atomic_int_set(&reconnect, 0);\n\t\t\tjanus_mutex_init(&ws_client->mutex);\n\t\t\tlws_callback_on_writable(wsi);\n\t\t\tJANUS_LOG(LOG_INFO, \"WebSocketsEventHandler connected\\n\");\n\t\t\tif(g_atomic_int_get(&dropped) > 0)\n\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- %d events lost in between reconnections\\n\", g_atomic_int_get(&dropped));\n\t\t\tg_atomic_int_set(&dropped, 0);\n\t\t\treturn 0;\n\t\t}\n\t\tcase LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {\n\t\t\tjanus_wsevh_calculate_reconnect_delay_on_fail();\n\t\t\tJANUS_LOG(LOG_ERR, \"WebSocketsEventHandler: Error connecting to backend (%s) (next retry in %ds)\\n\",\n\t\t\t\tin ? (char *)in : \"unknown error\",\n\t\t\t\treconnect_delay);\n\t\t\tdisconnected = janus_get_monotonic_time();\n\t\t\tg_atomic_int_set(&reconnect, 1);\n\t\t\tjanus_wsevh_schedule_connect_attempt();\n\t\t\treturn 1;\n\t\t}\n\t\tcase LWS_CALLBACK_CLIENT_RECEIVE: {\n\t\t\t/* We don't care */\n\t\t\treturn 0;\n\t\t}\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t/* On libwebsockets >= 3.x, we use this event to mark connections as writable in the event loop */\n\t\tcase LWS_CALLBACK_EVENT_WAIT_CANCELLED: {\n\t\t\tif(ws_client != NULL && ws_client->wsi != NULL)\n\t\t\t\tlws_callback_on_writable(ws_client->wsi);\n\t\t\treturn 0;\n\t\t}\n#endif\n\t\tcase LWS_CALLBACK_CLIENT_WRITEABLE: {\n\t\t\tif(ws_client == NULL || ws_client->wsi == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid WebSocket client instance...\\n\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif(!g_atomic_int_get(&stopping)) {\n\t\t\t\tjanus_mutex_lock(&ws_client->mutex);\n\t\t\t\t/* Check if we have a pending/partial write to complete first */\n\t\t\t\tif(ws_client->buffer && ws_client->bufpending > 0 && ws_client->bufoffset > 0\n\t\t\t\t\t\t&& !g_atomic_int_get(&stopping)) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Completing pending WebSocket write (still need to write last %d bytes)...\\n\",\n\t\t\t\t\t\tws_client->bufpending);\n\t\t\t\t\tint sent = lws_write(wsi, ws_client->buffer + ws_client->bufoffset, ws_client->bufpending, LWS_WRITE_TEXT);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Sent %d/%d bytes\\n\", sent, ws_client->bufpending);\n\t\t\t\t\tif(sent > -1 && sent < ws_client->bufpending) {\n\t\t\t\t\t\t/* We still couldn't send everything that was left, we'll try and complete this in the next round */\n\t\t\t\t\t\tws_client->bufpending -= sent;\n\t\t\t\t\t\tws_client->bufoffset += sent;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Clear the pending/partial write queue */\n\t\t\t\t\t\tws_client->bufpending = 0;\n\t\t\t\t\t\tws_client->bufoffset = 0;\n\t\t\t\t\t}\n\t\t\t\t\t/* Done for this round, check the next response/notification later */\n\t\t\t\t\tlws_callback_on_writable(wsi);\n\t\t\t\t\tjanus_mutex_unlock(&ws_client->mutex);\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\t/* Shoot all the pending messages */\n\t\t\t\tchar *event = janus_wsevh_stringify_events();\n\t\t\t\tif(event && !g_atomic_int_get(&stopping)) {\n\t\t\t\t\t/* Gotcha! */\n\t\t\t\t\tint buflen = LWS_PRE + strlen(event);\n\t\t\t\t\tif(ws_client->buffer == NULL) {\n\t\t\t\t\t\t/* Let's allocate a shared buffer */\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Allocating %d bytes (event is %zu bytes)\\n\", buflen, strlen(event));\n\t\t\t\t\t\tws_client->buflen = buflen;\n\t\t\t\t\t\tws_client->buffer = g_malloc0(buflen);\n\t\t\t\t\t} else if(buflen > ws_client->buflen) {\n\t\t\t\t\t\t/* We need a larger shared buffer */\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Re-allocating to %d bytes (was %d, event is %zu bytes)\\n\",\n\t\t\t\t\t\t\tbuflen, ws_client->buflen, strlen(event));\n\t\t\t\t\t\tws_client->buflen = buflen;\n\t\t\t\t\t\tws_client->buffer = g_realloc(ws_client->buffer, buflen);\n\t\t\t\t\t}\n\t\t\t\t\tmemcpy(ws_client->buffer + LWS_PRE, event, strlen(event));\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Sending WebSocket message (%zu bytes)...\\n\", strlen(event));\n\t\t\t\t\tint sent = lws_write(wsi, ws_client->buffer + LWS_PRE, strlen(event), LWS_WRITE_TEXT);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Sent %d/%zu bytes\\n\", sent, strlen(event));\n\t\t\t\t\tif(sent > -1 && sent < (int)strlen(event)) {\n\t\t\t\t\t\t/* We couldn't send everything in a single write, we'll complete this in the next round */\n\t\t\t\t\t\tws_client->bufpending = strlen(event) - sent;\n\t\t\t\t\t\tws_client->bufoffset = LWS_PRE + sent;\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Couldn't write all bytes (%d missing), setting offset %d\\n\",\n\t\t\t\t\t\t\tws_client->bufpending, ws_client->bufoffset);\n\t\t\t\t\t}\n\t\t\t\t\t/* We can get rid of the message */\n\t\t\t\t\tfree(event);\n\t\t\t\t\t/* Done for this round, check the next response/notification later */\n\t\t\t\t\tlws_callback_on_writable(wsi);\n\t\t\t\t\tjanus_mutex_unlock(&ws_client->mutex);\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&ws_client->mutex);\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\tcase LWS_CALLBACK_CLIENT_CLOSED: {\n#else\n\t\tcase LWS_CALLBACK_CLOSED: {\n#endif\n\t\t\treconnect_delay = 1;\n\t\t\tJANUS_LOG(LOG_INFO, \"Connection to WebSocketsEventHandler backend closed (next connection attempt in %ds)\\n\", reconnect_delay);\n\t\t\tif(ws_client != NULL) {\n\t\t\t\t/* Cleanup */\n\t\t\t\tjanus_mutex_lock(&ws_client->mutex);\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Destroying WebSocketsEventHandler client\\n\");\n\t\t\t\tws_client->wsi = NULL;\n\t\t\t\t/* Free the shared buffers */\n\t\t\t\tg_free(ws_client->buffer);\n\t\t\t\tws_client->buffer = NULL;\n\t\t\t\tws_client->buflen = 0;\n\t\t\t\tws_client->bufpending = 0;\n\t\t\t\tws_client->bufoffset = 0;\n\t\t\t\tjanus_mutex_unlock(&ws_client->mutex);\n\t\t\t\tjanus_mutex_destroy(&ws_client->mutex);\n\t\t\t}\n\t\t\t/* Check if we should reconnect */\n\t\t\tws_client = NULL;\n\t\t\twsi = NULL;\n\t\t\tdisconnected = janus_get_monotonic_time();\n\t\t\tg_atomic_int_set(&reconnect, 1);\n\t\t\tjanus_wsevh_schedule_connect_attempt();\n\t\t\treturn 0;\n\t\t}\n\t\tdefault:\n\t\t\tif(wsi)\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"%d (%s)\\n\", reason, janus_wsevh_reason_string(reason));\n\t\t\tbreak;\n\t}\n\treturn 0;\n}\n\n/* Implements the connecting attempt to the backend websocket server\n * sets the connection result (lws_client_connect_info) to static wsi */\nstatic void janus_wsevh_connect_attempt(lws_sorted_usec_list_t *sul) {\n\tstruct lws_client_connect_info i = { 0 };\n\ti.host = address;\n\ti.origin = address;\n\ti.address = address;\n\ti.port = port;\n\ti.path = path;\n\ti.context = context;\n\tif(!strcasecmp(protocol, \"wss\"))\n\t\ti.ssl_connection = 1;\n\ti.ietf_version_or_minus_one = -1;\n\ti.client_exts = exts;\n\ti.protocol = protocols[0].name;\n\tJANUS_LOG(LOG_INFO, \"WebSocketsEventHandler: Connecting to backend websocket server %s:%d...\\n\", address, port);\n\twsi = lws_client_connect_via_info(&i);\n\tif(!wsi) {\n\t\t/* As we specified a callback pointer in the context the NULL result is unlikely to happen */\n\t\tdisconnected = janus_get_monotonic_time();\n\t\tg_atomic_int_set(&reconnect, 1);\n\t\tJANUS_LOG(LOG_ERR, \"WebSocketsEventHandler: Connecting to backend websocket server %s:%d failed\\n\", address, port);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&reconnect, 0);\n}\n\n/* Adopts the reconnect_delay value in case of an error\n * Increases the value up to JANUS_WSEVH_MAX_RETRY_SECS */\nstatic void janus_wsevh_calculate_reconnect_delay_on_fail(void) {\n\tif(reconnect_delay == 0)\n\t\treconnect_delay = 1;\n\telse if(reconnect_delay < JANUS_WSEVH_MAX_RETRY_SECS) {\n\t\treconnect_delay += reconnect_delay;\n\t\tif(reconnect_delay > JANUS_WSEVH_MAX_RETRY_SECS)\n\t\t\treconnect_delay = JANUS_WSEVH_MAX_RETRY_SECS;\n\t}\n}\n\n/* Schedules a connect attempt using the lws scheduler as */\nstatic void janus_wsevh_schedule_connect_attempt(void) {\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 4)\n\tlws_sul_schedule(context, 0, &sul_stagger, janus_wsevh_connect_attempt, reconnect_delay * LWS_US_PER_SEC);\n#endif\n}\n"
  },
  {
    "path": "src/events.c",
    "content": "/*! \\file    events.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Event handler notifications\n * \\details  Event handler plugins can receive events from the Janus core\n * and other plugins, in order to handle them somehow. This methods\n * provide helpers to notify events to such handlers.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <stdarg.h>\n\n#include \"events.h\"\n#include \"utils.h\"\n\nstatic struct janus_event_types {\n\tint type;\n\tconst char *label;\n\tconst char *name;\n} event_types_string[] = {\n\t{ JANUS_EVENT_TYPE_NONE, \"no_events\", \"No events\"},\n\t{ JANUS_EVENT_TYPE_SESSION, \"sessions\", \"Sessions\"},\n\t{ JANUS_EVENT_TYPE_HANDLE, \"handles\", \"Handles\"},\n\t{ JANUS_EVENT_TYPE_EXTERNAL, \"external\", \"External\"},\n\t{ JANUS_EVENT_TYPE_JSEP, \"jsep\", \"Jsep\"},\n\t{ JANUS_EVENT_TYPE_WEBRTC, \"webrtc\", \"WebRTC\"},\n\t{ JANUS_EVENT_TYPE_MEDIA, \"media\", \"Media\"},\n\t{ JANUS_EVENT_TYPE_PLUGIN, \"plugins\", \"Plugins\"},\n\t{ JANUS_EVENT_TYPE_TRANSPORT, \"transports\", \"Transports\"},\n\t{ JANUS_EVENT_TYPE_CORE, \"core\", \"Core\"},\n\t{ -1, NULL, NULL},\n};\n\nstatic gboolean eventsenabled = FALSE;\nstatic char *server = NULL;\nstatic GHashTable *eventhandlers = NULL;\n\nstatic GAsyncQueue *events = NULL;\nstatic json_t exit_event;\n\nstatic GThread *events_thread;\nvoid *janus_events_thread(void *data);\n\nint janus_events_init(gboolean enabled, char *server_name, GHashTable *handlers) {\n\teventsenabled = enabled;\n\tif(eventsenabled) {\n\t\tevents = g_async_queue_new();\n\t\tif(server_name != NULL)\n\t\t\tserver = g_strdup(server_name);\n\t\teventhandlers = handlers;\n\t\t/* We setup a thread for passing events to the handlers */\n\t\tGError *error = NULL;\n\t\tevents_thread = g_thread_try_new(\"janus events thread\", janus_events_thread, NULL, &error);\n\t\tif(error != NULL) {\n\t\t\teventsenabled = FALSE;\n\t\t\tg_free(server);\n\t\t\tg_async_queue_unref(events);\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Events handler thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\treturn -1;\n\t\t}\n\t}\n\treturn 0;\n}\n\nvoid janus_events_deinit(void) {\n\teventsenabled = FALSE;\n\tif(events != NULL) {\n\t\tg_async_queue_push(events, &exit_event);\n\t}\n\tif(events_thread != NULL) {\n\t\tg_thread_join(events_thread);\n\t\tevents_thread = NULL;\n\t}\n\tif(events != NULL)\n\t\tg_async_queue_unref(events);\n\tg_free(server);\n}\n\ngboolean janus_events_is_enabled(void) {\n\treturn eventsenabled;\n}\n\nvoid janus_events_notify_handlers(int type, int subtype, guint64 session_id, ...) {\n\t/* This method has a variable list of arguments, depending on the event type */\n\tva_list args;\n\tva_start(args, session_id);\n\n\tif(!eventsenabled || eventhandlers == NULL || g_hash_table_size(eventhandlers) == 0) {\n\t\t/* Event handlers disabled, or no event handler plugins available: free resources, if needed */\n\t\tif(type == JANUS_EVENT_TYPE_MEDIA || type == JANUS_EVENT_TYPE_WEBRTC) {\n\t\t\t/* These events allocate a json_t object for their data, skip some arguments and unref it */\n\t\t\tva_arg(args, guint64);\n\t\t\tva_arg(args, char *);\n\t\t\tjson_t *body = va_arg(args, json_t *);\n\t\t\tjson_decref(body);\n\t\t} else if(type == JANUS_EVENT_TYPE_CORE) {\n\t\t\t/* Core events also allocate a json_t object for their data, unref it */\n\t\t\tjson_t *body = va_arg(args, json_t *);\n\t\t\tjson_decref(body);\n\t\t} else if(type == JANUS_EVENT_TYPE_EXTERNAL) {\n\t\t\t/* Admin API originated external events also allocate a json_t object for their data, unref it */\n\t\t\tva_arg(args, char *);\n\t\t\tjson_t *body = va_arg(args, json_t *);\n\t\t\tjson_decref(body);\n\t\t} else if(type == JANUS_EVENT_TYPE_SESSION) {\n\t\t\t/* Session events may allocate a json_t object for transport-related info, unref it */\n\t\t\tva_arg(args, char *);\n\t\t\tjson_t *transport = va_arg(args, json_t *);\n\t\t\tif(transport != NULL)\n\t\t\t\tjson_decref(transport);\n\t\t} else if(type == JANUS_EVENT_TYPE_PLUGIN) {\n\t\t\t/* Plugin originated events also allocate a json_t object for the plugin data, skip some arguments and unref it */\n\t\t\tva_arg(args, guint64);\n\t\t\tva_arg(args, char *);\n\t\t\tva_arg(args, char *);\n\t\t\tjson_t *data = va_arg(args, json_t *);\n\t\t\tjson_decref(data);\n\t\t} else if(type == JANUS_EVENT_TYPE_TRANSPORT) {\n\t\t\t/* Transport originated events also allocate a json_t object for the transport data, skip some arguments and unref it */\n\t\t\tva_arg(args, char *);\n\t\t\tva_arg(args, void *);\n\t\t\tjson_t *data = va_arg(args, json_t *);\n\t\t\tjson_decref(data);\n\t\t}\n\t\tva_end(args);\n\t\treturn;\n\t}\n\n\t/* Prepare the event to notify as a Jansson json_t object */\n\tjson_t *event = json_object();\n\tif(server != NULL)\n\t\tjson_object_set_new(event, \"emitter\", json_string(server));\n\tjson_object_set_new(event, \"type\", json_integer(type));\n\tif(subtype > 0)\n\t\tjson_object_set_new(event, \"subtype\", json_integer(subtype));\n\tjson_object_set_new(event, \"timestamp\", json_integer(janus_get_real_time()));\n\tif(type != JANUS_EVENT_TYPE_CORE && type != JANUS_EVENT_TYPE_EXTERNAL) {\n\t\t/* Core and Admin API originated events don't have a session ID */\n\t\tif(session_id == 0 && (type == JANUS_EVENT_TYPE_PLUGIN || type == JANUS_EVENT_TYPE_TRANSPORT)) {\n\t\t\t/* ... but plugin/transport events may not have one either */\n\t\t} else {\n\t\t\tjson_object_set_new(event, \"session_id\", json_integer(session_id));\n\t\t}\n\t}\n\tjson_t *body = NULL;\n\tif(type != JANUS_EVENT_TYPE_MEDIA && type != JANUS_EVENT_TYPE_WEBRTC && type != JANUS_EVENT_TYPE_CORE)\n\t\tbody = json_object();\n\n\t/* Each type may require different arguments */\n\tswitch(type) {\n\t\tcase JANUS_EVENT_TYPE_SESSION: {\n\t\t\t/* For sessions, there's just a generic event name (what happened) */\n\t\t\tchar *name = va_arg(args, char *);\n\t\t\tjson_object_set_new(body, \"name\", json_string(name));\n\t\t\tjson_t *transport = va_arg(args, json_t *);\n\t\t\tif(transport != NULL)\n\t\t\t\tjson_object_set_new(body, \"transport\", transport);\n\t\t\tbreak;\n\t\t}\n\t\tcase JANUS_EVENT_TYPE_HANDLE: {\n\t\t\t/* For handles, there's the handle ID, a generic event name (what happened)\n\t\t\t * and the plugin package name this handle is (or was) attached to */\n\t\t\tguint64 handle_id = va_arg(args, guint64);\n\t\t\tjson_object_set_new(event, \"handle_id\", json_integer(handle_id));\n\t\t\tchar *name = va_arg(args, char *);\n\t\t\tjson_object_set_new(body, \"name\", json_string(name));\n\t\t\tchar *plugin = va_arg(args, char *);\n\t\t\tjson_object_set_new(body, \"plugin\", json_string(plugin));\n\t\t\t/* Handle-related events may include an opaque ID provided by who's using the plugin:\n\t\t\t * in event handlers, it may be useful for inter-handle mappings or other things */\n\t\t\tchar *opaque_id = va_arg(args, char *);\n\t\t\tif(opaque_id != NULL) {\n\t\t\t\tjson_object_set_new(event, \"opaque_id\", json_string(opaque_id));\n\t\t\t\t/* We add it to the body as well for backwards compatibility, as\n\t\t\t\t * that's the only place we had the opaque_id present before */\n\t\t\t\tjson_object_set_new(body, \"opaque_id\", json_string(opaque_id));\n\t\t\t}\n\t\t\t/* The token used to attach may be provided as well: just as with the opaque_id,\n\t\t\t * in event handlers, it may be useful for inter-handle mappings or other things */\n\t\t\tchar *token = va_arg(args, char *);\n\t\t\tif(token != NULL)\n\t\t\t\tjson_object_set_new(body, \"token\", json_string(token));\n\t\t\tbreak;\n\t\t}\n\t\tcase JANUS_EVENT_TYPE_JSEP: {\n\t\t\t/* For JSEP-related events, there's the handle ID, whether the SDP is local or remote, the JSEP type and the SDP itself */\n\t\t\tguint64 handle_id = va_arg(args, guint64);\n\t\t\tjson_object_set_new(event, \"handle_id\", json_integer(handle_id));\n\t\t\tchar *opaque_id = va_arg(args, char *);\n\t\t\tif(opaque_id != NULL)\n\t\t\t\tjson_object_set_new(event, \"opaque_id\", json_string(opaque_id));\n\t\t\tchar *owner = va_arg(args, char *);\n\t\t\tjson_object_set_new(body, \"owner\", json_string(owner));\n\t\t\tjson_t *jsep = json_object();\n\t\t\tchar *sdp_type = va_arg(args, char *);\n\t\t\tjson_object_set_new(jsep, \"type\", json_string(sdp_type));\n\t\t\tchar *sdp = va_arg(args, char *);\n\t\t\tjson_object_set_new(jsep, \"sdp\", json_string(sdp));\n\t\t\tjson_object_set_new(body, \"jsep\", jsep);\n\t\t\tbreak;\n\t\t}\n\t\tcase JANUS_EVENT_TYPE_WEBRTC:\n\t\tcase JANUS_EVENT_TYPE_MEDIA: {\n\t\t\t/* For WebRTC and media-related events, there's the handle ID and a json_t object with info on what happened */\n\t\t\tguint64 handle_id = va_arg(args, guint64);\n\t\t\tjson_object_set_new(event, \"handle_id\", json_integer(handle_id));\n\t\t\tchar *opaque_id = va_arg(args, char *);\n\t\t\tif(opaque_id != NULL)\n\t\t\t\tjson_object_set_new(event, \"opaque_id\", json_string(opaque_id));\n\t\t\t/* The body is what we get from the event */\n\t\t\tbody = va_arg(args, json_t *);\n\t\t\tbreak;\n\t\t}\n\t\tcase JANUS_EVENT_TYPE_PLUGIN: {\n\t\t\t/* For plugin-originated events, there's the handle ID, the plugin name, and a generic, plugin specific, json_t object */\n\t\t\tguint64 handle_id = va_arg(args, guint64);\n\t\t\tif(handle_id > 0)\t/* Plugins and transports may not specify a session and handle ID for out of context events */\n\t\t\t\tjson_object_set_new(event, \"handle_id\", json_integer(handle_id));\n\t\t\tchar *opaque_id = va_arg(args, char *);\n\t\t\tif(opaque_id != NULL)\n\t\t\t\tjson_object_set_new(event, \"opaque_id\", json_string(opaque_id));\n\t\t\tchar *name = va_arg(args, char *);\n\t\t\tjson_object_set_new(body, \"plugin\", json_string(name));\n\t\t\tjson_t *data = va_arg(args, json_t *);\n\t\t\tjson_object_set_new(body, \"data\", data);\n\t\t\tbreak;\n\t\t}\n\t\tcase JANUS_EVENT_TYPE_TRANSPORT: {\n\t\t\tchar *name = va_arg(args, char *);\n\t\t\tjson_object_set_new(body, \"transport\", json_string(name));\n\t\t\tchar *instance = va_arg(args, void *);\n\t\t\tchar id[32];\n\t\t\tmemset(id, 0, sizeof(id));\n\t\t\t/* To avoid sending a stringified version of the transport pointer\n\t\t\t * around, we convert it to a number and hash it instead */\n\t\t\tuint64_t p = janus_uint64_hash(GPOINTER_TO_UINT(instance));\n\t\t\tg_snprintf(id, sizeof(id), \"%\"SCNu64, p);\n\t\t\tjson_object_set_new(body, \"id\", json_string(id));\n\t\t\tjson_t *data = va_arg(args, json_t *);\n\t\t\tjson_object_set_new(body, \"data\", data);\n\t\t\tbreak;\n\t\t}\n\t\tcase JANUS_EVENT_TYPE_CORE: {\n\t\t\t/* For core-related events, there's a json_t object with info on what happened */\n\t\t\tbody = va_arg(args, json_t *);\n\t\t\tbreak;\n\t\t}\n\t\tcase JANUS_EVENT_TYPE_EXTERNAL: {\n\t\t\tchar *schema = va_arg(args, char *);\n\t\t\tjson_object_set_new(body, \"schema\", json_string(schema));\n\t\t\tjson_t *data = va_arg(args, json_t *);\n\t\t\tjson_object_set_new(body, \"data\", data);\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_WARN, \"Unknown event type '%d'\\n\", type);\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(body);\n\t\t\tva_end(args);\n\t\t\treturn;\n\t}\n\tjson_object_set_new(event, \"event\", body);\n\tva_end(args);\n\n\tif(!eventsenabled) {\n\t\tjson_decref(event);\n\t\treturn;\n\t}\n\t/* Enqueue the event */\n\tg_async_queue_push(events, event);\n}\n\nvoid *janus_events_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining Events handler thread\\n\");\n\tjson_t *event = NULL;\n\n\twhile(eventsenabled) {\n\t\t/* Any event in queue? */\n\t\tevent = g_async_queue_pop(events);\n\t\tif(event == &exit_event)\n\t\t\tbreak;\n\n\t\t/* Notify all interested handlers, increasing the event reference to make sure it's not lost because of errors */\n\t\tint type = json_integer_value(json_object_get(event, \"type\"));\n\t\tguint count = g_hash_table_size(eventhandlers);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, eventhandlers);\n\t\tjson_incref(event);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_eventhandler *e = value;\n\t\t\tif(e == NULL)\n\t\t\t\tcontinue;\n\t\t\tif(!janus_flags_is_set(&e->events_mask, type))\n\t\t\t\tcontinue;\n\t\t\tif(count == 1) {\n\t\t\t\t/* Single event handler: pass this instance directly */\n\t\t\t\te->incoming_event(event);\n\t\t\t} else {\n\t\t\t\t/* Multiple event handlers, that may modify the event: pass a copy */\n\t\t\t\tjson_t *copy = json_deep_copy(event);\n\t\t\t\te->incoming_event(copy);\n\t\t\t\tjson_decref(copy);\n\t\t\t}\n\t\t}\n\t\tjson_decref(event);\n\n\t\t/* Unref the final event reference, interested handlers will have their own reference */\n\t\tjson_decref(event);\n\t}\n\n\t/* Cleanup pending events */\n\twhile((event = g_async_queue_try_pop(events)) != NULL) {\n\t\tif(event != &exit_event)\n\t\t\tjson_decref(event);\n\t}\n\n\tJANUS_LOG(LOG_VERB, \"Leaving Events handler thread\\n\");\n\treturn NULL;\n}\n\n/* Helper method to change the events mask */\nvoid janus_events_edit_events_mask(const char *list, janus_flags *target) {\n\tif(!list)\n\t\treturn;\n\tjanus_flags mask;\n\tjanus_flags_reset(&mask);\n\tif(!strcasecmp(list, \"none\")) {\n\t\t/* Don't subscribe to anything at all */\n\t\tjanus_flags_reset(&mask);\n\t} else if(!strcasecmp(list, \"all\")) {\n\t\t/* Subscribe to everything */\n\t\tjanus_flags_set(&mask, JANUS_EVENT_TYPE_ALL);\n\t} else {\n\t\t/* Check what we need to subscribe to */\n\t\tjanus_flags_reset(&mask);\n\t\tgchar **subscribe = g_strsplit(list, \",\", -1);\n\t\tif(subscribe != NULL) {\n\t\t\tgchar *index = subscribe[0];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\twhile(isspace(*index))\n\t\t\t\t\t\tindex++;\n\t\t\t\t\tif(strlen(index)) {\n\t\t\t\t\t\tstruct janus_event_types *ev = event_types_string;\n\t\t\t\t\t\twhile(ev->label) {\n\t\t\t\t\t\t\tif(!strcasecmp(index, ev->label)) {\n\t\t\t\t\t\t\t\tjanus_flags_set(&mask, ev->type);\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tev++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = subscribe[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_strfreev(subscribe);\n\t\t}\n\t}\n\tif(target)\n\t\tmemcpy(target, &mask, sizeof(janus_flags));\n}\n\n/* Helpers to convert an event type to a string label or a more verbose name */\nconst char *janus_events_type_to_label(int type) {\n\tstruct janus_event_types *ev = event_types_string;\n\twhile(ev->label) {\n\t\tif(type == ev->type)\n\t\t\treturn ev->label;\n\t\tev++;\n\t}\n\treturn (char *)NULL;\n}\n\nconst char *janus_events_type_to_name(int type) {\n\tstruct janus_event_types *ev = event_types_string;\n\twhile(ev->label) {\n\t\tif(type == ev->type)\n\t\t\treturn ev->name;\n\t\tev++;\n\t}\n\treturn (char *)NULL;\n}\n"
  },
  {
    "path": "src/events.h",
    "content": "/*! \\file    events.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Event handler notifications (headers)\n * \\details  Event handler plugins can receive events from the Janus core\n * and other plugins, in order to handle them somehow. This methods\n * provide helpers to notify events to such handlers.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_EVENTS_H\n#define JANUS_EVENTS_H\n\n#include \"debug.h\"\n#include \"events/eventhandler.h\"\n\n/*! \\brief Initialize the event handlers broadcaster\n * @param[in] enabled Whether broadcasting events should be supported at all\n * @param[in] server_name The name of this server, to be added to all events\n * @param[in] handlers Map of all registered event handlers\n * @returns 0 on success, a negative integer otherwise */\nint janus_events_init(gboolean enabled, char *server_name, GHashTable *handlers);\n\n/*! \\brief De-initialize the event handlers broadcaster */\nvoid janus_events_deinit(void);\n\n/*! \\brief Quick method to check whether event handlers are enabled at all or not\n * @returns TRUE if they're enabled, FALSE if not */\ngboolean janus_events_is_enabled(void);\n\n/*! \\brief Notify an event to all interested handlers\n * @note According to the type of event to notify, different arguments may\n * be required and used in order to prepare the actual object to pass to handlers.\n * @param[in] type Type of the event to notify\n * @param[in] subtype Subtype of the event to notify, where applicable (0 if not)\n * @param[in] session_id Janus session identifier this event refers to */\nvoid janus_events_notify_handlers(int type, int subtype, guint64 session_id, ...);\n\n/*! \\brief Helper method to change the mask of events a handler is interested in\n * @note Every time this is called, the mask is reset, which means that to\n * unsubscribe from a single event you have to pass an updated list\n * @param[in] list A comma separated string of event types to subscribe to\n * @param[out] target The mask to update */\nvoid janus_events_edit_events_mask(const char *list, janus_flags *target);\n\n/*! \\brief Helper method to stringify an event type to its label\n * @param[in] type The event type\n * @returns The event type label, if found, or NULL otherwise */\nconst char *janus_events_type_to_label(int type);\n\n/*! \\brief Helper method to stringify an event type to its prettified name\n * @param[in] type The event type\n * @returns The prettified name of the event type, if found, or NULL otherwise */\nconst char *janus_events_type_to_name(int type);\n\n#endif\n"
  },
  {
    "path": "src/ice.c",
    "content": "/*! \\file    ice.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Janus handles and ICE/STUN/TURN processing\n * \\details  A Janus handle represents an abstraction of the communication\n * between a user and a specific plugin, within a Janus session. This is\n * particularly important in terms of media connectivity, as each handle\n * can be associated with a single WebRTC PeerConnection. This code also\n * contains the implementation (based on libnice) of a WebRTC PeerConnection.\n * The code handles the whole ICE process, from the gathering of candidates\n * to the final setup of a virtual channel RTP and RTCP can be transported\n * on. Incoming RTP and RTCP packets from peers are relayed to the associated\n * plugins by means of the incoming_rtp and incoming_rtcp callbacks. Packets\n * to be sent to peers are relayed by peers invoking the relay_rtp and\n * relay_rtcp core callbacks instead.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#include <ifaddrs.h>\n#include <poll.h>\n#include <net/if.h>\n#include <sys/socket.h>\n#include <sys/time.h>\n#include <netdb.h>\n#include <fcntl.h>\n#include <stun/usages/bind.h>\n#include <nice/debug.h>\n\n#include \"janus.h\"\n#include \"debug.h\"\n#include \"ice.h\"\n#include \"turnrest.h\"\n#include \"sdp.h\"\n#include \"rtpsrtp.h\"\n#include \"rtcp.h\"\n#include \"apierror.h\"\n#include \"ip-utils.h\"\n#include \"events.h\"\n\n/* STUN server/port, if any */\nstatic char *janus_stun_server = NULL;\nstatic uint16_t janus_stun_port = 0;\n\nchar *janus_ice_get_stun_server(void) {\n\treturn janus_stun_server;\n}\nuint16_t janus_ice_get_stun_port(void) {\n\treturn janus_stun_port;\n}\n\n\n/* TURN server/port and credentials, if any */\nstatic char *janus_turn_server = NULL;\nstatic uint16_t janus_turn_port = 0;\nstatic char *janus_turn_user = NULL, *janus_turn_pwd = NULL;\nstatic NiceRelayType janus_turn_type = NICE_RELAY_TYPE_TURN_UDP;\n\nchar *janus_ice_get_turn_server(void) {\n\treturn janus_turn_server;\n}\nuint16_t janus_ice_get_turn_port(void) {\n\treturn janus_turn_port;\n}\n\n\n/* TURN REST API support, if any */\nchar *janus_ice_get_turn_rest_api(void) {\n#ifndef HAVE_TURNRESTAPI\n\treturn NULL;\n#else\n\treturn (char *)janus_turnrest_get_backend();\n#endif\n}\n\n/* Force relay settings */\nstatic gboolean force_relay_allowed = FALSE;\nvoid janus_ice_allow_force_relay(void) {\n\tforce_relay_allowed = TRUE;\n}\ngboolean janus_ice_is_force_relay_allowed(void) {\n\treturn force_relay_allowed;\n}\n\n/* ICE-Lite status */\nstatic gboolean janus_ice_lite_enabled;\ngboolean janus_ice_is_ice_lite_enabled(void) {\n\treturn janus_ice_lite_enabled;\n}\n\n/* ICE-TCP support (only libnice >= 0.1.8, currently broken) */\nstatic gboolean janus_ice_tcp_enabled;\ngboolean janus_ice_is_ice_tcp_enabled(void) {\n\treturn janus_ice_tcp_enabled;\n}\n\n/* Full-trickle support */\nstatic gboolean janus_full_trickle_enabled;\ngboolean janus_ice_is_full_trickle_enabled(void) {\n\treturn janus_full_trickle_enabled;\n}\n\n/* mDNS resolution support */\nstatic gboolean janus_mdns_enabled;\ngboolean janus_ice_is_mdns_enabled(void) {\n\treturn janus_mdns_enabled;\n}\n\n/* IPv6 support */\nstatic gboolean janus_ipv6_enabled;\nstatic gboolean janus_ipv6_linklocal_enabled;\ngboolean janus_ice_is_ipv6_enabled(void) {\n\treturn janus_ipv6_enabled;\n}\nstatic gboolean janus_ipv6_linklocal_enabled;\ngboolean janus_ice_is_ipv6_linklocal_enabled(void) {\n\treturn janus_ipv6_linklocal_enabled;\n}\n\n#ifdef HAVE_ICE_NOMINATION\n/* Since libnice 0.1.15, we can configure the ICE nomination mode: it was\n * always \"aggressive\" before, so we set it to \"aggressive\" by default as well */\nstatic NiceNominationMode janus_ice_nomination = NICE_NOMINATION_MODE_AGGRESSIVE;\nvoid janus_ice_set_nomination_mode(const char *nomination) {\n\tif(nomination == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid ICE nomination mode, falling back to 'aggressive'\\n\");\n\t} else if(!strcasecmp(nomination, \"regular\")) {\n\t\tJANUS_LOG(LOG_INFO, \"Configuring Janus to use ICE regular nomination\\n\");\n\t\tjanus_ice_nomination = NICE_NOMINATION_MODE_REGULAR;\n\t} else if(!strcasecmp(nomination, \"aggressive\")) {\n\t\tJANUS_LOG(LOG_INFO, \"Configuring Janus to use ICE aggressive nomination\\n\");\n\t\tjanus_ice_nomination = NICE_NOMINATION_MODE_AGGRESSIVE;\n\t} else {\n\t\tJANUS_LOG(LOG_WARN, \"Unsupported ICE nomination mode '%s', falling back to 'aggressive'\\n\", nomination);\n\t}\n}\nconst char *janus_ice_get_nomination_mode(void) {\n\treturn (janus_ice_nomination == NICE_NOMINATION_MODE_REGULAR ? \"regular\" : \"aggressive\");\n}\n#endif\n\n/* ICE consent freshness */\nstatic gboolean janus_ice_consent_freshness = FALSE;\nvoid janus_ice_set_consent_freshness_enabled(gboolean enabled) {\n#ifndef HAVE_CONSENT_FRESHNESS\n\tif(enabled) {\n\t\tJANUS_LOG(LOG_WARN, \"libnice version doesn't support consent freshness\\n\");\n\t\treturn;\n\t}\n#endif\n\tjanus_ice_consent_freshness = enabled;\n\tif(janus_ice_consent_freshness) {\n\t\tJANUS_LOG(LOG_INFO, \"Using content freshness checks in PeerConnection\\n\");\n\t\tjanus_ice_set_keepalive_conncheck_enabled(TRUE);\n\t}\n}\ngboolean janus_ice_is_consent_freshness_enabled(void) {\n\treturn janus_ice_consent_freshness;\n}\n\n/* Keepalive via connectivity checks */\nstatic gboolean janus_ice_keepalive_connchecks = FALSE;\nvoid janus_ice_set_keepalive_conncheck_enabled(gboolean enabled) {\n\tif(janus_ice_consent_freshness && !enabled) {\n\t\tJANUS_LOG(LOG_WARN, \"Can't disable connectivity checks as PeerConnection keep-alive, consent freshness is enabled\\n\");\n\t\treturn;\n\t}\n\tjanus_ice_keepalive_connchecks = enabled;\n\tif(janus_ice_keepalive_connchecks) {\n\t\tJANUS_LOG(LOG_INFO, \"Using connectivity checks as PeerConnection keep-alive\\n\");\n\t}\n}\ngboolean janus_ice_is_keepalive_conncheck_enabled(void) {\n\treturn janus_ice_keepalive_connchecks;\n}\n\n/* How to react to ICE failures */\nstatic gboolean janus_ice_hangup_on_failed = FALSE;\nvoid janus_ice_set_hangup_on_failed_enabled(gboolean enabled) {\n\tjanus_ice_hangup_on_failed = enabled;\n\tif(janus_ice_hangup_on_failed) {\n\t\tJANUS_LOG(LOG_INFO, \"Will hangup PeerConnections immediately on ICE failures\\n\");\n\t}\n}\ngboolean janus_ice_is_hangup_on_failed_enabled(void) {\n\treturn janus_ice_hangup_on_failed;\n}\n\n/* Opaque IDs set by applications are by default only passed to event handlers\n * for correlation purposes, but not sent back to the user or application in\n * the related Janus API responses or events, unless configured otherwise */\nstatic gboolean opaqueid_in_api = FALSE;\nvoid janus_enable_opaqueid_in_api(void) {\n\topaqueid_in_api = TRUE;\n}\ngboolean janus_is_opaqueid_in_api_enabled(void) {\n\treturn opaqueid_in_api;\n}\n\n/* Only needed in case we're using static event loops spawned at startup (disabled by default) */\ntypedef struct janus_ice_static_event_loop {\n\tint id;\n\tGMainContext *mainctx;\n\tGMainLoop *mainloop;\n\tGThread *thread;\n\tuint16_t handles;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_ice_static_event_loop;\nstatic void janus_ice_static_event_loop_destroy(janus_ice_static_event_loop *loop) {\n\tif(!g_atomic_int_compare_and_exchange(&loop->destroyed, 0, 1))\n\t\treturn;\n\tjanus_refcount_decrease(&loop->ref);\n}\nstatic void janus_ice_static_event_loop_free(const janus_refcount *loop_ref) {\n\tjanus_ice_static_event_loop *loop = janus_refcount_containerof(loop_ref, janus_ice_static_event_loop, ref);\n\tg_free(loop);\n}\nstatic int static_event_loops = 0;\nstatic gboolean allow_loop_indication = FALSE;\nstatic GSList *event_loops = NULL;\nstatic janus_mutex event_loops_mutex = JANUS_MUTEX_INITIALIZER;\nstatic void *janus_ice_static_event_loop_thread(void *data) {\n\tjanus_ice_static_event_loop *loop = data;\n\tJANUS_LOG(LOG_VERB, \"[loop#%d] Event loop thread started\\n\", loop->id);\n\tif(loop->mainloop == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[loop#%d] Invalid loop...\\n\", loop->id);\n\t\tg_thread_unref(g_thread_self());\n\t\tjanus_refcount_decrease(&loop->ref);\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_DBG, \"[loop#%d] Looping...\\n\", loop->id);\n\tg_main_loop_run(loop->mainloop);\n\t/* When the loop quits, we can unref it */\n\tg_main_loop_unref(loop->mainloop);\n\tg_main_context_unref(loop->mainctx);\n\tJANUS_LOG(LOG_VERB, \"[loop#%d] Event loop thread ended!\\n\", loop->id);\n\tjanus_refcount_decrease(&loop->ref);\n\treturn NULL;\n}\nint janus_ice_get_static_event_loops(void) {\n\treturn static_event_loops;\n}\ngboolean janus_ice_is_loop_indication_allowed(void) {\n\treturn allow_loop_indication;\n}\nvoid janus_ice_set_static_event_loops(int loops, gboolean allow_api) {\n\tif(loops == 0)\n\t\treturn;\n\telse if(loops < 1) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid number of static event loops (%d), disabling\\n\", loops);\n\t\treturn;\n\t}\n\t/* Create a pool of new event loops */\n\tint i = 0;\n\tfor(i=0; i<loops; i++) {\n\t\tjanus_ice_static_event_loop *loop = g_malloc0(sizeof(janus_ice_static_event_loop));\n\t\tloop->id = static_event_loops;\n\t\tloop->mainctx = g_main_context_new();\n\t\tloop->mainloop = g_main_loop_new(loop->mainctx, FALSE);\n\t\tjanus_refcount_init(&loop->ref, janus_ice_static_event_loop_free);\n\t\t/* Now spawn a thread for this loop */\n\t\tGError *error = NULL;\n\t\tchar tname[16];\n\t\tg_snprintf(tname, sizeof(tname), \"hloop %d\", loop->id);\n\t\tjanus_refcount_increase(&loop->ref);\n\t\tloop->thread = g_thread_try_new(tname, &janus_ice_static_event_loop_thread, loop, &error);\n\t\tif(error != NULL) {\n\t\t\tg_main_loop_unref(loop->mainloop);\n\t\t\tg_main_context_unref(loop->mainctx);\n\t\t\tjanus_refcount_decrease(&loop->ref);\n\t\t\tjanus_ice_static_event_loop_destroy(loop);\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch a new event loop thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t} else {\n\t\t\tevent_loops = g_slist_append(event_loops, loop);\n\t\t\tstatic_event_loops++;\n\t\t}\n\t}\n\tJANUS_LOG(LOG_INFO, \"Spawned %d static event loops (handles won't have a dedicated loop)\\n\", static_event_loops);\n\tallow_loop_indication = allow_api;\n\tJANUS_LOG(LOG_INFO, \"  -- Janus API %s be able to drive the loop choice for new handles\\n\",\n\t\tallow_loop_indication ? \"will\" : \"will NOT\");\n\treturn;\n}\njson_t *janus_ice_static_event_loops_info(void) {\n\tjson_t *list = json_array();\n\tif(static_event_loops < 1)\n\t\treturn list;\n\tjanus_mutex_lock(&event_loops_mutex);\n\tGSList *l = event_loops;\n\twhile(l) {\n\t\tjanus_ice_static_event_loop *loop = (janus_ice_static_event_loop *)l->data;\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"id\", json_integer(loop->id));\n\t\tjson_object_set_new(info, \"handles\", json_integer(loop->handles));\n\t\tjson_array_append_new(list, info);\n\t\tl = l->next;\n\t}\n\tjanus_mutex_unlock(&event_loops_mutex);\n\treturn list;\n}\nvoid janus_ice_stop_static_event_loops(void) {\n\tif(static_event_loops < 1)\n\t\treturn;\n\t/* Quit all the static loops and wait for the threads to leave */\n\tjanus_mutex_lock(&event_loops_mutex);\n\tGSList *l = event_loops;\n\twhile(l) {\n\t\tjanus_ice_static_event_loop *loop = (janus_ice_static_event_loop *)l->data;\n\t\tif(loop->mainloop != NULL && g_main_loop_is_running(loop->mainloop))\n\t\t\tg_main_loop_quit(loop->mainloop);\n\t\tg_thread_join(loop->thread);\n\t\tl = l->next;\n\t}\n\tg_slist_free_full(event_loops, (GDestroyNotify)janus_ice_static_event_loop_destroy);\n\tjanus_mutex_unlock(&event_loops_mutex);\n}\n\n/* NAT 1:1 stuff */\nstatic gboolean nat_1_1_enabled = FALSE;\nstatic gboolean keep_private_host = FALSE;\nvoid janus_ice_enable_nat_1_1(gboolean kph) {\n\tnat_1_1_enabled = TRUE;\n\tkeep_private_host = kph;\n}\n\n/* Interface/IP enforce/ignore lists */\nGList *janus_ice_enforce_list = NULL, *janus_ice_ignore_list = NULL;\njanus_mutex ice_list_mutex;\n\nvoid janus_ice_enforce_interface(const char *ip) {\n\tif(ip == NULL)\n\t\treturn;\n\t/* Is this an IP or an interface? */\n\tjanus_mutex_lock(&ice_list_mutex);\n\tjanus_ice_enforce_list = g_list_append(janus_ice_enforce_list, (gpointer)ip);\n\tjanus_mutex_unlock(&ice_list_mutex);\n}\ngboolean janus_ice_is_enforced(const char *ip) {\n\tjanus_mutex_lock(&ice_list_mutex);\n\tif(ip == NULL || janus_ice_enforce_list == NULL) {\n\t\tjanus_mutex_unlock(&ice_list_mutex);\n\t\treturn FALSE;\n\t}\n\tGList *temp = janus_ice_enforce_list;\n\twhile(temp) {\n\t\tconst char *enforced = (const char *)temp->data;\n\t\tif(enforced != NULL && strstr(ip, enforced) == ip) {\n\t\t\tjanus_mutex_unlock(&ice_list_mutex);\n\t\t\treturn TRUE;\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tjanus_mutex_unlock(&ice_list_mutex);\n\treturn FALSE;\n}\n\nvoid janus_ice_ignore_interface(const char *ip) {\n\tif(ip == NULL)\n\t\treturn;\n\t/* Is this an IP or an interface? */\n\tjanus_mutex_lock(&ice_list_mutex);\n\tjanus_ice_ignore_list = g_list_append(janus_ice_ignore_list, (gpointer)ip);\n\tif(janus_ice_enforce_list != NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Added %s to the ICE ignore list, but the ICE enforce list is not empty: the ICE ignore list will not be used\\n\", ip);\n\t}\n\tjanus_mutex_unlock(&ice_list_mutex);\n}\ngboolean janus_ice_is_ignored(const char *ip) {\n\tjanus_mutex_lock(&ice_list_mutex);\n\tif(ip == NULL || janus_ice_ignore_list == NULL) {\n\t\tjanus_mutex_unlock(&ice_list_mutex);\n\t\treturn FALSE;\n\t}\n\tGList *temp = janus_ice_ignore_list;\n\twhile(temp) {\n\t\tconst char *ignored = (const char *)temp->data;\n\t\tif(ignored != NULL && strstr(ip, ignored) == ip) {\n\t\t\tjanus_mutex_unlock(&ice_list_mutex);\n\t\t\treturn TRUE;\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tjanus_mutex_unlock(&ice_list_mutex);\n\treturn FALSE;\n}\n\n\n/* Frequency of statistics via event handlers (one second by default) */\nstatic int janus_ice_event_stats_period = 1;\nvoid janus_ice_set_event_stats_period(int period) {\n\tjanus_ice_event_stats_period = period;\n}\nint janus_ice_get_event_stats_period(void) {\n\treturn janus_ice_event_stats_period;\n}\n\n/* How to handle media statistic events (one per media or one per peerConnection) */\nstatic gboolean janus_ice_event_combine_media_stats = FALSE;\nvoid janus_ice_event_set_combine_media_stats(gboolean combine_media_stats_to_one_event) {\n\tjanus_ice_event_combine_media_stats = combine_media_stats_to_one_event;\n}\ngboolean janus_ice_event_get_combine_media_stats(void) {\n\treturn janus_ice_event_combine_media_stats;\n}\n\n/* Number of active PeerConnection (for stats) */\nstatic volatile gint pc_num = 0;\nint janus_ice_get_peerconnection_num(void) {\n\treturn g_atomic_int_get(&pc_num);\n}\n\n/* RTP/RTCP port range */\nstatic uint16_t rtp_range_min = 0;\nstatic uint16_t rtp_range_max = 0;\n\n\n#define JANUS_ICE_PACKET_AUDIO\t0\n#define JANUS_ICE_PACKET_VIDEO\t1\n#define JANUS_ICE_PACKET_TEXT\t2\n#define JANUS_ICE_PACKET_BINARY\t3\n#define JANUS_ICE_PACKET_SCTP\t4\n/* Helper to convert packet types to core types */\nstatic janus_media_type janus_media_type_from_packet(int type) {\n\tswitch(type) {\n\t\tcase JANUS_ICE_PACKET_AUDIO:\n\t\t\treturn JANUS_MEDIA_AUDIO;\n\t\tcase JANUS_ICE_PACKET_VIDEO:\n\t\t\treturn JANUS_MEDIA_VIDEO;\n\t\tcase JANUS_ICE_PACKET_TEXT:\n\t\tcase JANUS_ICE_PACKET_BINARY:\n\t\tcase JANUS_ICE_PACKET_SCTP:\n\t\t\treturn JANUS_MEDIA_DATA;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn JANUS_MEDIA_UNKNOWN;\n}\n/* Janus enqueued (S)RTP/(S)RTCP packet to send */\ntypedef struct janus_ice_queued_packet {\n\tgint mindex;\n\tchar *data;\n\tchar *label;\n\tchar *protocol;\n\tjanus_plugin_rtp_extensions extensions;\n\tgint length;\n\tgint type;\n\tgboolean control, control_ext;\n\tgboolean retransmission;\n\tgboolean encrypted;\n\tgint64 added;\n} janus_ice_queued_packet;\n/* A few static, fake, messages we use as a trigger: e.g., to start a\n * new DTLS handshake, hangup a PeerConnection or close a handle */\nstatic janus_ice_queued_packet\n\tjanus_ice_start_gathering,\n\tjanus_ice_add_candidates,\n\tjanus_ice_dtls_handshake,\n\tjanus_ice_media_stopped,\n\tjanus_ice_hangup_peerconnection,\n\tjanus_ice_detach_handle,\n\tjanus_ice_data_ready;\n\n/* Janus NACKed packet we're tracking (to avoid duplicates) */\ntypedef struct janus_ice_nacked_packet {\n\tjanus_ice_peerconnection_medium *medium;\n\tint vindex;\n\tguint16 seq_number;\n\tguint source_id;\n} janus_ice_nacked_packet;\nstatic gboolean janus_ice_nacked_packet_cleanup(gpointer user_data) {\n\tjanus_ice_nacked_packet *pkt = (janus_ice_nacked_packet *)user_data;\n\n\tif(pkt->medium && pkt->medium->pc && pkt->medium->pc->handle) {\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Cleaning up NACKed packet %\"SCNu16\" (SSRC %\"SCNu32\", vindex %d)...\\n\",\n\t\t\tpkt->medium->pc->handle->handle_id, pkt->seq_number, pkt->medium->ssrc_peer[pkt->vindex], pkt->vindex);\n\t\tg_hash_table_remove(pkt->medium->rtx_nacked[pkt->vindex], GUINT_TO_POINTER(pkt->seq_number));\n\t\tg_hash_table_remove(pkt->medium->pending_nacked_cleanup, GUINT_TO_POINTER(pkt->source_id));\n\t}\n\n\treturn G_SOURCE_REMOVE;\n}\n\nconst char *janus_media_type_str(janus_media_type type) {\n\tswitch(type) {\n\t\tcase JANUS_MEDIA_AUDIO:\n\t\t\treturn \"audio\";\n\t\tcase JANUS_MEDIA_VIDEO:\n\t\t\treturn \"video\";\n\t\tcase JANUS_MEDIA_DATA:\n\t\t\treturn \"data\";\n\t\tcase JANUS_MEDIA_UNKNOWN:\n\t\t\treturn \"unknown\";\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\n/* Deallocation helpers for handles and related structs */\nstatic void janus_ice_handle_free(const janus_refcount *handle_ref);\nstatic void janus_ice_webrtc_free(janus_ice_handle *handle);\nstatic void janus_ice_plugin_session_free(const janus_refcount *app_handle_ref);\nstatic void janus_ice_peerconnection_free(const janus_refcount *pc_ref);\nstatic void janus_ice_peerconnection_medium_free(const janus_refcount *medium_ref);\n\n/* Custom GSource for outgoing traffic */\ntypedef struct janus_ice_outgoing_traffic {\n\tGSource parent;\n\tjanus_ice_handle *handle;\n\tGDestroyNotify destroy;\n} janus_ice_outgoing_traffic;\nstatic gboolean janus_ice_outgoing_rtcp_handle(gpointer user_data);\nstatic gboolean janus_ice_outgoing_stats_handle(gpointer user_data);\nstatic gboolean janus_ice_outgoing_traffic_handle(janus_ice_handle *handle, janus_ice_queued_packet *pkt);\nstatic gboolean janus_ice_outgoing_traffic_prepare(GSource *source, gint *timeout) {\n\tjanus_ice_outgoing_traffic *t = (janus_ice_outgoing_traffic *)source;\n\treturn (g_async_queue_length(t->handle->queued_packets) > 0);\n}\nstatic gboolean janus_ice_outgoing_traffic_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) {\n\tjanus_ice_outgoing_traffic *t = (janus_ice_outgoing_traffic *)source;\n\tint ret = G_SOURCE_CONTINUE;\n\tjanus_ice_queued_packet *pkt = NULL;\n\twhile((pkt = g_async_queue_try_pop(t->handle->queued_packets)) != NULL) {\n\t\tif(janus_ice_outgoing_traffic_handle(t->handle, pkt) == G_SOURCE_REMOVE)\n\t\t\tret = G_SOURCE_REMOVE;\n\t}\n\treturn ret;\n}\nstatic void janus_ice_outgoing_traffic_finalize(GSource *source) {\n\tjanus_ice_outgoing_traffic *t = (janus_ice_outgoing_traffic *)source;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Finalizing loop source\\n\", t->handle->handle_id);\n\tif(static_event_loops > 0) {\n\t\t/* This handle was sharing an event loop with others */\n\t\tjanus_ice_webrtc_free(t->handle);\n\t\tjanus_refcount_decrease(&t->handle->ref);\n\t} else if(t->handle->mainloop != NULL && g_main_loop_is_running(t->handle->mainloop)) {\n\t\t/* This handle had a dedicated event loop, quit it */\n\t\tg_main_loop_quit(t->handle->mainloop);\n\t}\n\tjanus_refcount_decrease(&t->handle->ref);\n}\nstatic GSourceFuncs janus_ice_outgoing_traffic_funcs = {\n\tjanus_ice_outgoing_traffic_prepare,\n\tNULL,\t/* We don't need check */\n\tjanus_ice_outgoing_traffic_dispatch,\n\tjanus_ice_outgoing_traffic_finalize,\n\tNULL, NULL\n};\nstatic GSource *janus_ice_outgoing_traffic_create(janus_ice_handle *handle, GDestroyNotify destroy) {\n\tGSource *source = g_source_new(&janus_ice_outgoing_traffic_funcs, sizeof(janus_ice_outgoing_traffic));\n\tjanus_ice_outgoing_traffic *t = (janus_ice_outgoing_traffic *)source;\n\tchar name[255];\n\tg_snprintf(name, sizeof(name), \"source-%\"SCNu64, handle->handle_id);\n\tg_source_set_name(source, name);\n\tjanus_refcount_increase(&handle->ref);\n\tt->handle = handle;\n\tt->destroy = destroy;\n\treturn source;\n}\n\n/* Time, in seconds, that should pass with no media (audio or video) being\n * received before Janus notifies you about this with a receiving=false */\n#define DEFAULT_NO_MEDIA_TIMER\t1\nstatic uint no_media_timer = DEFAULT_NO_MEDIA_TIMER;\nvoid janus_set_no_media_timer(uint timer) {\n\tno_media_timer = timer;\n\tif(no_media_timer == 0)\n\t\tJANUS_LOG(LOG_VERB, \"Disabling no-media timer\\n\");\n\telse\n\t\tJANUS_LOG(LOG_VERB, \"Setting no-media timer to %us\\n\", no_media_timer);\n}\nuint janus_get_no_media_timer(void) {\n\treturn no_media_timer;\n}\n\n/* Number of lost packets per seconds on a media stream (uplink or downlink,\n * audio or video), that should result in a slow-link event to the user.\n * By default the feature is disabled (threshold=0), as it can be quite\n * verbose and is often redundant information, since the same info on lost\n * packets (in and out) can already be retrieved via client-side stats */\n#define DEFAULT_SLOWLINK_THRESHOLD\t0\nstatic uint slowlink_threshold = DEFAULT_SLOWLINK_THRESHOLD;\nvoid janus_set_slowlink_threshold(uint packets) {\n\tslowlink_threshold = packets;\n\tif(slowlink_threshold == 0)\n\t\tJANUS_LOG(LOG_VERB, \"Disabling slow-link events\\n\");\n\telse\n\t\tJANUS_LOG(LOG_VERB, \"Setting slowlink-threshold to %u packets\\n\", slowlink_threshold);\n}\nuint janus_get_slowlink_threshold(void) {\n\treturn slowlink_threshold;\n}\n\n/* Period, in milliseconds, to refer to for sending TWCC feedback */\n#define DEFAULT_TWCC_PERIOD\t\t200\nstatic uint twcc_period = DEFAULT_TWCC_PERIOD;\nvoid janus_set_twcc_period(uint period) {\n\ttwcc_period = period;\n\tif(twcc_period == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid TWCC period, falling back to default\\n\");\n\t\ttwcc_period = DEFAULT_TWCC_PERIOD;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Setting TWCC period to %ds\\n\", twcc_period);\n\t}\n}\nuint janus_get_twcc_period(void) {\n\treturn twcc_period;\n}\n\n/* DSCP value, which we can set via libnice: it's disabled by default */\nstatic int dscp_ef = 0;\nvoid janus_set_dscp(int dscp) {\n\tdscp_ef = dscp;\n\tif(dscp_ef > 0) {\n\t\tJANUS_LOG(LOG_VERB, \"Setting DSCP EF to %d\\n\", dscp_ef);\n\t}\n}\nint janus_get_dscp(void) {\n\treturn dscp_ef;\n}\n\n\nstatic inline void janus_ice_free_rtp_packet(janus_rtp_packet *pkt) {\n\tif(pkt == NULL) {\n\t\treturn;\n\t}\n\n\tg_free(pkt->data);\n\tg_free(pkt);\n}\n\nstatic void janus_ice_free_queued_packet(janus_ice_queued_packet *pkt) {\n\tif(pkt == NULL || pkt == &janus_ice_start_gathering ||\n\t\t\tpkt == &janus_ice_add_candidates ||\n\t\t\tpkt == &janus_ice_dtls_handshake ||\n\t\t\tpkt == &janus_ice_media_stopped ||\n\t\t\tpkt == &janus_ice_hangup_peerconnection ||\n\t\t\tpkt == &janus_ice_detach_handle ||\n\t\t\tpkt == &janus_ice_data_ready) {\n\t\treturn;\n\t}\n\tg_free(pkt->data);\n\tg_free(pkt->label);\n\tg_free(pkt->protocol);\n\tg_free(pkt);\n}\n\n/* Minimum and maximum value, in milliseconds, for the NACK queue/retransmissions (default=200ms/1000ms) */\n#define DEFAULT_MIN_NACK_QUEUE\t200\n#define DEFAULT_MAX_NACK_QUEUE\t1000\n/* Min/Max time to rate limit retransmissions of the same packet */\n#define MAX_NACK_IGNORE\t\t\tDEFAULT_MAX_NACK_QUEUE*1000\n#define MIN_NACK_IGNORE\t\t\t40000\n\nstatic gboolean nack_optimizations = FALSE;\nvoid janus_set_nack_optimizations_enabled(gboolean optimize) {\n\tnack_optimizations = optimize;\n}\ngboolean janus_is_nack_optimizations_enabled(void) {\n\treturn nack_optimizations;\n}\n\nstatic uint16_t min_nack_queue = DEFAULT_MIN_NACK_QUEUE;\nvoid janus_set_min_nack_queue(uint16_t mnq) {\n\tmin_nack_queue = mnq < DEFAULT_MAX_NACK_QUEUE ? mnq : DEFAULT_MAX_NACK_QUEUE;\n\tif(min_nack_queue == 0)\n\t\tJANUS_LOG(LOG_VERB, \"Disabling NACK queue\\n\");\n\telse\n\t\tJANUS_LOG(LOG_VERB, \"Setting min NACK queue to %dms\\n\", min_nack_queue);\n}\nuint16_t janus_get_min_nack_queue(void) {\n\treturn min_nack_queue;\n}\n/* Helper to clean old NACK packets in the buffer when they exceed the queue time limit */\nstatic void janus_cleanup_nack_buffer(gint64 now, janus_ice_peerconnection *pc, gboolean audio, gboolean video) {\n\t/* Iterate on all media */\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tuint mi=0;\n\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\tif(!medium)\n\t\t\tcontinue;\n\t\tif((medium->type == JANUS_MEDIA_AUDIO && !audio) || (medium->type == JANUS_MEDIA_VIDEO && !video))\n\t\t\tcontinue;\n\t\tif(medium->retransmit_buffer) {\n\t\t\tjanus_rtp_packet *p = (janus_rtp_packet *)g_queue_peek_head(medium->retransmit_buffer);\n\t\t\twhile(p && (!now || (now - p->created >= (gint64)medium->nack_queue_ms*1000))) {\n\t\t\t\t/* Packet is too old, get rid of it */\n\t\t\t\tg_queue_pop_head(medium->retransmit_buffer);\n\t\t\t\t/* Remove from hashtable too */\n\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)p->data;\n\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\tg_hash_table_remove(medium->retransmit_seqs, GUINT_TO_POINTER(seq));\n\t\t\t\t/* Free the packet */\n\t\t\t\tjanus_ice_free_rtp_packet(p);\n\t\t\t\tp = (janus_rtp_packet *)g_queue_peek_head(medium->retransmit_buffer);\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n#define SEQ_MISSING_WAIT 12000 /*  12ms */\n#define SEQ_NACKED_WAIT 155000 /* 155ms */\n/* janus_seq_info list functions */\nstatic void janus_seq_append(janus_seq_info **head, janus_seq_info *new_seq) {\n\tif(*head == NULL) {\n\t\tnew_seq->prev = new_seq;\n\t\tnew_seq->next = new_seq;\n\t\t*head = new_seq;\n\t} else {\n\t\tjanus_seq_info *last_seq = (*head)->prev;\n\t\tnew_seq->prev = last_seq;\n\t\tnew_seq->next = *head;\n\t\t(*head)->prev = new_seq;\n\t\tlast_seq->next = new_seq;\n\t}\n}\nstatic janus_seq_info *janus_seq_pop_head(janus_seq_info **head) {\n\tjanus_seq_info *pop_seq = *head;\n\tif(pop_seq) {\n\t\tjanus_seq_info *new_head = pop_seq->next;\n\t\tif(pop_seq == new_head || new_head == NULL) {\n\t\t\t*head = NULL;\n\t\t} else {\n\t\t\t*head = new_head;\n\t\t\tnew_head->prev = pop_seq->prev;\n\t\t\tnew_head->prev->next = new_head;\n\t\t}\n\t}\n\treturn pop_seq;\n}\nvoid janus_seq_list_free(janus_seq_info **head) {\n\tif(!*head)\n\t\treturn;\n\tjanus_seq_info *cur = *head;\n\tdo {\n\t\tjanus_seq_info *next = cur->next;\n\t\tg_free(cur);\n\t\tcur = next;\n\t} while(cur != *head);\n\t*head = NULL;\n}\nstatic int janus_seq_in_range(guint16 seqn, guint16 start, guint16 len) {\n\t/* Supports wrapping sequence (easier with int range) */\n\tint n = seqn;\n\tint nh = (1<<16) + n;\n\tint s = start;\n\tint e = s + len;\n\treturn (s <= n && n < e) || (s <= nh && nh < e);\n}\n\n\n/* Internal method for relaying RTCP messages, optionally filtering them in case they come from plugins */\nvoid janus_ice_relay_rtcp_internal(janus_ice_handle *handle, janus_ice_peerconnection_medium *medium,\n\tjanus_plugin_rtcp *packet, gboolean filter_rtcp);\n\n\n/* Map of active plugin sessions */\nstatic GHashTable *plugin_sessions;\nstatic janus_mutex plugin_sessions_mutex = JANUS_MUTEX_INITIALIZER;\ngboolean janus_plugin_session_is_alive(janus_plugin_session *plugin_session) {\n\tif(plugin_session == NULL || plugin_session < (janus_plugin_session *)0x1000 ||\n\t\t\tg_atomic_int_get(&plugin_session->stopped))\n\t\treturn FALSE;\n\t/* Make sure this plugin session is still alive */\n\tjanus_mutex_lock_nodebug(&plugin_sessions_mutex);\n\tjanus_plugin_session *result = g_hash_table_lookup(plugin_sessions, plugin_session);\n\tjanus_mutex_unlock_nodebug(&plugin_sessions_mutex);\n\tif(result == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid plugin session (%p)\\n\", plugin_session);\n\t}\n\treturn (result != NULL);\n}\nstatic void janus_plugin_session_dereference(janus_plugin_session *plugin_session) {\n\tif(plugin_session)\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n}\n\n\nstatic void janus_ice_clear_queued_candidates(janus_ice_handle *handle) {\n\tif(handle == NULL || handle->queued_candidates == NULL) {\n\t\treturn;\n\t}\n\twhile(g_async_queue_length(handle->queued_candidates) > 0) {\n\t\t(void)g_async_queue_try_pop(handle->queued_candidates);\n\t}\n}\n\nstatic void janus_ice_clear_queued_packets(janus_ice_handle *handle) {\n\tif(handle == NULL || handle->queued_packets == NULL) {\n\t\treturn;\n\t}\n\tjanus_ice_queued_packet *pkt = NULL;\n\twhile(g_async_queue_length(handle->queued_packets) > 0) {\n\t\tpkt = g_async_queue_try_pop(handle->queued_packets);\n\t\tjanus_ice_free_queued_packet(pkt);\n\t}\n}\n\n\nstatic void janus_ice_notify_trickle(janus_ice_handle *handle, char *buffer) {\n\tif(handle == NULL)\n\t\treturn;\n\tchar cbuffer[200];\n\tif(buffer != NULL)\n\t\tg_snprintf(cbuffer, sizeof(cbuffer), \"candidate:%s\", buffer);\n\t/* Send a \"trickle\" event to the browser */\n\tjanus_session *session = (janus_session *)handle->session;\n\tif(session == NULL)\n\t\treturn;\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"janus\", json_string(\"trickle\"));\n\tjson_object_set_new(event, \"session_id\", json_integer(session->session_id));\n\tjson_object_set_new(event, \"sender\", json_integer(handle->handle_id));\n\tif(opaqueid_in_api && handle->opaque_id != NULL)\n\t\tjson_object_set_new(event, \"opaque_id\", json_string(handle->opaque_id));\n\tjson_t *candidate = json_object();\n\tif(buffer != NULL) {\n\t\tjson_object_set_new(candidate, \"sdpMid\", json_string(handle->pc_mid));\n\t\tjson_object_set_new(candidate, \"sdpMLineIndex\", json_integer(0));\n\t\tjson_object_set_new(candidate, \"candidate\", json_string(cbuffer));\n\t} else {\n\t\tjson_object_set_new(candidate, \"completed\", json_true());\n\t}\n\tjson_object_set_new(event, \"candidate\", candidate);\n\t/* Send the event */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending trickle event (%s) to transport...\\n\",\n\t\thandle->handle_id, buffer ? \"candidate\" : \"end-of-candidates\");\n\tjanus_session_notify_event(session, event);\n}\n\nstatic void janus_ice_notify_media(janus_ice_handle *handle, char *mid, gboolean video, gboolean simulcast, int substream, gboolean up) {\n\tif(handle == NULL)\n\t\treturn;\n\t/* Prepare JSON event to notify user/application */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Notifying that we %s receiving %s on mid %s\\n\",\n\t\thandle->handle_id, up ? \"are\" : \"are NOT\", video ? \"video\" : \"audio\", mid);\n\tjanus_session *session = (janus_session *)handle->session;\n\tif(session == NULL)\n\t\treturn;\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"janus\", json_string(\"media\"));\n\tjson_object_set_new(event, \"session_id\", json_integer(session->session_id));\n\tjson_object_set_new(event, \"sender\", json_integer(handle->handle_id));\n\tif(opaqueid_in_api && handle->opaque_id != NULL)\n\t\tjson_object_set_new(event, \"opaque_id\", json_string(handle->opaque_id));\n\tjson_object_set_new(event, \"mid\", json_string(mid));\n\tjson_object_set_new(event, \"type\", json_string(video ? \"video\" : \"audio\"));\n\tif(simulcast)\n\t\tjson_object_set_new(event, \"substream\", json_integer(substream));\n\tjson_object_set_new(event, \"receiving\", up ? json_true() : json_false());\n\tif(!up && no_media_timer > 1)\n\t\tjson_object_set_new(event, \"seconds\", json_integer(no_media_timer));\n\t/* Send the event */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending event to transport...\\n\", handle->handle_id);\n\tjanus_session_notify_event(session, event);\n\t/* Notify event handlers as well */\n\tif(janus_events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"media\", json_string(video ? \"video\" : \"audio\"));\n\t\tjson_object_set_new(info, \"mid\", json_string(mid));\n\t\tif(simulcast)\n\t\t\tjson_object_set_new(info, \"substream\", json_integer(substream));\n\t\tjson_object_set_new(info, \"receiving\", up ? json_true() : json_false());\n\t\tif(!up && no_media_timer > 1)\n\t\t\tjson_object_set_new(info, \"seconds\", json_integer(no_media_timer));\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_MEDIA, JANUS_EVENT_SUBTYPE_MEDIA_STATE,\n\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t}\n}\n\nstatic void janus_ice_notify_ice_failed(janus_ice_handle *handle) {\n\tif(handle == NULL)\n\t\treturn;\n\t/* Prepare JSON event to notify user/application */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Notifying WebRTC ICE failure; %p\\n\", handle->handle_id, handle);\n\tjanus_session *session = (janus_session *)handle->session;\n\tif(session == NULL)\n\t\treturn;\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"janus\", json_string(\"ice-failed\"));\n\tjson_object_set_new(event, \"session_id\", json_integer(session->session_id));\n\tjson_object_set_new(event, \"sender\", json_integer(handle->handle_id));\n\tif(opaqueid_in_api && handle->opaque_id != NULL)\n\t\tjson_object_set_new(event, \"opaque_id\", json_string(handle->opaque_id));\n\t/* Send the event */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending event to transport...; %p\\n\", handle->handle_id, handle);\n\tjanus_session_notify_event(session, event);\n}\n\nvoid janus_ice_notify_hangup(janus_ice_handle *handle, const char *reason) {\n\tif(handle == NULL)\n\t\treturn;\n\t/* Prepare JSON event to notify user/application */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Notifying WebRTC hangup; %p\\n\", handle->handle_id, handle);\n\tjanus_session *session = (janus_session *)handle->session;\n\tif(session == NULL)\n\t\treturn;\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"janus\", json_string(\"hangup\"));\n\tjson_object_set_new(event, \"session_id\", json_integer(session->session_id));\n\tjson_object_set_new(event, \"sender\", json_integer(handle->handle_id));\n\tif(opaqueid_in_api && handle->opaque_id != NULL)\n\t\tjson_object_set_new(event, \"opaque_id\", json_string(handle->opaque_id));\n\tif(reason != NULL)\n\t\tjson_object_set_new(event, \"reason\", json_string(reason));\n\t/* Send the event */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending event to transport...; %p\\n\", handle->handle_id, handle);\n\tjanus_session_notify_event(session, event);\n\t/* Notify event handlers as well */\n\tif(janus_events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"connection\", json_string(\"hangup\"));\n\t\tif(reason != NULL)\n\t\t\tjson_object_set_new(info, \"reason\", json_string(reason));\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_WEBRTC, JANUS_EVENT_SUBTYPE_WEBRTC_STATE,\n\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t}\n}\n\n\n/* Trickle helpers */\njanus_ice_trickle *janus_ice_trickle_new(const char *transaction, json_t *candidate) {\n\tif(transaction == NULL || candidate == NULL)\n\t\treturn NULL;\n\tjanus_ice_trickle *trickle = g_malloc(sizeof(janus_ice_trickle));\n\ttrickle->handle = NULL;\n\ttrickle->received = janus_get_monotonic_time();\n\ttrickle->transaction = g_strdup(transaction);\n\ttrickle->candidate = json_deep_copy(candidate);\n\treturn trickle;\n}\n\ngint janus_ice_trickle_parse(janus_ice_handle *handle, json_t *candidate, const char **error) {\n\tconst char *ignore_error = NULL;\n\tif(error == NULL) {\n\t\terror = &ignore_error;\n\t}\n\tif(handle == NULL) {\n\t\t*error = \"Invalid handle\";\n\t\treturn JANUS_ERROR_HANDLE_NOT_FOUND;\n\t}\n\t/* Parse trickle candidate */\n\tif(!json_is_object(candidate) || json_object_get(candidate, \"completed\") != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"No more remote candidates for handle %\"SCNu64\"!\\n\", handle->handle_id);\n\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES);\n\t} else {\n\t\t/* Handle remote candidate */\n\t\tjson_t *mid = json_object_get(candidate, \"sdpMid\");\n\t\tif(mid && !json_is_string(mid)) {\n\t\t\t*error = \"Trickle error: invalid element type (sdpMid should be a string)\";\n\t\t\treturn JANUS_ERROR_INVALID_ELEMENT_TYPE;\n\t\t}\n\t\tjson_t *mline = json_object_get(candidate, \"sdpMLineIndex\");\n\t\tif(mline && (!json_is_integer(mline) || json_integer_value(mline) < 0)) {\n\t\t\t*error = \"Trickle error: invalid element type (sdpMLineIndex should be a positive integer)\";\n\t\t\treturn JANUS_ERROR_INVALID_ELEMENT_TYPE;\n\t\t}\n\t\tif(!mid && !mline) {\n\t\t\t*error = \"Trickle error: missing mandatory element (sdpMid or sdpMLineIndex)\";\n\t\t\treturn JANUS_ERROR_MISSING_MANDATORY_ELEMENT;\n\t\t}\n\t\tjson_t *rc = json_object_get(candidate, \"candidate\");\n\t\tif(!rc) {\n\t\t\t*error = \"Trickle error: missing mandatory element (candidate)\";\n\t\t\treturn JANUS_ERROR_MISSING_MANDATORY_ELEMENT;\n\t\t}\n\t\tif(!json_is_string(rc)) {\n\t\t\t*error = \"Trickle error: invalid element type (candidate should be a string)\";\n\t\t\treturn JANUS_ERROR_INVALID_ELEMENT_TYPE;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Trickle candidate (%s): %s\\n\", handle->handle_id, json_string_value(mid), json_string_value(rc));\n\t\t/* Parse it */\n\t\tint sdpMLineIndex = mline ? json_integer_value(mline) : -1;\n\t\tconst char *sdpMid = json_string_value(mid);\n\t\tif(sdpMLineIndex > 0 || (handle->pc_mid && sdpMid && strcmp(handle->pc_mid, sdpMid))) {\n\t\t\t/* FIXME We bundle everything, so we ignore candidates for anything beyond the first m-line */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Got a mid='%s' candidate (index %d) but we're bundling, ignoring...\\n\",\n\t\t\t\thandle->handle_id, json_string_value(mid), sdpMLineIndex);\n\t\t\treturn 0;\n\t\t}\n\t\tjanus_ice_peerconnection *pc = handle->pc;\n\t\tif(pc == NULL) {\n\t\t\t*error = \"Trickle error: invalid element type (no such PeerConnection)\";\n\t\t\treturn JANUS_ERROR_TRICKE_INVALID_STREAM;\n\t\t}\n\t\tint res = janus_sdp_parse_candidate(pc, json_string_value(rc), 1);\n\t\tif(res != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse candidate... (%d)\\n\", handle->handle_id, res);\n\t\t\t/* FIXME Should we return an error? */\n\t\t}\n\t}\n\treturn 0;\n}\n\nvoid janus_ice_trickle_destroy(janus_ice_trickle *trickle) {\n\tif(trickle == NULL)\n\t\treturn;\n\tg_free(trickle->transaction);\n\ttrickle->transaction = NULL;\n\tif(trickle->candidate)\n\t\tjson_decref(trickle->candidate);\n\ttrickle->candidate = NULL;\n\tg_free(trickle);\n}\n\n\n/* libnice initialization */\nvoid janus_ice_init(gboolean ice_lite, gboolean ice_tcp, gboolean full_trickle, gboolean ignore_mdns,\n\t\tgboolean ipv6, gboolean ipv6_linklocal, uint16_t rtp_min_port, uint16_t rtp_max_port) {\n\tjanus_ice_lite_enabled = ice_lite;\n\tjanus_ice_tcp_enabled = ice_tcp;\n\tjanus_full_trickle_enabled = full_trickle;\n\tjanus_mdns_enabled = !ignore_mdns;\n\tjanus_ipv6_enabled = ipv6;\n\tif(ipv6)\n\t\tjanus_ipv6_linklocal_enabled = ipv6_linklocal;\n\tJANUS_LOG(LOG_INFO, \"Initializing ICE stuff (%s mode, ICE-TCP candidates %s, %s-trickle, IPv6 support %s)\\n\",\n\t\tjanus_ice_lite_enabled ? \"Lite\" : \"Full\",\n\t\tjanus_ice_tcp_enabled ? \"enabled\" : \"disabled\",\n\t\tjanus_full_trickle_enabled ? \"full\" : \"half\",\n\t\tjanus_ipv6_enabled ? \"enabled\" : \"disabled\");\n\tif(janus_ice_tcp_enabled) {\n#ifndef HAVE_LIBNICE_TCP\n\t\tJANUS_LOG(LOG_WARN, \"libnice version < 0.1.8, disabling ICE-TCP support\\n\");\n\t\tjanus_ice_tcp_enabled = FALSE;\n#else\n\t\tif(!janus_ice_lite_enabled) {\n\t\t\tJANUS_LOG(LOG_WARN, \"You may experience problems when having ICE-TCP enabled without having ICE Lite enabled too in libnice\\n\");\n\t\t}\n#endif\n\t}\n\t/*! \\note The RTP/RTCP port range configuration may be just a placeholder: for\n\t * instance, libnice supports this since 0.1.0, but the 0.1.3 on Fedora fails\n\t * when linking with an undefined reference to \\c nice_agent_set_port_range\n\t * so this is checked by the install.sh script in advance. */\n\trtp_range_min = rtp_min_port;\n\trtp_range_max = rtp_max_port;\n\tif(rtp_range_max < rtp_range_min) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid ICE port range: %\"SCNu16\" > %\"SCNu16\"\\n\", rtp_range_min, rtp_range_max);\n\t} else if(rtp_range_min > 0 || rtp_range_max > 0) {\n#ifndef HAVE_PORTRANGE\n\t\tJANUS_LOG(LOG_WARN, \"nice_agent_set_port_range unavailable, port range disabled\\n\");\n#else\n\t\tJANUS_LOG(LOG_INFO, \"ICE port range: %\"SCNu16\"-%\"SCNu16\"\\n\", rtp_range_min, rtp_range_max);\n#endif\n\t}\n\tif(!janus_mdns_enabled)\n\t\tJANUS_LOG(LOG_WARN, \"mDNS resolution disabled, .local candidates will be ignored\\n\");\n\n\t/* We keep track of plugin sessions to avoid problems */\n\tplugin_sessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_plugin_session_dereference);\n\n#ifdef HAVE_TURNRESTAPI\n\t/* Initialize the TURN REST API client stack, whether we're going to use it or not */\n\tjanus_turnrest_init();\n#endif\n\n}\n\nvoid janus_ice_deinit(void) {\n#ifdef HAVE_TURNRESTAPI\n\tjanus_turnrest_deinit();\n#endif\n}\n\nint janus_ice_test_stun_server(janus_network_address *addr, uint16_t port,\n\t\tuint16_t local_port, janus_network_address *public_addr, uint16_t *public_port) {\n\tif(!addr || !public_addr)\n\t\treturn -1;\n\t/* Test the STUN server */\n\tStunAgent stun;\n\tstun_agent_init (&stun, STUN_ALL_KNOWN_ATTRIBUTES, STUN_COMPATIBILITY_RFC5389, 0);\n\tStunMessage msg;\n\tuint8_t buf[1500];\n\tsize_t len = stun_usage_bind_create(&stun, &msg, buf, 1500);\n\tJANUS_LOG(LOG_INFO, \"Testing STUN server: message is of %zu bytes\\n\", len);\n\t/* Use the janus_network_address info to drive the socket creation */\n\tint fd = socket(addr->family, SOCK_DGRAM, 0);\n\tif(fd < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error creating socket for STUN BINDING test\\n\");\n\t\treturn -1;\n\t}\n\tstruct sockaddr *address = NULL, *remote = NULL;\n\tstruct sockaddr_in address4 = { 0 }, remote4 = { 0 };\n\tstruct sockaddr_in6 address6 = { 0 }, remote6 = { 0 };\n\tsocklen_t addrlen = 0;\n\tif(addr->family == AF_INET) {\n\t\tmemset(&address4, 0, sizeof(address4));\n\t\taddress4.sin_family = AF_INET;\n\t\taddress4.sin_port = htons(local_port);\n\t\taddress4.sin_addr.s_addr = INADDR_ANY;\n\t\tmemset(&remote4, 0, sizeof(remote4));\n\t\tremote4.sin_family = AF_INET;\n\t\tremote4.sin_port = htons(port);\n\t\tmemcpy(&remote4.sin_addr, &addr->ipv4, sizeof(addr->ipv4));\n\t\taddress = (struct sockaddr *)(&address4);\n\t\tremote = (struct sockaddr *)(&remote4);\n\t\taddrlen = sizeof(remote4);\n\t} else if(addr->family == AF_INET6) {\n\t\tmemset(&address6, 0, sizeof(address6));\n\t\taddress6.sin6_family = AF_INET6;\n\t\taddress6.sin6_port = htons(local_port);\n\t\taddress6.sin6_addr = in6addr_any;\n\t\tmemset(&remote6, 0, sizeof(remote6));\n\t\tremote6.sin6_family = AF_INET6;\n\t\tremote6.sin6_port = htons(port);\n\t\tmemcpy(&remote6.sin6_addr, &addr->ipv6, sizeof(addr->ipv6));\n\t\tremote6.sin6_addr = addr->ipv6;\n\t\taddress = (struct sockaddr *)(&address6);\n\t\tremote = (struct sockaddr *)(&remote6);\n\t\taddrlen = sizeof(remote6);\n\t}\n\tif(bind(fd, address, addrlen) < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Bind failed for STUN BINDING test: %d (%s)\\n\", errno, g_strerror(errno));\n\t\tclose(fd);\n\t\treturn -1;\n\t}\n\tint bytes = sendto(fd, buf, len, 0, remote, addrlen);\n\tif(bytes < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error sending STUN BINDING test\\n\");\n\t\tclose(fd);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_VERB, \"  >> Sent %d bytes, waiting for reply...\\n\", bytes);\n\tstruct timeval timeout;\n\tfd_set readfds;\n\tFD_ZERO(&readfds);\n\tFD_SET(fd, &readfds);\n\ttimeout.tv_sec = 5;\t/* FIXME Don't wait forever */\n\ttimeout.tv_usec = 0;\n\tint err = select(fd+1, &readfds, NULL, NULL, &timeout);\n\tif(err < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error waiting for a response to our STUN BINDING test: %d (%s)\\n\", errno, g_strerror(errno));\n\t\tclose(fd);\n\t\treturn -1;\n\t}\n\tif(!FD_ISSET(fd, &readfds)) {\n\t\tJANUS_LOG(LOG_FATAL, \"No response to our STUN BINDING test\\n\");\n\t\tclose(fd);\n\t\treturn -1;\n\t}\n\tbytes = recvfrom(fd, buf, 1500, 0, remote, &addrlen);\n\tJANUS_LOG(LOG_VERB, \"  >> Got %d bytes...\\n\", bytes);\n\tclose(fd);\n\tif(bytes < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Failed to receive STUN\\n\");\n\t\treturn -1;\n\t}\n\tif(stun_agent_validate (&stun, &msg, buf, bytes, NULL, NULL) != STUN_VALIDATION_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Failed to validate STUN BINDING response\\n\");\n\t\treturn -1;\n\t}\n\tStunClass class = stun_message_get_class(&msg);\n\tStunMethod method = stun_message_get_method(&msg);\n\tif(class != STUN_RESPONSE || method != STUN_BINDING) {\n\t\tJANUS_LOG(LOG_FATAL, \"Unexpected STUN response: %d/%d\\n\", class, method);\n\t\treturn -1;\n\t}\n\tStunMessageReturn ret = stun_message_find_xor_addr(&msg, STUN_ATTRIBUTE_XOR_MAPPED_ADDRESS, (struct sockaddr_storage *)address, &addrlen);\n\tJANUS_LOG(LOG_VERB, \"  >> XOR-MAPPED-ADDRESS: %d\\n\", ret);\n\tif(ret == STUN_MESSAGE_RETURN_SUCCESS) {\n\t\tif(janus_network_address_from_sockaddr(address, public_addr) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve XOR-MAPPED-ADDRESS...\\n\");\n\t\t\treturn -1;\n\t\t}\n\t\tif(public_port != NULL) {\n\t\t\tif(address->sa_family == AF_INET) {\n\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)address;\n\t\t\t\t*public_port = ntohs(addr->sin_port);\n\t\t\t} else if(address->sa_family == AF_INET6) {\n\t\t\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)address;\n\t\t\t\t*public_port = ntohs(addr->sin6_port);\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n\tret = stun_message_find_addr(&msg, STUN_ATTRIBUTE_MAPPED_ADDRESS, (struct sockaddr_storage *)address, &addrlen);\n\tJANUS_LOG(LOG_VERB, \"  >> MAPPED-ADDRESS: %d\\n\", ret);\n\tif(ret == STUN_MESSAGE_RETURN_SUCCESS) {\n\t\tif(janus_network_address_from_sockaddr(address, public_addr) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve MAPPED-ADDRESS...\\n\");\n\t\t\treturn -1;\n\t\t}\n\t\tif(public_port != NULL) {\n\t\t\tif(address->sa_family == AF_INET) {\n\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)address;\n\t\t\t\t*public_port = ntohs(addr->sin_port);\n\t\t\t} else if(address->sa_family == AF_INET6) {\n\t\t\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)address;\n\t\t\t\t*public_port = ntohs(addr->sin6_port);\n\t\t\t}\n\t\t}\n\t\treturn 0;\n\t}\n\t/* No usable attribute? */\n\tJANUS_LOG(LOG_ERR, \"No XOR-MAPPED-ADDRESS or MAPPED-ADDRESS...\\n\");\n\treturn -1;\n}\n\nint janus_ice_set_stun_server(gchar *stun_server, uint16_t stun_port) {\n\tif(stun_server == NULL)\n\t\treturn 0;\t/* No initialization needed */\n\tif(stun_port == 0)\n\t\tstun_port = 3478;\n\tJANUS_LOG(LOG_INFO, \"STUN server to use: %s:%u\\n\", stun_server, stun_port);\n\t/* Resolve address to get an IP */\n\tstruct addrinfo *res = NULL;\n\tjanus_network_address addr;\n\tjanus_network_address_string_buffer addr_buf;\n\tif(getaddrinfo(stun_server, NULL, NULL, &res) != 0 ||\n\t\t\tjanus_network_address_from_sockaddr(res->ai_addr, &addr) != 0 ||\n\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) != 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve %s...\\n\", stun_server);\n\t\tif(res)\n\t\t\tfreeaddrinfo(res);\n\t\treturn -1;\n\t}\n\tfreeaddrinfo(res);\n\tjanus_stun_server = g_strdup(janus_network_address_string_from_buffer(&addr_buf));\n\tif(janus_stun_server == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve %s...\\n\", stun_server);\n\t\treturn -1;\n\t}\n\tjanus_stun_port = stun_port;\n\tJANUS_LOG(LOG_INFO, \"  >> %s:%u (%s)\\n\", janus_stun_server, janus_stun_port, addr.family == AF_INET ? \"IPv4\" : \"IPv6\");\n\n\t/* Test the STUN server */\n\tjanus_network_address public_addr = { 0 };\n\tif(janus_ice_test_stun_server(&addr, janus_stun_port, 0, &public_addr, NULL) < 0) {\n\t\tg_free(janus_stun_server);\n\t\tjanus_stun_server = NULL;\n\t\treturn -1;\n\t}\n\tif(janus_network_address_to_string_buffer(&public_addr, &addr_buf) != 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve public address...\\n\");\n\t\tg_free(janus_stun_server);\n\t\tjanus_stun_server = NULL;\n\t\treturn -1;\n\t}\n\tconst char *public_ip = janus_network_address_string_from_buffer(&addr_buf);\n\tJANUS_LOG(LOG_INFO, \"  >> Our public address is %s\\n\", public_ip);\n\tjanus_add_public_ip(public_ip);\n\treturn 0;\n}\n\nint janus_ice_set_turn_server(gchar *turn_server, uint16_t turn_port, gchar *turn_type, gchar *turn_user, gchar *turn_pwd) {\n\tif(turn_server == NULL)\n\t\treturn 0;\t/* No initialization needed */\n\tif(turn_type == NULL)\n\t\tturn_type = (char *)\"udp\";\n\tif(turn_port == 0)\n\t\tturn_port = 3478;\n\tJANUS_LOG(LOG_INFO, \"TURN server to use: %s:%u (%s)\\n\", turn_server, turn_port, turn_type);\n\tif(!strcasecmp(turn_type, \"udp\")) {\n\t\tjanus_turn_type = NICE_RELAY_TYPE_TURN_UDP;\n\t} else if(!strcasecmp(turn_type, \"tcp\")) {\n\t\tjanus_turn_type = NICE_RELAY_TYPE_TURN_TCP;\n\t} else if(!strcasecmp(turn_type, \"tls\")) {\n\t\tjanus_turn_type = NICE_RELAY_TYPE_TURN_TLS;\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported relay type '%s'...\\n\", turn_type);\n\t\treturn -1;\n\t}\n\t/* Resolve address to get an IP */\n\tstruct addrinfo *res = NULL;\n\tjanus_network_address addr;\n\tjanus_network_address_string_buffer addr_buf;\n\tif(getaddrinfo(turn_server, NULL, NULL, &res) != 0 ||\n\t\t\tjanus_network_address_from_sockaddr(res->ai_addr, &addr) != 0 ||\n\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) != 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve %s...\\n\", turn_server);\n\t\tif(res)\n\t\t\tfreeaddrinfo(res);\n\t\treturn -1;\n\t}\n\tfreeaddrinfo(res);\n\tg_free(janus_turn_server);\n\tjanus_turn_server = g_strdup(janus_network_address_string_from_buffer(&addr_buf));\n\tif(janus_turn_server == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve %s...\\n\", turn_server);\n\t\treturn -1;\n\t}\n\tjanus_turn_port = turn_port;\n\tJANUS_LOG(LOG_VERB, \"  >> %s:%u\\n\", janus_turn_server, janus_turn_port);\n\tg_free(janus_turn_user);\n\tjanus_turn_user = NULL;\n\tif(turn_user)\n\t\tjanus_turn_user = g_strdup(turn_user);\n\tg_free(janus_turn_pwd);\n\tjanus_turn_pwd = NULL;\n\tif(turn_pwd)\n\t\tjanus_turn_pwd = g_strdup(turn_pwd);\n\treturn 0;\n}\n\nint janus_ice_set_turn_rest_api(gchar *api_server, gchar *api_key, gchar *api_method, uint api_timeout) {\n#ifndef HAVE_TURNRESTAPI\n\tJANUS_LOG(LOG_ERR, \"Janus has been built with no libcurl support, TURN REST API unavailable\\n\");\n\treturn -1;\n#else\n\tif(api_server != NULL &&\n\t\t\t(strstr(api_server, \"http://\") != api_server && strstr(api_server, \"https://\") != api_server)) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid TURN REST API backend: not an HTTP address\\n\");\n\t\treturn -1;\n\t}\n\tjanus_turnrest_set_backend(api_server, api_key, api_method, api_timeout);\n\tJANUS_LOG(LOG_INFO, \"TURN REST API backend: %s\\n\", api_server ? api_server : \"(disabled)\");\n#endif\n\treturn 0;\n}\n\n\n/* ICE stuff */\nstatic const gchar *janus_ice_state_name[] =\n{\n\t\"disconnected\",\n\t\"gathering\",\n\t\"connecting\",\n\t\"connected\",\n\t\"ready\",\n\t\"failed\"\n};\nconst gchar *janus_get_ice_state_name(gint state) {\n\tif(state < 0 || state > 5)\n\t\treturn NULL;\n\treturn janus_ice_state_name[state];\n}\n\n\n/* Thread to take care of the handle loop */\nstatic void *janus_ice_handle_thread(void *data) {\n\tjanus_ice_handle *handle = data;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Handle thread started; %p\\n\", handle->handle_id, handle);\n\tif(handle->mainloop == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid loop...\\n\", handle->handle_id);\n\t\tjanus_refcount_decrease(&handle->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_DBG, \"[%\"SCNu64\"] Looping...\\n\", handle->handle_id);\n\tg_main_loop_run(handle->mainloop);\n\tjanus_ice_webrtc_free(handle);\n\thandle->thread = NULL;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Handle thread ended! %p\\n\", handle->handle_id, handle);\n\t/* Unref the handle */\n\tjanus_refcount_decrease(&handle->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\njanus_ice_handle *janus_ice_handle_create(void *core_session, const char *opaque_id, const char *token) {\n\tif(core_session == NULL)\n\t\treturn NULL;\n\tjanus_session *session = (janus_session *)core_session;\n\tjanus_ice_handle *handle = NULL;\n\tguint64 handle_id = 0;\n\twhile(handle_id == 0) {\n\t\thandle_id = janus_random_uint64();\n\t\thandle = janus_session_handles_find(session, handle_id);\n\t\tif(handle != NULL) {\n\t\t\t/* Handle ID already taken, try another one */\n\t\t\tjanus_refcount_decrease(&handle->ref);\t/* janus_session_handles_find increases it */\n\t\t\thandle_id = 0;\n\t\t}\n\t}\n\thandle = (janus_ice_handle *)g_malloc0(sizeof(janus_ice_handle));\n\tJANUS_LOG(LOG_INFO, \"Creating new handle in session %\"SCNu64\": %\"SCNu64\"; %p %p\\n\", session->session_id, handle_id, core_session, handle);\n\tjanus_refcount_init(&handle->ref, janus_ice_handle_free);\n\tjanus_refcount_increase(&session->ref);\n\thandle->session = core_session;\n\tif(opaque_id)\n\t\thandle->opaque_id = g_strdup(opaque_id);\n\tif(token)\n\t\thandle->token = g_strdup(token);\n\thandle->created = janus_get_monotonic_time();\n\thandle->handle_id = handle_id;\n\thandle->app = NULL;\n\thandle->app_handle = NULL;\n\thandle->queued_candidates = g_async_queue_new();\n\thandle->queued_packets = g_async_queue_new();\n\tjanus_mutex_init(&handle->mutex);\n\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT);\n\tjanus_session_handles_insert(session, handle);\n\treturn handle;\n}\n\ngint janus_ice_handle_attach_plugin(void *core_session, janus_ice_handle *handle, janus_plugin *plugin, int loop_index) {\n\tif(core_session == NULL)\n\t\treturn JANUS_ERROR_SESSION_NOT_FOUND;\n\tjanus_session *session = (janus_session *)core_session;\n\tif(plugin == NULL)\n\t\treturn JANUS_ERROR_PLUGIN_NOT_FOUND;\n\tif(handle == NULL)\n\t\treturn JANUS_ERROR_HANDLE_NOT_FOUND;\n\tif(handle->app != NULL) {\n\t\t/* This handle is already attached to a plugin */\n\t\treturn JANUS_ERROR_PLUGIN_ATTACH;\n\t}\n\tint error = 0;\n\tjanus_plugin_session *session_handle = g_malloc(sizeof(janus_plugin_session));\n\tsession_handle->gateway_handle = handle;\n\tsession_handle->plugin_handle = NULL;\n\tg_atomic_int_set(&session_handle->stopped, 0);\n\tplugin->create_session(session_handle, &error);\n\tif(error) {\n\t\t/* TODO Make error struct to pass verbose information */\n\t\tg_free(session_handle);\n\t\treturn error;\n\t}\n\tjanus_refcount_init(&session_handle->ref, janus_ice_plugin_session_free);\n\t/* Handle and plugin session reference each other */\n\tjanus_refcount_increase(&session_handle->ref);\n\tjanus_refcount_increase(&handle->ref);\n\thandle->app = plugin;\n\thandle->app_handle = session_handle;\n\t/* Add this plugin session to active sessions map */\n\tjanus_mutex_lock(&plugin_sessions_mutex);\n\tg_hash_table_insert(plugin_sessions, session_handle, session_handle);\n\tjanus_mutex_unlock(&plugin_sessions_mutex);\n\t/* Create a new context, loop, and source */\n\tif(static_event_loops == 0) {\n\t\thandle->mainctx = g_main_context_new();\n\t\thandle->mainloop = g_main_loop_new(handle->mainctx, FALSE);\n\t} else {\n\t\t/* We're actually using static event loops, pick one from the list */\n\t\tif(!allow_loop_indication && loop_index > -1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Manual allocation of event loops forbidden, ignoring provided loop index %d\\n\", handle->handle_id, loop_index);\n\t\t}\n\t\tjanus_refcount_increase(&handle->ref);\n\t\tjanus_mutex_lock(&event_loops_mutex);\n\t\tgboolean automatic_selection = TRUE;\n\t\tif(allow_loop_indication && loop_index != -1) {\n\t\t\t/* The API can drive the selection and an index was provided, check if it exists */\n\t\t\tjanus_ice_static_event_loop *loop = g_slist_nth_data(event_loops, loop_index);\n\t\t\tif(loop == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Invalid loop index %d, picking event loop automatically\\n\", handle->handle_id, loop_index);\n\t\t\t} else {\n\t\t\t\tjanus_refcount_increase(&loop->ref);\n\t\t\t\tautomatic_selection = FALSE;\n\t\t\t\thandle->mainctx = loop->mainctx;\n\t\t\t\thandle->mainloop = loop->mainloop;\n\t\t\t\tloop->handles++;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Manually added handle to loop #%d\\n\", handle->handle_id, loop->id);\n\t\t\t}\n\t\t}\n\t\tif(automatic_selection) {\n\t\t\t/* Pick an available loop automatically (least loaded) */\n\t\t\tint handles = -1;\n\t\t\tjanus_ice_static_event_loop *loop = NULL;\n\t\t\tGSList *l = event_loops;\n\t\t\twhile(l) {\n\t\t\t\tjanus_ice_static_event_loop *el = (janus_ice_static_event_loop *)l->data;\n\t\t\t\tif(el->handles == 0) {\n\t\t\t\t\t/* Best option, stop here */\n\t\t\t\t\tloop = el;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif(handles == -1 || el->handles < handles) {\n\t\t\t\t\thandles = el->handles;\n\t\t\t\t\tloop = el;\n\t\t\t\t}\n\t\t\t\tl = l->next;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&loop->ref);\n\t\t\tloop->handles++;\n\t\t\thandle->mainctx = loop->mainctx;\n\t\t\thandle->mainloop = loop->mainloop;\n\t\t\thandle->static_event_loop = loop;\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Automatically added handle to loop #%d\\n\", handle->handle_id, loop->id);\n\t\t}\n\t\tjanus_mutex_unlock(&event_loops_mutex);\n\t}\n\thandle->rtp_source = janus_ice_outgoing_traffic_create(handle, (GDestroyNotify)g_free);\n\tg_source_set_priority(handle->rtp_source, G_PRIORITY_DEFAULT);\n\tg_source_attach(handle->rtp_source, handle->mainctx);\n\tif(static_event_loops == 0) {\n\t\t/* Now spawn a thread for this loop */\n\t\tGError *terror = NULL;\n\t\tchar tname[16];\n\t\tg_snprintf(tname, sizeof(tname), \"hloop %\"SCNu64, handle->handle_id);\n\t\tjanus_refcount_increase(&handle->ref);\n\t\thandle->thread = g_thread_try_new(tname, &janus_ice_handle_thread, handle, &terror);\n\t\tif(terror != NULL) {\n\t\t\t/* FIXME We should clear some resources... */\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Got error %d (%s) trying to launch the handle thread...\\n\",\n\t\t\t\thandle->handle_id, terror->code, terror->message ? terror->message : \"??\");\n\t\t\tg_error_free(terror);\n\t\t\tjanus_refcount_decrease(&handle->ref);\t/* This is for the thread reference we just added */\n\t\t\tjanus_ice_handle_destroy(session, handle);\n\t\t\treturn -1;\n\t\t}\n\t}\n\t/* Notify event handlers */\n\tif(janus_events_is_enabled())\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_HANDLE, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\tsession->session_id, handle->handle_id, \"attached\", plugin->get_package(), handle->opaque_id, handle->token);\n\treturn 0;\n}\n\ngint janus_ice_handle_destroy(void *core_session, janus_ice_handle *handle) {\n\t/* session->mutex has to be locked when calling this function */\n\tjanus_session *session = (janus_session *)core_session;\n\tif(session == NULL)\n\t\treturn JANUS_ERROR_SESSION_NOT_FOUND;\n\tif(handle == NULL)\n\t\treturn JANUS_ERROR_HANDLE_NOT_FOUND;\n\tif(!g_atomic_int_compare_and_exchange(&handle->destroyed, 0, 1))\n\t\treturn 0;\n\t/* First of all, hangup the PeerConnection, if any */\n\tjanus_ice_webrtc_hangup(handle, \"Detach\");\n\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP);\n\t/* Remove the session from active sessions map */\n\tjanus_mutex_lock(&plugin_sessions_mutex);\n\tgboolean found = g_hash_table_remove(plugin_sessions, handle->app_handle);\n\tif(!found) {\n\t\tjanus_mutex_unlock(&plugin_sessions_mutex);\n\t\treturn JANUS_ERROR_HANDLE_NOT_FOUND;\n\t}\n\tjanus_mutex_unlock(&plugin_sessions_mutex);\n\tjanus_mutex_lock(&event_loops_mutex);\n\tif(handle->static_event_loop != NULL) {\n\t\tjanus_ice_static_event_loop *loop = (janus_ice_static_event_loop *)handle->static_event_loop;\n\t\tloop->handles--;\n\t\tjanus_refcount_decrease(&loop->ref);\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Manually removed handle from loop #%d\\n\", handle->handle_id, loop->id);\n\t}\n\tjanus_mutex_unlock(&event_loops_mutex);\n\tjanus_plugin *plugin_t = (janus_plugin *)handle->app;\n\tif(plugin_t == NULL) {\n\t\t/* There was no plugin attached, probably something went wrong there */\n\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT);\n\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP);\n\t\tif(handle->mainloop != NULL) {\n\t\t\tif(static_event_loops == 0 && handle->mainloop != NULL && g_main_loop_is_running(handle->mainloop)) {\n\t\t\t\tg_main_loop_quit(handle->mainloop);\n\t\t\t}\n\t\t}\n\t\tjanus_refcount_decrease(&handle->ref);\n\t\treturn 0;\n\t}\n\tJANUS_LOG(LOG_INFO, \"Detaching handle from %s; %p %p %p %p\\n\", plugin_t->get_name(),\n\t\thandle, handle ? handle->app_handle : NULL,\n\t\t(handle && handle->app_handle) ? handle->app_handle->gateway_handle : NULL,\n\t\t(handle && handle->app_handle) ? handle->app_handle->plugin_handle : NULL);\n\t/* Actually detach handle... */\n\tif(g_atomic_int_compare_and_exchange(&handle->app_handle->stopped, 0, 1)) {\n\t\t/* Notify the plugin that the session's over (the plugin will\n\t\t * remove the other reference to the plugin session handle) */\n\t\tg_async_queue_push(handle->queued_packets, &janus_ice_detach_handle);\n\t\tg_main_context_wakeup(handle->mainctx);\n\t}\n\t/* Get rid of the handle now */\n\tif(g_atomic_int_compare_and_exchange(&handle->dump_packets, 1, 0)) {\n\t\tjanus_text2pcap_close(handle->text2pcap);\n\t\tg_clear_pointer(&handle->text2pcap, janus_text2pcap_free);\n\t}\n\t/* We only actually destroy the handle later */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Handle detached, scheduling destruction\\n\", handle->handle_id);\n\t/* Unref the handle: we only unref the session too when actually freeing the handle, so that it is freed before that */\n\tjanus_refcount_decrease(&handle->ref);\n\treturn 0;\n}\n\nstatic void janus_ice_handle_free(const janus_refcount *handle_ref) {\n\tjanus_ice_handle *handle = janus_refcount_containerof(handle_ref, janus_ice_handle, ref);\n\t/* This stack can be destroyed, free all the resources */\n\tjanus_mutex_lock(&handle->mutex);\n\tif(handle->queued_candidates != NULL) {\n\t\tjanus_ice_clear_queued_candidates(handle);\n\t\tg_async_queue_unref(handle->queued_candidates);\n\t}\n\tif(handle->queued_packets != NULL) {\n\t\tjanus_ice_clear_queued_packets(handle);\n\t\tg_async_queue_unref(handle->queued_packets);\n\t}\n\tif(static_event_loops == 0 && handle->mainloop != NULL) {\n\t\tg_main_loop_unref(handle->mainloop);\n\t\thandle->mainloop = NULL;\n\t}\n\tif(static_event_loops == 0 && handle->mainctx != NULL) {\n\t\tg_main_context_unref(handle->mainctx);\n\t\thandle->mainctx = NULL;\n\t}\n\tjanus_mutex_unlock(&handle->mutex);\n\tjanus_ice_webrtc_free(handle);\n\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] Handle and related resources freed; %p %p\\n\", handle->handle_id, handle, handle->session);\n\t/* Finally, unref the session and free the handle */\n\tif(handle->session != NULL) {\n\t\tjanus_session *session = (janus_session *)handle->session;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tg_free(handle->opaque_id);\n\tg_free(handle->token);\n\tjanus_mutex_destroy(&handle->mutex);\n\tg_free(handle);\n}\n\n#ifdef HAVE_CLOSE_ASYNC\nstatic void janus_ice_cb_agent_closed(GObject *src, GAsyncResult *result, gpointer data) {\n\tjanus_ice_outgoing_traffic *t = (janus_ice_outgoing_traffic *)data;\n\tjanus_ice_handle *handle = t->handle;\n\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Disposing nice agent %p\\n\", handle->handle_id, handle->agent);\n\tg_object_unref(handle->agent);\n\thandle->agent = NULL;\n\tg_source_unref((GSource *)t);\n\tjanus_refcount_decrease(&handle->ref);\n}\n#endif\n\nstatic void janus_ice_plugin_session_free(const janus_refcount *app_handle_ref) {\n\tjanus_plugin_session *app_handle = janus_refcount_containerof(app_handle_ref, janus_plugin_session, ref);\n\t/* This app handle can be destroyed, free all the resources */\n\tif(app_handle->gateway_handle != NULL) {\n\t\tjanus_ice_handle *handle = (janus_ice_handle *)app_handle->gateway_handle;\n\t\tapp_handle->gateway_handle = NULL;\n\t\thandle->app_handle = NULL;\n\t\tjanus_refcount_decrease(&handle->ref);\n\t}\n\tg_free(app_handle);\n}\n\nvoid janus_ice_webrtc_hangup(janus_ice_handle *handle, const char *reason) {\n\tif(handle == NULL)\n\t\treturn;\n\tg_atomic_int_set(&handle->closepc, 0);\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn;\n\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT);\n\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEGOTIATED);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AUDIO);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_VIDEO);\n\t/* User will be notified only after the actual hangup */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Hanging up PeerConnection because of a %s\\n\",\n\t\thandle->handle_id, reason);\n\thandle->hangup_reason = reason;\n\t/* Let's message the loop, we'll notify the plugin from there */\n\tif(handle->queued_packets != NULL) {\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\t\tg_async_queue_push_front(handle->queued_packets, &janus_ice_hangup_peerconnection);\n#else\n\t\tg_async_queue_push(handle->queued_packets, &janus_ice_hangup_peerconnection);\n#endif\n\t\tg_main_context_wakeup(handle->mainctx);\n\t}\n\tif(g_atomic_int_dec_and_test(&handle->has_pc))\n\t\tg_atomic_int_dec_and_test(&pc_num);\n}\n\nstatic void janus_ice_webrtc_free(janus_ice_handle *handle) {\n\tif(handle == NULL)\n\t\treturn;\n\tjanus_mutex_lock(&handle->mutex);\n\tif(!handle->agent_created) {\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY);\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AGENT);\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_E2EE);\n\t\tjanus_mutex_unlock(&handle->mutex);\n\t\treturn;\n\t}\n\thandle->agent_created = 0;\n\thandle->agent_started = 0;\n\tif(handle->pc != NULL) {\n\t\tjanus_ice_peerconnection_destroy(handle->pc);\n\t\thandle->pc = NULL;\n\t}\n\tif(handle->agent != NULL) {\n#ifdef HAVE_CLOSE_ASYNC\n\t\tif(G_IS_OBJECT(handle->agent)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Removing stream %d from agent %p\\n\",\n\t\t\t\thandle->handle_id, handle->stream_id, handle->agent);\n\t\t\tnice_agent_remove_stream(handle->agent, handle->stream_id);\n\t\t\thandle->stream_id = 0;\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Closing nice agent %p\\n\", handle->handle_id, handle->agent);\n\t\t\tjanus_refcount_increase(&handle->ref);\n\t\t\tif(handle->rtp_source != NULL) {\n\t\t\t\t/* Destroy the agent asynchronously */\n\t\t\t\tg_source_ref(handle->rtp_source);\n\t\t\t\tnice_agent_close_async(handle->agent, janus_ice_cb_agent_closed, handle->rtp_source);\n\t\t\t} else {\n\t\t\t\t/* No traffic source, destroy it right away */\n\t\t\t\tif(G_IS_OBJECT(handle->agent))\n\t\t\t\t\tg_object_unref(handle->agent);\n\t\t\t\thandle->agent = NULL;\n\t\t\t\tjanus_refcount_decrease(&handle->ref);\n\t\t\t}\n\t\t}\n#else\n\t\tif(G_IS_OBJECT(handle->agent))\n\t\t\tg_object_unref(handle->agent);\n\t\thandle->agent = NULL;\n#endif\n\t}\n\tif(handle->pending_trickles) {\n\t\twhile(handle->pending_trickles) {\n\t\t\tGList *temp = g_list_first(handle->pending_trickles);\n\t\t\thandle->pending_trickles = g_list_remove_link(handle->pending_trickles, temp);\n\t\t\tjanus_ice_trickle *trickle = (janus_ice_trickle *)temp->data;\n\t\t\tg_list_free(temp);\n\t\t\tjanus_ice_trickle_destroy(trickle);\n\t\t}\n\t}\n\thandle->pending_trickles = NULL;\n\tjanus_ice_clear_queued_candidates(handle);\n\tg_free(handle->rtp_profile);\n\thandle->rtp_profile = NULL;\n\tg_free(handle->local_sdp);\n\thandle->local_sdp = NULL;\n\tg_free(handle->remote_sdp);\n\thandle->remote_sdp = NULL;\n\tg_free(handle->pc_mid);\n\thandle->pc_mid = NULL;\n\thandle->thread = NULL;\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AGENT);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_E2EE);\n\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) && handle->hangup_reason) {\n\t\tjanus_ice_notify_hangup(handle, handle->hangup_reason);\n\t}\n\thandle->hangup_reason = NULL;\n\tjanus_mutex_unlock(&handle->mutex);\n\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] WebRTC resources freed; %p %p\\n\", handle->handle_id, handle, handle->session);\n}\n\nvoid janus_ice_peerconnection_destroy(janus_ice_peerconnection *pc) {\n\tif(pc == NULL)\n\t\treturn;\n\t/* Remove all media instances */\n\tg_hash_table_remove_all(pc->media);\n\tg_hash_table_remove_all(pc->media_byssrc);\n\tg_hash_table_remove_all(pc->media_bymid);\n\tg_hash_table_remove_all(pc->media_bytype);\n\t/* Get rid of the DTLS stack */\n\tif(pc->dtlsrt_source != NULL) {\n\t\tg_source_destroy(pc->dtlsrt_source);\n\t\tg_source_unref(pc->dtlsrt_source);\n\t\tpc->dtlsrt_source = NULL;\n\t}\n\tif(pc->dtls != NULL) {\n\t\tjanus_dtls_srtp_destroy(pc->dtls);\n\t\tjanus_refcount_decrease(&pc->dtls->ref);\n\t\tpc->dtls = NULL;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(handle != NULL) {\n\t\tjanus_refcount_decrease(&handle->ref);\n\t\tpc->handle = NULL;\n\t}\n\tjanus_refcount_decrease(&pc->ref);\n}\n\nstatic void janus_ice_peerconnection_free(const janus_refcount *pc_ref) {\n\tjanus_ice_peerconnection *pc = janus_refcount_containerof(pc_ref, janus_ice_peerconnection, ref);\n\t/* This PeerConnection can be destroyed, free all the resources */\n\tpc->handle = NULL;\n\tg_hash_table_destroy(pc->media);\n\tg_hash_table_destroy(pc->media_byssrc);\n\tg_hash_table_destroy(pc->media_bymid);\n\tg_hash_table_destroy(pc->media_bytype);\n\tif(pc->icestate_source != NULL) {\n\t\tg_source_destroy(pc->icestate_source);\n\t\tg_source_unref(pc->icestate_source);\n\t\tpc->icestate_source = NULL;\n\t}\n\tg_free(pc->remote_hashing);\n\tpc->remote_hashing = NULL;\n\tg_free(pc->remote_fingerprint);\n\tpc->remote_fingerprint = NULL;\n\tg_free(pc->ruser);\n\tpc->ruser = NULL;\n\tg_free(pc->rpass);\n\tpc->rpass = NULL;\n\tg_slist_free_full(pc->transport_wide_received_seq_nums, (GDestroyNotify)g_free);\n\tpc->transport_wide_received_seq_nums = NULL;\n\tif(pc->candidates != NULL) {\n\t\tGSList *i = NULL, *candidates = pc->candidates;\n\t\tfor(i = candidates; i; i = i->next) {\n\t\t\tNiceCandidate *c = (NiceCandidate *) i->data;\n\t\t\tif(c != NULL) {\n\t\t\t\tnice_candidate_free(c);\n\t\t\t\tc = NULL;\n\t\t\t}\n\t\t}\n\t\tg_slist_free(candidates);\n\t\tcandidates = NULL;\n\t}\n\tpc->candidates = NULL;\n\tif(pc->local_candidates != NULL) {\n\t\tGSList *i = NULL, *candidates = pc->local_candidates;\n\t\tfor(i = candidates; i; i = i->next) {\n\t\t\tgchar *c = (gchar *) i->data;\n\t\t\tg_free(c);\n\t\t}\n\t\tg_slist_free(candidates);\n\t\tcandidates = NULL;\n\t}\n\tpc->local_candidates = NULL;\n\tif(pc->remote_candidates != NULL) {\n\t\tGSList *i = NULL, *candidates = pc->remote_candidates;\n\t\tfor(i = candidates; i; i = i->next) {\n\t\t\tgchar *c = (gchar *) i->data;\n\t\t\tg_free(c);\n\t\t}\n\t\tg_slist_free(candidates);\n\t\tcandidates = NULL;\n\t}\n\tpc->remote_candidates = NULL;\n\tg_free(pc->selected_pair);\n\tpc->selected_pair = NULL;\n\tif(pc->payload_types != NULL)\n\t\tg_hash_table_destroy(pc->payload_types);\n\tpc->payload_types = NULL;\n\tif(pc->clock_rates != NULL)\n\t\tg_hash_table_destroy(pc->clock_rates);\n\tpc->clock_rates = NULL;\n\tif(pc->rtx_payload_types != NULL)\n\t\tg_hash_table_destroy(pc->rtx_payload_types);\n\tpc->rtx_payload_types = NULL;\n\tif(pc->rtx_payload_types_rev != NULL)\n\t\tg_hash_table_destroy(pc->rtx_payload_types_rev);\n\tpc->rtx_payload_types_rev = NULL;\n\tif(pc->nacks_queue != NULL)\n\t\tg_queue_free(pc->nacks_queue);\n\tjanus_mutex_destroy(&pc->mutex);\n\tg_free(pc);\n}\n\njanus_ice_peerconnection_medium *janus_ice_peerconnection_medium_create(janus_ice_handle *handle, janus_media_type type) {\n\tif(handle == NULL || handle->pc == NULL)\n\t\treturn NULL;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tjanus_ice_peerconnection_medium *medium = g_malloc0(sizeof(janus_ice_peerconnection_medium));\n\tmedium->pc = pc;\n\tmedium->type = type;\n\tmedium->mindex = g_hash_table_size(pc->media);\n\tjanus_mutex_init(&medium->mutex);\n\tjanus_refcount_init(&medium->ref, janus_ice_peerconnection_medium_free);\n\tjanus_refcount_increase(&pc->ref);\n\tg_hash_table_insert(pc->media, GINT_TO_POINTER(medium->mindex), medium);\n\t/* If this is audio or video, fill in some other fields too */\n\tif(type == JANUS_MEDIA_AUDIO || type == JANUS_MEDIA_VIDEO) {\n\t\tmedium->payload_type = -1;\n\t\tmedium->rtx_payload_type = -1;\n\t\tmedium->ssrc = janus_random_uint32();\t/* FIXME Should we look for conflicts? */\n\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)) {\n\t\t\t/* Create an SSRC for RFC4588 as well */\n\t\t\tmedium->ssrc_rtx = janus_random_uint32();\t/* FIXME Should we look for conflicts? */\n\t\t}\n\t\tmedium->rtcp_ctx[0] = g_malloc0(sizeof(janus_rtcp_context));\n\t\tmedium->rtcp_ctx[0]->tb = (type == JANUS_MEDIA_VIDEO ? 90000 : 48000);\t/* May change later */\n\t\tmedium->rtcp_ctx[0]->in_link_quality = 100;\n\t\tmedium->rtcp_ctx[0]->in_media_link_quality = 100;\n\t\tmedium->rtcp_ctx[0]->out_link_quality = 100;\n\t\tmedium->rtcp_ctx[0]->out_media_link_quality = 100;\n\t\t/* We can address media by SSRC */\n\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc), medium);\n\t\tjanus_refcount_increase(&medium->ref);\n\t\tif(medium->ssrc_rtx > 0) {\n\t\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_rtx), medium);\n\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t}\n\t\tg_hash_table_insert(pc->media_bytype, GINT_TO_POINTER(type), medium);\n\t\tjanus_refcount_increase(&medium->ref);\n\t}\n\t/* For backwards compatibility, we address media by type too (e.g., first video stream) */\n\tg_hash_table_insert(pc->media_bytype, GINT_TO_POINTER(type), medium);\n\tjanus_refcount_increase(&medium->ref);\n\treturn medium;\n}\n\nstatic void janus_ice_peerconnection_medium_destroy(janus_ice_peerconnection_medium *medium) {\n\tif(medium == NULL)\n\t\treturn;\n\tjanus_ice_peerconnection *pc = medium->pc;\n\tif(pc != NULL) {\n\t\tjanus_refcount_decrease(&pc->ref);\n\t\tmedium->pc = NULL;\n\t}\n\tjanus_refcount_decrease(&medium->ref);\n}\n\nstatic void janus_ice_peerconnection_medium_dereference(janus_ice_peerconnection_medium *medium) {\n\tif(medium == NULL)\n\t\treturn;\n\tjanus_refcount_decrease(&medium->ref);\n}\n\nstatic void janus_ice_peerconnection_medium_free(const janus_refcount *medium_ref) {\n\tjanus_ice_peerconnection_medium *medium = janus_refcount_containerof(medium_ref, janus_ice_peerconnection_medium, ref);\n\tg_free(medium->mid);\n\tg_free(medium->msid);\n\tg_free(medium->mstid);\n\tg_free(medium->remote_msid);\n\tg_free(medium->remote_mstid);\n\tg_free(medium->rid[0]);\n\tmedium->rid[0] = NULL;\n\tg_free(medium->rid[1]);\n\tmedium->rid[1] = NULL;\n\tg_free(medium->rid[2]);\n\tmedium->rid[2] = NULL;\n\tg_list_free(medium->payload_types);\n\tmedium->payload_types = NULL;\n\tif(medium->rtx_payload_types != NULL)\n\t\tg_hash_table_destroy(medium->rtx_payload_types);\n\tmedium->rtx_payload_types = NULL;\n\tif(medium->clock_rates != NULL)\n\t\tg_hash_table_destroy(medium->clock_rates);\n\tmedium->clock_rates = NULL;\n\tg_free(medium->codec);\n\tmedium->codec = NULL;\n\tg_free(medium->rtcp_ctx[0]);\n\tmedium->rtcp_ctx[0] = NULL;\n\tg_free(medium->rtcp_ctx[1]);\n\tmedium->rtcp_ctx[1] = NULL;\n\tg_free(medium->rtcp_ctx[2]);\n\tmedium->rtcp_ctx[2] = NULL;\n\tif(medium->rtx_nacked[0])\n\t\tg_hash_table_destroy(medium->rtx_nacked[0]);\n\tmedium->rtx_nacked[0] = NULL;\n\tif(medium->rtx_nacked[1])\n\t\tg_hash_table_destroy(medium->rtx_nacked[1]);\n\tmedium->rtx_nacked[1] = NULL;\n\tif(medium->rtx_nacked[2])\n\t\tg_hash_table_destroy(medium->rtx_nacked[2]);\n\tmedium->rtx_nacked[2] = NULL;\n\tif(medium->pending_nacked_cleanup != NULL) {\n\t\tif(g_hash_table_size(medium->pending_nacked_cleanup) > 0) {\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer val;\n\t\t\tg_hash_table_iter_init(&iter, medium->pending_nacked_cleanup);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &val)) {\n\t\t\t\tGSource *source = val;\n\t\t\t\tg_source_destroy(source);\n\t\t\t}\n\t\t}\n\t\tg_hash_table_destroy(medium->pending_nacked_cleanup);\n\t}\n\tmedium->pending_nacked_cleanup = NULL;\n\tif(medium->retransmit_buffer != NULL) {\n\t\tjanus_rtp_packet *p = NULL;\n\t\twhile((p = (janus_rtp_packet *)g_queue_pop_head(medium->retransmit_buffer)) != NULL) {\n\t\t\t/* Remove from hashtable too */\n\t\t\tjanus_rtp_header *header = (janus_rtp_header *)p->data;\n\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\tg_hash_table_remove(medium->retransmit_seqs, GUINT_TO_POINTER(seq));\n\t\t\t/* Free the packet */\n\t\t\tjanus_ice_free_rtp_packet(p);\n\t\t}\n\t\tg_queue_free(medium->retransmit_buffer);\n\t\tg_hash_table_destroy(medium->retransmit_seqs);\n\t}\n\tif(medium->last_seqs[0])\n\t\tjanus_seq_list_free(&medium->last_seqs[0]);\n\tif(medium->last_seqs[1])\n\t\tjanus_seq_list_free(&medium->last_seqs[1]);\n\tif(medium->last_seqs[2])\n\t\tjanus_seq_list_free(&medium->last_seqs[2]);\n\tjanus_mutex_destroy(&medium->mutex);\n\tg_free(medium);\n}\n\n/* Call plugin slow_link callback if a minimum of lost packets are detected within a second */\nstatic void\njanus_slow_link_update(janus_ice_peerconnection_medium *medium, janus_ice_handle *handle,\n\t\tgboolean uplink, guint lost) {\n\t/* We keep the counters in different janus_ice_stats objects, depending on the direction */\n\tgboolean video = (medium->type == JANUS_MEDIA_VIDEO);\n\tguint sl_lost_last_count = uplink ? medium->in_stats.sl_lost_count : medium->out_stats.sl_lost_count;\n\tguint sl_lost_recently = (lost >= sl_lost_last_count) ? (lost - sl_lost_last_count) : 0;\n\tif(slowlink_threshold > 0 && sl_lost_recently >= slowlink_threshold) {\n\t\t/* Tell the plugin */\n\t\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\t\tif(plugin && plugin->slow_link && janus_plugin_session_is_alive(handle->app_handle) &&\n\t\t\t\t!g_atomic_int_get(&handle->destroyed))\n\t\t\tplugin->slow_link(handle->app_handle, medium->mindex, video, uplink);\n\t\t/* Notify the user/application too */\n\t\tjanus_session *session = (janus_session *)handle->session;\n\t\tif(session != NULL) {\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"janus\", json_string(\"slowlink\"));\n\t\t\tjson_object_set_new(event, \"session_id\", json_integer(session->session_id));\n\t\t\tjson_object_set_new(event, \"sender\", json_integer(handle->handle_id));\n\t\t\tif(opaqueid_in_api && handle->opaque_id != NULL)\n\t\t\t\tjson_object_set_new(event, \"opaque_id\", json_string(handle->opaque_id));\n\t\t\tjson_object_set_new(event, \"mid\", json_string(medium->mid));\n\t\t\tjson_object_set_new(event, \"media\", json_string(video ? \"video\" : \"audio\"));\n\t\t\tjson_object_set_new(event, \"uplink\", uplink ? json_true() : json_false());\n\t\t\tjson_object_set_new(event, \"lost\", json_integer(sl_lost_recently));\n\t\t\t/* Send the event */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending event to transport...; %p\\n\", handle->handle_id, handle);\n\t\t\tjanus_session_notify_event(session, event);\n\t\t\t/* Finally, notify event handlers */\n\t\t\tif(janus_events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"mid\", json_string(medium->mid));\n\t\t\t\tjson_object_set_new(info, \"media\", json_string(video ? \"video\" : \"audio\"));\n\t\t\t\tjson_object_set_new(info, \"slow_link\", json_string(uplink ? \"uplink\" : \"downlink\"));\n\t\t\t\tjson_object_set_new(info, \"lost_lastsec\", json_integer(sl_lost_recently));\n\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_MEDIA, JANUS_EVENT_SUBTYPE_MEDIA_SLOWLINK,\n\t\t\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t\t\t}\n\t\t}\n\t}\n\t/* Update the counter */\n\tif(uplink) {\n\t\tmedium->in_stats.sl_lost_count = lost;\n\t} else {\n\t\tmedium->out_stats.sl_lost_count = lost;\n\t}\n}\n\n\n/* ICE state check timer (needed to check if a failed really is definitive or if things can still improve) */\nstatic gboolean janus_ice_check_failed(gpointer data) {\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)data;\n\tif(!pc)\n\t\tgoto stoptimer;\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle)\n\t\tgoto stoptimer;\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) ||\n\t\t\tjanus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\tgoto stoptimer;\n\tif(pc->state == NICE_COMPONENT_STATE_CONNECTED || pc->state == NICE_COMPONENT_STATE_READY) {\n\t\t/* ICE succeeded in the meanwhile, get rid of this timer */\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] ICE succeeded, disabling ICE state check timer!\\n\", handle->handle_id);\n\t\tpc->icefailed_detected = 0;\n\t\tgoto stoptimer;\n\t}\n\t/* Still in the failed state, how much time passed since we first detected it? */\n\tif(janus_get_monotonic_time() - pc->icefailed_detected < 5*G_USEC_PER_SEC) {\n\t\t/* Let's wait a little longer */\n\t\treturn TRUE;\n\t}\n\t/* If we got here it means the timer expired, and we should check if this is a failure */\n\tgboolean trickle_recv = (!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE) || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES));\n\tgboolean answer_recv = janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER);\n\tgboolean alert_set = janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT);\n\t/* We may still be waiting for something... but we don't wait forever */\n\tgboolean do_wait = TRUE;\n\tif(janus_get_monotonic_time() - pc->icefailed_detected >= 15*G_USEC_PER_SEC) {\n\t\tdo_wait = FALSE;\n\t}\n\tif(!do_wait || (handle && trickle_recv && answer_recv && !alert_set)) {\n\t\t/* FIXME Should we really give up for what may be a failure in only one of the media? */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] ICE failed for component %d in stream %d...\\n\",\n\t\t\thandle->handle_id, pc->component_id, pc->stream_id);\n\t\tjanus_ice_webrtc_hangup(handle, \"ICE failed\");\n\t\tgoto stoptimer;\n\t}\n\t/* Let's wait a little longer */\n\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] ICE failed for component %d in stream %d, but we're still waiting for some info so we don't care... (trickle %s, answer %s, alert %s)\\n\",\n\t\thandle->handle_id, pc->component_id, pc->stream_id,\n\t\ttrickle_recv ? \"received\" : \"pending\",\n\t\tanswer_recv ? \"received\" : \"pending\",\n\t\talert_set ? \"set\" : \"not set\");\n\treturn TRUE;\n\nstoptimer:\n\tif(pc && pc->icestate_source != NULL) {\n\t\tg_source_destroy(pc->icestate_source);\n\t\tg_source_unref(pc->icestate_source);\n\t\tpc->icestate_source = NULL;\n\t}\n\treturn FALSE;\n}\n\n/* Callbacks */\nstatic void janus_ice_cb_candidate_gathering_done(NiceAgent *agent, guint stream_id, gpointer user_data) {\n\tjanus_ice_handle *handle = (janus_ice_handle *)user_data;\n\tif(!handle)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Gathering done for stream %d\\n\", handle->handle_id, stream_id);\n\thandle->cdone++;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc || pc->stream_id != stream_id) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]  No stream %d??\\n\", handle->handle_id, stream_id);\n\t\treturn;\n\t}\n\tpc->gathered = janus_get_monotonic_time();\n\tpc->cdone = TRUE;\n\t/* If we're doing full-trickle, send an event to the user too */\n\tif(janus_full_trickle_enabled) {\n\t\t/* Send a \"trickle\" event with completed:true to the browser */\n\t\tjanus_ice_notify_trickle(handle, NULL);\n\t}\n}\n\nstatic void janus_ice_cb_component_state_changed(NiceAgent *agent, guint stream_id, guint component_id, guint state, gpointer ice) {\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice;\n\tif(!handle)\n\t\treturn;\n\tif(component_id > 1) {\n\t\t/* State changed for a component we don't need anymore (rtcp-mux) */\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Component state changed for component %d in stream %d: %d (%s)\\n\",\n\t\thandle->handle_id, component_id, stream_id, state, janus_get_ice_state_name(state));\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc || pc->stream_id != stream_id) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     No stream %d??\\n\", handle->handle_id, stream_id);\n\t\treturn;\n\t}\n\tguint prev_state = pc->state;\n\tpc->state = state;\n\t/* Notify event handlers */\n\tif(janus_events_is_enabled()) {\n\t\tjanus_session *session = (janus_session *)handle->session;\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"ice\", json_string(janus_get_ice_state_name(state)));\n\t\tjson_object_set_new(info, \"stream_id\", json_integer(stream_id));\n\t\tjson_object_set_new(info, \"component_id\", json_integer(component_id));\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_WEBRTC, JANUS_EVENT_SUBTYPE_WEBRTC_ICE,\n\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t}\n\t/* FIXME Even in case the state is 'connected', we wait for the 'new-selected-pair' callback to do anything */\n\tif(state == NICE_COMPONENT_STATE_FAILED) {\n\t\t/* Failed doesn't mean necessarily we need to give up: we may be trickling */\n\t\tgboolean alert_set = janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT);\n\t\tif(alert_set)\n\t\t\treturn;\n\t\tif(prev_state == NICE_COMPONENT_STATE_CONNECTED || prev_state == NICE_COMPONENT_STATE_READY) {\n\t\t\t/* Failed after connected/ready means consent freshness detected something broken:\n\t\t\t * notify the user via a Janus API event and then fire the 'failed' timer as sual */\n\t\t\tjanus_ice_notify_ice_failed(handle);\n\t\t\t/* Check if we need to hangup right away, rather than start the grace period */\n\t\t\tif(janus_ice_hangup_on_failed && pc->icefailed_detected == 0) {\n\t\t\t\t/* We do, hangup the PeerConnection */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] ICE failed for component %d in stream %d...\\n\",\n\t\t\t\t\thandle->handle_id, component_id, stream_id);\n\t\t\t\tjanus_ice_webrtc_hangup(handle, \"ICE failed\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tgboolean trickle_recv = (!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE) || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES));\n\t\tgboolean answer_recv = janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER);\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] ICE failed for component %d in stream %d, but let's give it some time... (trickle %s, answer %s, alert %s)\\n\",\n\t\t\thandle->handle_id, component_id, stream_id,\n\t\t\ttrickle_recv ? \"received\" : \"pending\",\n\t\t\tanswer_recv ? \"received\" : \"pending\",\n\t\t\talert_set ? \"set\" : \"not set\");\n\t\t/* In case we haven't started a timer yet, let's do it now */\n\t\tif(pc->icestate_source == NULL && pc->icefailed_detected == 0) {\n\t\t\tpc->icefailed_detected = janus_get_monotonic_time();\n\t\t\tpc->icestate_source = g_timeout_source_new(500);\n\t\t\tg_source_set_callback(pc->icestate_source, janus_ice_check_failed, pc, NULL);\n\t\t\tguint id = g_source_attach(pc->icestate_source, handle->mainctx);\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Creating ICE state check timer with ID %u\\n\", handle->handle_id, id);\n\t\t}\n\t}\n}\n\n#ifndef HAVE_LIBNICE_TCP\nstatic void janus_ice_cb_new_selected_pair (NiceAgent *agent, guint stream_id, guint component_id, gchar *local, gchar *remote, gpointer ice) {\n#else\nstatic void janus_ice_cb_new_selected_pair (NiceAgent *agent, guint stream_id, guint component_id, NiceCandidate *local, NiceCandidate *remote, gpointer ice) {\n#endif\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice;\n\tif(!handle)\n\t\treturn;\n\tif(component_id > 1) {\n\t\t/* New selected pair for a component we don't need anymore (rtcp-mux) */\n\t\treturn;\n\t}\n#ifndef HAVE_LIBNICE_TCP\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] New selected pair for component %d in stream %d: %s <-> %s\\n\", handle ? handle->handle_id : 0, component_id, stream_id, local, remote);\n#else\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] New selected pair for component %d in stream %d: %s <-> %s\\n\", handle ? handle->handle_id : 0, component_id, stream_id, local->foundation, remote->foundation);\n#endif\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc || pc->stream_id != stream_id) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     No stream %d??\\n\", handle->handle_id, stream_id);\n\t\treturn;\n\t}\n\tchar sp[200];\n#ifndef HAVE_LIBNICE_TCP\n\tg_snprintf(sp, 200, \"%s <-> %s\", local, remote);\n#else\n\tgchar laddress[NICE_ADDRESS_STRING_LEN], raddress[NICE_ADDRESS_STRING_LEN];\n\tgint lport = 0, rport = 0;\n\tnice_address_to_string(&(local->addr), (gchar *)&laddress);\n\tnice_address_to_string(&(remote->addr), (gchar *)&raddress);\n\tlport = nice_address_get_port(&(local->addr));\n\trport = nice_address_get_port(&(remote->addr));\n\tconst char *ltype = NULL, *rtype = NULL;\n\tswitch(local->type) {\n\t\tcase NICE_CANDIDATE_TYPE_HOST:\n\t\t\tltype = \"host\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:\n\t\t\tltype = \"srflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_PEER_REFLEXIVE:\n\t\t\tltype = \"prflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_RELAYED:\n\t\t\tltype = \"relay\";\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\tswitch(remote->type) {\n\t\tcase NICE_CANDIDATE_TYPE_HOST:\n\t\t\trtype = \"host\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:\n\t\t\trtype = \"srflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_PEER_REFLEXIVE:\n\t\t\trtype = \"prflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_RELAYED:\n\t\t\trtype = \"relay\";\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\tg_snprintf(sp, sizeof(sp), \"%s:%d [%s,%s] <-> %s:%d [%s,%s]\",\n\t\tladdress, lport, ltype, local->transport == NICE_CANDIDATE_TRANSPORT_UDP ? \"udp\" : \"tcp\",\n\t\traddress, rport, rtype, remote->transport == NICE_CANDIDATE_TRANSPORT_UDP ? \"udp\" : \"tcp\");\n#endif\n\tgboolean newpair = FALSE;\n\tif(pc->selected_pair == NULL || strcmp(sp, pc->selected_pair)) {\n\t\tnewpair = TRUE;\n\t\tgchar *prev_selected_pair = pc->selected_pair;\n\t\tpc->selected_pair = g_strdup(sp);\n\t\tg_clear_pointer(&prev_selected_pair, g_free);\n\t}\n\t/* Notify event handlers */\n\tif(newpair && janus_events_is_enabled()) {\n\t\tjanus_session *session = (janus_session *)handle->session;\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"selected-pair\", json_string(sp));\n#ifdef HAVE_LIBNICE_TCP\n\t\tjson_t *candidates = json_object();\n\t\tjson_t *lcand = json_object();\n\t\tjson_object_set_new(lcand, \"address\", json_string(laddress));\n\t\tjson_object_set_new(lcand, \"port\", json_integer(lport));\n\t\tjson_object_set_new(lcand, \"type\", json_string(ltype));\n\t\tjson_object_set_new(lcand, \"transport\", json_string(local->transport == NICE_CANDIDATE_TRANSPORT_UDP ? \"udp\" : \"tcp\"));\n\t\tjson_object_set_new(lcand, \"family\", json_integer(nice_address_ip_version(&local->addr)));\n\t\tjson_object_set_new(candidates, \"local\", lcand);\n\t\tjson_t *rcand = json_object();\n\t\tjson_object_set_new(rcand, \"address\", json_string(raddress));\n\t\tjson_object_set_new(rcand, \"port\", json_integer(rport));\n\t\tjson_object_set_new(rcand, \"type\", json_string(rtype));\n\t\tjson_object_set_new(rcand, \"transport\", json_string(remote->transport == NICE_CANDIDATE_TRANSPORT_UDP ? \"udp\" : \"tcp\"));\n\t\tjson_object_set_new(rcand, \"family\", json_integer(nice_address_ip_version(&remote->addr)));\n\t\tjson_object_set_new(candidates, \"remote\", rcand);\n\t\tjson_object_set_new(info, \"candidates\", candidates);\n#endif\n\t\tjson_object_set_new(info, \"stream_id\", json_integer(stream_id));\n\t\tjson_object_set_new(info, \"component_id\", json_integer(component_id));\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_WEBRTC, JANUS_EVENT_SUBTYPE_WEBRTC_PAIR,\n\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t}\n\t/* Have we been here before? (might happen, when trickling) */\n\tif(pc->connected > 0)\n\t\treturn;\n\t/* FIXME Clear the queue */\n\tjanus_ice_clear_queued_packets(handle);\n\t/* Now we can start the DTLS handshake (FIXME This was on the 'connected' state notification, before) */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Component is ready enough, starting DTLS handshake...\\n\", handle->handle_id);\n\tpc->connected = janus_get_monotonic_time();\n\t/* Start the DTLS handshake, at last */\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\tg_async_queue_push_front(handle->queued_packets, &janus_ice_dtls_handshake);\n#else\n\tg_async_queue_push(handle->queued_packets, &janus_ice_dtls_handshake);\n#endif\n\tg_main_context_wakeup(handle->mainctx);\n}\n\n/* Candidates management */\nstatic int janus_ice_candidate_to_string(janus_ice_handle *handle, NiceCandidate *c, char *buffer, int buflen, gboolean log_candidate, gboolean force_private, guint public_ip_index);\n#ifndef HAVE_LIBNICE_TCP\nstatic void janus_ice_cb_new_local_candidate (NiceAgent *agent, guint stream_id, guint component_id, gchar *foundation, gpointer ice) {\n#else\nstatic void janus_ice_cb_new_local_candidate (NiceAgent *agent, NiceCandidate *candidate, gpointer ice) {\n#endif\n\tif(!janus_full_trickle_enabled) {\n\t\t/* Ignore if we're not full-trickling: for half-trickle\n\t\t * janus_ice_candidates_to_sdp() is used instead */\n\t\treturn;\n\t}\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice;\n\tif(!handle)\n\t\treturn;\n#ifndef HAVE_LIBNICE_TCP\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Discovered new local candidate for component %d in stream %d: foundation=%s\\n\", handle ? handle->handle_id : 0, component_id, stream_id, foundation);\n#else\n\tconst char *ctype = NULL;\n\tswitch(candidate->type) {\n\t\tcase NICE_CANDIDATE_TYPE_HOST:\n\t\t\tctype = \"host\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:\n\t\t\tctype = \"srflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_PEER_REFLEXIVE:\n\t\t\tctype = \"prflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_RELAYED:\n\t\t\tctype = \"relay\";\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\tguint stream_id = candidate->stream_id;\n\tguint component_id = candidate->component_id;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Discovered new local candidate for component %d in stream %d: type=%s\\n\", handle ? handle->handle_id : 0, component_id, stream_id, ctype);\n#endif\n\tif(component_id > 1) {\n\t\t/* New remote candidate for a component we don't need anymore (rtcp-mux) */\n\t\treturn;\n\t}\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc || pc->stream_id != stream_id) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     No stream %d??\\n\", handle->handle_id, stream_id);\n\t\treturn;\n\t}\n#ifndef HAVE_LIBNICE_TCP\n\t/* Get local candidates and look for the related foundation */\n\tNiceCandidate *candidate = NULL;\n\tGSList *candidates = nice_agent_get_local_candidates(agent, component_id, stream_id), *tmp = candidates;\n\twhile(tmp) {\n\t\tNiceCandidate *c = (NiceCandidate *)tmp->data;\n\t\t/* Check if this is what we're looking for */\n\t\tif(!candidate && !strcasecmp(c->foundation, foundation)) {\n\t\t\t/* It is! */\n\t\t\tcandidate = c;\n\t\t} else {\n\t\t\tnice_candidate_free(c);\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tg_slist_free(candidates);\n\tif(candidate == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Candidate with foundation %s not found?\\n\", foundation);\n\t\treturn;\n\t}\n#endif\n\tchar buffer[200];\n\tguint public_ip_index = 0;\n\tgboolean ipv6 = (nice_address_ip_version(&candidate->addr) == 6);\n\tgboolean same_family = (!ipv6 && janus_has_public_ipv4_ip()) || (ipv6 && janus_has_public_ipv6_ip());\n\tdo {\n\t\tif(janus_ice_candidate_to_string(handle, candidate, buffer, sizeof(buffer), TRUE, FALSE, public_ip_index) == 0) {\n\t\t\t/* Candidate encoded, send a \"trickle\" event to the browser (but only if it's not a 'prflx') */\n\t\t\tif(candidate->type == NICE_CANDIDATE_TYPE_PEER_REFLEXIVE) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping prflx candidate...\\n\", handle->handle_id);\n\t\t\t} else {\n\t\t\t\tif(strlen(buffer) > 0)\n\t\t\t\t\tjanus_ice_notify_trickle(handle, buffer);\n\t\t\t\t/* If nat-1-1 is enabled but we want to keep the private host, add another candidate */\n\t\t\t\tif(nat_1_1_enabled && public_ip_index == 0 && (keep_private_host || !same_family) &&\n\t\t\t\t\t\tjanus_ice_candidate_to_string(handle, candidate, buffer, sizeof(buffer), TRUE, TRUE, public_ip_index) == 0) {\n\t\t\t\t\tif(candidate->type == NICE_CANDIDATE_TYPE_PEER_REFLEXIVE) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping prflx candidate...\\n\", handle->handle_id);\n\t\t\t\t\t} else if(strlen(buffer) > 0) {\n\t\t\t\t\t\tjanus_ice_notify_trickle(handle, buffer);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tpublic_ip_index++;\n\t\tif(!same_family) {\n\t\t\t/* We don't have any nat-1-1 address of the same family as this candidate, we're done */\n\t\t\tbreak;\n\t\t}\n\t} while (public_ip_index < janus_get_public_ip_count());\n\n#ifndef HAVE_LIBNICE_TCP\n\tnice_candidate_free(candidate);\n#endif\n}\n\n#ifndef HAVE_LIBNICE_TCP\nstatic void janus_ice_cb_new_remote_candidate (NiceAgent *agent, guint stream_id, guint component_id, gchar *foundation, gpointer ice) {\n#else\nstatic void janus_ice_cb_new_remote_candidate (NiceAgent *agent, NiceCandidate *candidate, gpointer ice) {\n#endif\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice;\n\tif(!handle)\n\t\treturn;\n#ifndef HAVE_LIBNICE_TCP\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Discovered new remote candidate for component %d in stream %d: foundation=%s\\n\", handle ? handle->handle_id : 0, component_id, stream_id, foundation);\n#else\n\tconst char *ctype = NULL;\n\tswitch(candidate->type) {\n\t\tcase NICE_CANDIDATE_TYPE_HOST:\n\t\t\tctype = \"host\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:\n\t\t\tctype = \"srflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_PEER_REFLEXIVE:\n\t\t\tctype = \"prflx\";\n\t\t\tbreak;\n\t\tcase NICE_CANDIDATE_TYPE_RELAYED:\n\t\t\tctype = \"relay\";\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\tguint stream_id = candidate->stream_id;\n\tguint component_id = candidate->component_id;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Discovered new remote candidate for component %d in stream %d: type=%s\\n\", handle ? handle->handle_id : 0, component_id, stream_id, ctype);\n#endif\n\tif(component_id > 1) {\n\t\t/* New remote candidate for a component we don't need anymore (rtcp-mux) */\n\t\treturn;\n\t}\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc || pc->stream_id != stream_id) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     No stream %d??\\n\", handle->handle_id, stream_id);\n\t\treturn;\n\t}\n#ifndef HAVE_LIBNICE_TCP\n\t/* Get remote candidates and look for the related foundation */\n\tNiceCandidate *candidate = NULL;\n\tGSList *candidates = nice_agent_get_remote_candidates(agent, component_id, stream_id), *tmp = candidates;\n\twhile(tmp) {\n\t\tNiceCandidate *c = (NiceCandidate *)tmp->data;\n\t\tif(candidate == NULL) {\n\t\t\t/* Check if this is what we're looking for */\n\t\t\tif(!strcasecmp(c->foundation, foundation)) {\n\t\t\t\t/* It is! */\n\t\t\t\tcandidate = c;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tnice_candidate_free(c);\n\t\ttmp = tmp->next;\n\t}\n\tg_slist_free(candidates);\n\tif(candidate == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Candidate with foundation %s not found?\\n\", foundation);\n\t\treturn;\n\t}\n#endif\n\t/* Render the candidate and add it to the remote_candidates cache for the admin API */\n\tif(candidate->type != NICE_CANDIDATE_TYPE_PEER_REFLEXIVE) {\n\t\t/* ... but only if it's 'prflx', the others we add ourselves */\n\t\tgoto candidatedone;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Stream #%d, Component #%d\\n\", handle->handle_id, candidate->stream_id, candidate->component_id);\n\tgchar address[NICE_ADDRESS_STRING_LEN], base_address[NICE_ADDRESS_STRING_LEN];\n\tgint port = 0, base_port = 0;\n\tnice_address_to_string(&(candidate->addr), (gchar *)&address);\n\tport = nice_address_get_port(&(candidate->addr));\n\tnice_address_to_string(&(candidate->base_addr), (gchar *)&base_address);\n\tbase_port = nice_address_get_port(&(candidate->base_addr));\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Address:    %s:%d\\n\", handle->handle_id, address, port);\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Priority:   %d\\n\", handle->handle_id, candidate->priority);\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Foundation: %s\\n\", handle->handle_id, candidate->foundation);\n\tchar buffer[200];\n\tif(candidate->transport == NICE_CANDIDATE_TRANSPORT_UDP) {\n\t\tg_snprintf(buffer, sizeof(buffer),\n\t\t\t\"%s %d %s %d %s %d typ prflx raddr %s rport %d\\r\\n\",\n\t\t\t\tcandidate->foundation,\n\t\t\t\tcandidate->component_id,\n\t\t\t\t\"udp\",\n\t\t\t\tcandidate->priority,\n\t\t\t\taddress,\n\t\t\t\tport,\n\t\t\t\tbase_address,\n\t\t\t\tbase_port);\n\t} else {\n\t\tif(!janus_ice_tcp_enabled) {\n\t\t\t/* ICETCP support disabled */\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping prflx TCP candidate, ICETCP support disabled...\\n\", handle->handle_id);\n\t\t\tgoto candidatedone;\n\t\t}\n#ifndef HAVE_LIBNICE_TCP\n\t\t/* TCP candidates are only supported since libnice 0.1.8 */\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping prflx TCP candidate, the libnice version doesn't support it...\\n\", handle->handle_id);\n\t\t\tgoto candidatedone;\n#else\n\t\tconst char *type = NULL;\n\t\tswitch(candidate->transport) {\n\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_ACTIVE:\n\t\t\t\ttype = \"active\";\n\t\t\t\tbreak;\n\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_PASSIVE:\n\t\t\t\ttype = \"passive\";\n\t\t\t\tbreak;\n\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_SO:\n\t\t\t\ttype = \"so\";\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\tif(type == NULL) {\n\t\t\t/* FIXME Unsupported transport */\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unsupported transport, skipping nonUDP/TCP prflx candidate...\\n\", handle->handle_id);\n\t\t\tgoto candidatedone;\n\t\t} else {\n\t\t\tg_snprintf(buffer, sizeof(buffer),\n\t\t\t\t\"%s %d %s %d %s %d typ prflx raddr %s rport %d tcptype %s\\r\\n\",\n\t\t\t\t\tcandidate->foundation,\n\t\t\t\t\tcandidate->component_id,\n\t\t\t\t\t\"tcp\",\n\t\t\t\t\tcandidate->priority,\n\t\t\t\t\taddress,\n\t\t\t\t\tport,\n\t\t\t\t\tbase_address,\n\t\t\t\t\tbase_port,\n\t\t\t\t\ttype);\n\t\t}\n#endif\n\t}\n\n\t/* Now parse the candidate as if we received it from the Janus API */\n\tint res = janus_sdp_parse_candidate(pc, buffer, 1);\n\tif(res != 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse prflx candidate... (%d)\\n\", handle->handle_id, res);\n\t}\n\ncandidatedone:\n#ifndef HAVE_LIBNICE_TCP\n\tnice_candidate_free(candidate);\n#endif\n\treturn;\n}\n\nstatic void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_id, guint len, gchar *buf, gpointer ice) {\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)ice;\n\tif(!pc) {\n\t\tJANUS_LOG(LOG_ERR, \"No component %d in stream %d??\\n\", component_id, stream_id);\n\t\treturn;\n\t}\n\tjanus_ice_handle *handle = pc->handle;\n\tif(!handle) {\n\t\tJANUS_LOG(LOG_ERR, \"No handle for stream %d??\\n\", stream_id);\n\t\treturn;\n\t}\n\tjanus_session *session = (janus_session *)handle->session;\n\tif(!pc->dtls) {\t/* Still waiting for the DTLS stack */\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Still waiting for the DTLS stack for component %d in stream %d...\\n\", handle->handle_id, component_id, stream_id);\n\t\treturn;\n\t}\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) || janus_is_stopping()) {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Forced to stop it here...\\n\", handle->handle_id);\n\t\treturn;\n\t}\n\t/* What is this? */\n\tif(janus_is_dtls(buf) || (!janus_is_rtp(buf, len) && !janus_is_rtcp(buf, len))) {\n\t\t/* This is DTLS: either handshake stuff, or data coming from SCTP DataChannels */\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Looks like DTLS!\\n\", handle->handle_id);\n\t\tjanus_dtls_srtp_incoming_msg(pc->dtls, buf, len);\n\t\t/* Update stats (TODO Do the same for the last second window as well) */\n\t\tpc->dtls_in_stats.info[0].packets++;\n\t\tpc->dtls_in_stats.info[0].bytes += len;\n\t\t/* If there's a datachannel medium, update the stats there too */\n\t\tjanus_ice_peerconnection_medium *medium = g_hash_table_lookup(pc->media_bytype, GINT_TO_POINTER(JANUS_MEDIA_DATA));\n\t\tif(medium) {\n\t\t\tmedium->in_stats.info[0].packets++;\n\t\t\tmedium->in_stats.info[0].bytes += len;\n\t\t}\n\t\treturn;\n\t}\n\t/* Not DTLS... RTP or RTCP? (http://tools.ietf.org/html/rfc5761#section-4) */\n\tif(janus_is_rtp(buf, len)) {\n\t\t/* This is RTP */\n\t\tif(janus_is_webrtc_encryption_enabled() && (!pc->dtls || !pc->dtls->srtp_valid || !pc->dtls->srtp_in)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]     Missing valid SRTP session (packet arrived too early?), skipping...\\n\", handle->handle_id);\n\t\t} else {\n\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\tguint32 packet_ssrc = ntohl(header->ssrc);\n\t\t\t/* Which medium does this refer to? Is this audio or video? */\n\t\t\tint video = 0, vindex = 0, rtx = 0;\n\t\t\tjanus_ice_peerconnection_medium *medium = g_hash_table_lookup(pc->media_byssrc, GINT_TO_POINTER(packet_ssrc));\n\t\t\tif(medium == NULL) {\n\t\t\t\t/* SSRC not found, try the mid/rid RTP extensions if in use */\n\t\t\t\tif(pc->mid_ext_id > 0) {\n\t\t\t\t\tchar sdes_item[16];\n\t\t\t\t\tif(janus_rtp_header_extension_parse_mid(buf, len, pc->mid_ext_id, sdes_item, sizeof(sdes_item)) == 0) {\n\t\t\t\t\t\tmedium = g_hash_table_lookup(pc->media_bymid, sdes_item);\n\t\t\t\t\t\tif(medium != NULL) {\n\t\t\t\t\t\t\t/* Found! Associate this SSRC to this stream */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] SSRC %\"SCNu32\" is associated to mid %s\\n\",\n\t\t\t\t\t\t\t\thandle->handle_id, packet_ssrc, medium->mid);\n\t\t\t\t\t\t\tgboolean found = FALSE;\n\t\t\t\t\t\t\t/* Check if simulcasting is involved */\n\t\t\t\t\t\t\tjanus_mutex_lock(&handle->mutex);\n\t\t\t\t\t\t\tif(medium->rid[0] == NULL || pc->rid_ext_id < 1) {\n\t\t\t\t\t\t\t\tmedium->ssrc_peer[0] = packet_ssrc;\n\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif(janus_rtp_header_extension_parse_rid(buf, len, pc->rid_ext_id, sdes_item, sizeof(sdes_item)) == 0) {\n\t\t\t\t\t\t\t\t\t/* Try the RTP stream ID */\n\t\t\t\t\t\t\t\t\tif(medium->rid[0] != NULL && !strcmp(medium->rid[0], sdes_item)) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  -- Simulcasting: rid=%s\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer[0] = packet_ssrc;\n\t\t\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t\t\t} else if(medium->rid[1] != NULL && !strcmp(medium->rid[1], sdes_item)) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  -- Simulcasting #1: rid=%s\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer[1] = packet_ssrc;\n\t\t\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t\t\t} else if(medium->rid[2] != NULL && !strcmp(medium->rid[2], sdes_item)) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  -- Simulcasting #2: rid=%s\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer[2] = packet_ssrc;\n\t\t\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]  -- Simulcasting: unknown rid %s..?\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if(pc->ridrtx_ext_id > 0 &&\n\t\t\t\t\t\t\t\t\t\tjanus_rtp_header_extension_parse_rid(buf, len, pc->ridrtx_ext_id, sdes_item, sizeof(sdes_item)) == 0) {\n\t\t\t\t\t\t\t\t\t/* Try the repaired RTP stream ID */\n\t\t\t\t\t\t\t\t\tif(medium->rid[0] != NULL && !strcmp(medium->rid[0], sdes_item)) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  -- Simulcasting: rid=%s (rtx)\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer_rtx[0] = packet_ssrc;\n\t\t\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t\t\t} else if(medium->rid[1] != NULL && !strcmp(medium->rid[1], sdes_item)) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  -- Simulcasting #1: rid=%s (rtx)\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer_rtx[1] = packet_ssrc;\n\t\t\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t\t\t} else if(medium->rid[2] != NULL && !strcmp(medium->rid[2], sdes_item)) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  -- Simulcasting #2: rid=%s (rtx)\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer_rtx[2] = packet_ssrc;\n\t\t\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]  -- Simulcasting: unknown rid %s..?\\n\", handle->handle_id, sdes_item);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\t\t\t\tif(found) {\n\t\t\t\t\t\t\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(packet_ssrc), medium);\n\t\t\t\t\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tmedium = NULL;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(medium == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unknown SSRC, dropping packet (SSRC %\"SCNu32\")...\\n\",\n\t\t\t\t\thandle->handle_id, packet_ssrc);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvideo = (medium->type == JANUS_MEDIA_VIDEO);\n\t\t\t/* Make sure we're prepared to receive this media packet */\n\t\t\tif(!medium->recv)\n\t\t\t\treturn;\n\t\t\t/* If this is video, check if this is simulcast and/or a retransmission using RFC4588 */\n\t\t\tvindex = 0;\n\t\t\tif(video && medium->ssrc_peer[0] != packet_ssrc) {\n\t\t\t\tif(medium->ssrc_peer[1] == packet_ssrc) {\n\t\t\t\t\t/* FIXME Simulcast (1) */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Simulcast #1 (SSRC %\"SCNu32\")...\\n\", handle->handle_id, packet_ssrc);\n\t\t\t\t\tvindex = 1;\n\t\t\t\t} else if(medium->ssrc_peer[2] == packet_ssrc) {\n\t\t\t\t\t/* FIXME Simulcast (2) */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Simulcast #2 (SSRC %\"SCNu32\")...\\n\", handle->handle_id, packet_ssrc);\n\t\t\t\t\tvindex = 2;\n\t\t\t\t} else {\n\t\t\t\t\t/* Maybe a video retransmission using RFC4588? */\n\t\t\t\t\tif(medium->ssrc_peer_rtx[0] == packet_ssrc) {\n\t\t\t\t\t\trtx = 1;\n\t\t\t\t\t\tvindex = 0;\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] RFC4588 rtx packet on video (SSRC %\"SCNu32\")...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, packet_ssrc);\n\t\t\t\t\t} else if(medium->ssrc_peer_rtx[1] == packet_ssrc) {\n\t\t\t\t\t\trtx = 1;\n\t\t\t\t\t\tvindex = 1;\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] RFC4588 rtx packet on video #%d (SSRC %\"SCNu32\")...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, vindex, packet_ssrc);\n\t\t\t\t\t} else if(medium->ssrc_peer_rtx[2] == packet_ssrc) {\n\t\t\t\t\t\trtx = 1;\n\t\t\t\t\t\tvindex = 2;\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] RFC4588 rtx packet on video #%d (SSRC %\"SCNu32\")...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, vindex, packet_ssrc);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tint buflen = len;\n\t\t\tsrtp_err_status_t res = janus_is_webrtc_encryption_enabled() ?\n\t\t\t\tsrtp_unprotect(pc->dtls->srtp_in, buf, &buflen) : srtp_err_status_ok;\n\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\tif(res != srtp_err_status_replay_fail && res != srtp_err_status_replay_old) {\n\t\t\t\t\t/* Only print the error if it's not a 'replay fail' or 'replay old' (which is probably just the result of us NACKing a packet) */\n\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     SRTP unprotect error: %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")\\n\", handle->handle_id, janus_srtp_error_str(res), len, buflen, timestamp, seq);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif((!video && medium->ssrc_peer[0] == 0) || (vindex == 0 && medium->ssrc_peer[0] == 0)) {\n\t\t\t\t\tmedium->ssrc_peer[0] = ntohl(header->ssrc);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]     Peer #%d (%s) SSRC: %u\\n\",\n\t\t\t\t\t\thandle->handle_id, medium->mindex,\n\t\t\t\t\t\tmedium->type == JANUS_MEDIA_VIDEO ? \"video\" : \"audio\",\n\t\t\t\t\t\tmedium->ssrc_peer[0]);\n\t\t\t\t}\n\t\t\t\t/* Do we need to dump this packet for debugging? */\n\t\t\t\tif(g_atomic_int_get(&handle->dump_packets))\n\t\t\t\t\tjanus_text2pcap_dump(handle->text2pcap, JANUS_TEXT2PCAP_RTP, TRUE, buf, buflen,\n\t\t\t\t\t\t\"[session=%\"SCNu64\"][handle=%\"SCNu64\"]\", session->session_id, handle->handle_id);\n\t\t\t\t/* If this is a retransmission using RFC4588, we have to do something first to get the original packet */\n\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\t\tint plen = 0;\n\t\t\t\tchar *payload = janus_rtp_payload(buf, buflen, &plen);\n\t\t\t\tif (!payload) {\n\t\t\t\t\t  JANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     Error accessing the RTP payload len=%d\\n\", handle->handle_id, buflen);\n\t\t\t\t}\n\t\t\t\tif(rtx) {\n\t\t\t\t\t/* The original sequence number is in the first two bytes of the payload */\n\t\t\t\t\t/* Rewrite the header with the info from the original packet (payload type, SSRC, sequence number) */\n\t\t\t\t\theader->type = medium->payload_type;\n\t\t\t\t\tpacket_ssrc = medium->ssrc_peer[vindex];\n\t\t\t\t\theader->ssrc = htonl(packet_ssrc);\n\t\t\t\t\tif(plen > 0) {\n\t\t\t\t\t\tmemcpy(&header->seq_number, payload, 2);\n\t\t\t\t\t\t/* Finally, remove the original sequence number from the payload: move the whole\n\t\t\t\t\t\t * payload back two bytes rather than shifting the header forward (avoid misaligned access) */\n\t\t\t\t\t\tbuflen -= 2;\n\t\t\t\t\t\tplen -= 2;\n\t\t\t\t\t\tmemmove(payload, payload+2, plen);\n\t\t\t\t\t\theader = (janus_rtp_header *)buf;\n\t\t\t\t\t\tif(pc->rid_ext_id > 1 && pc->ridrtx_ext_id > 1) {\n\t\t\t\t\t\t\t/* Replace the 'repaired' extension ID as well with the 'regular' one */\n\t\t\t\t\t\t\tjanus_rtp_header_extension_replace_id(buf, buflen, pc->ridrtx_ext_id, pc->rid_ext_id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Check if we need to handle transport wide cc */\n\t\t\t\tif(pc->do_transport_wide_cc) {\n\t\t\t\t\tguint16 transport_seq_num;\n\t\t\t\t\t/* Get transport wide seq num */\n\t\t\t\t\tif(janus_rtp_header_extension_parse_transport_wide_cc(buf, buflen, pc->transport_wide_cc_ext_id, &transport_seq_num) == 0) {\n\t\t\t\t\t\t/* Get current timestamp */\n\t\t\t\t\t\tstruct timeval now;\n\t\t\t\t\t\tgettimeofday(&now,0);\n\t\t\t\t\t\t/* Create <seq num, time> pair */\n\t\t\t\t\t\tjanus_rtcp_transport_wide_cc_stats *stats = g_malloc0(sizeof(janus_rtcp_transport_wide_cc_stats));\n\t\t\t\t\t\t/* Check if we have a sequence wrap */\n\t\t\t\t\t\tif(transport_seq_num<0x0FFF && (pc->transport_wide_cc_last_seq_num&0xFFFF)>0xF000) {\n\t\t\t\t\t\t\t/* Increase cycles */\n\t\t\t\t\t\t\tpc->transport_wide_cc_cycles++;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Get extended value */\n\t\t\t\t\t\tguint32 transport_ext_seq_num = pc->transport_wide_cc_cycles<<16 | transport_seq_num;\n\t\t\t\t\t\t/* Store last received transport seq num */\n\t\t\t\t\t\tpc->transport_wide_cc_last_seq_num = transport_seq_num;\n\t\t\t\t\t\t/* Set stats values */\n\t\t\t\t\t\tstats->transport_seq_num = transport_ext_seq_num;\n\t\t\t\t\t\tstats->timestamp = (((guint64)now.tv_sec)*1E6+now.tv_usec);\n\t\t\t\t\t\t/* Lock and append to received list */\n\t\t\t\t\t\tjanus_mutex_lock(&pc->mutex);\n\t\t\t\t\t\tpc->transport_wide_received_seq_nums = g_slist_prepend(pc->transport_wide_received_seq_nums, stats);\n\t\t\t\t\t\tjanus_mutex_unlock(&pc->mutex);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(medium->do_nacks) {\n\t\t\t\t\t/* Check if this packet is a duplicate: can happen with RFC4588 */\n\t\t\t\t\tguint16 seqno = ntohs(header->seq_number);\n\t\t\t\t\tint nstate = medium->rtx_nacked[vindex] ?\n\t\t\t\t\t\tGPOINTER_TO_INT(g_hash_table_lookup(medium->rtx_nacked[vindex], GUINT_TO_POINTER(seqno))) : 0;\n\t\t\t\t\tif(nstate == 1) {\n\t\t\t\t\t\t/* Packet was NACKed and this is the first time we receive it: change state to received */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Received NACKed packet %\"SCNu16\" (SSRC %\"SCNu32\", vindex %d)...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, seqno, packet_ssrc, vindex);\n\t\t\t\t\t\tg_hash_table_insert(medium->rtx_nacked[vindex], GUINT_TO_POINTER(seqno), GUINT_TO_POINTER(2));\n\t\t\t\t\t} else if(nstate == 2) {\n\t\t\t\t\t\t/* We already received this packet: drop it */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Detected duplicate packet %\"SCNu16\" (SSRC %\"SCNu32\", vindex %d)...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, seqno, packet_ssrc, vindex);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if(rtx && nstate == 0) {\n\t\t\t\t\t\t/* We received a retransmission for a packet we didn't NACK: drop it\n\t\t\t\t\t\t * FIXME This seems to happen with Chrome when RFC4588 is enabled: in that case,\n\t\t\t\t\t\t * Chrome sends the first packet ~8 times as a retransmission, probably to ensure\n\t\t\t\t\t\t * we receive it, since the first packet cannot be NACKed (NACKs are triggered\n\t\t\t\t\t\t * when there's a gap in between two packets, and the first doesn't have a reference)\n\t\t\t\t\t\t * Rather than dropping, we should add a better check in the future */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Got a retransmission for non-NACKed packet %\"SCNu16\" (SSRC %\"SCNu32\", vindex %d)...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, seqno, packet_ssrc, vindex);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Backup the RTP header before passing it to the proper RTP switching context */\n\t\t\t\tjanus_rtp_header backup = *header;\n\t\t\t\tif(medium->ssrc_peer_orig[vindex] == 0)\n\t\t\t\t\tmedium->ssrc_peer_orig[vindex] = packet_ssrc;\n\t\t\t\tjanus_rtp_header_update(header, &medium->rtp_ctx[vindex], medium->type == JANUS_MEDIA_VIDEO, 0);\n\t\t\t\theader->ssrc = htonl(medium->ssrc_peer_orig[vindex]);\n\t\t\t\t/* Keep track of payload types too */\n\t\t\t\tif(medium->payload_type < 0) {\n\t\t\t\t\tmedium->payload_type = header->type;\n\t\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX) &&\n\t\t\t\t\t\t\tmedium->rtx_payload_types && g_hash_table_size(medium->rtx_payload_types) > 0) {\n\t\t\t\t\t\tmedium->rtx_payload_type = GPOINTER_TO_INT(g_hash_table_lookup(medium->rtx_payload_types, GINT_TO_POINTER(medium->payload_type)));\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Retransmissions will have payload type %d\\n\",\n\t\t\t\t\t\t\thandle->handle_id, medium->rtx_payload_type);\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->codec == NULL) {\n\t\t\t\t\t\tjanus_mutex_lock(&handle->mutex);\n\t\t\t\t\t\tconst char *codec = janus_get_codec_from_pt(handle->local_sdp, medium->payload_type);\n\t\t\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\t\t\tif(codec != NULL)\n\t\t\t\t\t\t\tmedium->codec = g_strdup(codec);\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->type == JANUS_MEDIA_VIDEO && medium->video_is_keyframe == NULL && medium->codec != NULL) {\n\t\t\t\t\t\tif(!strcasecmp(medium->codec, \"vp8\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_vp8_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"vp9\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_vp9_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"h264\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_h264_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"av1\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_av1_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"h265\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_h265_is_keyframe;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Prepare the data to pass to the responsible plugin */\n\t\t\t\tjanus_plugin_rtp rtp = { .mindex = medium->mindex, .video = video, .buffer = buf, .length = buflen };\n\t\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\t\t/* Parse RTP extensions before involving the plugin */\n\t\t\t\tif(!video && pc->audiolevel_ext_id != -1) {\n\t\t\t\t\tgboolean vad = FALSE;\n\t\t\t\t\tint level = -1;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_audio_level(buf, buflen,\n\t\t\t\t\t\t\tpc->audiolevel_ext_id, &vad, &level) == 0) {\n\t\t\t\t\t\trtp.extensions.audio_level = level;\n\t\t\t\t\t\trtp.extensions.audio_level_vad = vad;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(video && pc->videoorientation_ext_id != -1) {\n\t\t\t\t\tgboolean c = FALSE, f = FALSE, r1 = FALSE, r0 = FALSE;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_video_orientation(buf, buflen,\n\t\t\t\t\t\t\tpc->videoorientation_ext_id, &c, &f, &r1, &r0) == 0) {\n\t\t\t\t\t\trtp.extensions.video_rotation = 0;\n\t\t\t\t\t\tif(r1 && r0)\n\t\t\t\t\t\t\trtp.extensions.video_rotation = 270;\n\t\t\t\t\t\telse if(r1)\n\t\t\t\t\t\t\trtp.extensions.video_rotation = 180;\n\t\t\t\t\t\telse if(r0)\n\t\t\t\t\t\t\trtp.extensions.video_rotation = 90;\n\t\t\t\t\t\trtp.extensions.video_back_camera = c;\n\t\t\t\t\t\trtp.extensions.video_flipped = f;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(video && pc->playoutdelay_ext_id != -1) {\n\t\t\t\t\tuint16_t min = 0, max = 0;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_playout_delay(buf, buflen,\n\t\t\t\t\t\t\tpc->playoutdelay_ext_id, &min, &max) == 0) {\n\t\t\t\t\t\trtp.extensions.min_delay = min;\n\t\t\t\t\t\trtp.extensions.max_delay = max;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(video && pc->dependencydesc_ext_id != -1) {\n\t\t\t\t\tuint8_t dd[256];\n\t\t\t\t\tint len = sizeof(dd);\n\t\t\t\t\tif(janus_rtp_header_extension_parse_dependency_desc(buf, buflen,\n\t\t\t\t\t\t\tpc->dependencydesc_ext_id, dd, &len) == 0 && len > 0) {\n\t\t\t\t\t\t/* We copy the DD bytes as they are: it's up to plugins to parse it, if needed */\n\t\t\t\t\t\trtp.extensions.dd_len = len;\n\t\t\t\t\t\tmemcpy(rtp.extensions.dd_content, dd, len);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(pc->abs_capture_time_ext_id != -1) {\n\t\t\t\t\tuint64_t abs_ts = 0;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_abs_capture_time(buf, buflen,\n\t\t\t\t\t\t\tpc->abs_capture_time_ext_id, &abs_ts) == 0) {\n\t\t\t\t\t\trtp.extensions.abs_capture_ts = abs_ts;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(pc->videolayers_ext_id != -1) {\n\t\t\t\t\tint8_t spatial_layers = -1, temporal_layers = -1;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_video_layers_allocation(buf, buflen,\n\t\t\t\t\t\t\tpc->videolayers_ext_id, &spatial_layers, &temporal_layers) == 0) {\n\t\t\t\t\t\t/* We copy the VLA bytes as they are: it's up to plugins to parse it, if needed */\n\t\t\t\t\t\trtp.extensions.spatial_layers = spatial_layers;\n\t\t\t\t\t\trtp.extensions.temporal_layers = temporal_layers;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Pass the packet to the plugin */\n\t\t\t\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\t\t\t\tif(plugin && plugin->incoming_rtp && handle->app_handle &&\n\t\t\t\t\t\t!g_atomic_int_get(&handle->app_handle->stopped) &&\n\t\t\t\t\t\t!g_atomic_int_get(&handle->destroyed))\n\t\t\t\t\tplugin->incoming_rtp(handle->app_handle, &rtp);\n\t\t\t\t/* Restore the header for the stats (plugins may have messed with it) */\n\t\t\t\t*header = backup;\n\t\t\t\t/* Update stats (overall data received, and data received in the last second) */\n\t\t\t\tif(buflen > 0) {\n\t\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\t\tif(medium->in_stats.info[vindex].bytes == 0 || medium->in_stats.info[vindex].notified_lastsec) {\n\t\t\t\t\t\t/* We either received our first packet, or we started receiving it again after missing more than a second */\n\t\t\t\t\t\tmedium->in_stats.info[vindex].notified_lastsec = FALSE;\n\t\t\t\t\t\tjanus_ice_notify_media(handle, medium->mid, medium->type == JANUS_MEDIA_VIDEO, medium->rtcp_ctx[1] != NULL, vindex, TRUE);\n\t\t\t\t\t}\n\t\t\t\t\t/* Overall video data for this SSRC */\n\t\t\t\t\tmedium->in_stats.info[vindex].packets++;\n\t\t\t\t\tmedium->in_stats.info[vindex].bytes += buflen;\n\t\t\t\t\t/* Last second video data for this SSRC */\n\t\t\t\t\tif(medium->in_stats.info[vindex].updated == 0)\n\t\t\t\t\t\tmedium->in_stats.info[vindex].updated = now;\n\t\t\t\t\tif(now > medium->in_stats.info[vindex].updated &&\n\t\t\t\t\t\t\tnow - medium->in_stats.info[vindex].updated >= G_USEC_PER_SEC) {\n\t\t\t\t\t\tmedium->in_stats.info[vindex].bytes_lastsec = medium->in_stats.info[vindex].bytes_lastsec_temp;\n\t\t\t\t\t\tmedium->in_stats.info[vindex].bytes_lastsec_temp = 0;\n\t\t\t\t\t\tmedium->in_stats.info[vindex].updated = now;\n\t\t\t\t\t}\n\t\t\t\t\tmedium->in_stats.info[vindex].bytes_lastsec_temp += buflen;\n\t\t\t\t}\n\n\t\t\t\t/* Update the RTCP context as well */\n\t\t\t\trtcp_context *rtcp_ctx = medium->rtcp_ctx[vindex];\n\t\t\t\tgboolean retransmissions_disabled = !medium->do_nacks;\n\t\t\t\tjanus_rtcp_process_incoming_rtp(rtcp_ctx, buf, buflen,\n\t\t\t\t\t\t(video && rtx) ? TRUE : FALSE,\n\t\t\t\t\t\t(video && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)),\n\t\t\t\t\t\tretransmissions_disabled, medium->clock_rates\n\t\t\t\t);\n\n\t\t\t\t/* Keep track of RTP sequence numbers, in case we need to NACK them */\n\t\t\t\t/* \tNote: unsigned int overflow/underflow wraps (defined behavior) */\n\t\t\t\tif(retransmissions_disabled) {\n\t\t\t\t\t/* ... unless NACKs are disabled for this medium */\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tguint16 new_seqn = ntohs(header->seq_number);\n\t\t\t\t/* If this is video, check if this is a keyframe: if so, we empty our NACK queue */\n\t\t\t\tif(video && medium->video_is_keyframe) {\n\t\t\t\t\tif(medium->video_is_keyframe(payload, plen)) {\n\t\t\t\t\t\tif(rtcp_ctx && (int16_t)(new_seqn - rtcp_ctx->max_seq_nr) > 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Keyframe received with a highest sequence number, resetting NACK queue\\n\", handle->handle_id);\n\t\t\t\t\t\t\tjanus_seq_list_free(&medium->last_seqs[vindex]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tguint16 cur_seqn;\n\t\t\t\tint last_seqs_len = 0;\n\t\t\t\tjanus_mutex_lock(&medium->mutex);\n\t\t\t\tjanus_seq_info **last_seqs = &medium->last_seqs[vindex];\n\t\t\t\tjanus_seq_info *cur_seq = *last_seqs;\n\t\t\t\tif(cur_seq) {\n\t\t\t\t\tcur_seq = cur_seq->prev;\n\t\t\t\t\tcur_seqn = cur_seq->seq;\n\t\t\t\t} else {\n\t\t\t\t\t/* First seq, set up to add one seq */\n\t\t\t\t\tcur_seqn = new_seqn - (guint16)1; /* Can wrap */\n\t\t\t\t}\n\t\t\t\tif(!janus_seq_in_range(new_seqn, cur_seqn, LAST_SEQS_MAX_LEN) &&\n\t\t\t\t\t\t!janus_seq_in_range(cur_seqn, new_seqn, 1000)) {\n\t\t\t\t\t/* Jump too big, start fresh */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Big sequence number jump %hu -> %hu (%s stream #%d)\\n\",\n\t\t\t\t\t\thandle->handle_id, cur_seqn, new_seqn, video ? \"video\" : \"audio\", vindex);\n\t\t\t\t\tjanus_seq_list_free(last_seqs);\n\t\t\t\t\tcur_seq = NULL;\n\t\t\t\t\tcur_seqn = new_seqn - (guint16)1;\n\t\t\t\t}\n\n\t\t\t\tGSList *nacks = NULL;\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\n\t\t\t\tif(janus_seq_in_range(new_seqn, cur_seqn, LAST_SEQS_MAX_LEN)) {\n\t\t\t\t\t/* Add new seq objs forward */\n\t\t\t\t\twhile(cur_seqn != new_seqn) {\n\t\t\t\t\t\tcur_seqn += (guint16)1; /* can wrap */\n\t\t\t\t\t\tjanus_seq_info *seq_obj = g_malloc0(sizeof(janus_seq_info));\n\t\t\t\t\t\tseq_obj->seq = cur_seqn;\n\t\t\t\t\t\tseq_obj->ts = now;\n\t\t\t\t\t\tseq_obj->state = (cur_seqn == new_seqn) ? SEQ_RECVED : SEQ_MISSING;\n\t\t\t\t\t\tjanus_seq_append(last_seqs, seq_obj);\n\t\t\t\t\t\tlast_seqs_len++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(cur_seq) {\n\t\t\t\t\t/* Scan old seq objs backwards */\n\t\t\t\t\twhile(cur_seq != NULL) {\n\t\t\t\t\t\tlast_seqs_len++;\n\t\t\t\t\t\tif(cur_seq->seq == new_seqn) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Received missed sequence number %\"SCNu16\" (%s stream #%d)\\n\",\n\t\t\t\t\t\t\t\thandle->handle_id, cur_seq->seq, video ? \"video\" : \"audio\", vindex);\n\t\t\t\t\t\t\tcur_seq->state = SEQ_RECVED;\n\t\t\t\t\t\t} else if(cur_seq->state == SEQ_MISSING && now - cur_seq->ts > SEQ_MISSING_WAIT) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Missed sequence number %\"SCNu16\" (%s stream #%d), sending 1st NACK\\n\",\n\t\t\t\t\t\t\t\thandle->handle_id, cur_seq->seq, video ? \"video\" : \"audio\", vindex);\n\t\t\t\t\t\t\tnacks = g_slist_prepend(nacks, GUINT_TO_POINTER(cur_seq->seq));\n\t\t\t\t\t\t\tcur_seq->state = SEQ_NACKED;\n\t\t\t\t\t\t\tif(video && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)) {\n\t\t\t\t\t\t\t\t/* Keep track of this sequence number, we need to avoid duplicates */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Tracking NACKed packet %\"SCNu16\" (SSRC %\"SCNu32\", vindex %d)...\\n\",\n\t\t\t\t\t\t\t\t\thandle->handle_id, cur_seq->seq, packet_ssrc, vindex);\n\t\t\t\t\t\t\t\tif(medium->rtx_nacked[vindex] == NULL)\n\t\t\t\t\t\t\t\t\tmedium->rtx_nacked[vindex] = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\t\tg_hash_table_insert(medium->rtx_nacked[vindex], GUINT_TO_POINTER(cur_seq->seq), GINT_TO_POINTER(1));\n\t\t\t\t\t\t\t\t/* We don't track it forever, though: add a timed source to remove it in a few seconds */\n\t\t\t\t\t\t\t\tjanus_ice_nacked_packet *np = g_malloc(sizeof(janus_ice_nacked_packet));\n\t\t\t\t\t\t\t\tnp->medium = medium;\n\t\t\t\t\t\t\t\tnp->seq_number = cur_seq->seq;\n\t\t\t\t\t\t\t\tnp->vindex = vindex;\n\t\t\t\t\t\t\t\tif(medium->pending_nacked_cleanup == NULL)\n\t\t\t\t\t\t\t\t\tmedium->pending_nacked_cleanup = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\t\tGSource *timeout_source = g_timeout_source_new_seconds(5);\n\t\t\t\t\t\t\t\tg_source_set_callback(timeout_source, janus_ice_nacked_packet_cleanup, np, (GDestroyNotify)g_free);\n\t\t\t\t\t\t\t\tnp->source_id = g_source_attach(timeout_source, handle->mainctx);\n\t\t\t\t\t\t\t\tg_source_unref(timeout_source);\n\t\t\t\t\t\t\t\tg_hash_table_insert(medium->pending_nacked_cleanup, GUINT_TO_POINTER(np->source_id), timeout_source);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if(cur_seq->state == SEQ_NACKED  && now - cur_seq->ts > SEQ_NACKED_WAIT) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Missed sequence number %\"SCNu16\" (%s stream #%d), sending 2nd NACK\\n\",\n\t\t\t\t\t\t\t\thandle->handle_id, cur_seq->seq, video ? \"video\" : \"audio\", vindex);\n\t\t\t\t\t\t\tnacks = g_slist_prepend(nacks, GUINT_TO_POINTER(cur_seq->seq));\n\t\t\t\t\t\t\tcur_seq->state = SEQ_GIVEUP;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(cur_seq == *last_seqs) {\n\t\t\t\t\t\t\t/* Just processed head */\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcur_seq = cur_seq->prev;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twhile(last_seqs_len > LAST_SEQS_MAX_LEN) {\n\t\t\t\t\tjanus_seq_info *node = janus_seq_pop_head(last_seqs);\n\t\t\t\t\tg_free(node);\n\t\t\t\t\tlast_seqs_len--;\n\t\t\t\t}\n\n\t\t\t\tguint nacks_count = g_slist_length(nacks);\n\t\t\t\tif(nacks_count) {\n\t\t\t\t\t/* Generate a NACK and send it */\n\t\t\t\t\tJANUS_LOG(LOG_DBG, \"[%\"SCNu64\"] Now sending NACK for %u missed packets (%s stream #%d)\\n\",\n\t\t\t\t\t\thandle->handle_id, nacks_count, video ? \"video\" : \"audio\", vindex);\n\t\t\t\t\tchar nackbuf[120];\n\t\t\t\t\tint res = janus_rtcp_nacks(nackbuf, sizeof(nackbuf), nacks);\n\t\t\t\t\tif(res > 0) {\n\t\t\t\t\t\t/* Set the right local and remote SSRC in the RTCP packet */\n\t\t\t\t\t\tjanus_rtcp_fix_ssrc(NULL, nackbuf, res, 1,\n\t\t\t\t\t\t\tmedium->ssrc, medium->ssrc_peer[vindex]);\n\t\t\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = medium->mindex, .video = video, .buffer = nackbuf, .length = res };\n\t\t\t\t\t\tjanus_ice_relay_rtcp_internal(handle, medium, &rtcp, FALSE);\n\t\t\t\t\t}\n\t\t\t\t\t/* Update stats */\n\t\t\t\t\tmedium->nack_sent_recent_cnt += nacks_count;\n\t\t\t\t\tmedium->out_stats.info[vindex].nacks += nacks_count;\n\t\t\t\t}\n\t\t\t\tif(medium->nack_sent_recent_cnt &&\n\t\t\t\t\t\t(now - medium->nack_sent_log_ts) > 5*G_USEC_PER_SEC) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sent NACKs for %u missing packets (%s stream #%d)\\n\",\n\t\t\t\t\t\thandle->handle_id, medium->nack_sent_recent_cnt, video ? \"video\" : \"audio\", vindex);\n\t\t\t\t\tmedium->nack_sent_recent_cnt = 0;\n\t\t\t\t\tmedium->nack_sent_log_ts = now;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&medium->mutex);\n\t\t\t\tg_slist_free(nacks);\n\t\t\t\tnacks = NULL;\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else if(janus_is_rtcp(buf, len)) {\n\t\t/* This is RTCP */\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]  Got an RTCP packet\\n\", handle->handle_id);\n\t\tif(janus_is_webrtc_encryption_enabled() && (!pc->dtls || !pc->dtls->srtp_valid || !pc->dtls->srtp_in)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]     Missing valid SRTP session (packet arrived too early?), skipping...\\n\", handle->handle_id);\n\t\t} else {\n\t\t\tint buflen = len;\n\t\t\tsrtp_err_status_t res = janus_is_webrtc_encryption_enabled() ?\n\t\t\t\tsrtp_unprotect_rtcp(pc->dtls->srtp_in, buf, &buflen) : srtp_err_status_ok;\n\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     SRTCP unprotect error: %s (len=%d-->%d)\\n\", handle->handle_id, janus_srtp_error_str(res), len, buflen);\n\t\t\t} else {\n\t\t\t\t/* Do we need to dump this packet for debugging? */\n\t\t\t\tif(g_atomic_int_get(&handle->dump_packets))\n\t\t\t\t\tjanus_text2pcap_dump(handle->text2pcap, JANUS_TEXT2PCAP_RTCP, TRUE, buf, buflen,\n\t\t\t\t\t\t\"[session=%\"SCNu64\"][handle=%\"SCNu64\"]\", session->session_id, handle->handle_id);\n\t\t\t\t/* Check if there's an RTCP BYE: in case, let's log it */\n\t\t\t\tif(janus_rtcp_has_bye(buf, buflen)) {\n\t\t\t\t\t/* Note: we used to use this as a trigger to close the PeerConnection, but not anymore\n\t\t\t\t\t * Discussion here, https://groups.google.com/forum/#!topic/meetecho-janus/4XtfbYB7Jvc */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Got RTCP BYE on stream %u (component %u)\\n\", handle->handle_id, stream_id, component_id);\n\t\t\t\t}\n\t\t\t\t/* Is this audio or video? */\n\t\t\t\tint video = 0, vindex = 0;\n\t\t\t\t/* Bundled streams, should we check the SSRCs? */\n\t\t\t\tguint32 rtcp_ssrc = janus_rtcp_get_sender_ssrc(buf, buflen);\n\t\t\t\tjanus_ice_peerconnection_medium *medium = g_hash_table_lookup(pc->media_byssrc, GINT_TO_POINTER(rtcp_ssrc));\n\t\t\t\tif(medium == NULL) {\n\t\t\t\t\t/* We don't know the remote SSRC: this can happen for recvonly clients\n\t\t\t\t\t * (see https://groups.google.com/forum/#!topic/discuss-webrtc/5yuZjV7lkNc)\n\t\t\t\t\t * Check the local SSRC, compare it to what we have */\n\t\t\t\t\trtcp_ssrc = janus_rtcp_get_receiver_ssrc(buf, buflen);\n\t\t\t\t\tmedium = g_hash_table_lookup(pc->media_byssrc, GINT_TO_POINTER(rtcp_ssrc));\n\t\t\t\t\tif(medium == NULL) {\n\t\t\t\t\t\tif(rtcp_ssrc > 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Unknown SSRC, dropping RTCP packet (SSRC %\"SCNu32\")...\\n\",\n\t\t\t\t\t\t\t\thandle->handle_id, rtcp_ssrc);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)) {\n\t\t\t\t\tjanus_rtcp_swap_report_blocks(buf, buflen, medium->ssrc_rtx);\n\t\t\t\t}\n\t\t\t\tvideo = (medium->type == JANUS_MEDIA_VIDEO);\n\t\t\t\t/* If this is video, check if this is simulcast */\n\t\t\t\tif(video) {\n\t\t\t\t\tif(medium->ssrc_peer[1] == rtcp_ssrc) {\n\t\t\t\t\t\tvindex = 1;\n\t\t\t\t\t} else if(medium->ssrc_peer[2] == rtcp_ssrc) {\n\t\t\t\t\t\tvindex = 2;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t/* Let's process this RTCP (compound?) packet, and update the RTCP context for this stream in case */\n\t\t\t\trtcp_context *rtcp_ctx = medium->rtcp_ctx[vindex];\n\t\t\t\tuint32_t rtt = rtcp_ctx ? rtcp_ctx->rtt : 0;\n\t\t\t\tif(janus_rtcp_parse(rtcp_ctx, buf, buflen) < 0) {\n\t\t\t\t\t/* Drop the packet if the parsing function returns with an error */\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif(rtcp_ctx && rtcp_ctx->rtt != rtt) {\n\t\t\t\t\t/* Check the current RTT, to see if we need to update the size of the queue: we take\n\t\t\t\t\t * the RTT (should we include all media?) and add 100ms just to be conservative */\n\t\t\t\t\tuint32_t medium_rtt = janus_rtcp_context_get_rtt(medium->rtcp_ctx[0]);\n\t\t\t\t\tuint16_t nack_queue_ms = medium_rtt + 100;\n\t\t\t\t\tif(nack_queue_ms > DEFAULT_MAX_NACK_QUEUE)\n\t\t\t\t\t\tnack_queue_ms = DEFAULT_MAX_NACK_QUEUE;\n\t\t\t\t\telse if(nack_queue_ms < min_nack_queue)\n\t\t\t\t\t\tnack_queue_ms = min_nack_queue;\n\t\t\t\t\tuint16_t mavg = rtt ? ((7*medium->nack_queue_ms + nack_queue_ms)/8) : nack_queue_ms;\n\t\t\t\t\tif(mavg > DEFAULT_MAX_NACK_QUEUE)\n\t\t\t\t\t\tmavg = DEFAULT_MAX_NACK_QUEUE;\n\t\t\t\t\telse if(mavg < min_nack_queue)\n\t\t\t\t\t\tmavg = min_nack_queue;\n\t\t\t\t\tmedium->nack_queue_ms = mavg;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Got %s RTCP (%d bytes)\\n\", handle->handle_id, video ? \"video\" : \"audio\", buflen);\n\t\t\t\t/* See if there's any REMB bitrate to track */\n\t\t\t\tuint32_t bitrate = janus_rtcp_get_remb(buf, buflen);\n\t\t\t\tif(bitrate > 0)\n\t\t\t\t\tpc->remb_bitrate = bitrate;\n\n\t\t\t\t/* Now let's see if there are any NACKs to handle */\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\tif(pc->nacks_queue == NULL)\n\t\t\t\t\tpc->nacks_queue = g_queue_new();\n\t\t\t\tGQueue *nacks = pc->nacks_queue;\n\t\t\t\tjanus_rtcp_get_nacks(buf, buflen, nacks);\n\t\t\t\tguint nacks_count = g_queue_get_length(nacks);\n\t\t\t\tif(nacks_count && medium->do_nacks) {\n\t\t\t\t\t/* Handle NACK */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]     Just got some NACKS (%d) we should handle...\\n\", handle->handle_id, nacks_count);\n\t\t\t\t\tGHashTable *retransmit_seqs = medium->retransmit_seqs;\n\t\t\t\t\tGQueue *queue = (retransmit_seqs != NULL ? nacks : NULL);\n\t\t\t\t\tint retransmits_cnt = 0;\n\t\t\t\t\tjanus_mutex_lock(&medium->mutex);\n\t\t\t\t\twhile(queue != NULL && g_queue_get_length(queue) > 0) {\n\t\t\t\t\t\tunsigned int seqnr = GPOINTER_TO_UINT(g_queue_pop_tail(queue));\n\t\t\t\t\t\tJANUS_LOG(LOG_DBG, \"[%\"SCNu64\"]   >> %u\\n\", handle->handle_id, seqnr);\n\t\t\t\t\t\tint in_rb = 0;\n\t\t\t\t\t\t/* Check if we have the packet */\n\t\t\t\t\t\tjanus_rtp_packet *p = g_hash_table_lookup(retransmit_seqs, GUINT_TO_POINTER(seqnr));\n\t\t\t\t\t\tif(p == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]   >> >> Can't retransmit packet %u, we don't have it...\\n\", handle->handle_id, seqnr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Should we retransmit this packet? */\n\t\t\t\t\t\t\tif((p->last_retransmit > 0) && (now-p->last_retransmit < p->current_backoff)) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]   >> >> Packet %u was retransmitted just %\"SCNi64\"us ago, skipping\\n\", handle->handle_id, seqnr, now-p->last_retransmit);\n\t\t\t\t\t\t\t\tg_queue_pop_tail(queue);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tin_rb = 1;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]   >> >> Scheduling %u for retransmission due to NACK\\n\", handle->handle_id, seqnr);\n\t\t\t\t\t\t\tp->last_retransmit = now;\n\t\t\t\t\t\t\tif(p->current_backoff == 0) {\n\t\t\t\t\t\t\t\tp->current_backoff = MIN_NACK_IGNORE;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tp->current_backoff *= 2;\n\t\t\t\t\t\t\t\tif(p->current_backoff > MAX_NACK_IGNORE)\n\t\t\t\t\t\t\t\t\tp->current_backoff = MAX_NACK_IGNORE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tretransmits_cnt++;\n\t\t\t\t\t\t\t/* Enqueue it */\n\t\t\t\t\t\t\tjanus_ice_queued_packet *pkt = g_malloc(sizeof(janus_ice_queued_packet));\n\t\t\t\t\t\t\tpkt->mindex = medium->mindex;\n\t\t\t\t\t\t\tpkt->data = g_malloc(p->length+SRTP_MAX_TAG_LEN);\n\t\t\t\t\t\t\tmemcpy(pkt->data, p->data, p->length);\n\t\t\t\t\t\t\tpkt->length = p->length;\n\t\t\t\t\t\t\tpkt->type = video ? JANUS_ICE_PACKET_VIDEO : JANUS_ICE_PACKET_AUDIO;\n\t\t\t\t\t\t\tpkt->extensions = p->extensions;\n\t\t\t\t\t\t\tpkt->control = FALSE;\n\t\t\t\t\t\t\tpkt->control_ext = FALSE;\n\t\t\t\t\t\t\tpkt->retransmission = TRUE;\n\t\t\t\t\t\t\tpkt->label = NULL;\n\t\t\t\t\t\t\tpkt->protocol = NULL;\n\t\t\t\t\t\t\tpkt->added = janus_get_monotonic_time();\n\t\t\t\t\t\t\t/* What to send and how depends on whether we're doing RFC4588 or not */\n\t\t\t\t\t\t\tif(!video || !janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)) {\n\t\t\t\t\t\t\t\t/* We're not: just clarify the packet was already encrypted before */\n\t\t\t\t\t\t\t\tpkt->encrypted = TRUE;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t/* We are: overwrite the RTP header (which means we'll need a new SRTP encrypt) */\n\t\t\t\t\t\t\t\tpkt->encrypted = FALSE;\n\t\t\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)pkt->data;\n\t\t\t\t\t\t\t\theader->type = medium->rtx_payload_type;\n\t\t\t\t\t\t\t\theader->ssrc = htonl(medium->ssrc_rtx);\n\t\t\t\t\t\t\t\tmedium->rtx_seq_number++;\n\t\t\t\t\t\t\t\theader->seq_number = htons(medium->rtx_seq_number);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(handle->queued_packets != NULL) {\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\t\t\t\t\t\t\t\tg_async_queue_push_front(handle->queued_packets, pkt);\n#else\n\t\t\t\t\t\t\t\tg_async_queue_push(handle->queued_packets, pkt);\n#endif\n\t\t\t\t\t\t\t\tg_main_context_wakeup(handle->mainctx);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rtcp_ctx != NULL && in_rb) {\n\t\t\t\t\t\t\tg_atomic_int_inc(&rtcp_ctx->nack_count);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg_queue_pop_tail(queue);\n\t\t\t\t\t}\n\t\t\t\t\tmedium->retransmit_recent_cnt += retransmits_cnt;\n\t\t\t\t\t/* FIXME Remove the NACK compound packet, we've handled it */\n\t\t\t\t\tbuflen = janus_rtcp_remove_nacks(buf, buflen);\n\t\t\t\t\t/* Update stats */\n\t\t\t\t\tmedium->in_stats.info[vindex].nacks += nacks_count;\n\t\t\t\t\tjanus_mutex_unlock(&medium->mutex);\n\t\t\t\t}\n\t\t\t\tif(medium->retransmit_recent_cnt &&\n\t\t\t\t\t\tnow - medium->retransmit_log_ts > 5*G_USEC_PER_SEC) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Retransmitted %u packets due to NACK (%s stream #%d)\\n\",\n\t\t\t\t\t\thandle->handle_id, medium->retransmit_recent_cnt, video ? \"video\" : \"audio\", vindex);\n\t\t\t\t\tmedium->retransmit_recent_cnt = 0;\n\t\t\t\t\tmedium->retransmit_log_ts = now;\n\t\t\t\t}\n\n\t\t\t\t/* Fix packet data for RTCP SR and RTCP RR */\n\t\t\t\tjanus_rtp_switching_context *rtp_ctx = &medium->rtp_ctx[vindex];\n\t\t\t\tuint32_t base_ts = rtp_ctx->base_ts;\n\t\t\t\tuint32_t base_ts_prev = rtp_ctx->base_ts_prev;\n\t\t\t\tuint32_t ssrc_peer = medium->ssrc_peer_orig[vindex];\n\t\t\t\tuint32_t ssrc_local = medium->ssrc;\n\t\t\t\tuint32_t ssrc_expected = rtp_ctx->last_ssrc;\n\t\t\t\tif (janus_rtcp_fix_report_data(buf, buflen, base_ts, base_ts_prev, ssrc_peer, ssrc_local, ssrc_expected, video) < 0) {\n\t\t\t\t\t/* Drop packet in case of parsing error or SSRC different from the one expected. */\n\t\t\t\t\t/* This might happen at the very beginning of the communication or early after */\n\t\t\t\t\t/* a re-negotiation has been concluded. */\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = medium->mindex, .video = video, .buffer = buf, .length = buflen };\n\t\t\t\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\t\t\t\tif(plugin && plugin->incoming_rtcp && handle->app_handle &&\n\t\t\t\t\t\t!g_atomic_int_get(&handle->app_handle->stopped) &&\n\t\t\t\t\t\t!g_atomic_int_get(&handle->destroyed))\n\t\t\t\t\tplugin->incoming_rtcp(handle->app_handle, &rtcp);\n\t\t\t}\n\t\t}\n\t\treturn;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Not RTP and not RTCP... may these be data channels?\\n\", handle->handle_id);\n\t\tjanus_dtls_srtp_incoming_msg(pc->dtls, buf, len);\n\t\t/* Update stats (only overall data received) */\n\t\tif(len > 0) {\n\t\t\tpc->dtls_in_stats.info[0].packets++;\n\t\t\tpc->dtls_in_stats.info[0].bytes += len;\n\t\t}\n\t\treturn;\n\t}\n}\n\nvoid janus_ice_incoming_data(janus_ice_handle *handle, char *label, char *protocol, gboolean textdata, char *buffer, int length) {\n\tif(handle == NULL || buffer == NULL || length <= 0)\n\t\treturn;\n\tjanus_plugin_data data = { .label = label, .protocol = protocol, .binary = !textdata, .buffer = buffer, .length = length };\n\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\tif(plugin && plugin->incoming_data && handle->app_handle &&\n\t\t\t!g_atomic_int_get(&handle->app_handle->stopped) &&\n\t\t\t!g_atomic_int_get(&handle->destroyed))\n\t\tplugin->incoming_data(handle->app_handle, &data);\n}\n\n\n/* Helper: encoding local candidates to string/SDP */\nstatic int janus_ice_candidate_to_string(janus_ice_handle *handle, NiceCandidate *c, char *buffer, int buflen, gboolean log_candidate, gboolean force_private, guint public_ip_index) {\n\tif(!handle || !handle->agent || !c || !buffer || buflen < 1)\n\t\treturn -1;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc)\n\t\treturn -2;\n\tchar *host_ip = NULL;\n\tgboolean ipv6 = (nice_address_ip_version(&c->addr) == 6);\n\tif(nat_1_1_enabled && !force_private) {\n\t\t/* A 1:1 NAT mapping was specified, either overwrite all the host addresses with the public IP, or add new candidates */\n\t\thost_ip = janus_get_public_ip(public_ip_index);\n\t\tgboolean host_ip_v6 = (strchr(host_ip, ':') != NULL);\n\t\tif(host_ip_v6 != ipv6) {\n\t\t\t/* nat-1-1 address and candidate are not the same address family, don't do anything */\n\t\t\tbuffer[0] = '\\0';\n\t\t\treturn 0;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Public IP specified and 1:1 NAT mapping enabled (%s), using that as host address in the candidates\\n\", handle->handle_id, host_ip);\n\t}\n\t/* Encode the candidate to a string */\n\tgchar address[NICE_ADDRESS_STRING_LEN], base_address[NICE_ADDRESS_STRING_LEN];\n\tgint port = 0, base_port = 0;\n\tnice_address_to_string(&(c->addr), (gchar *)&address);\n\tport = nice_address_get_port(&(c->addr));\n\tnice_address_to_string(&(c->base_addr), (gchar *)&base_address);\n\tbase_port = nice_address_get_port(&(c->base_addr));\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Address:    %s:%d\\n\", handle->handle_id, address, port);\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Priority:   %d\\n\", handle->handle_id, c->priority);\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   Foundation: %s\\n\", handle->handle_id, c->foundation);\n\t/* Start */\n\tif(c->type == NICE_CANDIDATE_TYPE_HOST) {\n\t\t/* 'host' candidate */\n\t\tif(c->transport == NICE_CANDIDATE_TRANSPORT_UDP) {\n\t\t\tg_snprintf(buffer, buflen,\n\t\t\t\t\"%s %d %s %d %s %d typ host\",\n\t\t\t\t\tc->foundation, c->component_id,\n\t\t\t\t\t\"udp\", c->priority,\n\t\t\t\t\thost_ip ? host_ip : address, port);\n\t\t} else {\n\t\t\tif(!janus_ice_tcp_enabled) {\n\t\t\t\t/* ICE-TCP support disabled */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping host TCP candidate, ICE-TCP support disabled...\\n\", handle->handle_id);\n\t\t\t\treturn -4;\n\t\t\t}\n#ifndef HAVE_LIBNICE_TCP\n\t\t\t/* TCP candidates are only supported since libnice 0.1.8 */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping host TCP candidate, the libnice version doesn't support it...\\n\", handle->handle_id);\n\t\t\treturn -4;\n#else\n\t\t\tconst char *type = NULL;\n\t\t\tswitch(c->transport) {\n\t\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_ACTIVE:\n\t\t\t\t\ttype = \"active\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_PASSIVE:\n\t\t\t\t\ttype = \"passive\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_SO:\n\t\t\t\t\ttype = \"so\";\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(type == NULL) {\n\t\t\t\t/* FIXME Unsupported transport */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unsupported transport, skipping non-UDP/TCP host candidate...\\n\", handle->handle_id);\n\t\t\t\treturn -5;\n\t\t\t}\n\t\t\tg_snprintf(buffer, buflen,\n\t\t\t\t\"%s %d %s %d %s %d typ host tcptype %s\",\n\t\t\t\t\tc->foundation, c->component_id,\n\t\t\t\t\t\"tcp\", c->priority,\n\t\t\t\t\thost_ip ? host_ip : address, port, type);\n#endif\n\t\t}\n\t} else if(c->type == NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE ||\n\t\t\tc->type == NICE_CANDIDATE_TYPE_PEER_REFLEXIVE ||\n\t\t\tc->type == NICE_CANDIDATE_TYPE_RELAYED) {\n\t\t/* 'srflx', 'prflx', or 'relay' candidate: what is this, exactly? */\n\t\tconst char *ltype = NULL;\n\t\tswitch(c->type) {\n\t\t\tcase NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:\n\t\t\t\tltype = \"srflx\";\n\t\t\t\tbreak;\n\t\t\tcase NICE_CANDIDATE_TYPE_PEER_REFLEXIVE:\n\t\t\t\tltype = \"prflx\";\n\t\t\t\tbreak;\n\t\t\tcase NICE_CANDIDATE_TYPE_RELAYED:\n\t\t\t\tltype = \"relay\";\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\tif(ltype == NULL)\n\t\t\treturn -5;\n\t\tif(c->transport == NICE_CANDIDATE_TRANSPORT_UDP) {\n\t\t\tnice_address_to_string(&(c->base_addr), (gchar *)&base_address);\n\t\t\tgint base_port = nice_address_get_port(&(c->base_addr));\n\t\t\tg_snprintf(buffer, buflen,\n\t\t\t\t\"%s %d %s %d %s %d typ %s raddr %s rport %d\",\n\t\t\t\t\tc->foundation, c->component_id,\n\t\t\t\t\t\"udp\", c->priority,\n\t\t\t\t\taddress, port, ltype,\n\t\t\t\t\tbase_address, base_port);\n\t\t} else {\n\t\t\tif(!janus_ice_tcp_enabled) {\n\t\t\t\t/* ICE-TCP support disabled */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping srflx TCP candidate, ICE-TCP support disabled...\\n\", handle->handle_id);\n\t\t\t\treturn -4;\n\t\t\t}\n#ifndef HAVE_LIBNICE_TCP\n\t\t\t/* TCP candidates are only supported since libnice 0.1.8 */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping srflx TCP candidate, the libnice version doesn't support it...\\n\", handle->handle_id);\n\t\t\treturn -4;\n#else\n\t\t\tconst char *type = NULL;\n\t\t\tswitch(c->transport) {\n\t\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_ACTIVE:\n\t\t\t\t\ttype = \"active\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_PASSIVE:\n\t\t\t\t\ttype = \"passive\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase NICE_CANDIDATE_TRANSPORT_TCP_SO:\n\t\t\t\t\ttype = \"so\";\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(type == NULL) {\n\t\t\t\t/* FIXME Unsupported transport */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unsupported transport, skipping non-UDP/TCP srflx candidate...\\n\", handle->handle_id);\n\t\t\t\treturn -5;\n\t\t\t} else {\n\t\t\t\tg_snprintf(buffer, buflen,\n\t\t\t\t\t\"%s %d %s %d %s %d typ %s raddr %s rport %d tcptype %s\",\n\t\t\t\t\t\tc->foundation, c->component_id,\n\t\t\t\t\t\t\"tcp\", c->priority,\n\t\t\t\t\t\taddress, port, ltype,\n\t\t\t\t\t\tbase_address, base_port, type);\n\t\t\t}\n#endif\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]     %s\\n\", handle->handle_id, buffer);\n\tif(log_candidate) {\n\t\t/* Save for the summary, in case we need it */\n\t\tpc->local_candidates = g_slist_append(pc->local_candidates, g_strdup(buffer));\n\t\t/* Notify event handlers */\n\t\tif(janus_events_is_enabled()) {\n\t\t\tjanus_session *session = (janus_session *)handle->session;\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"local-candidate\", json_string(buffer));\n\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(pc->stream_id));\n\t\t\tjson_object_set_new(info, \"component_id\", json_integer(pc->component_id));\n\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_WEBRTC, JANUS_EVENT_SUBTYPE_WEBRTC_LCAND,\n\t\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t\t}\n\t}\n\treturn 0;\n}\n\nvoid janus_ice_candidates_to_sdp(janus_ice_handle *handle, janus_sdp_mline *mline, guint stream_id, guint component_id) {\n\tif(!handle || !handle->agent || !mline)\n\t\treturn;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc || pc->stream_id != stream_id) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]     No stream %d??\\n\", handle->handle_id, stream_id);\n\t\treturn;\n\t}\n\tNiceAgent *agent = handle->agent;\n\t/* Iterate on all */\n\tgchar buffer[200];\n\tGSList *candidates, *i;\n\tcandidates = nice_agent_get_local_candidates (agent, stream_id, component_id);\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] We have %d candidates for Stream #%d, Component #%d\\n\", handle->handle_id, g_slist_length(candidates), stream_id, component_id);\n\tgboolean log_candidates = (pc->local_candidates == NULL);\n\tfor(i = candidates; i; i = i->next) {\n\t\tNiceCandidate *c = (NiceCandidate *) i->data;\n\t\tgboolean ipv6 = (nice_address_ip_version(&c->addr) == 6);\n\t\tgboolean same_family = (!ipv6 && janus_has_public_ipv4_ip()) || (ipv6 && janus_has_public_ipv6_ip());\n\t\tguint public_ip_index = 0;\n\t\tdo {\n\t\t\tif(janus_ice_candidate_to_string(handle, c, buffer, sizeof(buffer), log_candidates, FALSE, public_ip_index) == 0) {\n\t\t\t\t/* Candidate encoded, add to the SDP (but only if it's not a 'prflx') */\n\t\t\t\tif(c->type == NICE_CANDIDATE_TYPE_PEER_REFLEXIVE) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping prflx candidate...\\n\", handle->handle_id);\n\t\t\t\t} else {\n\t\t\t\t\tif(strlen(buffer) > 0) {\n\t\t\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"candidate\", \"%s\", buffer);\n\t\t\t\t\t\tmline->attributes = g_list_append(mline->attributes, a);\n\t\t\t\t\t}\n\t\t\t\t\tif(nat_1_1_enabled && public_ip_index == 0 && (keep_private_host || !same_family) &&\n\t\t\t\t\t\t\tjanus_ice_candidate_to_string(handle, c, buffer, sizeof(buffer), log_candidates, TRUE, public_ip_index) == 0) {\n\t\t\t\t\t\t/* Candidate with private host encoded, add to the SDP (but only if it's not a 'prflx') */\n\t\t\t\t\t\tif(c->type == NICE_CANDIDATE_TYPE_PEER_REFLEXIVE) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Skipping prflx candidate...\\n\", handle->handle_id);\n\t\t\t\t\t\t} else if(strlen(buffer) > 0) {\n\t\t\t\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"candidate\", \"%s\", buffer);\n\t\t\t\t\t\t\tmline->attributes = g_list_append(mline->attributes, a);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpublic_ip_index++;\n\t\t\tif(!same_family) {\n\t\t\t\t/* We don't have any nat-1-1 address of the same family as this candidate, we're done */\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} while (public_ip_index < janus_get_public_ip_count());\n\t\tnice_candidate_free(c);\n\t}\n\t/* Done */\n\tg_slist_free(candidates);\n}\n\nvoid janus_ice_add_remote_candidate(janus_ice_handle *handle, NiceCandidate *c) {\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Queueing candidate %p\\n\", handle->handle_id, c);\n\tif(handle->queued_candidates != NULL)\n\t\tg_async_queue_push(handle->queued_candidates, c);\n\tif(handle->queued_packets != NULL) {\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\t\tg_async_queue_push_front(handle->queued_packets, &janus_ice_add_candidates);\n#else\n\t\tg_async_queue_push(handle->queued_packets, &janus_ice_add_candidates);\n#endif\n\t\tg_main_context_wakeup(handle->mainctx);\n\t}\n}\n\nvoid janus_ice_setup_remote_candidates(janus_ice_handle *handle, guint stream_id, guint component_id) {\n\tif(!handle || !handle->agent)\n\t\treturn;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc || pc->stream_id != stream_id) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No such stream %d: cannot setup remote candidates for component %d\\n\", handle->handle_id, stream_id, component_id);\n\t\treturn;\n\t}\n\tif(pc->process_started) {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Component %d in stream %d has already been set up\\n\", handle->handle_id, component_id, stream_id);\n\t\treturn;\n\t}\n\tif(!pc->candidates || !pc->candidates->data) {\n\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE)\n\t\t\t\t|| janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No remote candidates for component %d in stream %d: was the remote SDP parsed?\\n\", handle->handle_id, component_id, stream_id);\n\t\t}\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] ## Setting remote candidates: stream %d, component %d (%u in the list)\\n\",\n\t\thandle->handle_id, stream_id, component_id, g_slist_length(pc->candidates));\n\t/* Add all candidates */\n\tNiceCandidate *c = NULL;\n\tGSList *gsc = pc->candidates;\n\twhile(gsc) {\n\t\tc = (NiceCandidate *) gsc->data;\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Queueing candidate %p (startup)\\n\", handle->handle_id, c);\n\t\tif(handle->queued_candidates != NULL)\n\t\t\tg_async_queue_push(handle->queued_candidates, c);\n\t\tgsc = gsc->next;\n\t}\n\tif(handle->queued_packets != NULL) {\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\t\tg_async_queue_push_front(handle->queued_packets, &janus_ice_add_candidates);\n#else\n\t\tg_async_queue_push(handle->queued_packets, &janus_ice_add_candidates);\n#endif\n\t\tg_main_context_wakeup(handle->mainctx);\n\t}\n\tpc->process_started = TRUE;\n}\n\nint janus_ice_setup_local(janus_ice_handle *handle, gboolean offer, gboolean trickle, janus_dtls_role dtls_role) {\n\tif(!handle || g_atomic_int_get(&handle->destroyed))\n\t\treturn -1;\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AGENT)) {\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Agent already exists?\\n\", handle->handle_id);\n\t\treturn -2;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Setting ICE locally: got %s\\n\", handle->handle_id, offer ? \"OFFER\" : \"ANSWER\");\n\tg_atomic_int_set(&handle->closepc, 0);\n\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AGENT);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_START);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEGOTIATED);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AUDIO);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_VIDEO);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ICE_RESTART);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RESEND_TRICKLES);\n\n\t/* Note: in case this is not an OFFER, we don't know whether ICE trickling is supported on the other side or not yet */\n\tif(offer && trickle) {\n\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE);\n\t} else {\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE);\n\t}\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES);\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE_SYNCED);\n\n\t/* Note: NICE_COMPATIBILITY_RFC5245 is only available in more recent versions of libnice */\n\thandle->controlling = janus_ice_lite_enabled ? FALSE : !offer;\n\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] Creating ICE agent (ICE %s mode, %s)\\n\", handle->handle_id,\n\t\tjanus_ice_lite_enabled ? \"Lite\" : \"Full\", handle->controlling ? \"controlling\" : \"controlled\");\n\thandle->agent = g_object_new(NICE_TYPE_AGENT,\n\t\t\"compatibility\", NICE_COMPATIBILITY_RFC5245,\n\t\t\"main-context\", handle->mainctx,\n\t\t\"reliable\", FALSE,\n\t\t\"full-mode\", janus_ice_lite_enabled ? FALSE : TRUE,\n#ifdef HAVE_ICE_NOMINATION\n\t\t\"nomination-mode\", janus_ice_nomination,\n#endif\n#ifdef HAVE_CONSENT_FRESHNESS\n\t\t\"consent-freshness\", janus_ice_consent_freshness ? TRUE : FALSE,\n#endif\n\t\t\"keepalive-conncheck\", janus_ice_keepalive_connchecks ? TRUE : FALSE,\n#ifdef HAVE_LIBNICE_TCP\n\t\t\"ice-udp\", TRUE,\n\t\t\"ice-tcp\", janus_ice_tcp_enabled ? TRUE : FALSE,\n#endif\n\t\tNULL);\n\thandle->agent_created = janus_get_monotonic_time();\n\thandle->srtp_errors_count = 0;\n\thandle->last_srtp_error = 0;\n\t/* Any STUN server to use? */\n\tif(janus_stun_server != NULL && janus_stun_port > 0) {\n\t\tg_object_set(G_OBJECT(handle->agent),\n\t\t\t\"stun-server\", janus_stun_server,\n\t\t\t\"stun-server-port\", janus_stun_port,\n\t\t\tNULL);\n\t}\n\t/* Any dynamic TURN credentials to retrieve via REST API? */\n\tgboolean have_turnrest_credentials = FALSE;\n#ifdef HAVE_TURNRESTAPI\n\t/* When using the TURN REST API, we use the handle's opaque_id as a username\n\t * by default, and fall back to the session_id when it's missing. Refer to this\n\t * issue for more context: https://github.com/meetecho/janus-gateway/issues/2199 */\n\tchar turnrest_username[20];\n\tif(handle->opaque_id == NULL) {\n\t\tjanus_session *session = (janus_session *)handle->session;\n\t\tg_snprintf(turnrest_username, sizeof(turnrest_username), \"%\"SCNu64, session->session_id);\n\t}\n\tjanus_turnrest_response *turnrest_credentials = janus_turnrest_request((const char *)(handle->opaque_id ?\n\t\thandle->opaque_id : turnrest_username));\n\tif(turnrest_credentials != NULL) {\n\t\thave_turnrest_credentials = TRUE;\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Got credentials from the TURN REST API backend!\\n\", handle->handle_id);\n\t\tJANUS_LOG(LOG_HUGE, \"  -- Username: %s\\n\", turnrest_credentials->username);\n\t\tJANUS_LOG(LOG_HUGE, \"  -- Password: %s\\n\", turnrest_credentials->password);\n\t\tJANUS_LOG(LOG_HUGE, \"  -- TTL:      %\"SCNu32\"\\n\", turnrest_credentials->ttl);\n\t\tJANUS_LOG(LOG_HUGE, \"  -- Servers:  %d\\n\", g_list_length(turnrest_credentials->servers));\n\t\tGList *server = turnrest_credentials->servers;\n\t\twhile(server != NULL) {\n\t\t\tjanus_turnrest_instance *instance = (janus_turnrest_instance *)server->data;\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- -- URI: %s:%\"SCNu16\" (%d)\\n\", instance->server, instance->port, instance->transport);\n\t\t\tserver = server->next;\n\t\t}\n\t}\n#endif\n\tg_object_set(G_OBJECT(handle->agent), \"upnp\", FALSE, NULL);\n\tg_object_set(G_OBJECT(handle->agent), \"controlling-mode\", handle->controlling, NULL);\n\tg_signal_connect (G_OBJECT (handle->agent), \"candidate-gathering-done\",\n\t\tG_CALLBACK (janus_ice_cb_candidate_gathering_done), handle);\n\tg_signal_connect (G_OBJECT (handle->agent), \"component-state-changed\",\n\t\tG_CALLBACK (janus_ice_cb_component_state_changed), handle);\n#ifndef HAVE_LIBNICE_TCP\n\tg_signal_connect (G_OBJECT (handle->agent), \"new-selected-pair\",\n#else\n\tg_signal_connect (G_OBJECT (handle->agent), \"new-selected-pair-full\",\n#endif\n\t\tG_CALLBACK (janus_ice_cb_new_selected_pair), handle);\n\tif(janus_full_trickle_enabled) {\n#ifndef HAVE_LIBNICE_TCP\n\t\tg_signal_connect (G_OBJECT (handle->agent), \"new-candidate\",\n#else\n\t\tg_signal_connect (G_OBJECT (handle->agent), \"new-candidate-full\",\n#endif\n\t\t\tG_CALLBACK (janus_ice_cb_new_local_candidate), handle);\n\t}\n#ifndef HAVE_LIBNICE_TCP\n\tg_signal_connect (G_OBJECT (handle->agent), \"new-remote-candidate\",\n#else\n\tg_signal_connect (G_OBJECT (handle->agent), \"new-remote-candidate-full\",\n#endif\n\t\tG_CALLBACK (janus_ice_cb_new_remote_candidate), handle);\n\n\t/* Add all local addresses, except those in the ignore list */\n\tstruct ifaddrs *ifaddr, *ifa;\n\tint family, s, n;\n\tchar host[NI_MAXHOST];\n\tif(getifaddrs(&ifaddr) == -1) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error getting list of interfaces... %d (%s)\\n\",\n\t\t\thandle->handle_id, errno, g_strerror(errno));\n\t} else {\n\t\tfor(ifa = ifaddr, n = 0; ifa != NULL; ifa = ifa->ifa_next, n++) {\n\t\t\tif(ifa->ifa_addr == NULL)\n\t\t\t\tcontinue;\n\t\t\t/* Skip interfaces which are not up and running */\n\t\t\tif(!((ifa->ifa_flags & IFF_UP) && (ifa->ifa_flags & IFF_RUNNING)))\n\t\t\t\tcontinue;\n\t\t\t/* Skip loopback interfaces */\n\t\t\tif(ifa->ifa_flags & IFF_LOOPBACK)\n\t\t\t\tcontinue;\n\t\t\tfamily = ifa->ifa_addr->sa_family;\n\t\t\tif(family != AF_INET && family != AF_INET6)\n\t\t\t\tcontinue;\n\t\t\t/* We only add IPv6 addresses if support for them has been explicitly enabled */\n\t\t\tif(family == AF_INET6 && !janus_ipv6_enabled)\n\t\t\t\tcontinue;\n\t\t\t/* Check the interface name first, we can ignore that as well: enforce list would be checked later */\n\t\t\tif(janus_ice_enforce_list == NULL && ifa->ifa_name != NULL && janus_ice_is_ignored(ifa->ifa_name))\n\t\t\t\tcontinue;\n\t\t\ts = getnameinfo(ifa->ifa_addr,\n\t\t\t\t\t(family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6),\n\t\t\t\t\thost, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);\n\t\t\tif(s != 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] getnameinfo() failed: %s\\n\", handle->handle_id, gai_strerror(s));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* Skip 0.0.0.0, :: and, unless otherwise configured, local scoped addresses  */\n\t\t\tif(!strcmp(host, \"0.0.0.0\") || !strcmp(host, \"::\") || (!janus_ipv6_linklocal_enabled && !strncmp(host, \"fe80:\", 5)))\n\t\t\t\tcontinue;\n\t\t\t/* Check if this IP address is in the ignore/enforce list: the enforce list has the precedence but the ignore list can then discard candidates */\n\t\t\tif(janus_ice_enforce_list != NULL) {\n\t\t\t\tif(ifa->ifa_name != NULL && !janus_ice_is_enforced(ifa->ifa_name) && !janus_ice_is_enforced(host))\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(janus_ice_is_ignored(host))\n\t\t\t\tcontinue;\n\t\t\t/* Ok, add interface to the ICE agent */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Adding %s to the addresses to gather candidates for\\n\", handle->handle_id, host);\n\t\t\tNiceAddress addr_local;\n\t\t\tnice_address_init (&addr_local);\n\t\t\tif(!nice_address_set_from_string (&addr_local, host)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping invalid address %s\\n\", handle->handle_id, host);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tnice_agent_add_local_address (handle->agent, &addr_local);\n\t\t}\n\t\tfreeifaddrs(ifaddr);\n\t}\n\n\thandle->cdone = 0;\n\thandle->stream_id = 0;\n\t/* Now create a ICE stream for all the media we'll handle */\n\thandle->stream_id = nice_agent_add_stream(handle->agent, 1);\n\tif(dscp_ef > 0) {\n\t\t/* A DSCP value was configured, shift it and pass it to libnice as a TOS */\n\t\tnice_agent_set_stream_tos(handle->agent, handle->stream_id, dscp_ef << 2);\n\t}\n\t/* Create the PeerConnection object */\n\tjanus_ice_peerconnection *pc = g_malloc0(sizeof(janus_ice_peerconnection));\n\tjanus_refcount_init(&pc->ref, janus_ice_peerconnection_free);\n\tjanus_refcount_increase(&handle->ref);\n\tpc->stream_id = handle->stream_id;\n\tpc->handle = handle;\n\tpc->dtls_role = dtls_role;\n\tjanus_mutex_init(&pc->mutex);\n\tif(!have_turnrest_credentials) {\n\t\t/* No TURN REST API server and credentials, any static ones? */\n\t\tif(janus_turn_server != NULL) {\n\t\t\t/* We need relay candidates as well */\n\t\t\tgboolean ok = nice_agent_set_relay_info(handle->agent, handle->stream_id, 1,\n\t\t\t\tjanus_turn_server, janus_turn_port, janus_turn_user, janus_turn_pwd, janus_turn_type);\n\t\t\tif(!ok) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Could not set TURN server, is the address correct? (%s:%\"SCNu16\")\\n\",\n\t\t\t\t\tjanus_turn_server, janus_turn_port);\n\t\t\t}\n\t\t}\n#ifdef HAVE_TURNRESTAPI\n\t} else {\n\t\t/* We need relay candidates as well: add all those we got */\n\t\tGList *server = turnrest_credentials->servers;\n\t\twhile(server != NULL) {\n\t\t\tjanus_turnrest_instance *instance = (janus_turnrest_instance *)server->data;\n\t\t\tgboolean ok = nice_agent_set_relay_info(handle->agent, handle->stream_id, 1,\n\t\t\t\tinstance->server, instance->port,\n\t\t\t\tturnrest_credentials->username, turnrest_credentials->password,\n\t\t\t\tinstance->transport);\n\t\t\tif(!ok) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Could not set TURN server, is the address correct? (%s:%\"SCNu16\")\\n\",\n\t\t\t\t\tinstance->server, instance->port);\n\t\t\t}\n\t\t\tserver = server->next;\n\t\t}\n#endif\n\t}\n\thandle->pc = pc;\n\t/* Create the media instances we need */\n\tpc->media = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_ice_peerconnection_medium_destroy);\n\tpc->media_byssrc = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_ice_peerconnection_medium_dereference);\n\tpc->media_bymid = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_ice_peerconnection_medium_dereference);\n\tpc->media_bytype = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_ice_peerconnection_medium_dereference);\n#ifdef HAVE_PORTRANGE\n\t/* FIXME: libnice supports this since 0.1.0, but the 0.1.3 on Fedora fails with an undefined reference! */\n\tnice_agent_set_port_range(handle->agent, handle->stream_id, 1, rtp_range_min, rtp_range_max);\n#endif\n\t/* Gather now only if we're doing hanf-trickle */\n\tif(!janus_full_trickle_enabled && !nice_agent_gather_candidates(handle->agent, handle->stream_id)) {\n#ifdef HAVE_TURNRESTAPI\n\t\tif(turnrest_credentials != NULL) {\n\t\t\tjanus_turnrest_response_destroy(turnrest_credentials);\n\t\t\tturnrest_credentials = NULL;\n\t\t}\n#endif\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error gathering candidates...\\n\", handle->handle_id);\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AGENT);\n\t\tjanus_ice_webrtc_hangup(handle, \"Gathering error\");\n\t\treturn -1;\n\t}\n\tnice_agent_attach_recv(handle->agent, handle->stream_id, 1, g_main_loop_get_context(handle->mainloop),\n\t\tjanus_ice_cb_nice_recv, pc);\n#ifdef HAVE_TURNRESTAPI\n\tif(turnrest_credentials != NULL) {\n\t\tjanus_turnrest_response_destroy(turnrest_credentials);\n\t\tturnrest_credentials = NULL;\n\t}\n#endif\n\t/* Create DTLS-SRTP context, at last */\n\tpc->dtls = janus_dtls_srtp_create(pc, pc->dtls_role);\n\tif(!pc->dtls) {\n\t\t/* FIXME We should clear some resources... */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error creating DTLS-SRTP stack...\\n\", handle->handle_id);\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AGENT);\n\t\tjanus_ice_webrtc_hangup(handle, \"DTLS-SRTP stack error\");\n\t\treturn -1;\n\t}\n\tjanus_refcount_increase(&pc->dtls->ref);\n\t/* If we're doing full-tricke, start gathering asynchronously */\n\tif(janus_full_trickle_enabled) {\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\t\tg_async_queue_push_front(handle->queued_packets, &janus_ice_start_gathering);\n#else\n\t\tg_async_queue_push(handle->queued_packets, &janus_ice_start_gathering);\n#endif\n\t\tg_main_context_wakeup(handle->mainctx);\n\t}\n\treturn 0;\n}\n\nvoid janus_ice_restart(janus_ice_handle *handle) {\n\tif(!handle || !handle->agent || !handle->pc)\n\t\treturn;\n\t/* Restart ICE */\n\tif(nice_agent_restart(handle->agent) == FALSE) {\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] ICE restart failed...\\n\", handle->handle_id);\n\t}\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ICE_RESTART);\n}\n\nvoid janus_ice_resend_trickles(janus_ice_handle *handle) {\n\tif(!handle || !handle->agent)\n\t\treturn;\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RESEND_TRICKLES);\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc)\n\t\treturn;\n\tNiceAgent *agent = handle->agent;\n\t/* Iterate on all existing local candidates */\n\tgchar buffer[200];\n\tGSList *candidates, *i;\n\tcandidates = nice_agent_get_local_candidates (agent, pc->stream_id, pc->component_id);\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] We have %d candidates for Stream #%d, Component #%d\\n\",\n\t\thandle->handle_id, g_slist_length(candidates), pc->stream_id, pc->component_id);\n\tfor(i = candidates; i; i = i->next) {\n\t\tNiceCandidate *c = (NiceCandidate *) i->data;\n\t\tif(c->type == NICE_CANDIDATE_TYPE_PEER_REFLEXIVE) {\n\t\t\tnice_candidate_free(c);\n\t\t\tcontinue;\n\t\t}\n\n\t\tguint public_ip_index = 0;\n\t\tdo {\n\t\t\tif(janus_ice_candidate_to_string(handle, c, buffer, sizeof(buffer), FALSE, FALSE, public_ip_index) == 0) {\n\t\t\t\t/* Candidate encoded, send a \"trickle\" event to the browser */\n\t\t\t\tjanus_ice_notify_trickle(handle, buffer);\n\t\t\t\t/* If nat-1-1 is enabled but we want to keep the private host, add another candidate */\n\t\t\t\tif(nat_1_1_enabled && keep_private_host && public_ip_index == 0 &&\n\t\t\t\t\t\tjanus_ice_candidate_to_string(handle, c, buffer, sizeof(buffer), FALSE, TRUE, public_ip_index) == 0) {\n\t\t\t\t\t/* Candidate encoded, send a \"trickle\" event to the browser */\n\t\t\t\t\tjanus_ice_notify_trickle(handle, buffer);\n\t\t\t\t}\n\t\t\t}\n\t\t\tpublic_ip_index++;\n\t\t} while (public_ip_index < janus_get_public_ip_count());\n\t\tnice_candidate_free(c);\n\t}\n\t/* Send a \"completed\" trickle at the end */\n\tjanus_ice_notify_trickle(handle, NULL);\n}\n\nstatic void janus_ice_rtp_extension_update(janus_ice_handle *handle, janus_ice_peerconnection_medium *medium, janus_ice_queued_packet *packet) {\n\tif(handle == NULL || handle->pc == NULL || medium == NULL || packet == NULL || packet->data == NULL)\n\t\treturn;\n\tuint16_t totlen = RTP_HEADER_SIZE;\n\t/* Check how large the payload is */\n\tint plen = 0;\n\tchar *payload = janus_rtp_payload(packet->data, packet->length, &plen);\n\tif(payload != NULL)\n\t\ttotlen += plen;\n\t/* We need to strip extensions, here, and add those that need to be there manually */\n\tuint16_t extlen = 0;\n\tchar extensions[320];\n\tuint16_t extbufsize = sizeof(extensions);\n\tjanus_rtp_header *header = (janus_rtp_header *)packet->data;\n\theader->extension = 0;\n\t/* Add core and plugin extensions, if any */\n\tgboolean video = (packet->type == JANUS_ICE_PACKET_VIDEO);\n\tif(handle->pc->mid_ext_id > 0 || (video && handle->pc->abs_send_time_ext_id > 0) ||\n\t\t\t(video && handle->pc->transport_wide_cc_ext_id > 0) ||\n\t\t\t(!video && packet->extensions.audio_level > -1 && handle->pc->audiolevel_ext_id > 0) ||\n\t\t\t(video && packet->extensions.video_rotation > -1 && handle->pc->videoorientation_ext_id > 0) ||\n\t\t\t(video && packet->extensions.min_delay > -1 && packet->extensions.max_delay > -1 && handle->pc->playoutdelay_ext_id > 0) ||\n\t\t\t(video && packet->extensions.dd_len > 0 && handle->pc->dependencydesc_ext_id > 0) ||\n\t\t\t(packet->extensions.abs_capture_ts > 0 && handle->pc->abs_capture_time_ext_id > 0)) {\n\t\t/* Do we need 2-byte extemsions, or are 1-byte extensions fine? */\n\t\tgboolean use_2byte = (video && packet->extensions.dd_len > 16 && handle->pc->dependencydesc_ext_id > 0);\n\t\t/* Write the extension(s) */\n\t\theader->extension = 1;\n\t\tmemset(extensions, 0, sizeof(extensions));\n\t\tjanus_rtp_header_extension *extheader = (janus_rtp_header_extension *)extensions;\n\t\textheader->type = htons(use_2byte ? 0x1000 : 0xBEDE);\n\t\textheader->length = 0;\n\t\t/* Iterate on all extensions we need */\n\t\tchar *index = extensions + 4;\n\t\textbufsize -= 4;\n\t\t/* Check if we need to add the abs-send-time extension */\n\t\tif(video && handle->pc->abs_send_time_ext_id > 0) {\n\t\t\tint64_t now = (((janus_get_monotonic_time()/1000) << 18) + 500) / 1000;\n\t\t\tuint32_t abs_ts = (uint32_t)now & 0x00FFFFFF;\n\t\t\tuint32_t abs24 = htonl(abs_ts) >> 8;\n\t\t\tif(!use_2byte) {\n\t\t\t\t*index = (handle->pc->abs_send_time_ext_id << 4) + 2;\n\t\t\t\tmemcpy(index+1, &abs24, 3);\n\t\t\t\tindex += 4;\n\t\t\t\textlen += 4;\n\t\t\t\textbufsize -= 4;\n\t\t\t} else {\n\t\t\t\t*index = handle->pc->abs_send_time_ext_id;\n\t\t\t\t*(index+1) = 3;\n\t\t\t\tmemcpy(index+2, &abs24, 3);\n\t\t\t\tindex += 5;\n\t\t\t\textlen += 5;\n\t\t\t\textbufsize -= 5;\n\t\t\t}\n\t\t}\n\t\t/* Check if we need to add the transport-wide CC extension */\n\t\tif(video && handle->pc->transport_wide_cc_ext_id > 0) {\n\t\t\thandle->pc->transport_wide_cc_out_seq_num++;\n\t\t\tuint16_t transSeqNum = htons(handle->pc->transport_wide_cc_out_seq_num);\n\t\t\tif(!use_2byte) {\n\t\t\t\t*index = (handle->pc->transport_wide_cc_ext_id << 4) + 1;\n\t\t\t\tmemcpy(index+1, &transSeqNum, 2);\n\t\t\t\tindex += 3;\n\t\t\t\textlen += 3;\n\t\t\t\textbufsize -= 3;\n\t\t\t} else {\n\t\t\t\t*index = handle->pc->transport_wide_cc_ext_id;\n\t\t\t\t*(index+1) = 2;\n\t\t\t\tmemcpy(index+2, &transSeqNum, 2);\n\t\t\t\tindex += 4;\n\t\t\t\textlen += 4;\n\t\t\t\textbufsize -= 4;\n\t\t\t}\n\t\t}\n\t\t/* Check if the plugin (or source) included other extensions */\n\t\tif(!video && packet->extensions.audio_level > -1 && handle->pc->audiolevel_ext_id > 0) {\n\t\t\t/* Add audio-level extension */\n\t\t\tif(!use_2byte) {\n\t\t\t\t*index = (handle->pc->audiolevel_ext_id << 4);\n\t\t\t\t*(index+1) = (packet->extensions.audio_level_vad << 7) + (packet->extensions.audio_level & 0x7F);\n\t\t\t\tindex += 2;\n\t\t\t\textlen += 2;\n\t\t\t\textbufsize -= 2;\n\t\t\t} else {\n\t\t\t\t*index = handle->pc->audiolevel_ext_id;\n\t\t\t\t*(index+1) = 1;\n\t\t\t\t*(index+2) = (packet->extensions.audio_level_vad << 7) + (packet->extensions.audio_level & 0x7F);\n\t\t\t\tindex += 3;\n\t\t\t\textlen += 3;\n\t\t\t\textbufsize -= 3;\n\t\t\t}\n\t\t}\n\t\tif(video && packet->extensions.video_rotation > -1 && handle->pc->videoorientation_ext_id > 0) {\n\t\t\t/* Add video-orientation extension */\n\t\t\tgboolean c = (packet->extensions.video_back_camera == TRUE),\n\t\t\t\tf = (packet->extensions.video_flipped == TRUE), r1 = FALSE, r0 = FALSE;\n\t\t\tswitch(packet->extensions.video_rotation) {\n\t\t\t\tcase 270:\n\t\t\t\t\tr1 = TRUE;\n\t\t\t\t\tr0 = TRUE;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 180:\n\t\t\t\t\tr1 = TRUE;\n\t\t\t\t\tr0 = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 90:\n\t\t\t\t\tr1 = FALSE;\n\t\t\t\t\tr0 = TRUE;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 0:\n\t\t\t\tdefault:\n\t\t\t\t\tr1 = FALSE;\n\t\t\t\t\tr0 = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(!use_2byte) {\n\t\t\t\t*index = (handle->pc->videoorientation_ext_id << 4);\n\t\t\t\t*(index+1) = (c<<3) + (f<<2) + (r1<<1) + r0;\n\t\t\t\tindex += 2;\n\t\t\t\textlen += 2;\n\t\t\t\textbufsize -= 2;\n\t\t\t} else {\n\t\t\t\t*index = handle->pc->videoorientation_ext_id;\n\t\t\t\t*(index+1) = 1;\n\t\t\t\t*(index+2) = (c<<3) + (f<<2) + (r1<<1) + r0;\n\t\t\t\tindex += 3;\n\t\t\t\textlen += 3;\n\t\t\t\textbufsize -= 3;\n\t\t\t}\n\t\t}\n\t\tif(video && packet->extensions.min_delay > -1 && packet->extensions.max_delay > -1 && handle->pc->playoutdelay_ext_id > 0) {\n\t\t\t/* Add playout-delay extension */\n\t\t\tuint32_t min_delay = (uint32_t)packet->extensions.min_delay;\n\t\t\tuint32_t max_delay = (uint32_t)packet->extensions.max_delay;\n\t\t\tuint32_t pd = ((min_delay << 12) & 0x00FFF000) + (max_delay & 0x00000FFF);\n\t\t\tuint32_t pd24 = htonl(pd) >> 8;\n\t\t\tif(!use_2byte) {\n\t\t\t\t*index = (handle->pc->playoutdelay_ext_id << 4) + 2;\n\t\t\t\tmemcpy(index+1, &pd24, 3);\n\t\t\t\tindex += 4;\n\t\t\t\textlen += 4;\n\t\t\t\textbufsize -= 4;\n\t\t\t} else {\n\t\t\t\t*index = handle->pc->playoutdelay_ext_id;\n\t\t\t\t*(index+1) = 3;\n\t\t\t\tmemcpy(index+2, &pd24, 3);\n\t\t\t\tindex += 5;\n\t\t\t\textlen += 5;\n\t\t\t\textbufsize -= 5;\n\t\t\t}\n\t\t}\n\t\t/* Check if we need to add the mid extension */\n\t\tif(handle->pc->mid_ext_id > 0) {\n\t\t\tchar *mid = medium->mid;\n\t\t\tif(mid != NULL) {\n\t\t\t\tif(!use_2byte) {\n\t\t\t\t\tsize_t midlen = strlen(mid) & 0x0F;\n\t\t\t\t\tif(extbufsize < (midlen + 1)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Not enough room for mid extension, skipping it...\\n\", handle->handle_id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t*index = (handle->pc->mid_ext_id << 4) + (midlen ? midlen-1 : 0);\n\t\t\t\t\t\tmemcpy(index+1, mid, midlen);\n\t\t\t\t\t\tindex += (midlen + 1);\n\t\t\t\t\t\textlen += (midlen + 1);\n\t\t\t\t\t\textbufsize -= (midlen + 1);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsize_t midlen = strlen(mid);\n\t\t\t\t\tif(midlen > 16) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] mid too large, capping to first 16 characters...\\n\", handle->handle_id);\n\t\t\t\t\t\tmidlen = 16;\n\t\t\t\t\t} else if(extbufsize < (midlen + 2)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Not enough room for mid extension, skipping it...\\n\", handle->handle_id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t*index = handle->pc->mid_ext_id;\n\t\t\t\t\t\t*(index+1) = midlen;\n\t\t\t\t\t\tmemcpy(index+2, mid, midlen);\n\t\t\t\t\t\tindex += (midlen + 2);\n\t\t\t\t\t\textlen += (midlen + 2);\n\t\t\t\t\t\textbufsize -= (midlen + 2);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(video && packet->extensions.dd_len > 0 && handle->pc->dependencydesc_ext_id > 0) {\n\t\t\t/* Add dependency descriptor extension */\n\t\t\tif(extbufsize < (packet->extensions.dd_len + (use_2byte ? 2 : 1))) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Not enough room for dependency-descriptor extension, skipping it...\\n\", handle->handle_id);\n\t\t\t} else {\n\t\t\t\tif(!use_2byte) {\n\t\t\t\t\t*index = (handle->pc->dependencydesc_ext_id << 4) + (packet->extensions.dd_len-1);\n\t\t\t\t\tindex++;\n\t\t\t\t\tmemcpy(index, packet->extensions.dd_content, packet->extensions.dd_len);\n\t\t\t\t\tindex += packet->extensions.dd_len;\n\t\t\t\t\textlen += packet->extensions.dd_len + 1;\n\t\t\t\t\textbufsize -= packet->extensions.dd_len + 1;\n\t\t\t\t} else {\n\t\t\t\t\t*index = handle->pc->dependencydesc_ext_id;\n\t\t\t\t\t*(index+1) = packet->extensions.dd_len;\n\t\t\t\t\tmemcpy(index+2, packet->extensions.dd_content, packet->extensions.dd_len);\n\t\t\t\t\tindex += packet->extensions.dd_len + 2;\n\t\t\t\t\textlen += packet->extensions.dd_len + 2;\n\t\t\t\t\textbufsize -= packet->extensions.dd_len + 2;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Check if we need to add the abs-capture-time extension */\n\t\tif(packet->extensions.abs_capture_ts > 0 && handle->pc->abs_capture_time_ext_id > 0) {\n\t\t\tuint64_t abs64 = htonll(packet->extensions.abs_capture_ts);\n\t\t\tif(!use_2byte) {\n\t\t\t\t*index = (handle->pc->abs_capture_time_ext_id << 4) + 7;\n\t\t\t\tmemcpy(index+1, &abs64, 8);\n\t\t\t\tindex += 9;\n\t\t\t\textlen += 9;\n\t\t\t\textbufsize -= 9;\n\t\t\t} else {\n\t\t\t\t*index = handle->pc->abs_capture_time_ext_id;\n\t\t\t\t*(index+1) = 8;\n\t\t\t\tmemcpy(index+2, &abs64, 8);\n\t\t\t\tindex += 10;\n\t\t\t\textlen += 10;\n\t\t\t\textbufsize -= 10;\n\t\t\t}\n\t\t}\n\t\t/* Calculate the whole length */\n\t\tuint16_t words = extlen/4;\n\t\tif(extlen%4 != 0)\n\t\t\twords++;\n\t\textheader->length = htons(words);\n\t\t/* Update lengths (taking into account the RFC5285 header) */\n\t\textlen = 4 + (words*4);\n\t\ttotlen += extlen;\n\t}\n\t/* Check if we need to resize this packet buffer first */\n\tuint16_t payload_start = payload ? (payload - packet->data) : 0;\n\tif(packet->length < totlen)\n\t\tpacket->data = g_realloc(packet->data, totlen + SRTP_MAX_TAG_LEN);\n\t/* Now check if we need to move the payload */\n\tpayload = payload_start ? (packet->data + payload_start) : NULL;\n\tif(payload != NULL && plen > 0 && packet->length != totlen)\n\t\tmemmove(packet->data + RTP_HEADER_SIZE + extlen, payload, plen);\n\t/* Finally, copy RTP extensions, if any */\n\tif(extlen > 0) {\n\t\t/* Copy the extensions after the RTP header */\n\t\tmemcpy(packet->data + RTP_HEADER_SIZE, extensions, extlen);\n\t}\n\tpacket->length = totlen;\n}\n\nstatic gint rtcp_transport_wide_cc_stats_comparator(gconstpointer item1, gconstpointer item2) {\n\treturn ((rtcp_transport_wide_cc_stats*)item1)->transport_seq_num - ((rtcp_transport_wide_cc_stats*)item2)->transport_seq_num;\n}\nstatic gboolean janus_ice_outgoing_transport_wide_cc_feedback(gpointer user_data) {\n\tjanus_ice_handle *handle = (janus_ice_handle *)user_data;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\n\tguint32 ssrc_peer = 0;\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tif(pc) {\n\t\t/* Find inbound video medium */\n\t\tjanus_mutex_lock(&handle->mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, pc->media_bymid);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_ice_peerconnection_medium *m = value;\n\t\t\tif(m && m->type == JANUS_MEDIA_VIDEO && m->recv) {\n\t\t\t\t/* If a medium (or simulcast layer, if applicable) has not received data, its SSRC may be unknown. */\n\t\t\t\t/* Pick the first valid SSRC we find across all considered mediums */\n\t\t\t\tint i = 0;\n\t\t\t\tfor(i = 0; i < 3; i++) {\n\t\t\t\t\tif(m->ssrc_peer[i] != 0) {\n\t\t\t\t\t\tssrc_peer = m->ssrc_peer[i];\n\t\t\t\t\t\tmedium = m;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t/* Stop if we found a valid SSRC/medium to use */\n\t\t\t\tif(medium && ssrc_peer)\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&handle->mutex);\n\t}\n\n\tif(medium == NULL) {\n\t\tJANUS_LOG(LOG_HUGE, \"No medium with a valid peer SSRC found for transport-wide CC feedback\\n\");\n\t\treturn G_SOURCE_CONTINUE;\n\t}\n\n\tif(pc && pc->do_transport_wide_cc) {\n\t\t/* Create a transport wide feedback message */\n\t\tsize_t size = 1300;\n\t\tchar rtcpbuf[1300];\n\t\t/* Order packet list */\n\t\tpc->transport_wide_received_seq_nums = g_slist_sort(pc->transport_wide_received_seq_nums,\n\t\t\trtcp_transport_wide_cc_stats_comparator);\n\t\t/* Create full stats queue */\n\t\tGQueue *packets = g_queue_new();\n\t\t/* For all packets */\n\t\tGSList *it = NULL;\n\t\tfor(it = pc->transport_wide_received_seq_nums; it; it = it->next) {\n\t\t\t/* Get stat */\n\t\t\tjanus_rtcp_transport_wide_cc_stats *stats = (janus_rtcp_transport_wide_cc_stats *)it->data;\n\t\t\t/* Get transport seq */\n\t\t\tguint32 transport_seq_num = stats->transport_seq_num;\n\t\t\t/* Check if it is an out of order  */\n\t\t\tif(transport_seq_num < pc->transport_wide_cc_last_feedback_seq_num) {\n\t\t\t\t/* Skip, it was already reported as lost */\n\t\t\t\tg_free(stats);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* If not first */\n\t\t\tif(pc->transport_wide_cc_last_feedback_seq_num) {\n\t\t\t\t/* For each lost */\n\t\t\t\tguint32 i = 0;\n\t\t\t\tfor(i = pc->transport_wide_cc_last_feedback_seq_num+1; i<transport_seq_num; ++i) {\n\t\t\t\t\t/* Create new stat */\n\t\t\t\t\tjanus_rtcp_transport_wide_cc_stats *missing = g_malloc(sizeof(janus_rtcp_transport_wide_cc_stats));\n\t\t\t\t\t/* Add missing packet */\n\t\t\t\t\tmissing->transport_seq_num = i;\n\t\t\t\t\tmissing->timestamp = 0;\n\t\t\t\t\t/* Add it */\n\t\t\t\t\tg_queue_push_tail(packets, missing);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Store last */\n\t\t\tpc->transport_wide_cc_last_feedback_seq_num = transport_seq_num;\n\t\t\t/* Add this one */\n\t\t\tg_queue_push_tail(packets, stats);\n\t\t}\n\t\t/* Free and reset stats list */\n\t\tg_slist_free(pc->transport_wide_received_seq_nums);\n\t\tpc->transport_wide_received_seq_nums = NULL;\n\t\t/* Create and enqueue RTCP packets */\n\t\tguint packets_len = 0;\n\t\twhile((packets_len = g_queue_get_length(packets)) > 0) {\n\t\t\tGQueue *packets_to_process;\n\t\t\t/* If we have more than 400 packets to acknowledge, let's send more than one message */\n\t\t\tif(packets_len > 400) {\n\t\t\t\t/* Split the queue into two */\n\t\t\t\tGList *new_head = g_queue_peek_nth_link(packets, 400);\n\t\t\t\tGList *new_tail = new_head->prev;\n\t\t\t\tnew_head->prev = NULL;\n\t\t\t\tnew_tail->next = NULL;\n\t\t\t\tpackets_to_process = g_queue_new();\n\t\t\t\tpackets_to_process->head = packets->head;\n\t\t\t\tpackets_to_process->tail = new_tail;\n\t\t\t\tpackets_to_process->length = 400;\n\t\t\t\tpackets->head = new_head;\n\t\t\t\t/* packets->tail is unchanged */\n\t\t\t\tpackets->length = packets_len - 400;\n\t\t\t} else {\n\t\t\t\tpackets_to_process = packets;\n\t\t\t}\n\t\t\t/* Get feedback packet count and increase it for next one */\n\t\t\tguint8 feedback_packet_count = pc->transport_wide_cc_feedback_count++;\n\t\t\t/* Create RTCP packet */\n\t\t\tint len = janus_rtcp_transport_wide_cc_feedback(rtcpbuf, size,\n\t\t\t\tmedium->ssrc, ssrc_peer, feedback_packet_count, packets_to_process);\n\t\t\t/* Enqueue it, we'll send it later */\n\t\t\tif(len > 0) {\n\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = medium->mindex, .video = TRUE, .buffer = rtcpbuf, .length = len };\n\t\t\t\tjanus_ice_relay_rtcp_internal(handle, medium, &rtcp, FALSE);\n\t\t\t}\n\t\t\tif(packets_to_process != packets) {\n\t\t\t\tg_queue_free(packets_to_process);\n\t\t\t}\n\t\t}\n\t\t/* Free mem */\n\t\tg_queue_free(packets);\n\t}\n\treturn G_SOURCE_CONTINUE;\n}\n\nstatic inline void janus_ice_send_compound_rtcp(janus_ice_handle *handle,\n\t\tjanus_ice_peerconnection_medium *medium, char *rtcpbuf, int rtcpbuf_size, int *offset) {\n\t/* Enqueue the RTCP message(s) */\n\tjanus_plugin_rtcp rtcp = { .mindex = medium->mindex,\n\t\t.video = (medium->type == JANUS_MEDIA_VIDEO), .buffer = rtcpbuf, .length = *offset };\n\tjanus_ice_relay_rtcp_internal(handle, medium, &rtcp, FALSE);\n\t/* Reset the buffer */\n\tmemset(rtcpbuf, 0, rtcpbuf_size);\n\t*offset = 0;\n}\n\nstatic gboolean janus_ice_outgoing_rtcp_handle(gpointer user_data) {\n\tjanus_ice_handle *handle = (janus_ice_handle *)user_data;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\t/* Iterate on all media */\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tuint mi=0;\n\n\tint offset = 0;\n\tint srlen = 28;\n\tint sdeslen = 16;\n\tint rrlen = 32;\n\tint rtcpbuf_size = 1200;\n\tchar rtcpbuf[rtcpbuf_size];\n\tmemset(rtcpbuf, 0, rtcpbuf_size);\n\n\tstruct timeval tv;\n\tgettimeofday(&tv, NULL);\n\tuint32_t s = tv.tv_sec + 2208988800u;\n\tuint32_t u = tv.tv_usec;\n\tuint32_t f = (u << 12) + (u << 8) - ((u * 3650) >> 6);\n\tint64_t ntp = ((int64_t)tv.tv_sec)*G_USEC_PER_SEC + tv.tv_usec;\n\n\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\tif(!medium || (medium->type != JANUS_MEDIA_AUDIO && medium->type != JANUS_MEDIA_VIDEO))\n\t\t\tcontinue;\n\n\t\tuint32_t ssrc = htonl(medium->ssrc);\n\t\tif(medium->out_stats.info[0].packets > 0) {\n\t\t\tif((offset + srlen + sdeslen) >= rtcpbuf_size) {\n\t\t\t\t/* Our RTCP buffer is either full, or full enough: send the\n\t\t\t\t * messages we prepared so far first, and reset the buffer */\n\t\t\t\tjanus_ice_send_compound_rtcp(handle, medium, rtcpbuf, rtcpbuf_size, &offset);\n\t\t\t}\n\t\t\t/* Create a SR/SDES compound */\n\t\t\trtcp_sr *sr = (rtcp_sr *) &rtcpbuf[offset];\n\t\t\tsr->header.version = 2;\n\t\t\tsr->header.type = RTCP_SR;\n\t\t\tsr->header.rc = 0;\n\t\t\tsr->header.length = htons((srlen/4)-1);\n\t\t\tsr->ssrc = ssrc;\n\t\t\tsr->si.ntp_ts_msw = htonl(s);\n\t\t\tsr->si.ntp_ts_lsw = htonl(f);\n\t\t\t/* Compute an RTP timestamp coherent with the NTP one */\n\t\t\trtcp_context *rtcp_ctx = medium->rtcp_ctx[0];\n\t\t\tif(rtcp_ctx == NULL) {\n\t\t\t\tsr->si.rtp_ts = htonl(medium->last_rtp_ts);\t/* FIXME */\n\t\t\t} else {\n\t\t\t\tuint32_t rtp_ts = ((ntp-medium->last_ntp_ts)*(rtcp_ctx->tb))/1000000 + medium->last_rtp_ts;\n\t\t\t\tsr->si.rtp_ts = htonl(rtp_ts);\n\t\t\t}\n\t\t\tsr->si.s_packets = htonl(medium->out_stats.info[0].packets);\n\t\t\tsr->si.s_octets = htonl(medium->out_stats.info[0].bytes);\n\t\t\trtcp_sdes *sdes = (rtcp_sdes *)&rtcpbuf[offset + srlen];\n\t\t\tjanus_rtcp_sdes_cname((char *)sdes, sdeslen, \"janus\", 5);\n\t\t\tsdes->chunk.ssrc = ssrc;\n\n\t\t\t/* Check if we detected too many losses, and send a slowlink event in case */\n\t\t\tgint lost = janus_rtcp_context_get_lost_all(rtcp_ctx, TRUE);\n\t\t\tlost = lost > 0 ? lost : 0;\n\t\t\tjanus_slow_link_update(medium, handle, TRUE, lost);\n\n\t\t\toffset += srlen + sdeslen;\n\t\t}\n\t\tif(medium->recv) {\n\t\t\t/* Create a RR too (for each SSRC, if we're simulcasting) */\n\t\t\tint vindex=0;\n\t\t\tfor(vindex=0; vindex<3; vindex++) {\n\t\t\t\tif(medium->rtcp_ctx[vindex] && medium->rtcp_ctx[vindex]->rtp_recvd) {\n\t\t\t\t\tif((offset + rrlen) >= rtcpbuf_size) {\n\t\t\t\t\t\t/* Our RTCP buffer is either full, or full enough: send the\n\t\t\t\t\t\t * messages we prepared so far first, and reset the buffer */\n\t\t\t\t\t\tjanus_ice_send_compound_rtcp(handle, medium, rtcpbuf, rtcpbuf_size, &offset);\n\t\t\t\t\t}\n\t\t\t\t\t/* Create a RR */\n\t\t\t\t\trtcp_rr *rr = (rtcp_rr *) &rtcpbuf[offset];\n\t\t\t\t\trr->header.version = 2;\n\t\t\t\t\trr->header.type = RTCP_RR;\n\t\t\t\t\trr->header.rc = 1;\n\t\t\t\t\trr->header.length = htons((rrlen/4)-1);\n\t\t\t\t\trr->ssrc = ssrc;\n\t\t\t\t\tjanus_rtcp_report_block(medium->rtcp_ctx[vindex], &rr->rb[0]);\n\t\t\t\t\trr->rb[0].ssrc = htonl(medium->ssrc_peer[vindex]);\n\n\t\t\t\t\tif(vindex == 0) {\n\t\t\t\t\t\t/* Check if we detected too many losses, and send a slowlink event in case */\n\t\t\t\t\t\tgint lost = janus_rtcp_context_get_lost_all(medium->rtcp_ctx[vindex], FALSE);\n\t\t\t\t\t\tlost = lost > 0 ? lost : 0;\n\t\t\t\t\t\tjanus_slow_link_update(medium, handle, FALSE, lost);\n\t\t\t\t\t}\n\n\t\t\t\t\toffset += rrlen;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif(offset > 0) {\n\t\t/* We've got a batch of RTCP messages to send that we didn't\n\t\t * send as of yet: enqueue the buffer, we'll send it later */\n\t\tjanus_plugin_rtcp rtcp = { .mindex = medium->mindex,\n\t\t\t.video = (medium->type == JANUS_MEDIA_VIDEO), .buffer = rtcpbuf, .length = offset };\n\t\tjanus_ice_relay_rtcp_internal(handle, medium, &rtcp, FALSE);\n\t}\n\n\tif(twcc_period == 1000) {\n\t\t/* The Transport Wide CC feedback period is 1s as well, send it here */\n\t\tjanus_ice_outgoing_transport_wide_cc_feedback(handle);\n\t}\n\treturn G_SOURCE_CONTINUE;\n}\n\nstatic gboolean janus_ice_outgoing_stats_handle(gpointer user_data) {\n\tjanus_ice_handle *handle = (janus_ice_handle *)user_data;\n\t/* This callback is for stats and other things we need to do on a regular basis (typically called once per second) */\n\tjanus_session *session = (janus_session *)handle->session;\n\tgint64 now = janus_get_monotonic_time();\n\t/* Reset the last second counters if too much time passed with no data in or out */\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(pc == NULL)\n\t\treturn G_SOURCE_CONTINUE;\n\t/* Iterate on all media */\n\thandle->last_event_stats++;\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tjson_t *combined_event = NULL;\n\tuint mi=0;\n\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\tif(!medium)\n\t\t\tcontinue;\n\t\tint vindex = 0;\n\t\tfor(vindex=0; vindex < 3; vindex++) {\n\t\t\tif(vindex > 0 && (medium->type != JANUS_MEDIA_VIDEO || medium->rtcp_ctx[1] == NULL))\n\t\t\t\tcontinue;\t/* We won't need simulcast checks */\n\t\t\tgint64 last = medium->in_stats.info[vindex].updated;\n\t\t\tif(last && now > last && now-last >= 2*G_USEC_PER_SEC && medium->in_stats.info[vindex].bytes_lastsec_temp > 0) {\n\t\t\t\tmedium->in_stats.info[vindex].bytes_lastsec = 0;\n\t\t\t\tmedium->in_stats.info[vindex].bytes_lastsec_temp = 0;\n\t\t\t}\n\t\t\tlast = medium->out_stats.info[vindex].updated;\n\t\t\tif(last && now > last && now-last >= 2*G_USEC_PER_SEC && medium->out_stats.info[vindex].bytes_lastsec_temp > 0) {\n\t\t\t\tmedium->out_stats.info[vindex].bytes_lastsec = 0;\n\t\t\t\tmedium->out_stats.info[vindex].bytes_lastsec_temp = 0;\n\t\t\t}\n\t\t\tif(medium->type != JANUS_MEDIA_AUDIO && medium->type != JANUS_MEDIA_VIDEO)\n\t\t\t\tcontinue;\n\t\t\t/* Now let's see if we need to notify the user about no incoming audio or video */\n\t\t\tif(no_media_timer > 0 && pc->dtls && pc->dtls->dtls_connected > 0 && (now - pc->dtls->dtls_connected >= G_USEC_PER_SEC)) {\n\t\t\t\tgint64 last = medium->in_stats.info[vindex].updated;\n\t\t\t\tif(!medium->in_stats.info[vindex].notified_lastsec && last &&\n\t\t\t\t\t\t!medium->in_stats.info[vindex].bytes_lastsec && !medium->in_stats.info[vindex].bytes_lastsec_temp &&\n\t\t\t\t\t\t\tnow - last >= (gint64)no_media_timer*G_USEC_PER_SEC) {\n\t\t\t\t\t/* We missed more than no_second_timer seconds of video! */\n\t\t\t\t\tmedium->in_stats.info[vindex].notified_lastsec = TRUE;\n\t\t\t\t\tif(vindex == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Didn't receive %s for more than %u second(s)...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, medium->type == JANUS_MEDIA_VIDEO ? \"video\" : \"audio\", no_media_timer);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Didn't receive %s (substream #%d) for more than %u second(s)...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, medium->type == JANUS_MEDIA_VIDEO ? \"video\" : \"audio\", vindex, no_media_timer);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_ice_notify_media(handle, medium->mid, medium->type == JANUS_MEDIA_VIDEO, medium->rtcp_ctx[1] != NULL, vindex, FALSE);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* We also send live stats to event handlers every tot-seconds (configurable) */\n\t\tif(janus_ice_event_stats_period > 0 && handle->last_event_stats >= janus_ice_event_stats_period) {\n\t\t\tif(janus_events_is_enabled()) {\n\t\t\t\t/* Check if we should send dedicated events per media, or one per peerConnection */\n\t\t\t\tif(janus_events_is_enabled() && janus_ice_event_get_combine_media_stats() && combined_event == NULL)\n\t\t\t\t\tcombined_event = json_array();\n\t\t\t\tint vindex=0;\n\t\t\t\tfor(vindex=0; vindex<3; vindex++) {\n\t\t\t\t\tif(medium && ((medium->type == JANUS_MEDIA_DATA && vindex == 0) || medium->rtcp_ctx[vindex])) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"mid\", json_string(medium->mid));\n\t\t\t\t\t\tjson_object_set_new(info, \"mindex\", json_integer(medium->mindex));\n\t\t\t\t\t\tif(vindex == 0)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(janus_media_type_str(medium->type)));\n\t\t\t\t\t\telse if(vindex == 1)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video-sim1\"));\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video-sim2\"));\n\t\t\t\t\t\tif(medium->type == JANUS_MEDIA_AUDIO || medium->type == JANUS_MEDIA_VIDEO) {\n\t\t\t\t\t\t\tif(medium->codec)\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(medium->codec));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"base\", json_integer(medium->rtcp_ctx[vindex]->tb));\n\t\t\t\t\t\t\tif(vindex == 0) {\n\t\t\t\t\t\t\t\tuint32_t rtt = janus_rtcp_context_get_rtt(medium->rtcp_ctx[vindex]);\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"rtt\", json_integer(rtt));\n\t\t\t\t\t\t\t\tif(rtt > 0 && medium->rtcp_ctx[vindex]) {\n\t\t\t\t\t\t\t\t\tjson_t *rtt_vals = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(rtt_vals, \"ntp\", json_integer(medium->rtcp_ctx[vindex]->rtt_ntp));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(rtt_vals, \"lsr\", json_integer(medium->rtcp_ctx[vindex]->rtt_lsr));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(rtt_vals, \"dlsr\", json_integer(medium->rtcp_ctx[vindex]->rtt_dlsr));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"rtt-values\", rtt_vals);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_object_set_new(info, \"lost\", json_integer(janus_rtcp_context_get_lost_all(medium->rtcp_ctx[vindex], FALSE)));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"lost-by-remote\", json_integer(janus_rtcp_context_get_lost_all(medium->rtcp_ctx[vindex], TRUE)));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"jitter-local\", json_integer(janus_rtcp_context_get_jitter(medium->rtcp_ctx[vindex], FALSE)));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"jitter-remote\", json_integer(janus_rtcp_context_get_jitter(medium->rtcp_ctx[vindex], TRUE)));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"in-link-quality\", json_integer(janus_rtcp_context_get_in_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"in-media-link-quality\", json_integer(janus_rtcp_context_get_in_media_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"out-link-quality\", json_integer(janus_rtcp_context_get_out_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"out-media-link-quality\", json_integer(janus_rtcp_context_get_out_media_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_object_set_new(info, \"packets-received\", json_integer(medium->in_stats.info[vindex].packets));\n\t\t\t\t\t\tjson_object_set_new(info, \"packets-sent\", json_integer(medium->out_stats.info[vindex].packets));\n\t\t\t\t\t\tjson_object_set_new(info, \"bytes-received\", json_integer(medium->in_stats.info[vindex].bytes));\n\t\t\t\t\t\tjson_object_set_new(info, \"bytes-sent\", json_integer(medium->out_stats.info[vindex].bytes));\n\t\t\t\t\t\tif(medium->type == JANUS_MEDIA_AUDIO || medium->type == JANUS_MEDIA_VIDEO) {\n\t\t\t\t\t\t\tjson_object_set_new(info, \"bytes-received-lastsec\", json_integer(medium->in_stats.info[vindex].bytes_lastsec));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"bytes-sent-lastsec\", json_integer(medium->out_stats.info[vindex].bytes_lastsec));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"nacks-received\", json_integer(medium->in_stats.info[vindex].nacks));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"nacks-sent\", json_integer(medium->out_stats.info[vindex].nacks));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"retransmissions-received\", json_integer(medium->rtcp_ctx[vindex]->retransmitted));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(medium->mindex == 0 && pc->remb_bitrate > 0)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"remb-bitrate\", json_integer(pc->remb_bitrate));\n\t\t\t\t\t\tif(combined_event != NULL) {\n\t\t\t\t\t\t\tjson_array_append_new(combined_event, info);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_MEDIA, JANUS_EVENT_SUBTYPE_MEDIA_STATS,\n\t\t\t\t\t\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif(combined_event != NULL) {\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_MEDIA, JANUS_EVENT_SUBTYPE_MEDIA_STATS,\n\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, combined_event);\n\t}\n\t/* Reset stats event counter */\n\tif(handle->last_event_stats >= janus_ice_event_stats_period)\n\t\thandle->last_event_stats = 0;\n\t/* Should we clean up old NACK buffers for any of the streams? */\n\tjanus_cleanup_nack_buffer(now, handle->pc, TRUE, TRUE);\n\t/* Check if we should also print a summary of SRTP-related errors */\n\thandle->last_srtp_summary++;\n\tif(handle->last_srtp_summary == 0 || handle->last_srtp_summary == 2) {\n\t\tif(handle->srtp_errors_count > 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Got %d SRTP/SRTCP errors in the last few seconds (last error: %s)\\n\",\n\t\t\t\thandle->handle_id, handle->srtp_errors_count, janus_srtp_error_str(handle->last_srtp_error));\n\t\t\thandle->srtp_errors_count = 0;\n\t\t\thandle->last_srtp_error = 0;\n\t\t}\n\t\thandle->last_srtp_summary = 0;\n\t}\n\treturn G_SOURCE_CONTINUE;\n}\n\nstatic gboolean janus_ice_outgoing_traffic_handle(janus_ice_handle *handle, janus_ice_queued_packet *pkt) {\n\tjanus_session *session = (janus_session *)handle->session;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tif(pkt == &janus_ice_start_gathering) {\n\t\t/* Start gathering candidates */\n\t\tif(handle->agent == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] No ICE agent, not going to gather candidates...\\n\", handle->handle_id);\n\t\t} else if(!nice_agent_gather_candidates(handle->agent, handle->stream_id)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error gathering candidates...\\n\", handle->handle_id);\n\t\t\tjanus_ice_webrtc_hangup(handle, \"ICE gathering error\");\n\t\t}\n\t\treturn G_SOURCE_CONTINUE;\n\t} else if(pkt == &janus_ice_add_candidates) {\n\t\t/* There are remote candidates pending, add them now */\n\t\tGSList *candidates = NULL;\n\t\tNiceCandidate *c = NULL;\n\t\twhile((c = g_async_queue_try_pop(handle->queued_candidates)) != NULL) {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Processing candidate %p\\n\", handle->handle_id, c);\n\t\t\tif(c->priority > 0) {\n\t\t\t\tcandidates = g_slist_append(candidates, c);\n\t\t\t} else {\n\t\t\t\t/* Workaround for https://gitlab.freedesktop.org/libnice/libnice/-/issues/181 */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Candidate %p has priority 0, ignoring it\\n\", handle->handle_id, c);\n\t\t\t}\n\t\t}\n\t\tguint count = g_slist_length(candidates);\n\t\tif(pc != NULL && count > 0) {\n\t\t\tif(handle->agent_started == 0)\n\t\t\t\thandle->agent_started = janus_get_monotonic_time();\n\t\t\tint added = nice_agent_set_remote_candidates(handle->agent, pc->stream_id, pc->component_id, candidates);\n\t\t\tif(added < 0 || (guint)added != count) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Failed to add some remote candidates (added %u, expected %u)\\n\",\n\t\t\t\t\thandle->handle_id, added, count);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] %d remote %s added\\n\", handle->handle_id,\n\t\t\t\t\tcount, (count > 1 ? \"candidates\" : \"candidate\"));\n\t\t\t}\n\t\t}\n\t\tg_slist_free(candidates);\n\t\treturn G_SOURCE_CONTINUE;\n\t} else if(pkt == &janus_ice_dtls_handshake) {\n\t\tif(!janus_is_webrtc_encryption_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] WebRTC encryption disabled, skipping DTLS handshake\\n\", handle->handle_id);\n\t\t\tjanus_ice_dtls_handshake_done(handle);\n\t\t\treturn G_SOURCE_CONTINUE;\n\t\t} else if(!pc) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] PeerConnection not initialized, aborting DTLS handshake\\n\", handle->handle_id);\n\t\t\treturn G_SOURCE_CONTINUE;\n\t\t}\n\t\t/* Start the DTLS handshake */\n\t\tjanus_dtls_srtp_handshake(pc->dtls);\n\t\t/* Create retransmission timer */\n\t\tpc->dtlsrt_source = g_timeout_source_new(50);\n\t\tg_source_set_callback(pc->dtlsrt_source, janus_dtls_retry, pc->dtls, NULL);\n\t\tguint id = g_source_attach(pc->dtlsrt_source, handle->mainctx);\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Creating retransmission timer with ID %u\\n\", handle->handle_id, id);\n\t\treturn G_SOURCE_CONTINUE;\n\t} else if(pkt == &janus_ice_media_stopped) {\n\t\t/* Some media has been disabled on the way in, so use the callback to notify the peer */\n\t\tif(pc == NULL)\n\t\t\treturn G_SOURCE_CONTINUE;\n\t\tuint mi=0;\n\t\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\t\tif(!medium || (medium->type != JANUS_MEDIA_AUDIO && medium->type != JANUS_MEDIA_VIDEO))\n\t\t\t\tcontinue;\t/* We don't process data channels here */\n\t\t\tint vindex = 0;\n\t\t\tfor(vindex=0; vindex < 3; vindex++) {\n\t\t\t\tif(vindex > 0 && (medium->type != JANUS_MEDIA_VIDEO || medium->rtcp_ctx[1] == NULL))\n\t\t\t\t\tcontinue;\t/* We won't need simulcast checks */\n\t\t\t\tif(!medium->in_stats.info[vindex].notified_lastsec && medium->in_stats.info[vindex].bytes && !medium->recv) {\n\t\t\t\t\t/* This medium won't be received for a while, notify */\n\t\t\t\t\tmedium->in_stats.info[vindex].notified_lastsec = TRUE;\n\t\t\t\t\tjanus_ice_notify_media(handle, medium->mid, medium->type == JANUS_MEDIA_VIDEO, medium->rtcp_ctx[1] != NULL, vindex, FALSE);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn G_SOURCE_CONTINUE;\n\t} else if(pkt == &janus_ice_hangup_peerconnection) {\n\t\t/* The media session is over, send an alert on all streams and components */\n\t\tif(handle->pc && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY)) {\n\t\t\tjanus_dtls_srtp_send_alert(handle->pc->dtls);\n\t\t}\n\t\t/* Notify the plugin about the fact this PeerConnection has just gone */\n\t\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Telling the plugin about the hangup (%s)\\n\",\n\t\t\thandle->handle_id, plugin ? plugin->get_name() : \"??\");\n\t\tif(plugin != NULL && handle->app_handle != NULL) {\n\t\t\tplugin->hangup_media(handle->app_handle);\n\t\t}\n\t\t/* Get rid of the attached sources */\n\t\tif(handle->rtcp_source) {\n\t\t\tg_source_destroy(handle->rtcp_source);\n\t\t\tg_source_unref(handle->rtcp_source);\n\t\t\thandle->rtcp_source = NULL;\n\t\t}\n\t\tif(handle->twcc_source) {\n\t\t\tg_source_destroy(handle->twcc_source);\n\t\t\tg_source_unref(handle->twcc_source);\n\t\t\thandle->twcc_source = NULL;\n\t\t}\n\t\tif(handle->stats_source) {\n\t\t\tg_source_destroy(handle->stats_source);\n\t\t\tg_source_unref(handle->stats_source);\n\t\t\thandle->stats_source = NULL;\n\t\t}\n\t\t/* If event handlers are active, send stats one last time */\n\t\tif(janus_events_is_enabled()) {\n\t\t\thandle->last_event_stats = janus_ice_event_stats_period;\n\t\t\t(void)janus_ice_outgoing_stats_handle(handle);\n\t\t}\n\t\tjanus_ice_webrtc_free(handle);\n\t\treturn G_SOURCE_CONTINUE;\n\t} else if(pkt == &janus_ice_detach_handle) {\n\t\t/* This handle has just been detached, notify the plugin */\n\t\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Telling the plugin about the handle detach (%s)\\n\",\n\t\t\thandle->handle_id, plugin ? plugin->get_name() : \"??\");\n\t\tif(plugin != NULL && handle->app_handle != NULL) {\n\t\t\tint error = 0;\n\t\t\tplugin->destroy_session(handle->app_handle, &error);\n\t\t}\n\t\thandle->app_handle = NULL;\n\t\t/* TODO Get rid of the loop by removing the source */\n\t\tif(handle->rtp_source) {\n\t\t\tg_source_destroy(handle->rtp_source);\n\t\t\tg_source_unref(handle->rtp_source);\n\t\t\thandle->rtp_source = NULL;\n\t\t}\n\t\t/* Prepare JSON event to notify user/application */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"janus\", json_string(\"detached\"));\n\t\tjson_object_set_new(event, \"session_id\", json_integer(session->session_id));\n\t\tjson_object_set_new(event, \"sender\", json_integer(handle->handle_id));\n\t\tif(opaqueid_in_api && handle->opaque_id != NULL)\n\t\t\tjson_object_set_new(event, \"opaque_id\", json_string(handle->opaque_id));\n\t\t/* Send the event */\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending event to transport...; %p\\n\", handle->handle_id, handle);\n\t\tjanus_session_notify_event(session, event);\n\t\t/* Notify event handlers as well */\n\t\tif(janus_events_is_enabled())\n\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_HANDLE, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\tsession->session_id, handle->handle_id, \"detached\",\n\t\t\t\tplugin ? plugin->get_package() : NULL, handle->opaque_id, handle->token);\n\t\treturn G_SOURCE_REMOVE;\n\t} else if(pkt == &janus_ice_data_ready) {\n\t\t/* Data is writable on this PeerConnection, notify the plugin */\n\t\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\t\tif(plugin != NULL && plugin->data_ready != NULL && handle->app_handle != NULL) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Telling the plugin about the data channel being ready (%s)\\n\",\n\t\t\t\thandle->handle_id, plugin ? plugin->get_name() : \"??\");\n\t\t\tplugin->data_ready(handle->app_handle);\n\t\t}\n\t}\n\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY)) {\n\t\tjanus_ice_free_queued_packet(pkt);\n\t\treturn G_SOURCE_CONTINUE;\n\t}\n\t/* Now let's get on with the packet */\n\tif(pkt == NULL)\n\t\treturn G_SOURCE_CONTINUE;\n\tif(pkt->data == NULL || pc == NULL) {\n\t\tjanus_ice_free_queued_packet(pkt);\n\t\treturn G_SOURCE_CONTINUE;\n\t}\n\tgint64 age = (janus_get_monotonic_time() - pkt->added);\n\tif(age > G_USEC_PER_SEC) {\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Discarding too old outgoing packet (age=%\"SCNi64\"us)\\n\", handle->handle_id, age);\n\t\tjanus_ice_free_queued_packet(pkt);\n\t\treturn G_SOURCE_CONTINUE;\n\t}\n\tif(!pc->cdone) {\n\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && !pc->noerrorlog) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No candidates not gathered yet for stream??\\n\", handle->handle_id);\n\t\t\tpc->noerrorlog = TRUE;\t/* Don't flood with the same error all over again */\n\t\t}\n\t\tjanus_ice_free_queued_packet(pkt);\n\t\treturn G_SOURCE_CONTINUE;\n\t}\n\t/* Find the right medium instance */\n\tif(pkt->mindex != -1) {\n\t\tmedium = g_hash_table_lookup(pc->media, GINT_TO_POINTER(pkt->mindex));\n\t} else {\n\t\tjanus_media_type mtype = janus_media_type_from_packet(pkt->type);\n\t\tmedium = g_hash_table_lookup(pc->media_bytype, GINT_TO_POINTER(mtype));\n\t}\n\tif(medium == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No medium #%d associated to this packet??\\n\", handle->handle_id, pkt->mindex);\n\t\tjanus_ice_free_queued_packet(pkt);\n\t\treturn G_SOURCE_CONTINUE;\n\t}\n\tif(pkt->control) {\n\t\t/* RTCP */\n\t\tint video = (pkt->type == JANUS_ICE_PACKET_VIDEO);\n\t\tpc->noerrorlog = FALSE;\n\t\tif(janus_is_webrtc_encryption_enabled() && (!pc->dtls || !pc->dtls->srtp_valid || !pc->dtls->srtp_out)) {\n\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && !pc->noerrorlog) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] %s stream (#%u) component has no valid SRTP session (yet?)\\n\",\n\t\t\t\t\thandle->handle_id, video ? \"video\" : \"audio\", pc->stream_id);\n\t\t\t\tmedium->noerrorlog = TRUE;\t/* Don't flood with the same error all over again */\n\t\t\t}\n\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\treturn G_SOURCE_CONTINUE;\n\t\t}\n\t\tmedium->noerrorlog = FALSE;\n\t\tif(pkt->encrypted) {\n\t\t\t/* Already SRTCP */\n\t\t\tint sent = nice_agent_send(handle->agent, pc->stream_id, pc->component_id, pkt->length, (const gchar *)pkt->data);\n\t\t\tif(sent < pkt->length) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] ... only sent %d bytes? (was %d)\\n\", handle->handle_id, sent, pkt->length);\n\t\t\t}\n\t\t} else {\n\t\t\t/* Check if there's anything we need to do before sending */\n\t\t\tif(pkt->control_ext) {\n\t\t\t\t/* Fix all SSRCs before enqueueing, as we need to use the ones for this media\n\t\t\t\t * leg. Note that this is only needed for RTCP packets coming from plugins: the\n\t\t\t\t * ones created by the core already have the right SSRCs in the right place */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Fixing SSRCs (local %u, peer %u)\\n\", handle->handle_id,\n\t\t\t\t\tmedium->ssrc, medium->ssrc_peer[0]);\n\t\t\t\tjanus_rtcp_fix_ssrc(NULL, pkt->data, pkt->length, 1,\n\t\t\t\t\tmedium->ssrc, medium->ssrc_peer[0]);\n\t\t\t\t/* If this is a PLI and we're simulcasting, send a PLI on other layers as well */\n\t\t\t\tif(video && janus_rtcp_has_pli(pkt->data, pkt->length)) {\n\t\t\t\t\tif(medium->ssrc_peer[1]) {\n\t\t\t\t\t\tchar plibuf[12];\n\t\t\t\t\t\tmemset(plibuf, 0, 12);\n\t\t\t\t\t\tjanus_rtcp_pli((char *)&plibuf, 12);\n\t\t\t\t\t\tjanus_rtcp_fix_ssrc(NULL, plibuf, sizeof(plibuf), 1,\n\t\t\t\t\t\t\tmedium->ssrc, medium->ssrc_peer[1]);\n\t\t\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = medium->mindex, .video = TRUE, .buffer = plibuf, .length = sizeof(plibuf) };\n\t\t\t\t\t\tjanus_ice_relay_rtcp_internal(handle, medium, &rtcp, FALSE);\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->ssrc_peer[2]) {\n\t\t\t\t\t\tchar plibuf[12];\n\t\t\t\t\t\tmemset(plibuf, 0, 12);\n\t\t\t\t\t\tjanus_rtcp_pli((char *)&plibuf, 12);\n\t\t\t\t\t\tjanus_rtcp_fix_ssrc(NULL, plibuf, sizeof(plibuf), 1,\n\t\t\t\t\t\t\tmedium->ssrc, medium->ssrc_peer[2]);\n\t\t\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = medium->mindex, .video = TRUE, .buffer = plibuf, .length = sizeof(plibuf) };\n\t\t\t\t\t\tjanus_ice_relay_rtcp_internal(handle, medium, &rtcp, FALSE);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tuint32_t bitrate = janus_rtcp_get_remb(pkt->data, pkt->length);\n\t\t\tif(bitrate > 0) {\n\t\t\t\t/* There's a REMB, prepend a RR as it won't work otherwise */\n\t\t\t\tint rrlen = 8;\n\t\t\t\tchar *rtcpbuf = g_malloc0(rrlen+pkt->length+SRTP_MAX_TAG_LEN+4);\n\t\t\t\trtcp_rr *rr = (rtcp_rr *)rtcpbuf;\n\t\t\t\trr->header.version = 2;\n\t\t\t\trr->header.type = RTCP_RR;\n\t\t\t\trr->header.rc = 0;\n\t\t\t\trr->header.length = htons((rrlen/4)-1);\n\t\t\t\t/* Append REMB */\n\t\t\t\tmemcpy(rtcpbuf+rrlen, pkt->data, pkt->length);\n\t\t\t\t/* If we're simulcasting, set the extra SSRCs (the first one will be set by janus_rtcp_fix_ssrc) */\n\t\t\t\tif(medium->ssrc_peer[1] && pkt->length >= 28) {\n\t\t\t\t\trtcp_fb *rtcpfb = (rtcp_fb *)(rtcpbuf+rrlen);\n\t\t\t\t\trtcp_remb *remb = (rtcp_remb *)rtcpfb->fci;\n\t\t\t\t\tremb->ssrc[1] = htonl(medium->ssrc_peer[1]);\n\t\t\t\t\tif(medium->ssrc_peer[2] && pkt->length >= 32) {\n\t\t\t\t\t\tremb->ssrc[2] = htonl(medium->ssrc_peer[2]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Free old packet and update */\n\t\t\t\tchar *prev_data = pkt->data;\n\t\t\t\tpkt->data = rtcpbuf;\n\t\t\t\tpkt->length = rrlen+pkt->length;\n\t\t\t\tg_clear_pointer(&prev_data, g_free);\n\t\t\t}\n\t\t\t/* Do we need to dump this packet for debugging? */\n\t\t\tif(g_atomic_int_get(&handle->dump_packets))\n\t\t\t\tjanus_text2pcap_dump(handle->text2pcap, JANUS_TEXT2PCAP_RTCP, FALSE, pkt->data, pkt->length,\n\t\t\t\t\t\"[session=%\"SCNu64\"][handle=%\"SCNu64\"]\", session->session_id, handle->handle_id);\n\t\t\t/* Encrypt SRTCP */\n\t\t\tint protected = pkt->length;\n\t\t\tint res = janus_is_webrtc_encryption_enabled() ?\n\t\t\t\tsrtp_protect_rtcp(pc->dtls->srtp_out, pkt->data, &protected) : srtp_err_status_ok;\n\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t/* We don't spam the logs for every SRTP error: just take note of this, and print a summary later */\n\t\t\t\thandle->srtp_errors_count++;\n\t\t\t\thandle->last_srtp_error = res;\n\t\t\t\t/* If we're debugging, though, print every occurrence */\n\t\t\t\tJANUS_LOG(LOG_DBG, \"[%\"SCNu64\"] ... SRTCP protect error... %s (len=%d-->%d)...\\n\", handle->handle_id, janus_srtp_error_str(res), pkt->length, protected);\n\t\t\t} else {\n\t\t\t\t/* Shoot! */\n\t\t\t\tint sent = nice_agent_send(handle->agent, pc->stream_id, pc->component_id, protected, pkt->data);\n\t\t\t\tif(sent < protected) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] ... only sent %d bytes? (was %d)\\n\", handle->handle_id, sent, protected);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjanus_ice_free_queued_packet(pkt);\n\t} else {\n\t\t/* RTP or data */\n\t\tif(pkt->type == JANUS_ICE_PACKET_AUDIO || pkt->type == JANUS_ICE_PACKET_VIDEO) {\n\t\t\t/* RTP */\n\t\t\tint video = (pkt->type == JANUS_ICE_PACKET_VIDEO);\n\t\t\tif(!medium->send) {\n\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t}\n\t\t\tif(janus_is_webrtc_encryption_enabled() && (!pc->dtls || !pc->dtls->srtp_valid || !pc->dtls->srtp_out)) {\n\t\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && !medium->noerrorlog) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] %s stream component has no valid SRTP session (yet?)\\n\",\n\t\t\t\t\t\thandle->handle_id, video ? \"video\" : \"audio\");\n\t\t\t\t\tmedium->noerrorlog = TRUE;\t/* Don't flood with the same error all over again */\n\t\t\t\t}\n\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t}\n\t\t\tmedium->noerrorlog = FALSE;\n\t\t\tif(pkt->encrypted) {\n\t\t\t\t/* Already RTP (probably a retransmission?) */\n\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)pkt->data;\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] ... Retransmitting seq.nr %\"SCNu16\"\\n\\n\", handle->handle_id, ntohs(header->seq_number));\n\t\t\t\tint sent = nice_agent_send(handle->agent, pc->stream_id, pc->component_id, pkt->length, (const gchar *)pkt->data);\n\t\t\t\tif(sent < pkt->length) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] ... only sent %d bytes? (was %d)\\n\", handle->handle_id, sent, pkt->length);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Prune/update/set RTP extensions */\n\t\t\t\tjanus_ice_rtp_extension_update(handle, medium, pkt);\n\t\t\t\t/* Overwrite SSRC */\n\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)pkt->data;\n\t\t\t\tif(!pkt->retransmission) {\n\t\t\t\t\t/* ... but only if this isn't a retransmission (for those we already set it before) */\n\t\t\t\t\theader->ssrc = htonl(medium->ssrc);\n\t\t\t\t}\n\t\t\t\t/* Keep track of payload types too */\n\t\t\t\tif(medium->payload_type < 0) {\n\t\t\t\t\tmedium->payload_type = header->type;\n\t\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX) &&\n\t\t\t\t\t\t\tmedium->rtx_payload_types && g_hash_table_size(medium->rtx_payload_types) > 0) {\n\t\t\t\t\t\tmedium->rtx_payload_type = GPOINTER_TO_INT(g_hash_table_lookup(medium->rtx_payload_types, GINT_TO_POINTER(medium->payload_type)));\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Retransmissions will have payload type %d\\n\",\n\t\t\t\t\t\t\thandle->handle_id, medium->rtx_payload_type);\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->codec == NULL) {\n\t\t\t\t\t\tjanus_mutex_lock(&handle->mutex);\n\t\t\t\t\t\tconst char *codec = janus_get_codec_from_pt(handle->local_sdp, medium->payload_type);\n\t\t\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\t\t\tif(codec != NULL)\n\t\t\t\t\t\t\tmedium->codec = g_strdup(codec);\n\t\t\t\t\t}\n\t\t\t\t\tif(video && medium->video_is_keyframe == NULL && medium->codec != NULL) {\n\t\t\t\t\t\tif(!strcasecmp(medium->codec, \"vp8\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_vp8_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"vp9\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_vp9_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"h264\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_h264_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"av1\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_av1_is_keyframe;\n\t\t\t\t\t\telse if(!strcasecmp(medium->codec, \"h265\"))\n\t\t\t\t\t\t\tmedium->video_is_keyframe = &janus_h265_is_keyframe;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Do we need to dump this packet for debugging? */\n\t\t\t\tif(g_atomic_int_get(&handle->dump_packets))\n\t\t\t\t\tjanus_text2pcap_dump(handle->text2pcap, JANUS_TEXT2PCAP_RTP, FALSE, pkt->data, pkt->length,\n\t\t\t\t\t\t\"[session=%\"SCNu64\"][handle=%\"SCNu64\"]\", session->session_id, handle->handle_id);\n\t\t\t\t/* If this is video and NACK optimizations are enabled, check if this is\n\t\t\t\t * a keyframe: if so, we empty our retransmit buffer for incoming NACKs */\n\t\t\t\tif(video && nack_optimizations && medium->video_is_keyframe) {\n\t\t\t\t\tint plen = 0;\n\t\t\t\t\tchar *payload = janus_rtp_payload(pkt->data, pkt->length, &plen);\n\t\t\t\t\tif(medium->video_is_keyframe(payload, plen)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Keyframe sent, cleaning retransmit buffer\\n\", handle->handle_id);\n\t\t\t\t\t\tjanus_cleanup_nack_buffer(0, pc, FALSE, TRUE);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Before encrypting, check if we need to copy the unencrypted payload (e.g., for rtx/90000) */\n\t\t\t\tjanus_rtp_packet *p = NULL;\n\t\t\t\tif(medium->nack_queue_ms > 0 && !pkt->retransmission && pkt->type == JANUS_ICE_PACKET_VIDEO && medium->do_nacks &&\n\t\t\t\t\t\tjanus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)) {\n\t\t\t\t\t/* Save the packet for retransmissions that may be needed later: start by\n\t\t\t\t\t * making room for two more bytes to store the original sequence number */\n\t\t\t\t\tp = g_malloc(sizeof(janus_rtp_packet));\n\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)pkt->data;\n\t\t\t\t\tguint16 original_seq = header->seq_number;\n\t\t\t\t\tp->data = g_malloc(pkt->length+2);\n\t\t\t\t\tp->length = pkt->length+2;\n\t\t\t\t\t/* Check where the payload starts */\n\t\t\t\t\tint plen = 0;\n\t\t\t\t\tchar *payload = janus_rtp_payload(pkt->data, pkt->length, &plen);\n\t\t\t\t\tif(plen == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Discarding outgoing empty RTP packet\\n\", handle->handle_id);\n\t\t\t\t\t\tjanus_ice_free_rtp_packet(p);\n\t\t\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t\t\t}\n\t\t\t\t\tsize_t hsize = payload - pkt->data;\n\t\t\t\t\t/* Copy the header first */\n\t\t\t\t\tmemcpy(p->data, pkt->data, hsize);\n\t\t\t\t\t/* Copy the original sequence number */\n\t\t\t\t\tmemcpy(p->data+hsize, &original_seq, 2);\n\t\t\t\t\t/* Copy the extensions struct */\n\t\t\t\t\tp->extensions = pkt->extensions;\n\t\t\t\t\t/* Copy the payload */\n\t\t\t\t\tmemcpy(p->data+hsize+2, payload, pkt->length - hsize);\n\t\t\t\t}\n\t\t\t\t/* Encrypt SRTP */\n\t\t\t\tint protected = pkt->length;\n\t\t\t\tint res = janus_is_webrtc_encryption_enabled() ?\n\t\t\t\t\tsrtp_protect(pc->dtls->srtp_out, pkt->data, &protected) : srtp_err_status_ok;\n\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t/* We don't spam the logs for every SRTP error: just take note of this, and print a summary later */\n\t\t\t\t\thandle->srtp_errors_count++;\n\t\t\t\t\thandle->last_srtp_error = res;\n\t\t\t\t\t/* If we're debugging, though, print every occurrence */\n\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)pkt->data;\n\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\tJANUS_LOG(LOG_DBG, \"[%\"SCNu64\"] ... SRTP protect error... %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\thandle->handle_id, janus_srtp_error_str(res), pkt->length, protected, timestamp, seq);\n\t\t\t\t\tjanus_ice_free_rtp_packet(p);\n\t\t\t\t} else {\n\t\t\t\t\t/* Shoot! */\n\t\t\t\t\tint sent = nice_agent_send(handle->agent, pc->stream_id, pc->component_id, protected, pkt->data);\n\t\t\t\t\tif(sent < protected) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] ... only sent %d bytes? (was %d)\\n\", handle->handle_id, sent, protected);\n\t\t\t\t\t}\n\t\t\t\t\t/* Update stats */\n\t\t\t\t\tif(sent > 0) {\n\t\t\t\t\t\t/* Update the RTCP context as well */\n\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)pkt->data;\n\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\tmedium->out_stats.info[0].packets++;\n\t\t\t\t\t\tmedium->out_stats.info[0].bytes += pkt->length;\n\t\t\t\t\t\t/* Last second outgoing media */\n\t\t\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\t\t\tif(medium->out_stats.info[0].updated == 0)\n\t\t\t\t\t\t\tmedium->out_stats.info[0].updated = now;\n\t\t\t\t\t\tif(now > medium->out_stats.info[0].updated &&\n\t\t\t\t\t\t\t\tnow - medium->out_stats.info[0].updated >= G_USEC_PER_SEC) {\n\t\t\t\t\t\t\tmedium->out_stats.info[0].bytes_lastsec = medium->out_stats.info[0].bytes_lastsec_temp;\n\t\t\t\t\t\t\tmedium->out_stats.info[0].bytes_lastsec_temp = 0;\n\t\t\t\t\t\t\tmedium->out_stats.info[0].updated = now;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmedium->out_stats.info[0].bytes_lastsec_temp += pkt->length;\n\t\t\t\t\t\tstruct timeval tv;\n\t\t\t\t\t\tgettimeofday(&tv, NULL);\n\t\t\t\t\t\tif(medium->last_ntp_ts == 0 || (gint32)(timestamp - medium->last_rtp_ts) > 0) {\n\t\t\t\t\t\t\tmedium->last_ntp_ts = (gint64)tv.tv_sec*G_USEC_PER_SEC + tv.tv_usec;\n\t\t\t\t\t\t\tmedium->last_rtp_ts = timestamp;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(medium->first_ntp_ts[0] == 0) {\n\t\t\t\t\t\t\tmedium->first_ntp_ts[0] = (gint64)tv.tv_sec*G_USEC_PER_SEC + tv.tv_usec;\n\t\t\t\t\t\t\tmedium->first_rtp_ts[0] = timestamp;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Update sent packets counter */\n\t\t\t\t\t\trtcp_context *rtcp_ctx = medium->rtcp_ctx[0];\n\t\t\t\t\t\tif(rtcp_ctx) {\n\t\t\t\t\t\t\tg_atomic_int_inc(&rtcp_ctx->sent_packets_since_last_rr);\n\t\t\t\t\t\t\tif(pkt->type == JANUS_ICE_PACKET_AUDIO) {\n\t\t\t\t\t\t\t\t/* Let's check if this is not Opus: in case we may need to change the timestamp base */\n\t\t\t\t\t\t\t\tint pt = header->type;\n\t\t\t\t\t\t\t\tuint32_t clock_rate = medium->clock_rates ?\n\t\t\t\t\t\t\t\t\tGPOINTER_TO_UINT(g_hash_table_lookup(medium->clock_rates, GINT_TO_POINTER(pt))) : 48000;\n\t\t\t\t\t\t\t\tif(rtcp_ctx->tb != clock_rate)\n\t\t\t\t\t\t\t\t\trtcp_ctx->tb = clock_rate;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->nack_queue_ms > 0 && !pkt->retransmission) {\n\t\t\t\t\t\t/* Save the packet for retransmissions that may be needed later */\n\t\t\t\t\t\tif(!medium->do_nacks) {\n\t\t\t\t\t\t\t/* ... unless NACKs are disabled for this medium */\n\t\t\t\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(p == NULL) {\n\t\t\t\t\t\t\t/* If we're not doing RFC4588, we're saving the SRTP packet as it is */\n\t\t\t\t\t\t\tp = g_malloc(sizeof(janus_rtp_packet));\n\t\t\t\t\t\t\tp->data = g_malloc(protected);\n\t\t\t\t\t\t\tmemcpy(p->data, pkt->data, protected);\n\t\t\t\t\t\t\tp->length = protected;\n\t\t\t\t\t\t\tjanus_plugin_rtp_extensions_reset(&p->extensions);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tp->created = janus_get_monotonic_time();\n\t\t\t\t\t\tp->last_retransmit = 0;\n\t\t\t\t\t\tp->current_backoff = 0;\n\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)pkt->data;\n\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\tif(medium->retransmit_buffer == NULL) {\n\t\t\t\t\t\t\tmedium->retransmit_buffer = g_queue_new();\n\t\t\t\t\t\t\tmedium->retransmit_seqs = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg_queue_push_tail(medium->retransmit_buffer, p);\n\t\t\t\t\t\t/* Insert in the table too, for quick lookup */\n\t\t\t\t\t\tg_hash_table_insert(medium->retransmit_seqs, GUINT_TO_POINTER(seq), p);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_ice_free_rtp_packet(p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(pkt->type == JANUS_ICE_PACKET_TEXT || pkt->type == JANUS_ICE_PACKET_BINARY) {\n\t\t\t/* Data */\n\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)) {\n\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t}\n#ifdef HAVE_SCTP\n\t\t\tif(!pc->dtls) {\n\t\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && !medium->noerrorlog) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] SCTP stream component has no valid DTLS session (yet?)\\n\", handle->handle_id);\n\t\t\t\t\tmedium->noerrorlog = TRUE;\t/* Don't flood with the same error all over again */\n\t\t\t\t}\n\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t}\n\t\t\tmedium->noerrorlog = FALSE;\n\t\t\t/* TODO Support binary data */\n\t\t\tjanus_dtls_wrap_sctp_data(pc->dtls, pkt->label, pkt->protocol,\n\t\t\t\tpkt->type == JANUS_ICE_PACKET_TEXT, pkt->data, pkt->length);\n#endif\n\t\t} else if(pkt->type == JANUS_ICE_PACKET_SCTP) {\n\t\t\t/* SCTP data to push */\n\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)) {\n\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t}\n#ifdef HAVE_SCTP\n\t\t\t/* Encapsulate this data in DTLS and send it */\n\t\t\tif(!pc->dtls) {\n\t\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && !medium->noerrorlog) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] SCTP stream component has no valid DTLS session (yet?)\\n\", handle->handle_id);\n\t\t\t\t\tmedium->noerrorlog = TRUE;\t/* Don't flood with the same error all over again */\n\t\t\t\t}\n\t\t\t\tjanus_ice_free_queued_packet(pkt);\n\t\t\t\treturn G_SOURCE_CONTINUE;\n\t\t\t}\n\t\t\tmedium->noerrorlog = FALSE;\n\t\t\tjanus_dtls_send_sctp_data(pc->dtls, pkt->data, pkt->length);\n#endif\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Unsupported packet type %d\\n\", handle->handle_id, pkt->type);\n\t\t}\n\t\tjanus_ice_free_queued_packet(pkt);\n\t}\n\treturn G_SOURCE_CONTINUE;\n}\n\nstatic void janus_ice_queue_packet(janus_ice_handle *handle, janus_ice_queued_packet *pkt) {\n\t/* TODO: There is a potential race condition where the \"queued_packets\"\n\t * could get released between the condition and pushing the packet. */\n\tif(handle->queued_packets != NULL) {\n\t\tg_async_queue_push(handle->queued_packets, pkt);\n\t\tg_main_context_wakeup(handle->mainctx);\n\t} else {\n\t\tjanus_ice_free_queued_packet(pkt);\n\t}\n}\n\nvoid janus_ice_relay_rtp(janus_ice_handle *handle, janus_plugin_rtp *packet) {\n\tif(!handle || !handle->pc || handle->queued_packets == NULL || packet == NULL || packet->buffer == NULL ||\n\t\t\t!janus_is_rtp(packet->buffer, packet->length))\n\t\treturn;\n\t/* Queue this packet as it is (we'll prune/update/set extensions later) */\n\tjanus_ice_queued_packet *pkt = g_malloc(sizeof(janus_ice_queued_packet));\n\tpkt->mindex = packet->mindex;\n\tpkt->data = g_malloc(packet->length + SRTP_MAX_TAG_LEN);\n\tmemcpy(pkt->data, packet->buffer, packet->length);\n\tpkt->length = packet->length;\n\tpkt->type = packet->video ? JANUS_ICE_PACKET_VIDEO : JANUS_ICE_PACKET_AUDIO;\n\tpkt->extensions = packet->extensions;\n\tpkt->control = FALSE;\n\tpkt->control_ext = FALSE;\n\tpkt->encrypted = FALSE;\n\tpkt->retransmission = FALSE;\n\tpkt->label = NULL;\n\tpkt->protocol = NULL;\n\tpkt->added = janus_get_monotonic_time();\n\tjanus_ice_queue_packet(handle, pkt);\n}\n\nvoid janus_ice_relay_rtcp_internal(janus_ice_handle *handle, janus_ice_peerconnection_medium *medium,\n\t\tjanus_plugin_rtcp *packet, gboolean filter_rtcp) {\n\tif(!handle || !handle->pc || handle->queued_packets == NULL || packet == NULL || packet->buffer == NULL ||\n\t\t\t!janus_is_rtcp(packet->buffer, packet->length))\n\t\treturn;\n\t/* We use this internal method to check whether we need to filter RTCP (e.g., to make\n\t * sure we don't just forward any SR/RR from peers/plugins, but use our own) or it has\n\t * already been done, and so this is actually a packet added by the ICE send thread */\n\tchar *rtcp_buf = packet->buffer;\n\tint rtcp_len = packet->length;\n\tgboolean has_medium = (medium != NULL);\n\tif(filter_rtcp) {\n\t\t/* Strip RR/SR/SDES/NACKs/etc. */\n\t\trtcp_buf = janus_rtcp_filter(packet->buffer, packet->length, &rtcp_len);\n\t\tif(rtcp_buf == NULL || rtcp_len < 1) {\n\t\t\tg_free(rtcp_buf);\n\t\t\treturn;\n\t\t}\n\t\tif(has_medium) {\n\t\t\t/* Fix all SSRCs before enqueueing, as we need to use the ones for this media\n\t\t\t* leg. Note that this is only needed for RTCP packets coming from plugins: the\n\t\t\t* ones created by the core already have the right SSRCs in the right place */\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Fixing SSRCs (local %u, peer %u)\\n\", handle->handle_id,\n\t\t\t\tmedium->ssrc, medium->ssrc_peer[0]);\n\t\t\tjanus_rtcp_fix_ssrc(NULL, rtcp_buf, rtcp_len, 1,\n\t\t\t\tmedium->ssrc, medium->ssrc_peer[0]);\n\t\t}\n\t}\n\t/* Queue this packet */\n\tjanus_ice_queued_packet *pkt = g_malloc(sizeof(janus_ice_queued_packet));\n\tpkt->mindex = (has_medium) ? medium->mindex : packet->mindex;\n\tpkt->data = g_malloc(rtcp_len+SRTP_MAX_TAG_LEN+4);\n\tmemcpy(pkt->data, rtcp_buf, rtcp_len);\n\tpkt->length = rtcp_len;\n\tpkt->type = packet->video ? JANUS_ICE_PACKET_VIDEO : JANUS_ICE_PACKET_AUDIO;\n\tmemset(&pkt->extensions, 0, sizeof(pkt->extensions));\n\tpkt->control = TRUE;\n\tpkt->control_ext = !has_medium;\t/* We could do further processing for this packet in the loop */\n\tpkt->encrypted = FALSE;\n\tpkt->retransmission = FALSE;\n\tpkt->label = NULL;\n\tpkt->protocol = NULL;\n\tpkt->added = janus_get_monotonic_time();\n\tjanus_ice_queue_packet(handle, pkt);\n\tif(rtcp_buf != packet->buffer) {\n\t\t/* We filtered the original packet, deallocate it */\n\t\tg_free(rtcp_buf);\n\t}\n}\n\nvoid janus_ice_relay_rtcp(janus_ice_handle *handle, janus_plugin_rtcp *packet) {\n\tjanus_ice_relay_rtcp_internal(handle, NULL, packet, TRUE);\n}\n\nvoid janus_ice_send_pli(janus_ice_handle *handle) {\n\tif(handle == NULL || handle->pc == NULL)\n\t\treturn;\n\t/* Iterate on all video streams, and send the PLI there */\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tuint mi=0;\n\tfor(mi=0; mi<g_hash_table_size(handle->pc->media); mi++) {\n\t\tmedium = g_hash_table_lookup(handle->pc->media, GUINT_TO_POINTER(mi));\n\t\tif(!medium || medium->type != JANUS_MEDIA_VIDEO)\n\t\t\tcontinue;\n\t\tjanus_ice_send_pli_stream(handle, medium->mindex);\n\t}\n}\n\nvoid janus_ice_send_pli_stream(janus_ice_handle *handle, int mindex) {\n\tchar rtcpbuf[12];\n\tmemset(rtcpbuf, 0, 12);\n\tjanus_rtcp_pli((char *)&rtcpbuf, 12);\n\t/* FIXME We send the PLI on the first video m-line we have */\n\tjanus_plugin_rtcp rtcp = { .mindex = mindex, .video = TRUE, .buffer = rtcpbuf, .length = 12 };\n\tjanus_ice_relay_rtcp(handle, &rtcp);\n}\n\nvoid janus_ice_send_remb(janus_ice_handle *handle, uint32_t bitrate) {\n\tchar rtcpbuf[24];\n\tjanus_rtcp_remb((char *)&rtcpbuf, 24, bitrate);\n\t/* FIXME We send the PLI on the first video m-line we have */\n\tjanus_plugin_rtcp rtcp = { .mindex = -1, .video = TRUE, .buffer = rtcpbuf, .length = 24 };\n\tjanus_ice_relay_rtcp(handle, &rtcp);\n}\n\n#ifdef HAVE_SCTP\nvoid janus_ice_relay_data(janus_ice_handle *handle, janus_plugin_data *packet) {\n\tif(!handle || !handle->pc || handle->queued_packets == NULL || packet == NULL || packet->buffer == NULL || packet->length < 1)\n\t\treturn;\n\tjanus_ice_queued_packet *pkt = g_malloc(sizeof(janus_ice_queued_packet));\n\tpkt->data = g_malloc(packet->length);\n\tpkt->mindex = -1;\n\tmemcpy(pkt->data, packet->buffer, packet->length);\n\tpkt->length = packet->length;\n\tpkt->type = packet->binary ? JANUS_ICE_PACKET_BINARY : JANUS_ICE_PACKET_TEXT;\n\tmemset(&pkt->extensions, 0, sizeof(pkt->extensions));\n\tpkt->control = FALSE;\n\tpkt->control_ext = FALSE;\n\tpkt->encrypted = FALSE;\n\tpkt->retransmission = FALSE;\n\tpkt->label = packet->label ? g_strdup(packet->label) : NULL;\n\tpkt->protocol = packet->protocol ? g_strdup(packet->protocol) : NULL;\n\tpkt->added = janus_get_monotonic_time();\n\tjanus_ice_queue_packet(handle, pkt);\n}\n#endif\n\nvoid janus_ice_relay_sctp(janus_ice_handle *handle, char *buffer, int length) {\n#ifdef HAVE_SCTP\n\tif(!handle || !handle->pc || handle->queued_packets == NULL || buffer == NULL || length < 1)\n\t\treturn;\n\t/* Find the right medium instance */\n\tjanus_ice_peerconnection_medium *medium = g_hash_table_lookup(handle->pc->media_bytype,\n\t\tGINT_TO_POINTER(JANUS_MEDIA_DATA));\n\tif(!medium)\t/* Queue this packet */\n\t\treturn;\n\t/* Queue this packet */\n\tjanus_ice_queued_packet *pkt = g_malloc(sizeof(janus_ice_queued_packet));\n\tpkt->data = g_malloc(length);\n\tpkt->mindex = medium->mindex;\n\tmemcpy(pkt->data, buffer, length);\n\tpkt->length = length;\n\tpkt->type = JANUS_ICE_PACKET_SCTP;\n\tmemset(&pkt->extensions, 0, sizeof(pkt->extensions));\n\tpkt->control = FALSE;\n\tpkt->control_ext = FALSE;\n\tpkt->encrypted = FALSE;\n\tpkt->retransmission = FALSE;\n\tpkt->label = NULL;\n\tpkt->protocol = NULL;\n\tpkt->added = janus_get_monotonic_time();\n\tjanus_ice_queue_packet(handle, pkt);\n#endif\n}\n\nvoid janus_ice_notify_data_ready(janus_ice_handle *handle) {\n#ifdef HAVE_SCTP\n\tif(!handle || handle->queued_packets == NULL)\n\t\treturn;\n\t/* Queue this event */\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\tg_async_queue_push_front(handle->queued_packets, &janus_ice_data_ready);\n#else\n\tg_async_queue_push(handle->queued_packets, &janus_ice_data_ready);\n#endif\n\tg_main_context_wakeup(handle->mainctx);\n#endif\n}\n\nvoid janus_ice_notify_media_stopped(janus_ice_handle *handle) {\n\tif(!handle || handle->queued_packets == NULL)\n\t\treturn;\n\t/* Queue this event */\n#if GLIB_CHECK_VERSION(2, 46, 0)\n\tg_async_queue_push_front(handle->queued_packets, &janus_ice_media_stopped);\n#else\n\tg_async_queue_push(handle->queued_packets, &janus_ice_media_stopped);\n#endif\n\tg_main_context_wakeup(handle->mainctx);\n}\n\nvoid janus_ice_dtls_handshake_done(janus_ice_handle *handle) {\n\tif(!handle || !handle->pc)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] The DTLS handshake for the component %d in stream %d has been completed\\n\",\n\t\thandle->handle_id, handle->pc->component_id, handle->pc->stream_id);\n\t/* Check if all components are ready */\n\tjanus_mutex_lock(&handle->mutex);\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY)) {\n\t\t/* Already notified */\n\t\tjanus_mutex_unlock(&handle->mutex);\n\t\treturn;\n\t}\n\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY);\n\t/* Create a source for RTCP and one for stats */\n\thandle->rtcp_source = g_timeout_source_new_seconds(1);\n\tg_source_set_priority(handle->rtcp_source, G_PRIORITY_DEFAULT);\n\tg_source_set_callback(handle->rtcp_source, janus_ice_outgoing_rtcp_handle, handle, NULL);\n\tg_source_attach(handle->rtcp_source, handle->mainctx);\n\tif(twcc_period != 1000) {\n\t\t/* The Transport Wide CC feedback period is different, create another source */\n\t\thandle->twcc_source = g_timeout_source_new(twcc_period);\n\t\tg_source_set_priority(handle->twcc_source, G_PRIORITY_DEFAULT);\n\t\tg_source_set_callback(handle->twcc_source, janus_ice_outgoing_transport_wide_cc_feedback, handle, NULL);\n\t\tg_source_attach(handle->twcc_source, handle->mainctx);\n\t}\n\thandle->last_event_stats = 0;\n\thandle->last_srtp_summary = -1;\n\thandle->stats_source = g_timeout_source_new_seconds(1);\n\tg_source_set_callback(handle->stats_source, janus_ice_outgoing_stats_handle, handle, NULL);\n\tg_source_set_priority(handle->stats_source, G_PRIORITY_DEFAULT);\n\tg_source_attach(handle->stats_source, handle->mainctx);\n\tjanus_mutex_unlock(&handle->mutex);\n\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] The DTLS handshake has been completed\\n\", handle->handle_id);\n\t/* Notify the plugin that the WebRTC PeerConnection is ready to be used */\n\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\tif(plugin != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Telling the plugin about it (%s)\\n\", handle->handle_id, plugin->get_name());\n\t\tif(plugin && plugin->setup_media && janus_plugin_session_is_alive(handle->app_handle))\n\t\t\tplugin->setup_media(handle->app_handle);\n\t}\n\t/* Also prepare JSON event to notify user/application */\n\tjanus_session *session = (janus_session *)handle->session;\n\tif(session == NULL)\n\t\treturn;\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"janus\", json_string(\"webrtcup\"));\n\tjson_object_set_new(event, \"session_id\", json_integer(session->session_id));\n\tjson_object_set_new(event, \"sender\", json_integer(handle->handle_id));\n\tif(opaqueid_in_api && handle->opaque_id != NULL)\n\t\tjson_object_set_new(event, \"opaque_id\", json_string(handle->opaque_id));\n\t/* Send the event */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending event to transport...; %p\\n\", handle->handle_id, handle);\n\tjanus_session_notify_event(session, event);\n\t/* Notify event handlers as well */\n\tif(janus_events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"connection\", json_string(\"webrtcup\"));\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_WEBRTC, JANUS_EVENT_SUBTYPE_WEBRTC_STATE,\n\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t}\n\tg_atomic_int_set(&handle->has_pc, 1);\n\tg_atomic_int_inc(&pc_num);\n}\n"
  },
  {
    "path": "src/ice.h",
    "content": "/*! \\file    ice.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Janus handles and ICE/STUN/TURN processing (headers)\n * \\details  A Janus handle represents an abstraction of the communication\n * between a user and a specific plugin, within a Janus session. This is\n * particularly important in terms of media connectivity, as each handle\n * can be associated with a single WebRTC PeerConnection. This code also\n * contains the implementation (based on libnice) of a WebRTC PeerConnection.\n * The code handles the whole ICE process, from the gathering of candidates\n * to the final setup of a virtual channel RTP and RTCP can be transported\n * on. Incoming RTP and RTCP packets from peers are relayed to the associated\n * plugins by means of the incoming_rtp and incoming_rtcp callbacks. Packets\n * to be sent to peers are relayed by peers invoking the relay_rtp and\n * relay_rtcp core callbacks instead.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_ICE_H\n#define JANUS_ICE_H\n\n#include <glib.h>\n#include <agent.h>\n\n#include \"sdp.h\"\n#include \"dtls.h\"\n#include \"sctp.h\"\n#include \"rtcp.h\"\n#include \"text2pcap.h\"\n#include \"utils.h\"\n#include \"ip-utils.h\"\n#include \"refcount.h\"\n#include \"plugins/plugin.h\"\n\n\n/*! \\brief ICE stuff initialization\n * @param[in] ice_lite Whether the ICE Lite mode should be enabled or not\n * @param[in] ice_tcp Whether ICE-TCP support should be enabled or not (only libnice >= 0.1.8, currently broken)\n * @param[in] full_trickle Whether full-trickle must be used (instead of half-trickle)\n * @param[in] ignore_mdns Whether mDNS candidates should be ignored, instead of resolved\n * @param[in] ipv6 Whether IPv6 candidates must be negotiated or not\n * @param[in] ipv6_linklocal Whether IPv6 link-local candidates should be gathered\n * @param[in] rtp_min_port Minimum port to use for RTP/RTCP, if a range is to be used\n * @param[in] rtp_max_port Maximum port to use for RTP/RTCP, if a range is to be used */\nvoid janus_ice_init(gboolean ice_lite, gboolean ice_tcp, gboolean full_trickle, gboolean ignore_mdns,\n\tgboolean ipv6, gboolean ipv6_linklocal, uint16_t rtp_min_port, uint16_t rtp_max_port);\n/*! \\brief ICE stuff de-initialization */\nvoid janus_ice_deinit(void);\n/*! \\brief Method to check whether a STUN server is reachable\n * @param[in] addr Address of the STUN server as a janus_network_address instance\n * @param[in] port Port of the STUN server\n * @param[in] local_port Local port to bind to (0 means random choice)\n * @param[out] public_addr Public address returned by the STUN server as a janus_network_address instance\n * @param[out] public_port Public port returned by the STUN server\n * @returns 0 in case of success, a negative integer on errors */\nint janus_ice_test_stun_server(janus_network_address *addr, uint16_t port, uint16_t local_port, janus_network_address *public_addr, uint16_t *public_port);\n/*! \\brief Method to force Janus to use a STUN server when gathering candidates\n * @param[in] stun_server STUN server address to use\n * @param[in] stun_port STUN port to use\n * @returns 0 in case of success, a negative integer on errors */\nint janus_ice_set_stun_server(gchar *stun_server, uint16_t stun_port);\n/*! \\brief Method to force Janus to use a TURN server when gathering candidates\n * @param[in] turn_server TURN server address to use\n * @param[in] turn_port TURN port to use\n * @param[in] turn_type Relay type (udp, tcp or tls)\n * @param[in] turn_user TURN username, if needed\n * @param[in] turn_pwd TURN password, if needed\n * @returns 0 in case of success, a negative integer on errors */\nint janus_ice_set_turn_server(gchar *turn_server, uint16_t turn_port, gchar *turn_type, gchar *turn_user, gchar *turn_pwd);\n/*! \\brief Method to force Janus to contact a TURN REST API server to get a TURN service to use when gathering candidates.\n * The TURN REST API takes precedence over any static credential passed via janus_ice_set_turn_server\n * @note Requires libcurl to be available, and a working TURN REST API backend (see turnrest.h)\n * @param[in] api_server TURN REST API backend (NULL to disable the API)\n * @param[in] api_key API key to use, if required\n * @param[in] api_method HTTP method to use (POST by default)\n * @param[in] api_timeout total timeout for HTTP method in seconds\n * @returns 0 in case of success, a negative integer on errors */\nint janus_ice_set_turn_rest_api(gchar *api_server, gchar *api_key, gchar *api_method, uint api_timeout);\n/*! \\brief Method to get the STUN server IP address\n * @returns The currently used STUN server IP address, if available, or NULL if not */\nchar *janus_ice_get_stun_server(void);\n/*! \\brief Method to get the STUN server port\n * @returns The currently used STUN server port, if available, or 0 if not */\nuint16_t janus_ice_get_stun_port(void);\n/*! \\brief Method to get the TURN server IP address\n * @returns The currently used TURN server IP address, if available, or NULL if not */\nchar *janus_ice_get_turn_server(void);\n/*! \\brief Method to get the TURN server port\n * @returns The currently used TURN server port, if available, or 0 if not */\nuint16_t janus_ice_get_turn_port(void);\n/*! \\brief Method to get the specified TURN REST API backend, if any\n * @returns The currently specified  TURN REST API backend, if available, or NULL if not */\nchar *janus_ice_get_turn_rest_api(void);\n/*! \\brief Method to enable applications to force Janus to use TURN */\nvoid janus_ice_allow_force_relay(void);\n/*! \\brief Method to check whether applications are allowed to force Janus to use TURN\n * @returns TRUE if they're allowed, FALSE otherwise */\ngboolean janus_ice_is_force_relay_allowed(void);\n/*! \\brief Helper method to force Janus to overwrite all host candidates with the public IP\n * @param[in] keep_private_host Whether we should keep the original private host as a separate candidate, or replace it */\nvoid janus_ice_enable_nat_1_1(gboolean keep_private_host);\n/*! \\brief Method to add an interface/IP to the enforce list for ICE (that is, only gather candidates from these and ignore the others)\n * \\note This method is especially useful to speed up the ICE gathering process on the server: in fact,\n * if you know in advance which interface must be used (e.g., the main interface connected to the internet),\n * adding it to the enforce list will prevent libnice from gathering candidates from other interfaces.\n * If you're interested in excluding interfaces explicitly, instead, check janus_ice_ignore_interface.\n * @param[in] ip Interface/IP to enforce (e.g., 192.168. or eth0) */\nvoid janus_ice_enforce_interface(const char *ip);\n/*! \\brief Method to check whether an interface is currently in the enforce list for ICE (that is, won't have candidates)\n * @param[in] ip Interface/IP to check (e.g., 192.168.244.1 or eth1)\n * @returns true if the interface/IP is in the enforce list, false otherwise */\ngboolean janus_ice_is_enforced(const char *ip);\n/*! \\brief Method to add an interface/IP to the ignore list for ICE (that is, don't gather candidates)\n * \\note This method is especially useful to speed up the ICE gathering process on the server: in fact,\n * if you know in advance an interface is not going to be used (e.g., one of those created by VMware),\n * adding it to the ignore list will prevent libnice from gathering a candidate for it.\n * Unlike the enforce list, the ignore list also accepts IP addresses, partial or complete.\n * If you're interested in only using specific interfaces, instead, check janus_ice_enforce_interface.\n * @param[in] ip Interface/IP to ignore (e.g., 192.168. or eth1) */\nvoid janus_ice_ignore_interface(const char *ip);\n/*! \\brief Method to check whether an interface/IP is currently in the ignore list for ICE (that is, won't have candidates)\n * @param[in] ip Interface/IP to check (e.g., 192.168.244.1 or eth1)\n * @returns true if the interface/IP is in the ignore list, false otherwise */\ngboolean janus_ice_is_ignored(const char *ip);\n/*! \\brief Method to check whether ICE Lite mode is enabled or not (still WIP)\n * @returns true if ICE-TCP support is enabled/supported, false otherwise */\ngboolean janus_ice_is_ice_lite_enabled(void);\n/*! \\brief Method to check whether ICE-TCP support is enabled/supported or not (still WIP)\n * @returns true if ICE-TCP support is enabled/supported, false otherwise */\ngboolean janus_ice_is_ice_tcp_enabled(void);\n/*! \\brief Method to check whether full-trickle support is enabled or not\n * @returns true if full-trickle support is enabled, false otherwise */\ngboolean janus_ice_is_full_trickle_enabled(void);\n/*! \\brief Method to check whether mDNS resolution is enabled or not\n * @returns true if mDNS resolution is enabled, false otherwise */\ngboolean janus_ice_is_mdns_enabled(void);\n/*! \\brief Method to check whether IPv6 candidates are enabled/supported or not\n * @returns true if IPv6 candidates are enabled/supported, false otherwise */\ngboolean janus_ice_is_ipv6_enabled(void);\n/*! \\brief Method to check whether IPv6 link-local candidates will be gathered or not\n * \\note This obviously only makes sense if IPv6 support is enabled in general\n * @returns true if IPv6 link-local candidates will be gathered, false otherwise */\ngboolean janus_ice_is_ipv6_linklocal_enabled(void);\n#ifdef HAVE_ICE_NOMINATION\n/*! \\brief Method to configure the ICE nomination mode (regular or aggressive)\n * @param[in] nomination The ICE nomination mode (regular or aggressive) */\nvoid janus_ice_set_nomination_mode(const char *nomination);\n/*! \\brief Method to return a string description of the configured ICE nomination mode\n * @returns \"regular\" or \"aggressive\" */\nconst char *janus_ice_get_nomination_mode(void);\n#endif\n/*! \\brief Method to enable/disable consent freshness in PeerConnections.\n * \\note This is only available on libnice >= 0.1.19, and automatically enables\n * keepalive connectivity checks too. Documentation for the setting:\n * https://libnice.freedesktop.org/libnice/NiceAgent.html#NiceAgent--consent-freshness\n * @param[in] enabled Whether the functionality should be enabled or disabled */\nvoid janus_ice_set_consent_freshness_enabled(gboolean enabled);\n/*! \\brief Method to check whether consent fresnhess will be enabled in ICE\n * @returns true if enabled, false (default) otherwise */\ngboolean janus_ice_is_consent_freshness_enabled(void);\n/*! \\brief Method to enable/disable connectivity checks as keepalives for PeerConnections.\n * \\note The main rationale behind this setting is provided in the libnice documentation:\n * https://libnice.freedesktop.org/libnice/NiceAgent.html#NiceAgent--keepalive-conncheck\n * @param[in] enabled Whether the functionality should be enabled or disabled */\nvoid janus_ice_set_keepalive_conncheck_enabled(gboolean enabled);\n/*! \\brief Method to check whether connectivity checks will be used as keepalives\n * @returns true if enabled, false (default) otherwise */\ngboolean janus_ice_is_keepalive_conncheck_enabled(void);\n/*! \\brief Method to enable/disable immediate hangups of PeerConnectionss on ICE failures.\n * @param[in] enabled Whether the functionality should be enabled or disabled */\nvoid janus_ice_set_hangup_on_failed_enabled(gboolean enabled);\n/*! \\brief Method to check whether ICE failures will result in immediate hangups\n * @returns true if enabled, false (default) otherwise */\ngboolean janus_ice_is_hangup_on_failed_enabled(void);\n/*! \\brief Method to modify the min NACK value (i.e., the minimum time window of packets per handle to store for retransmissions)\n * @param[in] mnq The new min NACK value */\nvoid janus_set_min_nack_queue(uint16_t mnq);\n/*! \\brief Method to get the current min NACK value (i.e., the minimum time window of packets per handle to store for retransmissions)\n * @returns The current min NACK value */\nuint16_t janus_get_min_nack_queue(void);\n/*! \\brief Method to enable/disable the NACK optimizations on outgoing keyframes: when\n * enabled, the NACK buffer for a PeerConnection is cleaned any time Janus sends a\n * keyframe, as any missing packet won't be needed since the keyframe will allow the\n * media recipient to still restore a complete image anyway. Since this optimization\n * seems to cause some issues in some edge cases, it's disabled by default.\n * @param[in] optimize Whether the optimization should be enabled or disabled */\nvoid janus_set_nack_optimizations_enabled(gboolean optimize);\n/*! \\brief Method to check whether NACK optimizations on outgoing keyframes are enabled or not\n * @returns optimize if optimizations are enabled, false otherwise */\ngboolean janus_is_nack_optimizations_enabled(void);\n/*! \\brief Method to modify the no-media event timer (i.e., the number of seconds where no media arrives before Janus notifies this)\n * @param[in] timer The new timer value, in seconds */\nvoid janus_set_no_media_timer(uint timer);\n/*! \\brief Method to get the current no-media event timer (see above)\n * @returns The current no-media event timer */\nuint janus_get_no_media_timer(void);\n/*! \\brief Method to modify the slowlink-threshold property (i.e., the number of lost packets per seconds that should trigger a slow-link event)\n * @param[in] packets The new value, in lost packets per seconds */\nvoid janus_set_slowlink_threshold(uint packets);\n/*! \\brief Method to get the current slowlink-threshold value (see above)\n * @returns The current slowlink-threshold value */\nuint janus_get_slowlink_threshold(void);\n/*! \\brief Method to modify the TWCC feedback period (i.e., how often TWCC feedback is sent back to media senders)\n * @param[in] period The new period value, in milliseconds */\nvoid janus_set_twcc_period(uint period);\n/*! \\brief Method to get the current TWCC period (see above)\n * @returns The current TWCC period */\nuint janus_get_twcc_period(void);\n/*! \\brief Method to modify the DSCP value to set, which is disabled by default\n * @param[in] dscp The new DSCP value (0 to disable) */\nvoid janus_set_dscp(int dscp);\n/*! \\brief Method to get the current DSCP value (see above)\n * @returns The current DSCP value (0 if disabled) */\nint janus_get_dscp(void);\n/*! \\brief Method to modify the event handler statistics period (i.e., the number of seconds that should pass before Janus notifies event handlers about media statistics for a PeerConnection)\n * @param[in] period The new period value, in seconds */\nvoid janus_ice_set_event_stats_period(int period);\n/*! \\brief Method to get the current event handler statistics period (see above)\n * @returns The current event handler stats period */\nint janus_ice_get_event_stats_period(void);\n/*! \\brief Method to get the number of active PeerConnection (for stats)\n * @returns The current number of active PeerConnections */\nint janus_ice_get_peerconnection_num(void);\n\n/*! \\brief Method to define whether the media stats shall be dispatched in one event (true) or in dedicated single events (false - default)\n * @param[in] combine_media_stats_to_one_event true to combine media statistics in on event or false to send dedicated events */\nvoid janus_ice_event_set_combine_media_stats(gboolean combine_media_stats_to_one_event);\n/*! \\brief Method to retrieve whether media statistic events shall be dispatched combined or in single events\n * @returns true to combine events */\ngboolean janus_ice_event_get_combine_media_stats(void);\n\n/*! \\brief Method to enable opaque ID in Janus API responses/events */\nvoid janus_enable_opaqueid_in_api(void);\n/*! \\brief Method to check whether opaque ID have to be added to Janus API responses/events\n * @returns TRUE if they need to be present, FALSE otherwise */\ngboolean janus_is_opaqueid_in_api_enabled(void);\n\n\n/*! \\brief Helper method to get a string representation of a libnice ICE state\n * @param[in] state The libnice ICE state\n * @returns A string representation of the libnice ICE state */\nconst gchar *janus_get_ice_state_name(gint state);\n\n\n/*! \\brief Janus ICE handle in a session */\ntypedef struct janus_ice_handle janus_ice_handle;\n/*! \\brief WebRTC PeerConnection associated to a specific handle */\ntypedef struct janus_ice_peerconnection janus_ice_peerconnection;\n/*! \\brief A single medium (i.e., m-line) in a Janus handle PeerConnection: can be bidirectional */\ntypedef struct janus_ice_peerconnection_medium janus_ice_peerconnection_medium;\n/*! \\brief Helper to handle pending trickle candidates (e.g., when we're still waiting for an offer) */\ntypedef struct janus_ice_trickle janus_ice_trickle;\n\n#define JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER\t(1 << 0)\n#define JANUS_ICE_HANDLE_WEBRTC_START\t\t\t\t(1 << 1)\n#define JANUS_ICE_HANDLE_WEBRTC_READY\t\t\t\t(1 << 2)\n#define JANUS_ICE_HANDLE_WEBRTC_STOP\t\t\t\t(1 << 3)\n#define JANUS_ICE_HANDLE_WEBRTC_ALERT\t\t\t\t(1 << 4)\n#define JANUS_ICE_HANDLE_WEBRTC_NEGOTIATED\t\t\t(1 << 5)\n#define JANUS_ICE_HANDLE_WEBRTC_TRICKLE\t\t\t\t(1 << 7)\n#define JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES\t\t(1 << 8)\n#define JANUS_ICE_HANDLE_WEBRTC_TRICKLE_SYNCED\t\t(1 << 9)\n#define JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS\t\t(1 << 10)\n#define JANUS_ICE_HANDLE_WEBRTC_CLEANING\t\t\t(1 << 11)\n#define JANUS_ICE_HANDLE_WEBRTC_HAS_AUDIO\t\t\t(1 << 12)\n#define JANUS_ICE_HANDLE_WEBRTC_HAS_VIDEO\t\t\t(1 << 13)\n#define JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER\t\t\t(1 << 14)\n#define JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER\t\t\t(1 << 15)\n#define JANUS_ICE_HANDLE_WEBRTC_HAS_AGENT\t\t\t(1 << 16)\n#define JANUS_ICE_HANDLE_WEBRTC_ICE_RESTART\t\t\t(1 << 17)\n#define JANUS_ICE_HANDLE_WEBRTC_RESEND_TRICKLES\t\t(1 << 18)\n#define JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX\t\t\t(1 << 19)\n#define JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP\t(1 << 20)\n#define JANUS_ICE_HANDLE_WEBRTC_E2EE\t\t\t\t(1 << 21)\n\n\n/*! \\brief Janus media types */\ntypedef enum janus_media_type {\n\tJANUS_MEDIA_UNKNOWN = 0,\n\tJANUS_MEDIA_AUDIO,\n\tJANUS_MEDIA_VIDEO,\n\tJANUS_MEDIA_DATA\n} janus_media_type;\n/*! \\brief Helper method to get the string associated to a janus_media_mtype value\n * @param[in] type The type to stringify\n * @returns The type as a string, if valid, or NULL otherwise */\nconst char *janus_media_type_str(janus_media_type type);\n\n/*! \\brief Janus media statistics\n * \\note To improve with more stuff */\ntypedef struct janus_ice_stats_info {\n\t/*! \\brief Packets sent or received */\n\tguint32 packets;\n\t/*! \\brief Bytes sent or received */\n\tguint64 bytes;\n\t/*! \\brief Bytes sent or received in the last second */\n\tguint32 bytes_lastsec, bytes_lastsec_temp;\n\t/*! \\brief Time we last updated the last second counter */\n\tgint64 updated;\n\t/*! \\brief Whether or not we notified about lastsec issues already */\n\tgboolean notified_lastsec;\n\t/*! \\brief Number of NACKs sent or received */\n\tguint32 nacks;\n} janus_ice_stats_info;\n\n/*! \\brief Janus media statistics container\n * \\note To improve with more stuff */\ntypedef struct janus_ice_stats {\n\t/*! \\brief Media stats info (considering we may be simulcasting) */\n\tjanus_ice_stats_info info[3];\n\t/*! \\brief Last known count of lost packets (for slow_link) */\n\tguint sl_lost_count;\n} janus_ice_stats;\n\n/*! \\brief Quick helper method to notify a WebRTC hangup through the Janus API\n * @param handle The janus_ice_handle instance this event refers to\n * @param reason A description of why this happened */\nvoid janus_ice_notify_hangup(janus_ice_handle *handle, const char *reason);\n\n\n/*! \\brief Quick helper method to check if a plugin session associated with a Janus handle is still valid\n * @param plugin_session The janus_plugin_session instance to validate\n * @returns true if the plugin session is valid, false otherwise */\ngboolean janus_plugin_session_is_alive(janus_plugin_session *plugin_session);\n\n\n/*! \\brief A helper struct for determining when to send NACKs */\ntypedef struct janus_seq_info {\n\tgint64 ts;\n\tguint16 seq;\n\tguint16 state;\n\tstruct janus_seq_info *next;\n\tstruct janus_seq_info *prev;\n} janus_seq_info;\nvoid janus_seq_list_free(janus_seq_info **head);\nenum {\n\tSEQ_MISSING,\n\tSEQ_NACKED,\n\tSEQ_GIVEUP,\n\tSEQ_RECVED\n};\n\n\n/*! \\brief Janus ICE handle */\nstruct janus_ice_handle {\n\t/*! \\brief Opaque pointer to the core/peer session */\n\tvoid *session;\n\t/*! \\brief Handle identifier, guaranteed to be non-zero */\n\tguint64 handle_id;\n\t/*! \\brief Opaque identifier, e.g., to provide inter-handle relationships to external tools */\n\tchar *opaque_id;\n\t/*! \\brief Token that was used to attach the handle, if required */\n\tchar *token;\n\t/*! \\brief Monotonic time of when the handle has been created */\n\tgint64 created;\n\t/*! \\brief Opaque application (plugin) pointer */\n\tvoid *app;\n\t/*! \\brief Opaque core/plugin session pointer */\n\tjanus_plugin_session *app_handle;\n\t/*! \\brief Mask of WebRTC-related flags for this handle */\n\tjanus_flags webrtc_flags;\n\t/*! \\brief Number of gathered candidates */\n\tgint cdone;\n\t/*! \\brief GLib context for the handle and libnice */\n\tGMainContext *mainctx;\n\t/*! \\brief GLib loop for the handle and libnice */\n\tGMainLoop *mainloop;\n\t/*! \\brief In case static event loops are used, opaque pointer to the loop */\n\tvoid *static_event_loop;\n\t/*! \\brief GLib thread for the handle and libnice */\n\tGThread *thread;\n\t/*! \\brief GLib sources for outgoing traffic, recurring RTCP, and stats (and optionally TWCC) */\n\tGSource *rtp_source, *rtcp_source, *stats_source, *twcc_source;\n\t/*! \\brief libnice ICE agent */\n\tNiceAgent *agent;\n\t/*! \\brief Monotonic time of when the ICE agent has been created */\n\tgint64 agent_created;\n\t/*! \\brief Monotonic time of when the ICE agent has been started (remote credentials set) */\n\tgint64 agent_started;\n\t/*! \\brief ICE role (controlling or controlled) */\n\tgboolean controlling;\n\t/*! \\brief Main mid */\n\tgchar *pc_mid;\n\t/*! \\brief ICE Stream ID */\n\tguint stream_id;\n\t/*! \\brief WebRTC PeerConnection, if any */\n\tjanus_ice_peerconnection *pc;\n\t/*! \\brief RTP profile set by caller (so that we can match it) */\n\tgchar *rtp_profile;\n\t/*! \\brief SDP generated locally (just for debugging purposes) */\n\tgchar *local_sdp;\n\t/*! \\brief SDP received by the peer (just for debugging purposes) */\n\tgchar *remote_sdp;\n\t/*! \\brief Reason this handle has been hung up*/\n\tconst gchar *hangup_reason;\n\t/*! \\brief List of pending trickle candidates (those we received before getting the JSEP offer) */\n\tGList *pending_trickles;\n\t/*! \\brief Queue of remote candidates that still need to be processed */\n\tGAsyncQueue *queued_candidates;\n\t/*! \\brief Queue of events in the loop and outgoing packets to send */\n\tGAsyncQueue *queued_packets;\n\t/*! \\brief Count of the recent SRTP replay errors, in order to avoid spamming the logs */\n\tguint srtp_errors_count;\n\t/*! \\brief Count of the recent SRTP replay errors, in order to avoid spamming the logs */\n\tgint last_srtp_error, last_srtp_summary;\n\t/*! \\brief Count of how many seconds passed since the last stats passed to event handlers */\n\tgint last_event_stats;\n\t/*! \\brief Flag to decide whether or not packets need to be dumped to a text2pcap file */\n\tvolatile gint dump_packets;\n\t/*! \\brief In case this session must be saved to text2pcap, the instance to dump packets to */\n\tjanus_text2pcap *text2pcap;\n\t/*! \\brief Mutex to lock/unlock the ICE session */\n\tjanus_mutex mutex;\n\t/*! \\brief Atomic flag to check whether a PeerConnection was established */\n\tvolatile gint has_pc;\n\t/*! \\brief Whether a close_pc was requested recently on the PeerConnection */\n\tvolatile gint closepc;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n};\n\n/*! \\brief Janus handle WebRTC PeerConnection */\nstruct janus_ice_peerconnection {\n\t/*! \\brief Janus ICE handle this stream belongs to */\n\tjanus_ice_handle *handle;\n\t/*! \\brief libnice ICE stream ID */\n\tguint stream_id;\n\t/*! \\brief libnice ICE component ID */\n\tguint component_id;\n\t/*! \\brief Whether this stream is ready to be used */\n\tgboolean cdone;\n\t/*! \\brief libnice ICE component state */\n\tguint state;\n\t/*! \\brief Monotonic time of when gathering has completed */\n\tgint64 gathered;\n\t/*! \\brief Monotonic time of when ICE has successfully connected */\n\tgint64 connected;\n\t/*! \\brief GLib list of libnice remote candidates for this component */\n\tGSList *candidates;\n\t/*! \\brief GLib list of local candidates for this component (summary) */\n\tGSList *local_candidates;\n\t/*! \\brief GLib list of remote candidates for this component (summary) */\n\tGSList *remote_candidates;\n\t/*! \\brief String representation of the selected pair as notified by libnice (foundations) */\n\tgchar *selected_pair;\n\t/*! \\brief Whether the setup of remote candidates for this component has started or not */\n\tgboolean process_started;\n\t/*! \\brief Timer to check when we should consider ICE as failed */\n\tGSource *icestate_source;\n\t/*! \\brief Time of when we first detected an ICE failed (we'll need this for the timer above) */\n\tgint64 icefailed_detected;\n\t/*! \\brief Re-transmission timer for DTLS */\n\tGSource *dtlsrt_source;\n\t/*! \\brief DTLS-SRTP stack */\n\tjanus_dtls_srtp *dtls;\n\t/*! \\brief SDES mid RTP extension ID */\n\tgint mid_ext_id;\n\t/*! \\brief RTP Stream extension ID, and the related rtx one */\n\tgint rid_ext_id, ridrtx_ext_id;\n\t/*! \\brief Audio levels extension ID */\n\tgint audiolevel_ext_id;\n\t/*! \\brief Video orientation extension ID */\n\tgint videoorientation_ext_id;\n\t/*! \\brief Playout delay extension ID */\n\tgint playoutdelay_ext_id;\n\t/*! \\brief Dependency descriptor extension ID */\n\tgint dependencydesc_ext_id;\n\t/*! \\brief Absolute Send Time ext ID */\n\tgint abs_send_time_ext_id;\n\t/*! \\brief Absolute Capture Time ext ID */\n\tgint abs_capture_time_ext_id;\n\t/*! \\brief Video Layers Allocation ext ID */\n\tgint videolayers_ext_id;\n\t/*! \\brief Whether we do transport wide cc */\n\tgboolean do_transport_wide_cc;\n\t/*! \\brief Transport wide cc rtp ext ID */\n\tgint transport_wide_cc_ext_id;\n\t/*! \\brief Last sent transport wide seq num */\n\tguint16 transport_wide_cc_out_seq_num;\n\t/*! \\brief Last received transport wide seq num */\n\tguint32 transport_wide_cc_last_seq_num;\n\t/*! \\brief Last transport wide seq num sent on feedback */\n\tguint32 transport_wide_cc_last_feedback_seq_num;\n\t/*! \\brief Transport wide cc transport seq num wrap cycles */\n\tguint16 transport_wide_cc_cycles;\n\t/*! \\brief Transport wide cc rtp ext ID */\n\tguint transport_wide_cc_feedback_count;\n\t/*! \\brief GLib list of transport wide cc stats in reverse received order */\n\tGSList *transport_wide_received_seq_nums;\n\t/*! \\brief Latest REMB feedback we received */\n\tuint32_t remb_bitrate;\n\t/*! \\brief DTLS role of the server for this stream */\n\tjanus_dtls_role dtls_role;\n\t/*! \\brief Data exchanged for DTLS handshakes and messages */\n\tjanus_ice_stats dtls_in_stats, dtls_out_stats;\n\t/*! \\brief Hashing algorithm used by the peer for the DTLS certificate (e.g., \"SHA-256\") */\n\tgchar *remote_hashing;\n\t/*! \\brief Hashed fingerprint of the peer's certificate, as parsed in SDP */\n\tgchar *remote_fingerprint;\n\t/*! \\brief The ICE username for this stream */\n\tgchar *ruser;\n\t/*! \\brief The ICE password for this stream */\n\tgchar *rpass;\n\t/*! \\brief GLib hash table of media (m-line indexes are the keys) */\n\tGHashTable *media;\n\t/*! \\brief GLib hash table of media (SSRCs are the keys) */\n\tGHashTable *media_byssrc;\n\t/*! \\brief GLib hash table of media (mids are the keys) */\n\tGHashTable *media_bymid;\n\t/*! \\brief GLib hash table of media (media types are the keys)\n\t * @note This is just a convenience hash table to track the very first audio\n\t * or video m-line, in order to make it easier for plugins that don't do\n\t * multistream. That said, we don't plan to keep it forever */\n\tGHashTable *media_bytype;\n\t/*! \\brief List of payload types we can expect */\n\tGHashTable *payload_types;\n\t/*! \\brief Mapping of payload types to their clock rates, as advertised in the SDP */\n\tGHashTable *clock_rates;\n\t/*! \\brief Mapping of rtx payload types to actual media-related packet types */\n\tGHashTable *rtx_payload_types;\n\t/*! \\brief Reverse mapping of rtx payload types to actual media-related packet types */\n\tGHashTable *rtx_payload_types_rev;\n\t/*! \\brief Helper queue for storing requested packets from NACKs */\n\tGQueue *nacks_queue;\n\t/*! \\brief Helper flag to avoid flooding the console with the same error all over again */\n\tgboolean noerrorlog;\n\t/*! \\brief Mutex to lock/unlock this stream */\n\tjanus_mutex mutex;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n};\n\n#define LAST_SEQS_MAX_LEN 160\n/*! \\brief A single media in a PeerConnection */\nstruct janus_ice_peerconnection_medium {\n\t/*! \\brief WebRTC PeerConnection this m-line belongs to */\n\tjanus_ice_peerconnection *pc;\n\t/*! \\brief Type of this medium */\n\tjanus_media_type type;\n\t/*! \\brief Index of this medium in the media list */\n\tint mindex;\n\t/*! \\brief Media ID */\n\tchar *mid;\n\t/*! \\brief Media Stream ID info */\n\tchar *msid, *mstid, *remote_msid, *remote_mstid;\n\t/*! \\brief SSRC of the server for this medium */\n\tguint32 ssrc;\n\t/*! \\brief Retransmission SSRC of the server for this medium */\n\tguint32 ssrc_rtx;\n\t/*! \\brief SSRC(s) of the peer for this medium (may be simulcasting) */\n\tguint32 ssrc_peer[3], ssrc_peer_new[3], ssrc_peer_orig[3], ssrc_peer_temp;\n\t/*! \\brief Retransmissions SSRC(s) of the peer for this medium (may be simulcasting) */\n\tguint32 ssrc_peer_rtx[3], ssrc_peer_rtx_new[3], ssrc_peer_rtx_orig[3];\n\t/*! \\brief Array of RTP Stream IDs (for simulcasting, if enabled) */\n\tchar *rid[3];\n\t/*! \\brief Which simulcast rids are currently disabled, as per the latest negotiation */\n\tgboolean disabled_rid[3];\n\t/*! \\brief Whether the order of the rids in the SDP will be h-m-l (TRUE) or l-m-h (FALSE) */\n\tgboolean rids_hml;\n\t/*! \\brief Whether we should use the legacy simulcast syntax (a=simulcast:recv rid=..) or the proper one (a=simulcast:recv ..) */\n\tgboolean legacy_rid;\n\t/*! \\brief RTP switching context(s) in case of renegotiations (audio+video and/or simulcast) */\n\tjanus_rtp_switching_context rtp_ctx[3];\n\t/*! \\brief List of payload types we can expect */\n\tGList *payload_types;\n\t/*! \\brief opus/red payload type, if enabled */\n\tint opusred_pt;\n\t/*! \\brief Mapping of rtx payload types to actual media-related packet types */\n\tGHashTable *rtx_payload_types;\n\t/*! \\brief Mapping of payload types to their clock rates, as advertised in the SDP */\n\tGHashTable *clock_rates;\n\t/*! \\brief RTP payload types for this medium */\n\tgint payload_type, rtx_payload_type;\n\t/*! \\brief Codec used in this medium */\n\tchar *codec;\n\t/*! \\brief Pointer to function to check if a packet is a keyframe (depends on negotiated codec; video only) */\n\tgboolean (* video_is_keyframe)(const char* buffer, int len);\n\t/*! \\brief Media direction */\n\tgboolean send, recv;\n\t/*! \\brief RTCP context(s) for the medium (may be simulcasting) */\n\tjanus_rtcp_context *rtcp_ctx[3];\n\t/*! \\brief Size of the NACK queue (in ms), dynamically updated per the RTT */\n\tuint16_t nack_queue_ms;\n\t/*! \\brief Map(s) of the NACKed packets (to track retransmissions and avoid duplicates) */\n\tGHashTable *rtx_nacked[3];\n\t/*! \\brief Map of the pending NACKed cleanup callback */\n\tGHashTable *pending_nacked_cleanup;\n\t/*! \\brief First received NTP timestamp */\n\tgint64 first_ntp_ts[3];\n\t/*! \\brief First received RTP timestamp */\n\tguint32 first_rtp_ts[3];\n\t/*! \\brief Last sent NTP timestamp */\n\tgint64 last_ntp_ts;\n\t/*! \\brief Last sent RTP timestamp */\n\tguint32 last_rtp_ts;\n\t/*! \\brief Whether we should do NACKs (in or out) for this medium */\n\tgboolean do_nacks;\n\t/*! \\brief List of previously sent janus_rtp_packet RTP packets, in case we receive NACKs */\n\tGQueue *retransmit_buffer;\n\t/*! \\brief HashTable of retransmittable sequence numbers, in case we receive NACKs */\n\tGHashTable *retransmit_seqs;\n\t/*! \\brief Current sequence number for the RFC4588 rtx SSRC session */\n\tguint16 rtx_seq_number;\n\t/*! \\brief Last time a log message about sending retransmits was printed */\n\tgint64 retransmit_log_ts;\n\t/*! \\brief Number of retransmitted packets since last log message */\n\tguint retransmit_recent_cnt;\n\t/*! \\brief Last time a log message about sending NACKs was printed */\n\tgint64 nack_sent_log_ts;\n\t/*! \\brief Number of NACKs sent since last log message */\n\tguint nack_sent_recent_cnt;\n\t/*! \\brief List of recently received sequence numbers (as a support to NACK generation, for each simulcast SSRC) */\n\tjanus_seq_info *last_seqs[3];\n\t/*! \\brief Stats for incoming data (audio/video/data) */\n\tjanus_ice_stats in_stats;\n\t/*! \\brief Stats for outgoing data (audio/video/data) */\n\tjanus_ice_stats out_stats;\n\t/*! \\brief Helper flag to avoid flooding the console with the same error all over again */\n\tgboolean noerrorlog;\n\t/*! \\brief Mutex to lock/unlock this medium */\n\tjanus_mutex mutex;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n};\n/*! \\brief Method to quickly create a medium to be added to a handle PeerConnection\n * @note This will autogenerate SSRCs, if needed\n * @param[in] handle The Janus handle instance to add the medium to\n * @param[in] type The medium type\n * @returns A pointer to the new medium, if successful, or NULL otherwise */\njanus_ice_peerconnection_medium *janus_ice_peerconnection_medium_create(janus_ice_handle *handle, janus_media_type type);\n\n/*! \\brief Helper to handle pending trickle candidates (e.g., when we're still waiting for an offer) */\nstruct janus_ice_trickle {\n\t/*! \\brief Janus ICE handle this trickle candidate belongs to */\n\tjanus_ice_handle *handle;\n\t/*! \\brief Monotonic time of when this trickle candidate has been received */\n\tgint64 received;\n\t/*! \\brief Janus API transaction ID of the original trickle request */\n\tchar *transaction;\n\t/*! \\brief JSON object of the trickle candidate(s) */\n\tjson_t *candidate;\n};\n\n/** @name Janus ICE trickle candidates methods\n */\n///@{\n/*! \\brief Helper method to allocate a janus_ice_trickle instance\n * @param[in] transaction The Janus API ID of the original trickle request\n * @param[in] candidate The trickle candidate, as a Jansson object\n * @returns a pointer to the new instance, if successful, NULL otherwise */\njanus_ice_trickle *janus_ice_trickle_new(const char *transaction, json_t *candidate);\n/*! \\brief Helper method to parse trickle candidates\n * @param[in] handle The Janus ICE handle this candidate belongs to\n * @param[in] candidate The trickle candidate to parse, as a Jansson object\n * @param[in,out] error Error string describing the failure, if any\n * @returns 0 in case of success, any code from apierror.h in case of failure */\ngint janus_ice_trickle_parse(janus_ice_handle *handle, json_t *candidate, const char **error);\n/*! \\brief Helper method to destroy a janus_ice_trickle instance\n * @param[in] trickle The janus_ice_trickle instance to destroy */\nvoid janus_ice_trickle_destroy(janus_ice_trickle *trickle);\n///@}\n\n\n/** @name Janus ICE handle methods\n */\n///@{\n/*! \\brief Method to create a new Janus ICE handle\n * @param[in] core_session The core/peer session this ICE handle will belong to\n * @param[in] opaque_id The opaque identifier provided by the creator, if any (optional)\n * @param[in] token The auth token provided by the creator, if any (optional)\n * @returns The created Janus ICE handle if successful, NULL otherwise */\njanus_ice_handle *janus_ice_handle_create(void *core_session, const char *opaque_id, const char *token);\n/*! \\brief Method to attach a Janus ICE handle to a plugin\n * \\details This method is very important, as it allows plugins to send/receive media (RTP/RTCP) to/from a WebRTC peer.\n * @param[in] core_session The core/peer session this ICE handle belongs to\n * @param[in] handle The Janus ICE handle\n * @param[in] plugin The plugin the ICE handle needs to be attached to\n * @param[in] loop_index In case static event loops are used, an indication on which loop to use for this handle\n * (-1 will let the core pick one; in case API selection is disabled in the settings, this value is ignored)\n * @returns 0 in case of success, a negative integer otherwise */\ngint janus_ice_handle_attach_plugin(void *core_session, janus_ice_handle *handle, janus_plugin *plugin, int loop_index);\n/*! \\brief Method to destroy a Janus ICE handle\n * @param[in] core_session The core/peer session this ICE handle belongs to\n * @param[in] handle The Janus ICE handle to destroy\n * @returns 0 in case of success, a negative integer otherwise */\ngint janus_ice_handle_destroy(void *core_session, janus_ice_handle *handle);\n/*! \\brief Method to only hangup (e.g., DTLS alert) the WebRTC PeerConnection allocated by a Janus ICE handle\n * @param[in] handle The Janus ICE handle instance managing the WebRTC PeerConnection to hangup\n * @param[in] reason A description of why this happened */\nvoid janus_ice_webrtc_hangup(janus_ice_handle *handle, const char *reason);\n/*! \\brief Method to only free resources related to a specific Webrtc PeerConnection allocated by a Janus ICE handle\n * @param[in] pc The Janus ICE component instance to free */\nvoid janus_ice_peerconnection_destroy(janus_ice_peerconnection *pc);\n///@}\n\n\n/** @name Janus ICE media relaying callbacks\n */\n///@{\n/*! \\brief Core RTP callback, called when a plugin has an RTP packet to send to a peer\n * @param[in] handle The Janus ICE handle associated with the peer\n * @param[in] packet The RTP packet to send */\nvoid janus_ice_relay_rtp(janus_ice_handle *handle, janus_plugin_rtp *packet);\n/*! \\brief Core RTCP callback, called when a plugin has an RTCP message to send to a peer\n * @param[in] handle The Janus ICE handle associated with the peer\n * @param[in] packet The RTCP message to send */\nvoid janus_ice_relay_rtcp(janus_ice_handle *handle, janus_plugin_rtcp *packet);\n/*! \\brief Core SCTP/DataChannel callback, called when a plugin has data to send to a peer\n * @param[in] handle The Janus ICE handle associated with the peer\n * @param[in] packet The message to send */\nvoid janus_ice_relay_data(janus_ice_handle *handle, janus_plugin_data *packet);\n/*! \\brief Helper core callback, called when a plugin wants to send a RTCP PLI to a peer\n * @param[in] handle The Janus ICE handle associated with the peer */\nvoid janus_ice_send_pli(janus_ice_handle *handle);\n/*! \\brief Helper core callback, called when a plugin wants to send a RTCP PLI to a single video stream of a peer\n * @param[in] handle The Janus ICE handle associated with the peer\n * @param[in] mindex Index of the stream to send the PLI to (relative to the SDP) */\nvoid janus_ice_send_pli_stream(janus_ice_handle *handle, int mindex);\n/*! \\brief Helper core callback, called when a plugin wants to send a RTCP REMB to a peer\n * @param[in] handle The Janus ICE handle associated with the peer\n * @param[in] bitrate The bitrate value to put in the REMB message */\nvoid janus_ice_send_remb(janus_ice_handle *handle, uint32_t bitrate);\n/*! \\brief Plugin SCTP/DataChannel callback, called by the SCTP stack when when there's data for a plugin\n * @param[in] handle The Janus ICE handle associated with the peer\n * @param[in] label The label of the data channel the message is from\n * @param[in] protocol The protocol of the data channel to use\n * @param[in] textdata Whether the buffer is text (domstring) or binary data\n * @param[in] buffer The message data (buffer)\n * @param[in] length The buffer length */\nvoid janus_ice_incoming_data(janus_ice_handle *handle, char *label, char *protocol, gboolean textdata, char *buffer, int length);\n/*! \\brief Core SCTP/DataChannel callback, called by the SCTP stack when when there's data to send.\n * @param[in] handle The Janus ICE handle associated with the peer\n * @param[in] buffer The message data (buffer)\n * @param[in] length The buffer length */\nvoid janus_ice_relay_sctp(janus_ice_handle *handle, char *buffer, int length);\n/*! \\brief Plugin SCTP/DataChannel callback, called by the SCTP stack when data can be written\n * @param[in] handle The Janus ICE handle associated with the peer */\nvoid janus_ice_notify_data_ready(janus_ice_handle *handle);\n/*! \\brief Core SDP callback, called by the SDP stack when a stream has been paused by a negotiation\n * @param[in] handle The Janus ICE handle associated with the peer */\nvoid janus_ice_notify_media_stopped(janus_ice_handle *handle);\n///@}\n\n\n/** @name Janus ICE handle helpers\n */\n///@{\n/*! \\brief Method to locally set up the ICE candidates (initialization and gathering)\n * @param[in] handle The Janus ICE handle this method refers to\n * @param[in] offer Whether this is for an OFFER or an ANSWER\n * @param[in] trickle Whether ICE trickling is supported or not\n * @param[in] dtls_role The DTLS role that should be taken for this PeerConnection\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_ice_setup_local(janus_ice_handle *handle, gboolean offer, gboolean trickle, janus_dtls_role dtls_role);\n/*! \\brief Method to add local candidates to a janus_sdp SDP object representation\n * @param[in] handle The Janus ICE handle this method refers to\n * @param[in] mline The Janus SDP m-line object to add candidates to\n * @param[in] stream_id The stream ID of the candidate to add to the SDP\n * @param[in] component_id The component ID of the candidate to add to the SDP */\nvoid janus_ice_candidates_to_sdp(janus_ice_handle *handle, janus_sdp_mline *mline, guint stream_id, guint component_id);\n/*! \\brief Method to queue a remote candidate for processing\n * @param[in] handle The Janus ICE handle this method refers to\n * @param[in] c The remote NiceCandidate to process */\nvoid janus_ice_add_remote_candidate(janus_ice_handle *handle, NiceCandidate *c);\n/*! \\brief Method to handle remote candidates and start the connectivity checks\n * @param[in] handle The Janus ICE handle this method refers to\n * @param[in] stream_id The stream ID of the candidate to add to the SDP\n * @param[in] component_id The component ID of the candidate to add to the SDP */\nvoid janus_ice_setup_remote_candidates(janus_ice_handle *handle, guint stream_id, guint component_id);\n/*! \\brief Callback to be notified when the DTLS handshake for a specific component has been completed\n * \\details This method also decides when to notify attached plugins about the availability of a reliable PeerConnection\n * @param[in] handle The Janus ICE handle this callback refers to */\nvoid janus_ice_dtls_handshake_done(janus_ice_handle *handle);\n/*! \\brief Method to restart ICE and the connectivity checks\n * @param[in] handle The Janus ICE handle this method refers to */\nvoid janus_ice_restart(janus_ice_handle *handle);\n/*! \\brief Method to resend all the existing candidates via trickle (e.g., after an ICE restart)\n * @param[in] handle The Janus ICE handle this method refers to */\nvoid janus_ice_resend_trickles(janus_ice_handle *handle);\n///@}\n\n\n/*! \\brief Method to configure the static event loops mechanism at startup\n * @note Check the \\c event_loops property in the \\c janus.jcfg configuration\n * for an explanation of this feature, and the possible impact on Janus and users\n * @param[in] loops The number of static event loops to start (0 to disable the feature)\n * @param[in] allow_api Whether allocation on a specific loop driven via API should be allowed or not (false by default) */\nvoid janus_ice_set_static_event_loops(int loops, gboolean allow_api);\n/*! \\brief Method to return the number of static event loops, if enabled\n * @returns The number of static event loops, if configured, or 0 if the feature is disabled */\nint janus_ice_get_static_event_loops(void);\n/*! \\brief Method to check whether loop indication via API is allowed\n * @returns true if allowed, false otherwise */\ngboolean janus_ice_is_loop_indication_allowed(void);\n/*! \\brief Helper method to return a summary of the static loops activity\n * @note This is only used by the Admin API\n * @returns a json_t array with the required info */\njson_t *janus_ice_static_event_loops_info(void);\n/*! \\brief Method to stop all the static event loops, if enabled\n * @note This will wait for the related threads to exit, and so may delay the shutdown process */\nvoid janus_ice_stop_static_event_loops(void);\n\n#endif\n"
  },
  {
    "path": "src/ip-utils.c",
    "content": "/*! \\file    ip-utils.c\n *\\author   Johan Ouwerkerk <jm.ouwerkerk@gmail.com>\n *\\copyright GNU General Public License v3\n *\\brief    IP address related utility functions\n *\\details  Provides functions to query for network devices with a given device name or address.\n *Devices may be looked up by either a device name or by the IPv4 or IPv6 address of the configured network interface.\n *This functionality may be used to bind to user configurable network devices instead of relying on unpredictable implementation defined defaults.\n *Parsing IPv4/IPv6 addresses is done using inet_pton(), making these functions generally robust against malformed input.\n *\n *\\ingroup core\n *\\ref core\n */\n\n#include <errno.h>\n#include <sys/socket.h>\n#include <sys/types.h>\n#include <arpa/inet.h>\n#include <netdb.h>\n#include <net/if.h>\n#include <string.h>\n#include <unistd.h>\n\n#include <glib.h>\n\n#include \"ip-utils.h\"\n\nstatic int janus_ip_compare_byte_arrays(const uint8_t *b1, const uint8_t *b2, const size_t size) {\n\tsize_t i;\n\tfor(i = 0; i < size; ++i) {\n\t\tif(b1[i] != b2[i]) {\n\t\t\treturn b1[i] > b2[i] ? 1 : -1;\n\t\t}\n\t}\n\treturn 0;\n}\n\nstatic int janus_ip_iface_matches_name(const struct ifaddrs *ifa, const char *name) {\n\treturn ifa->ifa_addr && (ifa->ifa_addr->sa_family == AF_INET || ifa->ifa_addr->sa_family == AF_INET6)\n\t\t&& name && ifa->ifa_name && strncmp(ifa->ifa_name, name, IFNAMSIZ) == 0 ? 1 : 0;\n}\n\nstatic int janus_ip_iface_matches_ipv4(const struct ifaddrs *ifa, const struct in_addr *ipv4) {\n\tif(ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET) {\n\t\tstruct sockaddr_in *iface = (struct sockaddr_in *) ifa->ifa_addr;\n\t\tif(iface->sin_addr.s_addr == ipv4->s_addr) {\n\t\t\treturn 1;\n\t\t}\n\t}\n\treturn 0;\n}\n\nstatic int janus_ip_iface_matches_ipv6(const struct ifaddrs *ifa, const struct in6_addr *ipv6) {\n\tif(ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET6) {\n\t\tstruct sockaddr_in6 *iface = (struct sockaddr_in6 *) ifa->ifa_addr;\n\t\tif(janus_ip_compare_byte_arrays(iface->sin6_addr.s6_addr, ipv6->s6_addr, 16) == 0) {\n\t\t\treturn 1;\n\t\t}\n\t}\n\treturn 0;\n}\n\nstatic int janus_ip_iface_matches(const struct ifaddrs *ifa, const janus_network_query_config *query) {\n\tif(ifa && query) {\n\t\treturn\n\t\t\t((query->mode & janus_network_query_options_ipv4) && janus_ip_iface_matches_ipv4(ifa, &query->ipv4) == 1) ||\n\t\t\t((query->mode & janus_network_query_options_ipv6) && janus_ip_iface_matches_ipv6(ifa, &query->ipv6) == 1) ||\n\t\t\t((query->mode & janus_network_query_options_name) && janus_ip_iface_matches_name(ifa, query->device_name) == 1) ? 1 : 0;\n\t} else {\n\t\treturn -EINVAL;\n\t}\n}\n\nconst struct ifaddrs *janus_network_query_devices(const struct ifaddrs *ifa, const janus_network_query_config *query) {\n\twhile(ifa) {\n\t\tif(janus_ip_iface_matches(ifa, query) == 1) {\n\t\t\treturn ifa;\n\t\t}\n\t\tifa = ifa->ifa_next;\n\t}\n\treturn NULL;\n}\n\nint janus_network_prepare_device_query(const char *user_value, const janus_network_query_options query_mode, janus_network_query_config *query) {\n\tif(user_value && query) {\n\t\tquery->mode = janus_network_query_options_none;\n\n\t\tif((query_mode & janus_network_query_options_ipv4) && inet_pton(AF_INET, user_value, &query->ipv4) > 0) {\n\t\t\tquery->mode |= janus_network_query_options_ipv4;\n\t\t}\n\n\t\tif((query_mode & janus_network_query_options_ipv6) && inet_pton(AF_INET6, user_value, &query->ipv6) > 0) {\n\t\t\tquery->mode |= janus_network_query_options_ipv6;\n\t\t}\n\n\t\tif(query_mode & janus_network_query_options_name) {\n\t\t\tquery->device_name = user_value;\n\t\t\tquery->mode |= janus_network_query_options_name;\n\t\t}\n\n\t\treturn ((query_mode == janus_network_query_options_none) == (query->mode == janus_network_query_options_none)) ? 0 : -EINVAL;\n\t} else {\n\t\treturn -EINVAL;\n\t}\n}\n\nint janus_network_prepare_device_query_default(const char *user_value, janus_network_query_config *query) {\n\treturn janus_network_prepare_device_query(user_value, janus_network_query_options_any, query);\n}\n\nstatic int janus_ip_copy_ipv4(const struct sockaddr_in *iface, struct in_addr *result) {\n\tresult->s_addr = iface->sin_addr.s_addr;\n\treturn 0;\n}\n\nint janus_network_get_devices_ipv4(const struct ifaddrs *ifa, const janus_network_query_config *query, struct in_addr *result) {\n\tif(ifa && ifa->ifa_addr && (ifa->ifa_addr->sa_family == AF_INET) && result && query && (query->mode & janus_network_query_options_ipv4)) {\n\t\treturn janus_ip_copy_ipv4((struct sockaddr_in *) ifa->ifa_addr, result);\n\t} else {\n\t\treturn -EINVAL;\n\t}\n}\n\nstatic int janus_ip_copy_ipv6(const struct sockaddr_in6 *iface, struct in6_addr *result) {\n\tsize_t i;\n\tconst uint8_t *src = iface->sin6_addr.s6_addr;\n\tuint8_t *dst = result->s6_addr;\n\tfor(i = 0; i < 16; ++i) {\n\t\tdst[i] = src[i];\n\t}\n\treturn 0;\n}\n\nint janus_network_get_devices_ipv6(const struct ifaddrs *ifa, const janus_network_query_config *query, struct in6_addr *result) {\n\tif(ifa && ifa->ifa_addr && (ifa->ifa_addr->sa_family == AF_INET6) && result && query && (query->mode & janus_network_query_options_ipv6)) {\n\t\treturn janus_ip_copy_ipv6((struct sockaddr_in6 *) ifa->ifa_addr, result);\n\t} else {\n\t\treturn -EINVAL;\n\t}\n}\n\nint janus_network_get_device_address(const struct ifaddrs *ifa, janus_network_address *result) {\n\tif(ifa && ifa->ifa_addr && result) {\n\t\tswitch(ifa->ifa_addr->sa_family) {\n\t\t\tcase AF_INET:\n\t\t\t\tresult->family = AF_INET;\n\t\t\t\treturn janus_ip_copy_ipv4((struct sockaddr_in *) ifa->ifa_addr, &result->ipv4);\n\t\t\tcase AF_INET6:\n\t\t\t\tresult->family = AF_INET6;\n\t\t\t\treturn janus_ip_copy_ipv6((struct sockaddr_in6 *) ifa->ifa_addr, &result->ipv6);\n\t\t\tdefault:\n\t\t\t\treturn -EINVAL;\n\t\t}\n\t} else {\n\t\treturn -EINVAL;\n\t}\n}\n\nvoid janus_network_address_nullify(janus_network_address *a) {\n\tif(a) {\n\t\tmemset(a, '\\0', sizeof(janus_network_address));\n\t\ta->family = AF_UNSPEC;\n\t}\n}\n\nint janus_network_address_is_null(const janus_network_address *a) {\n\treturn !a || a->family == AF_UNSPEC;\n}\n\nint janus_network_address_from_sockaddr(struct sockaddr *s, janus_network_address *a) {\n\tif(!s || !a)\n\t\treturn -EINVAL;\n\tif(s->sa_family == AF_INET) {\n\t\ta->family = AF_INET;\n\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)s;\n\t\ta->ipv4 = addr->sin_addr;\n\t\treturn 0;\n\t} else if(s->sa_family == AF_INET6) {\n\t\ta->family = AF_INET6;\n\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)s;\n\t\ta->ipv6 = addr->sin6_addr;\n\t\treturn 0;\n\t}\n\treturn -EINVAL;\n}\n\nint janus_network_address_to_string_buffer(const janus_network_address *a, janus_network_address_string_buffer *buf) {\n\tif(buf && !janus_network_address_is_null(a)) {\n\t\tjanus_network_address_string_buffer_nullify(buf);\n\t\tbuf->family = a->family;\n\t\tif(a->family == AF_INET) {\n\t\t\treturn inet_ntop(AF_INET, &a->ipv4, buf->ipv4, INET_ADDRSTRLEN) ? 0 : -errno;\n\t\t} else {\n\t\t\treturn inet_ntop(AF_INET6, &a->ipv6, buf->ipv6, INET6_ADDRSTRLEN) ? 0 : -errno;\n\t\t}\n\t} else {\n\t\treturn -EINVAL;\n\t}\n}\n\nvoid janus_network_address_string_buffer_nullify(janus_network_address_string_buffer *b) {\n\tif(b) {\n\t\tmemset(b, '\\0', sizeof(janus_network_address_string_buffer));\n\t\tb->family = AF_UNSPEC;\n\t}\n}\n\nint janus_network_address_string_buffer_is_null(const janus_network_address_string_buffer *b) {\n\treturn !b || b->family == AF_UNSPEC;\n}\n\nconst char *janus_network_address_string_from_buffer(const janus_network_address_string_buffer *b) {\n\tif(janus_network_address_string_buffer_is_null(b)) {\n\t\treturn NULL;\n\t} else {\n\t\treturn b->family == AF_INET ? b->ipv4 : b->ipv6;\n\t}\n}\n\nint janus_network_string_is_valid_address(janus_network_query_options addr_type, const char *user_value) {\n\tjanus_network_address a;\n\treturn janus_network_string_to_address(addr_type, user_value, &a) == 0;\n}\n\nint janus_network_string_to_address(janus_network_query_options addr_type, const char *user_value, janus_network_address *result) {\n\tif((addr_type != janus_network_query_options_ipv4 &&\n\t\t\taddr_type != janus_network_query_options_ipv6 &&\n\t\t\taddr_type != janus_network_query_options_any_ip) || !user_value || !result) {\n\t\treturn -EINVAL;\n\t}\n\tif((addr_type & janus_network_query_options_ipv4) && inet_pton(AF_INET, user_value, &result->ipv4) > 0) {\n\t\tresult->family = AF_INET;\n\t\treturn 0;\n\t}\n\tif((addr_type & janus_network_query_options_ipv6) && inet_pton(AF_INET6, user_value, &result->ipv6) > 0) {\n\t\tresult->family = AF_INET6;\n\t\treturn 0;\n\t}\n\treturn -EINVAL;\n}\n\nint janus_network_lookup_interface(const struct ifaddrs *ifas, const char *iface, janus_network_address *result) {\n\tif(ifas == NULL || iface == NULL || result == NULL)\n\t\treturn -EINVAL;\n\tjanus_network_address_nullify(result);\n\tif(!strcmp(iface, \"0.0.0.0\")) {\n\t\tresult->family = AF_INET;\n\t\tresult->ipv4.s_addr = INADDR_ANY;\n\t\treturn 0;\n\t} else if(!strcmp(iface, \"::\")) {\n\t\tresult->family = AF_INET6;\n\t\tresult->ipv6 = in6addr_any;\n\t\treturn 0;\n\t}\n\tjanus_network_query_config q;\n\t/* Let's see if iface is an IPv4 address, an IPv6 address, or possibly an interface name */\n\tint res = janus_network_prepare_device_query(iface,\n\t\t\tjanus_network_query_options_ipv4 | janus_network_query_options_ipv6 | janus_network_query_options_name, &q);\n\tif(res != 0) {\n\t\t/* None of them..? */\n\t\treturn res;\n\t}\n\tconst struct ifaddrs *found = janus_network_query_devices(ifas, &q);\n\tif(!found || janus_network_get_device_address(found, result)) {\n\t\t/* Couldn't find anything on iface */\n\t\treturn -EINVAL;\n\t}\n\t/* Done */\n\treturn 0;\n}\n\nint janus_network_detect_local_ip(janus_network_query_options addr_type, janus_network_address *result) {\n\tif(result == NULL)\n\t\treturn -EINVAL;\n\tjanus_network_address_nullify(result);\n\tgboolean found = FALSE;\n\tint fd = -1;\n\tif(addr_type == janus_network_query_options_ipv4 || addr_type == janus_network_query_options_any_ip) {\n\t\t/* Let's try IPv4 (FIXME Should probably use other internal methods) */\n\t\tstruct sockaddr_in addr = { 0 };\n\t\tsocklen_t len;\n\t\tfd = socket(AF_INET, SOCK_DGRAM, 0);\n\t\tif(fd > -1) {\n\t\t\taddr.sin_family = AF_INET;\n\t\t\taddr.sin_port = htons(1);\n\t\t\tinet_pton(AF_INET, \"1.2.3.4\", &addr.sin_addr);\n\t\t\tif(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) > -1) {\n\t\t\t\tlen = sizeof(addr);\n\t\t\t\tif(getsockname(fd, (struct sockaddr*)&addr, &len) > -1) {\n\t\t\t\t\tresult->family = AF_INET;\n\t\t\t\t\tif(janus_ip_copy_ipv4((struct sockaddr_in *)&addr, &result->ipv4) == 0) {\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif(fd != -1)\n\t\tclose(fd);\n\tfd = -1;\n\tif(!found && (addr_type == janus_network_query_options_ipv6 || addr_type == janus_network_query_options_any_ip)) {\n\t\t/* Let's try IPv6 (FIXME Should probably use other internal methods) */\n\t\tstruct sockaddr_in6 addr = { 0 };\n\t\tsocklen_t len;\n\t\tfd = socket(AF_INET6, SOCK_DGRAM, 0);\n\t\tif(fd > -1) {\n\t\t\taddr.sin6_family = AF_INET6;\n\t\t\taddr.sin6_port = htons(1);\n\t\t\tinet_pton(AF_INET6, \"::1.2.3.4\", &addr.sin6_addr);\n\t\t\tif(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) > -1) {\n\t\t\t\tlen = sizeof(addr);\n\t\t\t\tif(getsockname(fd, (struct sockaddr*)&addr, &len) > -1) {\n\t\t\t\t\tresult->family = AF_INET6;\n\t\t\t\t\tif(janus_ip_copy_ipv6((struct sockaddr_in6 *)&addr, &result->ipv6) == 0) {\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif(fd != -1)\n\t\tclose(fd);\n\treturn found ? 0 : -EINVAL;\n}\n\nchar *janus_network_detect_local_ip_as_string(janus_network_query_options addr_type) {\n\tjanus_network_address addr;\n\tjanus_network_address_string_buffer buf;\n\tint res = janus_network_detect_local_ip(addr_type, &addr) || janus_network_address_to_string_buffer(&addr, &buf);\n\tif(res != 0)\n\t\treturn NULL;\n\treturn g_strdup(janus_network_address_string_from_buffer(&buf));\n}\n\nint janus_network_resolve_address(const char *host, struct sockaddr_storage *address) {\n\tif(!host || !address)\n\t\treturn -EINVAL;\n\t/* Check whether we need to resolve the address*/\n\tgboolean resolved = FALSE;\n\tif(strstr(host, \":\")) {\n\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)address;\n\t\taddr6->sin6_family = AF_INET6;\n\t\tif(inet_pton(AF_INET6, host, &addr6->sin6_addr) == 1) {\n\t\t\t/* Numeric IPv6 address */\n\t\t\tresolved = TRUE;\n\t\t}\n\t} else {\n\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)address;\n\t\taddr->sin_family = AF_INET;\n\t\tif(inet_pton(AF_INET, host, &addr->sin_addr) == 1) {\n\t\t\t/* Numeric IPv4 address */\n\t\t\tresolved = TRUE;\n\t\t}\n\t}\n\tif(!resolved) {\n\t\t/* Perform a getaddrinfo on the address */\n\t\tstruct addrinfo *result = NULL;\n\t\tint res = getaddrinfo(host, NULL, NULL, &result);\n\t\tif(res == 0) {\n\t\t\t/* Address resolved */\n\t\t\tstruct addrinfo *temp = result;\n\t\t\twhile(temp && !resolved) {\n\t\t\t\tif(result->ai_family == AF_INET6) {\n\t\t\t\t\tresolved = TRUE;\n\t\t\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)address;\n\t\t\t\t\tstruct sockaddr_in6 *remote = (struct sockaddr_in6 *)result->ai_addr;\n\t\t\t\t\tmemcpy(&addr6->sin6_addr, &remote->sin6_addr, sizeof(addr6->sin6_addr));\n\t\t\t\t} else if(result->ai_family == AF_INET) {\n\t\t\t\t\tresolved = TRUE;\n\t\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)address;\n\t\t\t\t\tstruct sockaddr_in *remote = (struct sockaddr_in *)result->ai_addr;\n\t\t\t\t\tmemcpy(&addr->sin_addr, &remote->sin_addr, sizeof(addr->sin_addr));\n\t\t\t\t}\n\t\t\t\ttemp = temp->ai_next;\n\t\t\t}\n\t\t\tfreeaddrinfo(result);\n\t\t}\n\t}\n\t/* Done */\n\treturn resolved ? 0 : -1;\n}\n"
  },
  {
    "path": "src/ip-utils.h",
    "content": "/*! \\file    ip-utils.h\n * \\author   Johan Ouwerkerk <jm.ouwerkerk@gmail.com>\n * \\copyright GNU General Public License v3\n * \\brief    IP address related utility functions (headers)\n * \\details  Provides functions to query for network devices with a given device name or address.\n * Devices may be looked up by either a device name or by the IPv4 or IPv6 address of the configured network interface.\n * This functionality may be used to bind to user configurable network devices instead of relying on unpredictable implementation defined defaults.\n *\n * Parsing IPv4/IPv6 addresses is robust against malformed input.\n *\n * \\see man 3 getifaddrs\n * \\see man 3 inet_pton\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_IP_UTILS_H\n#define JANUS_IP_UTILS_H\n\n#include <ifaddrs.h>\n#include <netinet/in.h>\n\n\n/** @name Janus helper methods to match names and addresses with network interfaces/devices.\n */\n///@{\ntypedef enum janus_network_query_options {\n\tjanus_network_query_options_none = 0,\n\tjanus_network_query_options_name = 1,\n\tjanus_network_query_options_ipv4 = 2,\n\tjanus_network_query_options_ipv6 = 4,\n\tjanus_network_query_options_any_ip = 6,\n\tjanus_network_query_options_any = 7\n} janus_network_query_options;\n\n/*!\n * \\brief Internal object representation of a network device query (configuration).\n */\ntypedef struct janus_network_query_config {\n\tconst char *device_name;\n\tjanus_network_query_options mode;\n\tstruct in_addr ipv4;\n\tstruct in6_addr ipv6;\n} janus_network_query_config;\n\n/*!\n * \\brief Structure to hold network addresses in a tagged union which should be IPv4 and IPv6 compatible.\n * Use the \\c family member (either \\c AF_INET or \\c AF_INET6) to determine which type of address is contained.\n * \\see man 7 ip\n * \\see man 7 ipv6\n * \\see \\c janus_network_get_device_address\n */\ntypedef struct janus_network_address {\n\t/*!\n\t * Should be either \\c AF_INET for IPv4 or \\c AF_INET6 for IPv6.\n\t */\n\tint family;\n\tunion {\n\t\tstruct in_addr ipv4;\n\t\tstruct in6_addr ipv6;\n\t};\n} janus_network_address;\n\n/*!\n * \\brief Structure to hold human readable forms of network addresses in a tagged union which should be IPv4 and IPv6 compatible.\n * Use the \\c family member (either \\c AF_INET or \\c AF_INET6) to determine which type of representation is contained.\n * \\see man 7 ip\n * \\see man 7 ipv6\n * \\see \\c janus_network_address_to_string_buffer\n */\ntypedef struct janus_network_address_string_buffer {\n\t/*!\n\t * Should be either \\c AF_INET for IPv4 or \\c AF_INET6 for IPv6.\n\t */\n\tint family;\n\tunion {\n\t\tchar ipv4[INET_ADDRSTRLEN];\n\t\tchar ipv6[INET6_ADDRSTRLEN];\n\t};\n} janus_network_address_string_buffer;\n\n/*!\n * \\brief Initialise a network device query.\n * \\param user_value The user-supplied string which is supposed to describe either the device name or its IP address.\n * \\param query_mode (A mask of) Options describing the supported types of matches which should be accepted when performing a look up with this query.\n * This can be used to restrict the query to 'by device name' or 'IPv4 only' type searches.\n * \\param query The query object to configure.\n * \\return 0 on success or -EINVAL if any of the arguments are NULL or the given value to look for does not correspond to a valid IPv4/IPv6 address and\n * matching by name is disabled.\n * \\see \\c janus_network_query_options\n */\nint janus_network_prepare_device_query(const char *user_value, const janus_network_query_options query_mode, janus_network_query_config *query);\n\n/*!\n * \\brief Initialise a network device query with default query options.\n * \\p This function will Initialise the query to accept any supported match type.\n * \\param user_value The user-supplied string which is supposed to describe either the device name or its IP address.\n * \\param query The query object to configure.\n * \\return 0 on success, or -EINVAL if any of the arguments are NULL\n * \\see \\c janus_network_prepare_device_query\n */\nint janus_network_prepare_device_query_default(const char *user_value, janus_network_query_config *query);\n\n/*!\n * \\brief Look up network devices matching the given query.\n * The first matching device is returned, so to find all matching devices\n * simply pass the `ifa_next` of the returned device in a subsequent call to this function to find more matches.\n * \\param ifas The first node of the list of network interfaces to search through. This should be obtained (indirectly) from\n * \\c getifaddrs().\n * \\param query A description of the criteria to look for when determining whether or not a network interface is a match.\n * \\return a pointer to a node describing the matching network interface or `NULL` if no (further) match was found.\n * \\see man 3 getifaddrs\n */\nconst struct ifaddrs *janus_network_query_devices(const struct ifaddrs *ifas, const janus_network_query_config *query);\n\n/*!\n * \\brief Copies the IPv4 address from a network interface description to the given result structure.\n * \\param ifa The network interface description to grab the IPv4 address from. It should be obtained with `janus_network_query_devices()`.\n * \\param query A description of the criteria to look for when determining whether or not a network interface is a match\n * \\param result Pointer to a structure to populate with the IPv4 address of the given network interface\n * \\return 0 on success, -EINVAL if any argument is NULL or the network interface description or the network device query do not correspond to an IPv4 configuration.\n * \\see man 7 ip\n * \\see \\c janus_network_query_devices\n */\nint janus_network_get_devices_ipv4(const struct ifaddrs *ifa, const janus_network_query_config *query, struct in_addr *result);\n\n/*!\n * \\brief Copies the IPv6 address from a network interface description to the given result structure.\n * \\param ifa The network interface description to grab the IPv6 address from. It should be obtained with `janus_network_query_devices()`.\n * \\param query A description of the criteria to look for when determining whether or not a network interface is a match\n * \\param result Pointer to a structure to populate with the IPv6 address of the given network interface\n * \\return 0 on success, -EINVAL if any argument is NULL or the network interface description or the network device query do not correspond to an IPv6 configuration.\n * \\see man 7 ipv6\n * \\see \\c janus_network_query_devices\n */\nint janus_network_get_devices_ipv6(const struct ifaddrs *ifa, const janus_network_query_config *query, struct in6_addr *result);\n\n/*!\n * \\brief Copies the IP address from a network interface description to the given result structure.\n * \\return 0 on success, or -EINVAL if any argument is NULL or the given network interface does not correspond to an IP address.\n * \\see \\c janus_network_address\n */\nint janus_network_get_device_address(const struct ifaddrs *ifa, janus_network_address *result);\n\n/*!\n * \\brief Set the given network address to a null/nil value.\n * \\param a The address to nullify. Nothing is done if the pointer is NULL itself.\n * \\see \\c janus_network_address_is_null\n */\nvoid janus_network_address_nullify(janus_network_address *a);\n\n/*!\n * \\brief Test if a given network address is null-valued\n * \\param a The address to check\n * \\return A positive integer if the given address is null-valued, 0 otherwise.\n * \\see \\c janus_network_address_nullify\n */\nint janus_network_address_is_null(const janus_network_address *a);\n\n/*!\n * \\brief Convert a struct sockaddr to a janus_network_address\n * \\param s The struct sockaddr to convert\n * \\param a The address to write to\n * \\return 0 on success, or -EINVAL otherwise.\n */\nint janus_network_address_from_sockaddr(struct sockaddr *s, janus_network_address *a);\n\n/*!\n * \\brief Convert the given network address to a form which can be used to extract a human readable network address from.\n * \\param a The address to convert\n * \\param buf A buffer to contain the human readable form.\n * \\return 0 on success, or -EINVAL if any argument is NULL.\n * \\see \\c janus_network_address\n * \\see \\c janus_network_address_string_buffer\n * \\see \\c janus_network_address_string_from_buffer\n * \\see man 3 inet_ntop\n */\nint janus_network_address_to_string_buffer(const janus_network_address *a, janus_network_address_string_buffer *buf);\n\n/*!\n * \\brief Set the given network address string buffer to a null/nil value.\n * \\param b The address to nullify. Nothing is done if the pointer is NULL itself.\n * \\see \\c janus_network_address_string_buffer_is_null\n */\nvoid janus_network_address_string_buffer_nullify(janus_network_address_string_buffer *b);\n\n/*!\n * \\brief Test if a given network address string buffer is null-valued\n * \\param b The buffer to check\n * \\return A positive integer if the given buffer is null-valued, 0 otherwise.\n * \\see \\c janus_network_address_string_buffer_nullify\n */\nint janus_network_address_string_buffer_is_null(const janus_network_address_string_buffer *b);\n\n/*!\n * \\brief Extract the human readable representation of a network address from a given buffer.\n * \\param b The buffer containing the given network\n * \\return A pointer to the human readable representation of the network address inside the given buffer, or NULL if the buffer is invalid or NULL.\n * \\see \\c janus_network_address_to_string_buffer\n */\nconst char *janus_network_address_string_from_buffer(const janus_network_address_string_buffer *b);\n\n/*!\n * \\brief Test if a given IP address string is a valid address of the specified type\n * \\param addr_type The type of address you're interested in (janus_network_query_options_ipv4,\n * janus_network_query_options_ipv6 or janus_network_query_options_any_ip)\n * \\param user_value The IP address string to check\n * \\return A positive integer if the given string is a valid address, 0 otherwise.\n */\nint janus_network_string_is_valid_address(janus_network_query_options addr_type, const char *user_value);\n\n/*!\n * \\brief Convert an IP address string to a janus_network_address instance\n * \\param addr_type The type of address you're interested in (janus_network_query_options_ipv4,\n * janus_network_query_options_ipv6 or janus_network_query_options_any_ip)\n * \\param user_value The IP address string to check\n * \\param result Pointer to a valid janus_network_address instance that will contain the result\n * \\return 0 in case of success, -EINVAL otherwise\n */\nint janus_network_string_to_address(janus_network_query_options addr_type, const char *user_value, janus_network_address *result);\n\n/*!\n * \\brief Convert an interface name or IP address to a janus_network_address instance\n * \\param ifas The list of interfaces to look into (e.g., as returned from getifaddrs)\n * \\param iface The interface name or IP address to look for\n * \\param result Pointer to a valid janus_network_address instance that will contain the result\n * \\return 0 in case of success, -EINVAL otherwise\n */\nint janus_network_lookup_interface(const struct ifaddrs *ifas, const char *iface, janus_network_address *result);\n\n/*!\n * \\brief Helper method to find a valid local IP address, that is an address that can be used to communicate\n * \\param addr_type The type of address you're interested in (janus_network_query_options_ipv4,\n * janus_network_query_options_ipv6 or janus_network_query_options_any_ip)\n * \\param result Pointer to a valid janus_network_address instance that will contain the result\n * \\return 0 in case of success, -EINVAL otherwise\n */\nint janus_network_detect_local_ip(janus_network_query_options addr_type, janus_network_address *result);\n\n/*!\n * \\brief Wrapper to janus_network_detect_local_ip that returns a string instead\n * \\note The string is allocated with g_strdup and so needs to be freed by the caller\n * \\param addr_type The type of address you're interested in (janus_network_query_options_ipv4,\n * janus_network_query_options_ipv6 or janus_network_query_options_any_ip)\n * \\return 0 in case of success, -EINVAL otherwise\n */\nchar *janus_network_detect_local_ip_as_string(janus_network_query_options addr_type);\n///@}\n\n/** @name Janus helper methods to resolve external addresses\n */\n/*!\n * \\brief Wrapper inet_pton or getaddrinfo to fill a struct sockaddr_storage structure from an address\n * \\note The method will only do inet_pton if it detects a numeric address, and will perform\n * a getaddrinfo otherwise. Notice that, since the request is synchronous, it may have to wait\n * for a DNS response to that request.\n * \\param host The address to resolve\n * \\param address A pointer to the struct sockaddr_storage to write the result to\n * \\return 0 in case of success, a negative integer otherwise\n */\nint janus_network_resolve_address(const char *host, struct sockaddr_storage *address);\n///@}\n\n#endif\n"
  },
  {
    "path": "src/janus-cfgconv.1",
    "content": ".TH JANUS-PP-REC 1\n.SH NAME\njanus-cfgconv \\- Janus config file conversion utility.\n.SH SYNOPSIS\n.B janus-cfgconv\n.IR source.[cfg|jcfg]\n.IR destination.[cfg|jcfg]\n.SH DESCRIPTION\n.B janus-cfgconv\nis a simple utility that allows you to convert Janus INI configuration files to libconfig format and vice-versa.\n.SH OPTIONS\n.TP\n.BR \\-h \", \" \\-\\-help\nPrint help and exit\n.SH EXAMPLES\n\\fBjanus-cfgconv janus.cfg janus.jcfg\\fR \\- Convert provided INI file to libconfig format\n.SH BUGS\n.TP\nIf you think you found a bug or want to contribute a feature, you can issue or a pull request on https://github.com/meetecho/janus-gateway/issues.\n.TP\nAnyway, before doing that make sure you read the documentation at https://janus.conf.meetecho.com/docs/ and that it has not been discussed already at https://janus.discourse.group/. We only use Github for code issues, and \\fBNOT\\fR for configuration or usage issues: use the group for that.\n.SH SEE ALSO\n.TP\nhttps://github.com/meetecho/janus-gateway \\- Official repository\n.TP\nhttps://janus.conf.meetecho.com \\- Demos and documentation\n.TP\nhttps://janus.discourse.group/ \\- Community\n.TP\nhttps://www.meetecho.com/blog/ \\- Tutorials and blog posts on Janus\n.SH AUTHORS\nLorenzo Miniero (lorenzo@meetecho.com)\n"
  },
  {
    "path": "src/janus-cfgconv.c",
    "content": "/*! \\file    janus-cfgconv.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Simple utility to convert Janus .cfg files to .jcfg and vice-versa\n * \\details  Historically, Janus has made use of INI .cfg files for the\n * configuration of core and plugins. Recently, support for the libconfig format\n * has been added too. Due to the more expressive nature of libconfig, .jcfg\n * files have been made the default: while support for .cfg files still\n * exists as a fallback, new features may only be available in .jcfg files.\n * As such, you may want to convert your existing .cfg configuration files\n * to .jcfg as soon as possible, which is what this tool allows you to do.\n * Notice that the tool also allows you to go the other way around, although\n * libconfig concepts that cannot be expressed in INI will be lost in the process.\n *\n * Using the utility is quite simple. Just pass, as arguments to the tool,\n * the path to the file you want to convert (.cfg or .jcfg) and the path to\n * the target file (.jcfg or .cfg), e.g.:\n *\n\\verbatim\n./janus-cfgconv /path/to/config.cfg /path/to/config.jcfg\n\\endverbatim\n *\n * \\ingroup tools\n * \\ref tools\n */\n\n#include <stdlib.h>\n#include <string.h>\n\n#include \"config.h\"\n#include \"debug.h\"\n#include \"version.h\"\n\nint janus_log_level = 4;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = TRUE;\nchar *janus_log_global_prefix = NULL;\nint lock_debug = 0;\n\n/* Main Code */\nint main(int argc, char *argv[])\n{\n\tjanus_log_init(FALSE, TRUE, NULL, NULL);\n\tatexit(janus_log_destroy);\n\n\tJANUS_LOG(LOG_INFO, \"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\tJANUS_LOG(LOG_INFO, \"Janus commit: %s\\n\", janus_build_git_sha);\n\tJANUS_LOG(LOG_INFO, \"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\n\t/* Evaluate arguments */\n\tif(argc != 3) {\n\t\tJANUS_LOG(LOG_INFO, \"Usage: %s source.[cfg|jcfg] destination.[cfg|jcfg]\\n\", argv[0]);\n\t\tJANUS_LOG(LOG_INFO, \"       %s --parse source.[cfg|jcfg]\\n\", argv[0]);\n\t\texit(1);\n\t}\n\tchar *source = NULL, *destination = NULL;\n\t/* Parse or convert the configuration files */\n\tgboolean only_parse = FALSE;\n\tsource = argv[1];\n\tif(!strcmp(source, \"--parse\")) {\n\t\tonly_parse = TRUE;\n\t} else {\n\t\tif(!strstr(source, \".cfg\") && !strstr(source, \".jcfg\")) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported file: %s\\n\", source);\n\t\t\texit(1);\n\t\t}\n\t}\n\tdestination = argv[2];\n\tif(!strstr(destination, \".cfg\") && !strstr(destination, \".jcfg\")) {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported file: %s\\n\", destination);\n\t\texit(1);\n\t}\n\tif(only_parse) {\n\t\tsource = destination;\n\t\tdestination = NULL;\n\t\tJANUS_LOG(LOG_INFO, \"Parsing:\\n\");\n\t\tJANUS_LOG(LOG_INFO, \"   -- IN:  %s\\n\", source);\n\t} else {\n\t\tJANUS_LOG(LOG_INFO, \"Converting:\\n\");\n\t\tJANUS_LOG(LOG_INFO, \"   -- IN:  %s\\n\", source);\n\t\tJANUS_LOG(LOG_INFO, \"   -- OUT: %s\\n\\n\", destination);\n\t}\n\t/* Open the source */\n\tjanus_config *config = janus_config_parse(source);\n\tif(config == NULL)\n\t\texit(1);\n\tjanus_config_print_as(config, LOG_INFO);\n\tJANUS_LOG(LOG_INFO, \"\\n\");\n\tif(only_parse) {\n\t\t/* We're done */\n\t\tjanus_config_destroy(config);\n\t\tJANUS_LOG(LOG_INFO, \"Bye!\\n\");\n\t\treturn 0;\n\t}\n\t/* If the source is an INI, check if there are numeric categories or attribute names */\n\tif(!config->is_jcfg) {\n\t\tGList *list = config->list;\n\t\twhile(list) {\n\t\t\tjanus_config_container *c = (janus_config_container *)list->data;\n\t\t\tif(c && c->type == janus_config_type_category && c->name && atol(c->name) > 0) {\n\t\t\t\t/* FIXME Ugly hack to add the \"room-\" prefix to category names\n\t\t\t\t * (currently only needed in VideoRoom/AudioBridge/TextRoom) */\n\t\t\t\tchar newname[50];\n\t\t\t\tg_snprintf(newname, sizeof(newname), \"room-%s\", c->name);\n\t\t\t\tg_free((char *)c->name);\n\t\t\t\tc->name = g_strdup(newname);\n\t\t\t}\n\t\t\tlist = list->next;\n\t\t}\n\t}\n\t/* Is the target an INI or a libconfig file? */\n\tconfig->is_jcfg = strstr(destination, \".jcfg\") != NULL;\n\t/* Remove extension: janus_config_save adds it for us */\n\tchar *target = g_strdup(destination);\n\tchar *extension = config->is_jcfg ? strstr(target, \".jcfg\") : strstr(target, \".cfg\");\n\t*extension = '\\0';\n\t/* Save to destination */\n\tif(janus_config_save(config, NULL, target) < 0) {\n\t\tg_free(target);\n\t\tjanus_config_destroy(config);\n\t\tJANUS_LOG(LOG_ERR, \"Error saving converted file\\n\");\n\t\texit(1);\n\t}\n\tjanus_config_destroy(config);\n\t/* Make sure everything's fine */\n\tconfig = janus_config_parse(destination);\n\tif(config == NULL) {\n\t\tg_free(target);\n\t\tJANUS_LOG(LOG_ERR, \"Error parsing converted file\\n\");\n\t\texit(1);\n\t}\n\tjanus_config_print_as(config, LOG_INFO);\n\tJANUS_LOG(LOG_INFO, \"\\n\");\n\tjanus_config_destroy(config);\n\t/* Done */\n\tFILE *file = fopen(destination, \"rb\");\n\tif(file == NULL) {\n\t\tg_free(target);\n\t\tJANUS_LOG(LOG_WARN, \"No destination file %s??\\n\", destination);\n\t\texit(1);\n\t}\n\tfseek(file, 0L, SEEK_END);\n\tsize_t fsize = ftell(file);\n\tfseek(file, 0L, SEEK_SET);\n\tJANUS_LOG(LOG_INFO, \"%s is %zu bytes\\n\", destination, fsize);\n\tfclose(file);\n\tg_free(target);\n\n\tJANUS_LOG(LOG_INFO, \"Bye!\\n\");\n\treturn 0;\n}\n"
  },
  {
    "path": "src/janus-valgrind.supp",
    "content": "# annoying glib2 object type registration stuff\n{\n\tglib2-type-register-fundamental\n\tMemcheck:Leak\n\tfun:calloc\n\tfun:g_malloc0\n\t...\n\tfun:g_type_register_fundamental\n\t...\n}\n{\n\tglib2-type-register-fundamental-realloc\n\tMemcheck:Leak\n\t...\n\tfun:g_realloc\n\t...\n\tfun:g_type_register_fundamental\n\t...\n}\n{\n\tglib2-type-register-static\n\tMemcheck:Leak\n\tfun:malloc\n\tfun:g_malloc\n\t...\n\tfun:type_node_any_new_W\n\tfun:g_type_register_static\n\t...\n}\n{\n\tglib2-type-register-static-realloc\n\tMemcheck:Leak\n\t...\n\tfun:g_realloc\n\t...\n\tfun:type_node_any_new_W\n\tfun:g_type_register_static\n\t...\n}\n{\n\tglib2-type-add-interface-static\n\tMemcheck:Leak\n\tfun:malloc\n\tfun:g_malloc\n\t...\n\tfun:type_add_interface_Wm\n\tfun:g_type_add_interface_static\n\t...\n}\n{\n\tglib2-dl-init-gobject-type-data\n\tMemcheck:Leak\n\tfun:calloc\n\tfun:g_malloc0\n\tfun:type_data_make_W\n\tfun:gobject_init_ctor\n\t...\n\tfun:_dl_init\n\t...\n}\n{\n\tglib2-dl-init-gobject-type-node\n\tMemcheck:Leak\n\tfun:calloc\n\tfun:g_malloc0\n\t...\n\tfun:type_node_fundamental_new_W\n\tfun:gobject_init_ctor\n\t...\n\tfun:_dl_init\n\t...\n}\n{\n\tglib2-signal-new-class\n\tMemcheck:Leak\n\tfun:calloc\n\tfun:g_malloc0\n\t...\n\tfun:g_signal_type_cclosure_new\n\tfun:g_signal_new\n\t...\n\tfun:g_object_do_class_init\n\t...\n}\n{\n\tglib2-signal-new-class-intern\n\tMemcheck:Leak\n\tfun:calloc\n\tfun:g_malloc0\n\t...\n\tfun:g_signal_type_cclosure_new\n\tfun:g_signal_new\n\t...\n\tfun:g_cancellable_class_intern_init\n\t...\n}\n{\n\tnice-socket-new-vtable-base\n\tMemcheck:Leak\n\tfun:malloc\n\tfun:g_malloc\n\t...\n\tfun:type_iface_vtable_base_init_Wm\n\t...\n\tfun:nice_udp_bsd_socket_new\n\t...\n}\n{\n\tnice-socket-new-class\n\tMemcheck:Leak\n\tfun:calloc\n\tfun:g_malloc0\n\t...\n\tfun:g_socket_class_intern_init\n\t...\n\tfun:nice_udp_bsd_socket_new\n\t...\n}\n\n{\n\tnice-agent-class-init\n\tMemcheck:Leak\n\tfun:calloc\n\tfun:g_malloc0\n\t...\n\tfun:g_type_create_instance\n\t...\n\tfun:nice_agent_class_init\n\t...\n}\n\n# probably a bug in sofia-sip\n{\n\tsofia-sip-su-home\n\tMemcheck:Leak\n\tfun:calloc\n\t...\n\tfun:su_home_new\n\t...\n\tfun:main\n}\n\n# GLib Valgrind suppressions file\n#\n# This provides a list of suppressions for all of GLib (including GIO), for all\n# Valgrind tools (memcheck, drd, helgrind, etc.) for the false positives and\n# deliberate one-time leaks which GLib causes to be reported when running under\n# Valgrind.\n#\n# When running an application which links to GLib under Valgrind, you can pass\n# this suppression file to Valgrind using --suppressions=/path/to/glib-2.0.supp.\n#\n# http://valgrind.org/docs/manual/manual-core.html#manual-core.suppress\n#\n# Note that there is currently no way for Valgrind to load this automatically\n# (https://bugs.kde.org/show_bug.cgi?id=160905), so the best GLib can currently\n# do is to install this file as part of its development package.\n#\n# This file should be updated if GLib introduces a new deliberate one-time leak,\n# or another false race positive in Valgrind: please file bugs at:\n#\n# https://gitlab.gnome.org/GNOME/glib/issues/new\n\n{\n\tgnutls-init-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:gtls_gnutls_init\n}\n\n{\n\tgnutls-init-realloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:realloc\n\t...\n\tfun:gtls_gnutls_init\n}\n\n{\n\tg-tls-backend-gnutls-init\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:g_once_impl\n\tfun:g_tls_backend_gnutls_init\n}\n\n{\n\tp11-tokens-init\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:create_tokens_inlock\n\tfun:initialize_module_inlock_reentrant\n}\n\n# One-time allocation from libc for getpwnam() results\n{\n\tg-local-vfs-getpwnam\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:getpwnam\n\tfun:g_local_vfs_parse_name\n}\n\n{\n\tglib-init-malloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_quark_init\n}\n\n{\n\tglib-init-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_quark_init\n}\n\n{\n\tgobject-init-malloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:gobject_init*\n}\n\n{\n\tgobject-init-realloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:realloc\n\t...\n\tfun:gobject_init*\n}\n\n{\n\tgobject-init-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable\n\tfun:calloc\n\t...\n\tfun:gobject_init*\n}\n\n{\n\tg-type-register-dynamic\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_type_register_dynamic\n}\n\n{\n\tg-type-register-static\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable\n\tfun:malloc\n\t...\n\tfun:g_type_register_static\n}\n\n{\n\tg-type-register-static-realloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable\n\tfun:realloc\n\t...\n\tfun:g_type_register_static\n}\n\n{\n\tg-type-register-static-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable\n\tfun:calloc\n\t...\n\tfun:g_type_register_static\n}\n\n{\n\tg-type-register-fundamental\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable\n\tfun:malloc\n\t...\n\tfun:g_type_register_fundamental\n}\n\n{\n\tg-type-register-fundamental-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable\n\tfun:calloc\n\t...\n\tfun:g_type_register_fundamental\n}\n\n{\n\tg-type-add-interface-dynamic\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_type_add_interface_dynamic\n}\n\n{\n\tg-type-add-interface-static\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_type_add_interface_static\n}\n\n{\n\tg-type-add-interface-static-realloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:realloc\n\t...\n\tfun:g_type_add_interface_static\n}\n\n{\n\tg-type-add-interface-static-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_type_add_interface_static\n}\n\n{\n\tg-test-rand-init\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_rand_new_with_seed_array\n\tfun:test_run_seed\n\t...\n\tfun:g_test_run\n}\n\n{\n\tg-rand-init2\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_rand_new_with_seed_array\n\t...\n\tfun:get_global_random\n}\n\n{\n\tg-quark-table-new\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:g_hash_table_new\n\t...\n\tfun:quark_new\n}\n\n{\n\tg-quark-table-resize\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\t...\n\tfun:g_hash_table_resize\n\t...\n\tfun:quark_new\n}\n\n{\n\tg-type-interface-init\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:type_iface_vtable_base_init_Wm\n}\n\n{\n\tg-type-class-init-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:type_class_init_Wm\n}\n\n{\n\tg-type-class-init\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:g_type_create_instance\n\t...\n\tfun:type_class_init_Wm\n}\n\n{\n\tg-object-do-class-init-signals\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\t...\n\tfun:g_signal_new\n\t...\n\tfun:type_class_init_Wm\n}\n\n{\n\tg-type-prerequisites\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:realloc\n\t...\n\tfun:type_iface_add_prerequisite_W\n}\n\n{\n\tg-type-add-interface-check\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_type_add_interface_check\n\t...\n\tfun:type_class_init_Wm\n}\n\n{\n\tg-type-add-interface-check-realloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:realloc\n\t...\n\tfun:g_type_add_interface_check\n\t...\n\tfun:type_class_init_Wm\n}\n\n{\n\tg-object-class-install-property\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:validate_and_install_class_property\n\t...\n\tfun:type_class_init_Wm\n}\n\n{\n\tg-param-spec-pool-new\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_param_spec_pool_new\n\t...\n\tfun:type_class_init_Wm\n}\n\n# weak_locations_lock in gobject.c\n{\n\tg-weak-ref-lock\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_rw_lock_get_impl\n\t...\n\tfun:g_weak_ref_set\n}\n\n{\n\tg-object-base-class-init-construct-pproperties\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_slist_copy\n\tfun:g_object_base_class_init\n\tfun:type_class_init_Wm\n}\n\n{\n        g-type-class-ref\n        Memcheck:Leak\n        fun:calloc\n        ...\n        fun:type_class_init_Wm\n        ...\n        fun:g_type_class_ref\n}\n\n{\n\tg-type-class-ref-inlined\n\tMemcheck:Leak\n\tfun:calloc\n\t...\n\tfun:UnknownInlinedFun\n\t...\n\tfun:g_type_class_ref\n}\n\n{\n\tg-io-module-default-singleton-malloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_type_create_instance\n\t...\n\tfun:_g_io_module_get_default\n}\n\n{\n\tg-io-module-default-singleton-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable,definite\n\tfun:calloc\n\t...\n\tfun:g_type_create_instance\n\t...\n\tfun:_g_io_module_get_default*\n}\n\n# This one seems to show up sometimes with g_type_create_instance() at the top\n# of the stack, as well.\n{\n\tg-io-module-default-singleton\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:g_type_create_instance\n\t...\n\tfun:_g_io_module_get_default\n}\n\n{\n\tg-io-module-default-singleton-module\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_module_open\n\t...\n\tfun:_g_io_module_get_default\n}\n\n{\n\tg-io-module-default-singleton-name\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_strdup\n\t...\n\tfun:_g_io_module_get_default*\n}\n\n{\n\tg-io-module-default-singleton-weak-ref\n\tMemcheck:Leak\n\tfun:calloc\n\t...\n\tfun:_g_io_module_get_default\n}\n\n{\n\tg-get-language-names-malloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_get_language_names\n}\n\n{\n\tg-get-language-names-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_get_language_names\n}\n\n{\n        g-get-language_names-with-category-malloc\n        Memcheck:Leak\n        match-leak-kinds:possible,reachable,definite\n        fun:malloc\n        ...\n        fun:g_get_language_names_with_category\n}\n\n{\n        g-get-language_names-with-category-calloc\n        Memcheck:Leak\n        match-leak-kinds:possible,reachable,definite\n        fun:calloc\n        ...\n        fun:g_get_language_names_with_category\n}\n\n{\n        g-get-language_names-with-category-realloc\n        Memcheck:Leak\n        match-leak-kinds:possible,reachable,definite\n        fun:realloc\n        ...\n        fun:g_get_language_names_with_category\n}\n\n{\n\tg-static-mutex\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_static_mutex_get_mutex_impl\n}\n\n{\n\tg-system-thread-init\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable\n\tfun:calloc\n\t...\n\tfun:g_system_thread_new\n}\n\n{\n\tg-system-thread-init-malloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable,definite\n\tfun:malloc\n\t...\n\tfun:g_system_thread_new\n}\n\n{\n\tg-task-thread-pool-init\n\tMemcheck:Leak\n\tmatch-leak-kinds:possible,reachable,definite\n\tfun:malloc\n\t...\n\tfun:g_thread_new\n\t...\n\tfun:g_task_thread_pool_init\n}\n\n{\n\tg-io-module-default-proxy-resolver-gnome\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_proxy_resolver_gnome_init\n\t...\n\tfun:_g_io_module_get_default\n}\n\n# One-time getaddrinfo() configuration loading\n{\n\tg-threaded-resolver-getaddrinfo-config\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable,definite\n\tfun:malloc\n\t...\n\tfun:__resolv_conf_allocate\n\t...\n\tfun:getaddrinfo\n\tfun:do_lookup_by_name\n}\n\n# memcheck checks that the third argument to ioctl() is a valid pointer, but\n# some ioctls use that argument as an integer\n{\n\tioctl-with-non-pointer-param\n\tMemcheck:Param\n\tioctl(generic)\n\tfun:ioctl\n\tfun:btrfs_reflink_with_progress\n}\n\n{\n\tg-private-get\n\tdrd:ConflictingAccess\n\tfun:g_private_get\n}\n{\n\tg-private-get-helgrind\n\tHelgrind:Race\n\tfun:g_private_get\n}\n\n\n{\n\tg-private-set\n\tdrd:ConflictingAccess\n\tfun:g_private_set\n}\n{\n\tg-private-set-helgrind\n\tHelgrind:Race\n\tfun:g_private_set\n}\n\n{\n\tg-type-construct-free\n\tdrd:ConflictingAccess\n\tfun:g_type_free_instance\n}\n{\n\tg-type-construct-free-helgrind\n\tHelgrind:Race\n\tfun:g_type_free_instance\n}\n\n{\n\tg-variant-unref\n\tdrd:ConflictingAccess\n\tfun:g_variant_unref\n}\n{\n\tg-variant-unref-helgrind\n\tHelgrind:Race\n\tfun:g_variant_unref\n}\n\n{\n\tg-unix-signals-main\n\tdrd:ConflictingAccess\n\tfun:_g_main_create_unix_signal_watch\n}\n{\n\tg-unix-signals-dispatch\n\tdrd:ConflictingAccess\n\t...\n\tfun:dispatch_unix_signals*\n}\n{\n\tg-unix-signals-dispatch-helgrind\n\tHelgrind:Race\n\t...\n\tfun:dispatch_unix_signals*\n}\n{\n\tg-unix-signals-other\n\tdrd:ConflictingAccess\n\tfun:g_unix_signal_watch*\n}\n{\n\tg-unix-signals-other-helgrind\n\tHelgrind:Race\n\tfun:g_unix_signal_watch*\n}\n{\n\tg-unix-signals-handler\n\tdrd:ConflictingAccess\n\tfun:g_unix_signal_handler*\n}\n{\n\tg-unix-signals-handler-helgrind\n\tHelgrind:Race\n\tfun:g_unix_signal_handler*\n}\n{\n\tg-unix-signals-worker\n\tdrd:ConflictingAccess\n\tfun:glib_worker_main\n}\n{\n\tg-unix-signals-worker-helgrind\n\tHelgrind:Race\n\tfun:glib_worker_main\n}\n\n{\n\tg-wakeup-acknowledge\n\tdrd:ConflictingAccess\n\tfun:read\n\tfun:g_wakeup_acknowledge\n}\n\n{\n\tg-type-fundamental\n\tdrd:ConflictingAccess\n\tfun:g_type_fundamental\n}\n{\n\tg-type-fundamental-helgrind\n\tHelgrind:Race\n\tfun:g_type_fundamental\n}\n{\n\tg-type-class-peek-static\n\tdrd:ConflictingAccess\n\tfun:g_type_class_peek_static\n}\n{\n\tg-type-class-peek-static-helgrind\n\tHelgrind:Race\n\tfun:g_type_class_peek_static\n}\n{\n\tg-type-is-a\n\tdrd:ConflictingAccess\n\t...\n\tfun:g_type_is_a\n}\n{\n\tg-type-is-a-helgrind\n\tHelgrind:Race\n\t...\n\tfun:g_type_is_a\n}\n\n{\n\tg-inet-address-get-type\n\tdrd:ConflictingAccess\n\tfun:g_inet_address_get_type\n}\n{\n\tg-inet-address-get-type-helgrind\n\tHelgrind:Race\n\tfun:g_inet_address_get_type\n}\n\n# From: https://github.com/fredericgermain/valgrind/blob/HEAD/glibc-2.X-drd.supp\n{\n\tdrd-libc-stdio\n\tdrd:ConflictingAccess\n\tobj:*/lib*/libc-*\n}\n{\n\tdrd-libc-recv\n\tdrd:ConflictingAccess\n\tfun:recv\n}\n{\n\tdrd-libc-send\n\tdrd:ConflictingAccess\n\tfun:send\n}\n\n# GSources do an opportunistic ref count check\n{\n\tg-source-set-ready-time\n\tdrd:ConflictingAccess\n\tfun:g_source_set_ready_time\n}\n{\n\tg-source-set-ready-time-helgrind\n\tHelgrind:Race\n\tfun:g_source_set_ready_time\n}\n\n{\n\tg-source-iter-next\n\tHelgrind:Race\n\tfun:g_source_iter_next\n\tfun:g_main_context_*\n\tfun:g_main_context_iterate\n}\n\n{\n\tg-object-instance-private\n\tdrd:ConflictingAccess\n\tfun:*_get_instance_private\n}\n{\n\tg-object-instance-private-helgrind\n\tHelgrind:Race\n\tfun:*_get_instance_private\n}\n\n# GLib legitimately calls pthread_cond_signal without a mutex held\n{\n\tg-task-thread-complete\n\tdrd:CondErr\n\t...\n\tfun:g_cond_signal\n\tfun:g_task_thread_complete\n}\n{\n\tg-task-thread-complete\n\tHelgrind:Misc\n\t...\n\tfun:g_cond_signal\n\tfun:g_task_thread_complete\n}\n\n# False positive, but I can't explain how (FIXME)\n{\n\tg-task-cond\n\tHelgrind:Misc\n\t...\n\tfun:g_cond_clear\n\tfun:g_task_finalize\n}\n\n# Real race, but is_cancelled() is an opportunistic function anyway\n{\n\tg-cancellable-is-cancelled\n\tHelgrind:Race\n\tfun:g_cancellable_is_cancelled\n}\n\n# False positive\n{\n\tg-main-context-cond\n\tHelgrind:Misc\n\t...\n\tfun:g_cond_clear\n\tfun:g_main_context_unref\n}\n\n# False positives\n{\n\tg-source-unlocked\n\tHelgrind:Race\n\tfun:g_source_*_unlocked\n}\n{\n\tg-source-internal\n\tHelgrind:Race\n\tfun:g_source_*_internal\n}\n\n# False positive\n{\n\tg_object_real_dispose\n\tHelgrind:Race\n\tfun:g_object_real_dispose\n}\n\n# False positive\n{\n\tg_object_new_valist\n\tHelgrind:Race\n\t...\n\tfun:g_object_new_valist\n}\n\n# g_set_user_dirs() deliberately leaks the previous cached g_get_user_*() values.\n# These will not all be reachable on exit.\n{\n\tg_set_user_dirs_str\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable,possible\n\tfun:malloc\n\t...\n\tfun:set_str_if_different\n\tfun:g_set_user_dirs\n}\n\n# g_set_user_dirs() deliberately leaks the previous cached g_get_user_*() values.\n# These will not all be reachable on exit.\n{\n\tg_set_user_dirs_strv\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable,possible\n\tfun:malloc\n\t...\n\tfun:set_strv_if_different\n\tfun:g_set_user_dirs\n}\n\n# _g_unset_cached_tmp_dir() deliberately leaks the previous cached g_get_tmp_dir() values.\n# These will not all be reachable on exit.\n{\n\tg_get_tmp_dir_test_init\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:malloc\n\t...\n\tfun:g_get_tmp_dir\n\t...\n\tfun:g_test_init\n}\n\n# g_get_tmp_dir() caches a one-time allocation\n{\n\tg_get_tmp_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:malloc\n\t...\n\tfun:g_get_tmp_dir\n}\n\n# g_get_system_data_dirs() caches a one-time allocation\n{\n\tg_get_system_data_dirs\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:malloc\n\t...\n\tfun:g_build_system_data_dirs\n\tfun:g_get_system_data_dirs\n}\n\n# g_get_user_data_dir() caches a one-time allocation\n{\n\tg_get_user_data_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:realloc\n\t...\n\tfun:g_build_user_data_dir\n\tfun:g_get_user_data_dir\n}\n\n# g_get_home_dir() caches a one-time allocation\n{\n\tg_get_home_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:malloc\n\t...\n\tfun:g_build_home_dir\n\tfun:g_get_home_dir\n}\n\n# gcontenttype-fdo.c caches a one-time allocation global array of @global_mime_dirs.\n{\n\tcontent_type_mime_dirs_realloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:realloc\n\t...\n\tfun:_g_content_type_set_mime_dirs_locked\n}\n\n# gdesktopappinfo.c caches a one-time allocation global table of @desktop_file_dirs.\n{\n\tdesktop_file_dirs_malloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:desktop_file_dirs_lock\n}\n\n# gdesktopappinfo.c caches a one-time allocation global table of @desktop_file_dirs.\n{\n\tdesktop_file_dirs_realloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:realloc\n\t...\n\tfun:desktop_file_dirs_lock\n}\n\n# gdesktopappinfo.c caches a one-time allocation global table of @desktop_file_dirs.\n{\n\tdesktop_file_dir_unindexed_setup_search\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:desktop_file_dir_unindexed_setup_search\n\tfun:desktop_file_dir_unindexed_setup_search\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_user_data_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_user_data_dir\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_filename\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_filename\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_home_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_home_dir\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_path\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_path\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_system_config_dirs\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:realloc\n\t...\n\tfun:g_build_system_config_dirs\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_system_data_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_system_data_dir\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_system_data_dirs\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:realloc\n\t...\n\tfun:g_build_system_data_dirs\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_user_cache_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_user_cache_dir\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_user_config_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_user_config_dir\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_user_data_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_user_data_dir\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_user_runtime_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_user_runtime_dir\n}\n\n#gutils.c caches system and user dirs and may need to replace them during tests.\n{\n\tg_build_user_state_dir\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:malloc\n\t...\n\tfun:g_build_user_state_dir\n}\n\n# g_io_extension_point_register() caches a one-time allocation global table of @extension_points.\n{\n\tg_io_extension_point_register\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_io_extension_point_register\n}\n\n# g_strerror() caches a one-time allocation global table of @errors.\n{\n\tg_strerror\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_locale_to_utf8\n\tfun:g_strerror\n}\n\n# g_socket_connection_factory_register_type() caches a one-time allocation global table of @connection_types.\n{\n\tg_socket_connection_factory_register_type\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_socket_connection_factory_register_type\n}\n\n# g_dbus_error_quark() never unregisters itself as a GDBusError domain, as it’s always available\n{\n\tg_dbus_error_quark\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_dbus_error_register_error_domain\n\tfun:g_dbus_error_quark\n}\n\n# g_win32_registry_get_os_dirs_w*() caches an array of strings that is allocated only once.\n{\n\tg_win32_registry_get_os_dirs\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable,definite\n\tfun:malloc\n\t...\n\tfun:g_win32_registry_get_os_dirs*\n}\n\n# Thread-private data allocated once per thread\n{\n\tg_private_set_alloc0\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:malloc\n\t...\n\tfun:g_private_set_alloc0\n}\n{\n\tg_private_set_alloc0-calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:calloc\n\t...\n\tfun:g_private_set_alloc0\n}\n\n# Keys for thread-private data\n{\n\tg_private_key\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\tfun:g_private_impl_new\n}\n\n# Thread-private GMainContext stack\n{\n\tg_main_context_push_thread_default\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite,reachable\n\tfun:malloc\n\t...\n\tfun:g_queue_new\n\tfun:g_main_context_push_thread_default\n}\n\n# One-time allocations for #GFileInfo attribute cache\n{\n\tg_file_info_attribute_cache\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:ensure_attribute_hash\n\t...\n\tfun:g_file_*\n}\n{\n\tg_file_info_attribute_cache2\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:ensure_attribute_hash\n\t...\n\tfun:g_file_*\n}\n{\n\tg_file_info_attribute_cache3\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:lookup_namespace\n\t...\n\tfun:g_file_*\n}\n{\n\tg_file_info_attribute_cache4\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:lookup_namespace\n\t...\n\tfun:g_file_*\n}\n\n# Cached charset\n{\n\tg_get_charset\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_get_charset\n}\n\n{\n\tg_get_charset_calloc\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:calloc\n\t...\n\tfun:g_get_charset\n}\n\n# Global unused thread queue\n{\n\tg_thread_pool_unused_thread_queue\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_async_queue_new_full\n\t...\n\tfun:g_thread_pool_new\n}\n\n# One-time program name storage\n{\n\tg_set_prgname\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_set_prgname\n}\n\n# Error domains hash\n{\n\tg_error_init\n\tMemcheck:Leak\n\tmatch-leak-kinds: reachable\n\tfun:malloc\n\t...\n\tfun:g_hash_table_new_full\n\tfun:g_error_init\n}\n\n# Error domain static registration\n{\n\tg_error_domain_register_static\n\tMemcheck:Leak\n\tmatch-leak-kinds: reachable\n\tfun:malloc\n\t...\n\tfun:g_hash_table_insert\n\tfun:error_domain_register\n\tfun:g_error_domain_register_static\n}\n\n{\n\tnew_quark\n\tMemcheck:Leak\n\tmatch-leak-kinds:reachable\n\tfun:malloc\n\t...\n\tfun:g_hash_table_insert\n\tfun:quark_new\n}\n\n{\n\txdg_mime_init_malloc\n\tMemcheck:Leak\n\tfun:malloc\n\t...\n\tfun:xdg_mime_init\n}\n\n{\n\txdg_mime_init_calloc\n\tMemcheck:Leak\n\tfun:calloc\n\t...\n\tfun:xdg_mime_init\n}\n\n# One-time allocations for default log writer lock and domains\n{\n\tshould_drop_message_rw_lock\n\tMemcheck:Leak\n\tmatch-leak-kinds: reachable\n\tfun:malloc\n\tfun:g_rw_lock_impl_new\n\tfun:g_rw_lock_get_impl\n\tfun:g_rw_lock_reader_lock\n\tfun:should_drop_message\n}\n\n{\n\tshould_drop_message_strdup\n\tMemcheck:Leak\n\tmatch-leak-kinds: reachable\n\tfun:malloc\n\tfun:g_malloc\n\tfun:g_strdup\n\tfun:g_strdup_inline\n\tfun:should_drop_message\n}\n\n{\n\tg_log_writer_default_set_debug_strdup\n\tMemcheck:Leak\n\tmatch-leak-kinds: reachable\n\tfun:malloc\n\tfun:g_malloc\n\tfun:g_strdup_inline\n\tfun:g_log_writer_default_set_debug_domains\n}\n\n{\n\tg_log_writer_default_set_debug_rw_lock\n\tMemcheck:Leak\n\tmatch-leak-kinds: reachable\n\tfun:malloc\n\tfun:g_rw_lock_impl_new\n\tfun:g_rw_lock_get_impl\n\tfun:g_rw_lock_writer_lock\n\tfun:g_log_writer_default_set_debug_domains\n}\n\n# This can be removed when versions of valgrind including the fix are widely used.\n# See https://gitlab.gnome.org/GNOME/glib/-/issues/3292\n{\n   g_utf8_collate_key wcsxfrm false-positive\n   Memcheck:Addr32\n   ...\n   fun:wcsxfrm*\n   fun:g_utf8_collate_key\n}\n\n# sysprof deliberately leaks one SysprofCollector per thread\n{\n\tglib-trace-collector\n\tMemcheck:Leak\n\tmatch-leak-kinds:definite\n\tfun:calloc\n\t...\n\tfun:sysprof_collector_get\n\t...\n\tfun:g_trace_mark\n}\n"
  },
  {
    "path": "src/janus.1",
    "content": ".TH JANUS 1\n.SH NAME\njanus \\- WebRTC server/gateway\n.SH SYNOPSIS\n.B janus\n[options]\n.SH DESCRIPTION\n.B janus\nis a WebRTC server/gateway developed by Meetecho conceived to be a general purpose one. As such, it doesn't provide any functionality per se other than implementing the means to set up a WebRTC media communication with a browser or application, exchanging JSON messages with it over different transports, and relaying RTP/RTCP and messages between clients and the server-side application logic they're attached to. Any specific feature/application is provided by server side plugins, that browsers can then contact via the server to take advantage of the functionality they provide. Example of such plugins can be implementations of applications like echo tests, conference bridges, media recorders, SIP gateways and the like.\n.TP\nThe reason for this is simple: we wanted something that would have a small footprint (hence a C implementation) and that we could only equip with what was really needed (hence pluggable modules). That is, something that would allow us to deploy either a full-fledged WebRTC server on the cloud, or a small nettop/box to handle a specific use case.\n.SH OPTIONS\n.TP\n.BR \\-h \", \" \\-\\-help\nPrint help and exit\n.TP\n.BR \\-V \", \" \\-\\-version\nPrint version and exit\n.TP\n.BR \\-b \", \" \\-\\-daemon\nLaunch Janus in background as a daemon (default=off)\n.TP\n.BR \\-p \", \" \\-\\-pid-file=\\fIpath\\fR\nOpen the specified PID file when starting Janus (default=none)\n.TP\n.BR \\-N \", \" \\-\\-disable-stdout\nDisable stdout based logging (default=off)\n.TP\n.BR \\-L \", \" \\-\\-log-stdout\nLog to stdout, even when the process is daemonized (default=off)\n.TP\n.BR \\-L \", \" \\-\\-log-file=\\fIpath\\fR\nLog to the specified file (default=stdout only)\n.TP\n.BR \\-H \", \" \\-\\-cwd-path=\\fIpath\\fR\nWorking directory for Janus daemon process (default=/)\n.TP\n.BR \\-i \", \" \\-\\-interface=\\fIipaddress\\fR\nInterface to use (will be the public IP)\n.TP\n.BR \\-P \", \" \\-\\-plugins-folder=\\fIpath\\fR\nPlugins folder (default=./plugins)\n.TP\n.BR \\-C \", \" \\-\\-config=\\fIfilename\\fR\nConfiguration file to use\n.TP\n.BR \\-F \", \" \\-\\-configs-folder=\\fIpath\\fR\nConfiguration files folder (default=./conf)\n.TP\n.BR \\-c \", \" \\-\\-cert-pem=\\fIfilename\\fR\nDTLS certificate\n.TP\n.BR \\-k \", \" \\-\\-cert-key=\\fIfilename\\fR\nDTLS certificate key\n.TP\n.BR \\-K \", \" \\-\\-cert-pwd=\\fItext\\fR\nDTLS certificate key passphrase (if needed)\n.TP\n.BR \\-S \", \" \\-\\-stun-server=\\fIaddress:port\\fR\nSTUN server(:port) to use, if needed (e.g., Janus behind NAT, default=none)\n.TP\n.BR \\-1 \", \" \\-\\-nat-1-1=\\fIips\\fR\nComma-separated list of public IPs to put in all host candidates, assuming a 1:1 NAT is in place (e.g., Amazon EC2 instances, default=none)\n.TP\n.BR \\-2 \", \" \\-\\-keep-private-host\nWhen nat-1-1 is used (e.g., Amazon EC2 instances), don't remove the private host, but keep both to simulate STUN (default=off)\n.TP\n.BR \\-E \", \" \\-\\-ice-enforce-list=\\fIlist\\fR\nComma-separated list of the only interfaces to use for ICE gathering; partial strings are supported (e.g., eth0 or eno1,wlan0, default=none)\n.TP\n.BR \\-X \", \" \\-\\-ice-ignore-list=\\fIlist\\fR\nComma-separated list of interfaces or IP addresses to ignore for ICE gathering; partial strings are supported (e.g., vmnet8,192.168.0.1,10.0.0.1 or vmnet,192.168., default=vmnet)\n.TP\n.BR \\-6 \", \" \\-\\-ipv6-candidates\nWhether to enable IPv6 candidates or not (default=off)\n.TP\n.BR \\-O \", \" \\-\\-ipv6-link-local\nWhether IPv6 link-local candidates should be gathered as well (default=off)\n.TP\n.BR \\-l \", \" \\-\\-libnice-debug\nWhether to enable libnice debugging or not (default=off)\n.TP\n.BR \\-f \", \" \\-\\-full-trickle\nDo full-trickle instead of half-trickle (default=off)\n.TP\n.BR \\-I \", \" \\-\\-ice-lite\nWhether to enable the ICE Lite mode or not (default=off)\n.TP\n.BR \\-T \", \" \\-\\-ice-tcp\nWhether to enable ICE-TCP or not (warning: only works with ICE Lite) (default=off)\n.TP\n.BR \\-Q \", \" \\-\\-min-nack-queue=\\fInumber\\fR\nMinimum size of the NACK queue (in ms) per user for retransmissions, no matter the RTT\n.TP\n.BR \\-t \", \" \\-\\-no-media-timer=\\fInumber\\fR\nTime (in s) that should pass with no media (audio or video) being received before Janus notifies you about this\n.TP\n.BR \\-W \", \" \\-\\-slowlink-threshold=\\fInumber\\fR\nNumber of lost packets (per s) that should trigger a 'slowlink' Janus API event to users (default=0, feature disabled)\n.TP\n.BR \\-r \", \" \\-\\-rtp-port-range=\\fImin\\-max\\fR\nPort range to use for RTP/RTCP\n.TP\n.BR \\-B \", \" \\-\\-twcc-period=\\fInumber\\fR\nHow often (in ms) to send TWCC feedback back to senders, if negotiated (default=200ms)\n.TP\n.BR \\-n \", \" \\-\\-server-name=\\fIname\\fR\nPublic name of this Janus instance (default=MyJanusInstance)\n.TP\n.BR \\-s \", \" \\-\\-session-timeout=\\fInumber\\fR\nSession timeout value, in seconds (default=60)\n.TP\n.BR \\-m \", \" \\-\\-reclaim-session-timeout=\\fInumber\\fR\nReclaim session timeout value, in seconds (default=0)\n.TP\n.BR \\-d \", \" \\-\\-debug-level=\\fI1\\-7\\fR\nDebug/logging level (0=disable debugging, 7=maximum debug level; default=4)\n.TP\n.BR \\-D \", \" \\-\\-debug-timestamps\nEnable debug/logging timestamps (default=off)\n.TP\n.BR \\-o \", \" \\-\\-disable-colors\nDisable color in the logging (default=off)\n.TP\n.BR \\-M \", \" \\-\\-debug-locks\nEnable debugging of locks/mutexes (very verbose! default=off)\n.TP\n.BR \\-a \", \" \\-\\-apisecret=\\fIrandomstring\\fR\nAPI secret all requests need to pass in order to be accepted by Janus (useful when wrapping Janus API requests in a server, none by default)\n.TP\n.BR \\-A \", \" \\-\\-token-auth\nEnable token-based authentication for all requests (default=off)\n.TP\n.BR \\-e \", \" \\-\\-event-handlers\nEnable event handlers (default=off)\n.TP\n.BR \\-w \", \" \\-\\-no-webrtc-encryption\nDisable WebRTC encryption, so no DTLS or SRTP (only for debugging!) (default=off)\n.SH EXAMPLES\n\\fBjanus\\fR \\- Launch Janus with all options from configurations files\n.TP\n\\fBjanus \\-b \\-L /tmp/januslog\\fR \\- Launch Janus as a daemon and log to the specified file\n.TP\n\\fBjanus \\-6\\fR \\- Launch Janus with IPv6 support enabled\n.TP\n\\fBjanus \\-f\\fR \\- Launch Janus with full-trickle enabled\n.SH BUGS\n.TP\nIf you think you found a bug or want to contribute a feature, you can issue or a pull request on https://github.com/meetecho/janus-gateway/issues.\n.TP\nAnyway, before doing that make sure you read the documentation at https://janus.conf.meetecho.com/docs/ and that it has not been discussed already at https://janus.discourse.group/. We only use Github for code issues, and \\fBNOT\\fR for configuration or usage issues: use the group for that.\n.SH SEE ALSO\n.TP\nhttps://github.com/meetecho/janus-gateway \\- Official repository\n.TP\nhttps://janus.conf.meetecho.com \\- Demos and documentation\n.TP\nhttps://janus.discourse.group/ \\- Community\n.TP\nhttps://www.meetecho.com/blog/ \\- Tutorials and blog posts on Janus\n.SH AUTHORS\nLorenzo Miniero (lorenzo@meetecho.com)\n"
  },
  {
    "path": "src/janus.c",
    "content": "/*! \\file   janus.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus core\n * \\details Implementation of the Janus core. This code takes care of\n * the server initialization (command line/configuration) and setup,\n * and makes use of the available transport plugins (by default HTTP,\n * WebSockets, RabbitMQ, if compiled) and Janus protocol (a JSON-based\n * protocol) to interact with the applications, whether they're web based\n * or not. The core also takes care of bridging peers and plugins\n * accordingly, in terms of both messaging and real-time media transfer\n * via WebRTC.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <dlfcn.h>\n#include <dirent.h>\n#include <net/if.h>\n#include <netdb.h>\n#include <signal.h>\n#include <getopt.h>\n#include <sys/resource.h>\n#include <sys/stat.h>\n#include <fcntl.h>\n#include <poll.h>\n\n#include <openssl/rand.h>\n#ifdef HAVE_TURNRESTAPI\n#include <curl/curl.h>\n#endif\n\n#include \"janus.h\"\n#include \"version.h\"\n#include \"options.h\"\n#include \"config.h\"\n#include \"apierror.h\"\n#include \"debug.h\"\n#include \"ip-utils.h\"\n#include \"rtcp.h\"\n#include \"rtpfwd.h\"\n#include \"auth.h\"\n#include \"record.h\"\n#include \"events.h\"\n\n\n#define JANUS_NAME\t\t\t\t\"Janus WebRTC Server\"\n#define JANUS_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_SERVER_NAME\t\t\"MyJanusInstance\"\n\n#ifdef __MACH__\n#define SHLIB_EXT \"2.dylib\"\n#else\n#define SHLIB_EXT \".so\"\n#endif\n\n\n/* Command line options */\nstatic janus_options options = { 0 };\n\n/* Configuration file */\nstatic janus_config *config = NULL;\nstatic char *config_file = NULL;\nstatic char *configs_folder = NULL;\n\nstatic GHashTable *transports = NULL;\nstatic GHashTable *transports_so = NULL;\n\nstatic GHashTable *eventhandlers = NULL;\nstatic GHashTable *eventhandlers_so = NULL;\n\nstatic GHashTable *loggers = NULL;\nstatic GHashTable *loggers_so = NULL;\n\nstatic GHashTable *plugins = NULL;\nstatic GHashTable *plugins_so = NULL;\n\n\n/* Daemonization */\nstatic gboolean daemonize = FALSE;\nstatic int pipefd[2];\n\n\n#ifdef REFCOUNT_DEBUG\n/* Reference counters debugging */\nGHashTable *counters = NULL;\njanus_mutex counters_mutex;\n#endif\n\n\n/* API secrets */\nstatic char *api_secret = NULL, *admin_api_secret = NULL;\n\n/* JSON parameters */\nstatic int janus_process_error_string(janus_request *request, uint64_t session_id, const char *transaction, gint error, gchar *error_string);\n\nstatic struct janus_json_parameter incoming_request_parameters[] = {\n\t{\"transaction\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"janus\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter attach_parameters[] = {\n\t{\"plugin\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"opaque_id\", JSON_STRING, 0},\n\t{\"loop_index\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n};\nstatic struct janus_json_parameter body_parameters[] = {\n\t{\"body\", JSON_OBJECT, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter jsep_parameters[] = {\n\t{\"type\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"sdp\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"trickle\", JANUS_JSON_BOOL, 0},\n\t{\"rid_order\", JSON_STRING, 0},\n\t{\"force_relay\", JANUS_JSON_BOOL, 0},\n\t{\"e2ee\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter add_token_parameters[] = {\n\t{\"token\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"plugins\", JSON_ARRAY, 0}\n};\nstatic struct janus_json_parameter token_parameters[] = {\n\t{\"token\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter admin_parameters[] = {\n\t{\"transaction\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"janus\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter debug_parameters[] = {\n\t{\"debug\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter timeout_parameters[] = {\n\t{\"timeout\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter session_timeout_parameters[] = {\n\t{\"timeout\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter level_parameters[] = {\n\t{\"level\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter timestamps_parameters[] = {\n\t{\"timestamps\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter colors_parameters[] = {\n\t{\"colors\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter mnq_parameters[] = {\n\t{\"min_nack_queue\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter nopt_parameters[] = {\n\t{\"nack_optimizations\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter nmt_parameters[] = {\n\t{\"no_media_timer\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter st_parameters[] = {\n\t{\"slowlink_threshold\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter ans_parameters[] = {\n\t{\"accept\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter querytransport_parameters[] = {\n\t{\"transport\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"request\", JSON_OBJECT, 0}\n};\nstatic struct janus_json_parameter queryhandler_parameters[] = {\n\t{\"handler\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"request\", JSON_OBJECT, 0}\n};\nstatic struct janus_json_parameter querylogger_parameters[] = {\n\t{\"logger\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"request\", JSON_OBJECT, 0}\n};\nstatic struct janus_json_parameter messageplugin_parameters[] = {\n\t{\"plugin\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"request\", JSON_OBJECT, 0}\n};\nstatic struct janus_json_parameter customevent_parameters[] = {\n\t{\"schema\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"data\", JSON_OBJECT, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter customlogline_parameters[] = {\n\t{\"line\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"level\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter text2pcap_parameters[] = {\n\t{\"folder\", JSON_STRING, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"truncate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter handleinfo_parameters[] = {\n\t{\"plugin_only\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter resaddr_parameters[] = {\n\t{\"address\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter teststun_parameters[] = {\n\t{\"address\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"port\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"localport\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\n\n/* Admin/Monitor helpers */\njson_t *janus_admin_peerconnection_summary(janus_ice_peerconnection *pc);\njson_t *janus_admin_peerconnection_medium_summary(janus_ice_peerconnection_medium *medium);\n\n\n/* IP addresses */\nstatic gchar *local_ip = NULL;\ngchar *janus_get_local_ip(void) {\n\treturn local_ip;\n}\nstatic GHashTable *public_ips_table = NULL;\nstatic GList *public_ips = NULL;\ngboolean public_ips_ipv4 = FALSE, public_ips_ipv6 = FALSE;\nguint janus_get_public_ip_count(void) {\n\treturn public_ips_table ? g_hash_table_size(public_ips_table) : 0;\n}\ngchar *janus_get_public_ip(guint index) {\n\tif (!janus_get_public_ip_count()) {\n\t\t/* Fallback to the local IP, if we have no public one */\n\t\treturn local_ip;\n\t}\n\tif (index >= g_hash_table_size(public_ips_table)) {\n\t\tindex = g_hash_table_size(public_ips_table) - 1;\n\t}\n\treturn (char *)g_list_nth(public_ips, index)->data;\n}\nvoid janus_add_public_ip(const gchar *ip) {\n\tif(ip == NULL) {\n\t\treturn;\n\t}\n\n\tif(!public_ips_table) {\n\t\tpublic_ips_table = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t}\n\tif (g_hash_table_insert(public_ips_table, g_strdup(ip), NULL)) {\n\t\tg_list_free(public_ips);\n\t\tpublic_ips = g_hash_table_get_keys(public_ips_table);\n\t}\n\t/* Take note of whether we received at least one IPv4 and/or IPv6 address */\n\tif(strchr(ip, ':')) {\n\t\tpublic_ips_ipv6 = TRUE;\n\t} else {\n\t\tpublic_ips_ipv4 = TRUE;\n\t}\n}\ngboolean janus_has_public_ipv4_ip(void) {\n\treturn public_ips_ipv4;\n}\ngboolean janus_has_public_ipv6_ip(void) {\n\treturn public_ips_ipv6;\n}\n\nstatic volatile gint stop = 0;\nstatic gint stop_signal = 0;\ngint janus_is_stopping(void) {\n\treturn g_atomic_int_get(&stop);\n}\nstatic GMainLoop *mainloop = NULL;\n\n\n/* Public instance name */\nstatic gchar *server_name = NULL;\n\nstatic json_t *janus_create_message(const char *status, uint64_t session_id, const char *transaction) {\n\tjson_t *msg = json_object();\n\tjson_object_set_new(msg, \"janus\", json_string(status));\n\tif(session_id > 0)\n\t\tjson_object_set_new(msg, \"session_id\", json_integer(session_id));\n\tif(transaction != NULL)\n\t\tjson_object_set_new(msg, \"transaction\", json_string(transaction));\n\treturn msg;\n}\n\n/* The default timeout for sessions is 60 seconds: this means that, if\n * we don't get any activity (i.e., no request) on this session for more\n * than 60 seconds, then it's considered expired and we destroy it. That's\n * why we have a keep-alive method in the API. This can be overridden in\n * either janus.cfg/.jcfg or from the command line. Setting this to 0 will\n * disable the timeout mechanism, which is NOT suggested as it may risk\n * having orphaned sessions (sessions not controlled by any transport\n * and never freed). Besides, notice that if you make this shorter than\n * 30s, you'll have to update the timers in janus.js when the long\n * polling mechanism is used and shorten them as well, or you'll risk\n * incurring in unexpected timeouts (when HTTP is used in janus.js, the\n * long poll is used as a keepalive mechanism). */\n#define DEFAULT_SESSION_TIMEOUT\t\t60\nstatic uint global_session_timeout = DEFAULT_SESSION_TIMEOUT;\n\n#define DEFAULT_RECLAIM_SESSION_TIMEOUT\t\t0\nstatic uint reclaim_session_timeout = DEFAULT_RECLAIM_SESSION_TIMEOUT;\n\n/* We can programmatically change whether we want to accept new sessions\n * or not: the default is of course TRUE, but we may want to temporarily\n * change that in some cases, e.g., if we don't want the load on this\n * server to grow too much, or because we're draining the server. */\nstatic gboolean accept_new_sessions = TRUE;\n\n/* We don't hold (trickle) candidates indefinitely either: by default, we\n * only store them for 45 seconds. After that, they're discarded, in order\n * to avoid leaks or orphaned media details. This means that, if for instance\n * you're trying to set up a call with someone, and that someone only answers\n * a minute later, the candidates you sent initially will be discarded and\n * the call will fail. You can modify the default value in janus.jcfg */\n#define DEFAULT_CANDIDATES_TIMEOUT\t\t45\nstatic uint candidates_timeout = DEFAULT_CANDIDATES_TIMEOUT;\n\n/* By default we list dependencies details, but some may prefer not to */\nstatic gboolean hide_dependencies = FALSE;\n\n/* By default we do not exit if a shared library cannot be loaded or is missing an expected symbol */\nstatic gboolean exit_on_dl_error = FALSE;\n\n/* WebRTC encryption is obviously enabled by default. In the rare cases\n * you want to disable it for debugging purposes, though, you can do\n * that either via command line (-w) or in the main configuration file */\nstatic gboolean webrtc_encryption = TRUE;\ngboolean janus_is_webrtc_encryption_enabled(void) {\n\treturn webrtc_encryption;\n}\n\n/* Information */\nstatic json_t *janus_info(const char *transaction) {\n\t/* Prepare a summary on the Janus instance */\n\tjson_t *info = janus_create_message(\"server_info\", 0, transaction);\n\tjson_object_set_new(info, \"name\", json_string(JANUS_NAME));\n\tjson_object_set_new(info, \"version\", json_integer(janus_version));\n\tjson_object_set_new(info, \"version_string\", json_string(janus_version_string));\n\tjson_object_set_new(info, \"author\", json_string(JANUS_AUTHOR));\n\tjson_object_set_new(info, \"commit-hash\", json_string(janus_build_git_sha));\n\tjson_object_set_new(info, \"compile-time\", json_string(janus_build_git_time));\n\tjson_object_set_new(info, \"log-to-stdout\", janus_log_is_stdout_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"log-to-file\", janus_log_is_logfile_enabled() ? json_true() : json_false());\n\tif(janus_log_is_logfile_enabled())\n\t\tjson_object_set_new(info, \"log-path\", json_string(janus_log_get_logfile_path()));\n#ifdef HAVE_SCTP\n\tjson_object_set_new(info, \"data_channels\", json_true());\n#else\n\tjson_object_set_new(info, \"data_channels\", json_false());\n#endif\n\tjson_object_set_new(info, \"accepting-new-sessions\", accept_new_sessions ? json_true() : json_false());\n\tjson_object_set_new(info, \"session-timeout\", json_integer(global_session_timeout));\n\tjson_object_set_new(info, \"reclaim-session-timeout\", json_integer(reclaim_session_timeout));\n\tjson_object_set_new(info, \"candidates-timeout\", json_integer(candidates_timeout));\n\tjson_object_set_new(info, \"server-name\", json_string(server_name ? server_name : JANUS_SERVER_NAME));\n\tjson_object_set_new(info, \"local-ip\", json_string(local_ip));\n\tguint public_ip_count = janus_get_public_ip_count();\n\tif(public_ip_count > 0) {\n\t\tjson_object_set_new(info, \"public-ip\", json_string(janus_get_public_ip(0)));\n\t}\n\tif(public_ip_count > 1) {\n\t\tguint i;\n\t\tjson_t *ips = json_array();\n\t\tfor (i = 0; i < public_ip_count; i++) {\n\t\t\tjson_array_append_new(ips, json_string(janus_get_public_ip(i)));\n\t\t}\n\t\tjson_object_set_new(info, \"public-ips\", ips);\n\t}\n\tjson_object_set_new(info, \"ipv6\", janus_ice_is_ipv6_enabled() ? json_true() : json_false());\n\tif(janus_ice_is_ipv6_enabled())\n\t\tjson_object_set_new(info, \"ipv6-link-local\", janus_ice_is_ipv6_linklocal_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"ice-lite\", janus_ice_is_ice_lite_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"ice-tcp\", janus_ice_is_ice_tcp_enabled() ? json_true() : json_false());\n#ifdef HAVE_ICE_NOMINATION\n\tjson_object_set_new(info, \"ice-nomination\", json_string(janus_ice_get_nomination_mode()));\n#endif\n\tjson_object_set_new(info, \"ice-consent-freshness\", janus_ice_is_consent_freshness_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"ice-keepalive-conncheck\", janus_ice_is_keepalive_conncheck_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"hangup-on-failed\", janus_ice_is_hangup_on_failed_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"full-trickle\", janus_ice_is_full_trickle_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"mdns-enabled\", janus_ice_is_mdns_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"min-nack-queue\", json_integer(janus_get_min_nack_queue()));\n\tjson_object_set_new(info, \"nack-optimizations\", janus_is_nack_optimizations_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"twcc-period\", json_integer(janus_get_twcc_period()));\n\tif(janus_get_dscp() > 0)\n\t\tjson_object_set_new(info, \"dscp\", json_integer(janus_get_dscp()));\n\tjson_object_set_new(info, \"dtls-mtu\", json_integer(janus_dtls_bio_agent_get_mtu()));\n\tif(janus_ice_get_stun_server() != NULL) {\n\t\tchar server[255];\n\t\tg_snprintf(server, 255, \"%s:%\"SCNu16, janus_ice_get_stun_server(), janus_ice_get_stun_port());\n\t\tjson_object_set_new(info, \"stun-server\", json_string(server));\n\t}\n\tif(janus_ice_get_turn_server() != NULL) {\n\t\tchar server[255];\n\t\tg_snprintf(server, 255, \"%s:%\"SCNu16, janus_ice_get_turn_server(), janus_ice_get_turn_port());\n\t\tjson_object_set_new(info, \"turn-server\", json_string(server));\n\t}\n\tif(janus_ice_is_force_relay_allowed())\n\t\tjson_object_set_new(info, \"allow-force-relay\", json_true());\n\tjson_object_set_new(info, \"static-event-loops\", json_integer(janus_ice_get_static_event_loops()));\n\tif(janus_ice_get_static_event_loops())\n\t\tjson_object_set_new(info, \"loop-indication\", janus_ice_is_loop_indication_allowed() ? json_true() : json_false());\n\tjson_object_set_new(info, \"api_secret\", api_secret ? json_true() : json_false());\n\tjson_object_set_new(info, \"auth_token\", janus_auth_is_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"event_handlers\", janus_events_is_enabled() ? json_true() : json_false());\n\tjson_object_set_new(info, \"opaqueid_in_api\", janus_is_opaqueid_in_api_enabled() ? json_true() : json_false());\n\tif(!webrtc_encryption)\n\t\tjson_object_set_new(info, \"webrtc_encryption\", json_false());\n\t/* Dependencies */\n\tif(!hide_dependencies) {\n\t\tjson_t *deps = json_object();\n\t\tchar glib2_version[20];\n\t\tg_snprintf(glib2_version, sizeof(glib2_version), \"%d.%d.%d\", glib_major_version, glib_minor_version, glib_micro_version);\n\t\tjson_object_set_new(deps, \"glib2\", json_string(glib2_version));\n\t\tjson_object_set_new(deps, \"jansson\", json_string(JANSSON_VERSION));\n\t\tjson_object_set_new(deps, \"libnice\", json_string(libnice_version_string));\n\t\tjson_object_set_new(deps, \"libsrtp\", json_string(srtp_get_version_string()));\n\t#ifdef HAVE_TURNRESTAPI\n\t\tcurl_version_info_data *curl_version = curl_version_info(CURLVERSION_NOW);\n\t\tif(curl_version && curl_version->version)\n\t\t\tjson_object_set_new(deps, \"libcurl\", json_string(curl_version->version));\n\t#endif\n\t\tjson_object_set_new(deps, \"crypto\", json_string(janus_get_ssl_version()));\n\t\tjson_object_set_new(info, \"dependencies\", deps);\n\t}\n\t/* Available transports */\n\tjson_t *t_data = json_object();\n\tif(transports && g_hash_table_size(transports) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, transports);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_transport *t = value;\n\t\t\tif(t == NULL) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *transport = json_object();\n\t\t\tjson_object_set_new(transport, \"name\", json_string(t->get_name()));\n\t\t\tjson_object_set_new(transport, \"author\", json_string(t->get_author()));\n\t\t\tjson_object_set_new(transport, \"description\", json_string(t->get_description()));\n\t\t\tjson_object_set_new(transport, \"version_string\", json_string(t->get_version_string()));\n\t\t\tjson_object_set_new(transport, \"version\", json_integer(t->get_version()));\n\t\t\tjson_object_set_new(t_data, t->get_package(), transport);\n\t\t}\n\t}\n\tjson_object_set_new(info, \"transports\", t_data);\n\t/* Available event handlers */\n\tjson_t *e_data = json_object();\n\tif(eventhandlers && g_hash_table_size(eventhandlers) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, eventhandlers);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_eventhandler *e = value;\n\t\t\tif(e == NULL) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *eventhandler = json_object();\n\t\t\tjson_object_set_new(eventhandler, \"name\", json_string(e->get_name()));\n\t\t\tjson_object_set_new(eventhandler, \"author\", json_string(e->get_author()));\n\t\t\tjson_object_set_new(eventhandler, \"description\", json_string(e->get_description()));\n\t\t\tjson_object_set_new(eventhandler, \"version_string\", json_string(e->get_version_string()));\n\t\t\tjson_object_set_new(eventhandler, \"version\", json_integer(e->get_version()));\n\t\t\tjson_object_set_new(e_data, e->get_package(), eventhandler);\n\t\t}\n\t}\n\tjson_object_set_new(info, \"events\", e_data);\n\t/* Available external loggers */\n\tjson_t *l_data = json_object();\n\tif(loggers && g_hash_table_size(loggers) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, loggers);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_logger *l = value;\n\t\t\tif(l == NULL) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *logger = json_object();\n\t\t\tjson_object_set_new(logger, \"name\", json_string(l->get_name()));\n\t\t\tjson_object_set_new(logger, \"author\", json_string(l->get_author()));\n\t\t\tjson_object_set_new(logger, \"description\", json_string(l->get_description()));\n\t\t\tjson_object_set_new(logger, \"version_string\", json_string(l->get_version_string()));\n\t\t\tjson_object_set_new(logger, \"version\", json_integer(l->get_version()));\n\t\t\tjson_object_set_new(l_data, l->get_package(), logger);\n\t\t}\n\t}\n\tjson_object_set_new(info, \"loggers\", l_data);\n\t/* Available plugins */\n\tjson_t *p_data = json_object();\n\tif(plugins && g_hash_table_size(plugins) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, plugins);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_plugin *p = value;\n\t\t\tif(p == NULL) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *plugin = json_object();\n\t\t\tjson_object_set_new(plugin, \"name\", json_string(p->get_name()));\n\t\t\tjson_object_set_new(plugin, \"author\", json_string(p->get_author()));\n\t\t\tjson_object_set_new(plugin, \"description\", json_string(p->get_description()));\n\t\t\tjson_object_set_new(plugin, \"version_string\", json_string(p->get_version_string()));\n\t\t\tjson_object_set_new(plugin, \"version\", json_integer(p->get_version()));\n\t\t\tjson_object_set_new(p_data, p->get_package(), plugin);\n\t\t}\n\t}\n\tjson_object_set_new(info, \"plugins\", p_data);\n\n\treturn info;\n}\n\n\n/* Logging */\nint janus_log_level = LOG_INFO;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = FALSE;\nchar *janus_log_global_prefix = NULL;\nstatic int janus_log_rotate_sig = 0;\nint lock_debug = 0;\n#ifdef REFCOUNT_DEBUG\nint refcount_debug = 1;\n#else\nint refcount_debug = 0;\n#endif\n\n\n/*! \\brief Signal handler (just used to intercept CTRL+C, SIGTERM and SIGUSR1) */\nstatic void janus_handle_signal(int signum) {\n\tif(signum == janus_log_rotate_sig && janus_log_rotate_sig > 0) {\n\t\tif(stop_signal > 0)\n\t\t\t/* Skip log rotation while stopping */\n\t\t\treturn;\n\t\tjanus_log_reload();\n\t\treturn;\n\t}\n\t/* If we got here it's either a SIGINT or a SIGTERM */\n\tstop_signal = signum;\n\tswitch(g_atomic_int_get(&stop)) {\n\t\tcase 0:\n\t\t\tJANUS_PRINT(\"Stopping server, please wait...\\n\");\n\t\t\tbreak;\n\t\tcase 1:\n\t\t\tJANUS_PRINT(\"In a hurry? I'm trying to free resources cleanly, here!\\n\");\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tJANUS_PRINT(\"Ok, leaving immediately...\\n\");\n\t\t\tbreak;\n\t}\n\tg_atomic_int_inc(&stop);\n\tif(g_atomic_int_get(&stop) > 2)\n\t\texit(1);\n\tif(mainloop && g_main_loop_is_running(mainloop))\n\t\tg_main_loop_quit(mainloop);\n}\n\n/*! \\brief Termination handler (atexit) */\nstatic void janus_termination_handler(void) {\n\t/* Free the instance name, if provided */\n\tg_free(server_name);\n\t/* Remove the PID file if we created it */\n\tjanus_pidfile_remove();\n\t/* Close the logger */\n\tjanus_log_destroy();\n\t/* Get rid of external loggers too, if any */\n\tif(loggers != NULL && g_hash_table_size(loggers) > 0) {\n\t\tg_hash_table_foreach(loggers, janus_logger_close, NULL);\n\t\tg_hash_table_destroy(loggers);\n\t}\n\tif(loggers_so != NULL && g_hash_table_size(loggers_so) > 0) {\n\t\tg_hash_table_foreach(loggers_so, janus_loggerso_close, NULL);\n\t\tg_hash_table_destroy(loggers_so);\n\t}\n\t/* If we're daemonizing, we send an error code to the parent */\n\tif(daemonize) {\n\t\tint code = 1;\n\t\tssize_t res = 0;\n\t\tdo {\n\t\t\tres = write(pipefd[1], &code, sizeof(int));\n\t\t} while(res == -1 && errno == EINTR);\n\t}\n}\n\n\n/** @name Transport plugin callback interface\n * These are the callbacks implemented by the Janus core, as part of\n * the janus_transport_callbacks interface. Everything the transport\n * plugins send the core is handled here.\n */\n///@{\nvoid janus_transport_incoming_request(janus_transport *plugin, janus_transport_session *transport, void *request_id, gboolean admin, json_t *message, json_error_t *error);\nvoid janus_transport_gone(janus_transport *plugin, janus_transport_session *transport);\ngboolean janus_transport_is_api_secret_needed(janus_transport *plugin);\ngboolean janus_transport_is_api_secret_valid(janus_transport *plugin, const char *apisecret);\ngboolean janus_transport_is_auth_token_needed(janus_transport *plugin);\ngboolean janus_transport_is_auth_token_valid(janus_transport *plugin, const char *token);\nvoid janus_transport_notify_event(janus_transport *plugin, void *transport, json_t *event);\n\nstatic janus_transport_callbacks janus_handler_transport =\n\t{\n\t\t.incoming_request = janus_transport_incoming_request,\n\t\t.transport_gone = janus_transport_gone,\n\t\t.is_api_secret_needed = janus_transport_is_api_secret_needed,\n\t\t.is_api_secret_valid = janus_transport_is_api_secret_valid,\n\t\t.is_auth_token_needed = janus_transport_is_auth_token_needed,\n\t\t.is_auth_token_valid = janus_transport_is_auth_token_valid,\n\t\t.events_is_enabled = janus_events_is_enabled,\n\t\t.notify_event = janus_transport_notify_event,\n\t};\nstatic GAsyncQueue *requests = NULL;\nstatic janus_request exit_message;\nstatic GThreadPool *tasks = NULL;\nvoid janus_transport_task(gpointer data, gpointer user_data);\n///@}\n\n\n/** @name Plugin callback interface\n * These are the callbacks implemented by the Janus core, as part of\n * the janus_callbacks interface. Everything the plugins send the\n * core is handled here.\n */\n///@{\nint janus_plugin_push_event(janus_plugin_session *plugin_session, janus_plugin *plugin, const char *transaction, json_t *message, json_t *jsep);\njson_t *janus_plugin_handle_sdp(janus_plugin_session *plugin_session, janus_plugin *plugin, const char *sdp_type, const char *sdp, gboolean restart);\nvoid janus_plugin_relay_rtp(janus_plugin_session *plugin_session, janus_plugin_rtp *packet);\nvoid janus_plugin_relay_rtcp(janus_plugin_session *plugin_session, janus_plugin_rtcp *packet);\nvoid janus_plugin_relay_data(janus_plugin_session *plugin_session, janus_plugin_data *message);\nvoid janus_plugin_send_pli(janus_plugin_session *plugin_session);\nvoid janus_plugin_send_pli_stream(janus_plugin_session *plugin_session, int mindex);\nvoid janus_plugin_send_remb(janus_plugin_session *plugin_session, uint32_t bitrate);\nvoid janus_plugin_close_pc(janus_plugin_session *plugin_session);\nvoid janus_plugin_end_session(janus_plugin_session *plugin_session);\nvoid janus_plugin_notify_event(janus_plugin *plugin, janus_plugin_session *plugin_session, json_t *event);\ngboolean janus_plugin_auth_is_signed(void);\ngboolean janus_plugin_auth_is_signature_valid(janus_plugin *plugin, const char *token);\ngboolean janus_plugin_auth_signature_contains(janus_plugin *plugin, const char *token, const char *desc);\nstatic janus_callbacks janus_handler_plugin =\n\t{\n\t\t.push_event = janus_plugin_push_event,\n\t\t.relay_rtp = janus_plugin_relay_rtp,\n\t\t.relay_rtcp = janus_plugin_relay_rtcp,\n\t\t.relay_data = janus_plugin_relay_data,\n\t\t.send_pli = janus_plugin_send_pli,\n\t\t.send_pli_stream = janus_plugin_send_pli_stream,\n\t\t.send_remb = janus_plugin_send_remb,\n\t\t.close_pc = janus_plugin_close_pc,\n\t\t.end_session = janus_plugin_end_session,\n\t\t.events_is_enabled = janus_events_is_enabled,\n\t\t.notify_event = janus_plugin_notify_event,\n\t\t.auth_is_signed = janus_plugin_auth_is_signed,\n\t\t.auth_is_signature_valid = janus_plugin_auth_is_signature_valid,\n\t\t.auth_signature_contains = janus_plugin_auth_signature_contains,\n\t};\n///@}\n\n\n/* Core Sessions */\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\nstatic GHashTable *sessions = NULL;\nstatic GMainContext *sessions_watchdog_context = NULL;\n\n/* Counters */\nstatic volatile gint sessions_num = 0;\nstatic volatile gint handles_num = 0;\n\nstatic void janus_ice_handle_dereference(janus_ice_handle *handle) {\n\tif(handle)\n\t\tjanus_refcount_decrease(&handle->ref);\n}\n\nstatic void janus_session_free(const janus_refcount *session_ref) {\n\tjanus_session *session = janus_refcount_containerof(session_ref, janus_session, ref);\n\t/* This session can be destroyed, free all the resources */\n\tif(session->ice_handles != NULL) {\n\t\tg_hash_table_destroy(session->ice_handles);\n\t\tsession->ice_handles = NULL;\n\t}\n\tif(session->source != NULL) {\n\t\tjanus_request_destroy(session->source);\n\t\tsession->source = NULL;\n\t}\n\tjanus_mutex_destroy(&session->mutex);\n\tg_free(session);\n}\n\nstatic janus_request *janus_session_get_request(janus_session *session) {\n\tif(session == NULL)\n\t\treturn NULL;\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_request *source = session->source;\n\tif(source != NULL && !g_atomic_int_get(&source->destroyed)) {\n\t\tjanus_refcount_increase(&source->ref);\n\t} else {\n\t\tsource = NULL;\n\t}\n\tjanus_mutex_unlock(&session->mutex);\n\treturn source;\n}\nstatic void janus_request_unref(janus_request *request) {\n\tif(request)\n\t\tjanus_refcount_decrease(&request->ref);\n}\n\nstatic gboolean janus_check_sessions(gpointer user_data) {\n\tjanus_mutex_lock(&sessions_mutex);\n\tif(sessions && g_hash_table_size(sessions) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, sessions);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_session *session = (janus_session *) value;\n\t\t\tif(!session || g_atomic_int_get(&session->destroyed))\n\t\t\t\tcontinue;\n\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t/* Use either session-specific timeout or global. */\n\t\t\tgint64 timeout = (gint64)session->timeout;\n\t\t\tif(timeout == -1)\n\t\t\t\ttimeout = (gint64)global_session_timeout;\n\t\t\tif((timeout > 0 && (now - session->last_activity >= timeout * G_USEC_PER_SEC)) ||\n\t\t\t\t\t((g_atomic_int_get(&session->transport_gone) && now - session->last_activity >= (gint64)reclaim_session_timeout * G_USEC_PER_SEC))) {\n\t\t\t\tif(g_atomic_int_compare_and_exchange(&session->timedout, 0, 1)) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Timeout expired for session %\"SCNu64\"...\\n\", session->session_id);\n\t\t\t\t\t/* Mark the session as over, we'll deal with it later */\n\t\t\t\t\tjanus_session_handles_clear(session);\n\t\t\t\t\t/* Notify the transport */\n\t\t\t\t\tjanus_request *source = janus_session_get_request(session);\n\t\t\t\t\tif(source) {\n\t\t\t\t\t\tjson_t *event = janus_create_message(\"timeout\", session->session_id, NULL);\n\t\t\t\t\t\t/* Send this to the transport client and notify the session's over */\n\t\t\t\t\t\tsource->transport->send_message(source->instance, NULL, FALSE, event);\n\t\t\t\t\t\tsource->transport->session_over(source->instance, session->session_id, TRUE, FALSE);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_request_unref(source);\n\t\t\t\t\t/* Notify event handlers as well */\n\t\t\t\t\tif(janus_events_is_enabled())\n\t\t\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_SESSION, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\t\t\t\tsession->session_id, \"timeout\", NULL);\n\t\t\t\t\tg_hash_table_iter_remove(&iter);\n\t\t\t\t\tg_atomic_int_dec_and_test(&sessions_num);\n\t\t\t\t\tjanus_session_destroy(session);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn G_SOURCE_CONTINUE;\n}\n\nstatic gpointer janus_sessions_watchdog(gpointer user_data) {\n\tGMainLoop *loop = (GMainLoop *) user_data;\n\tGMainContext *watchdog_context = g_main_loop_get_context(loop);\n\tGSource *timeout_source;\n\n\ttimeout_source = g_timeout_source_new_seconds(2);\n\tg_source_set_callback(timeout_source, janus_check_sessions, watchdog_context, NULL);\n\tg_source_attach(timeout_source, watchdog_context);\n\tg_source_unref(timeout_source);\n\n\tJANUS_LOG(LOG_INFO, \"Sessions watchdog started\\n\");\n\n\tg_main_loop_run(loop);\n\n\tJANUS_LOG(LOG_INFO, \"Sessions watchdog stopped\\n\");\n\n\treturn NULL;\n}\n\nstatic gboolean janus_status_sessions(gpointer user_data) {\n\tif(janus_events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"status\", json_string(\"update\"));\n\t\tjson_t *details = json_object();\n\t\tjson_object_set_new(details, \"sessions\", json_integer(g_atomic_int_get(&sessions_num)));\n\t\tjson_object_set_new(details, \"handles\", json_integer(g_atomic_int_get(&handles_num)));\n\t\tjson_object_set_new(details, \"peerconnections\", json_integer(janus_ice_get_peerconnection_num()));\n\t\tjson_object_set_new(details, \"stats-period\", json_integer(janus_ice_get_event_stats_period()));\n\t\tjson_object_set_new(info, \"info\", details);\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_CORE, JANUS_EVENT_SUBTYPE_CORE_STARTUP, 0, info);\n\t}\n\treturn TRUE;\n}\n\njanus_session *janus_session_create(guint64 session_id) {\n\tjanus_session *session = NULL;\n\tif(session_id == 0) {\n\t\twhile(session_id == 0) {\n\t\t\tsession_id = janus_random_uint64();\n\t\t\tsession = janus_session_find(session_id);\n\t\t\tif(session != NULL) {\n\t\t\t\t/* Session ID already taken, try another one */\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\tsession_id = 0;\n\t\t\t}\n\t\t}\n\t}\n\tsession = (janus_session *)g_malloc(sizeof(janus_session));\n\tJANUS_LOG(LOG_INFO, \"Creating new session: %\"SCNu64\"; %p\\n\", session_id, session);\n\tsession->session_id = session_id;\n\tjanus_refcount_init(&session->ref, janus_session_free);\n\tsession->source = NULL;\n\tsession->timeout = -1; /* Negative means rely on global timeout */\n\tg_atomic_int_set(&session->destroyed, 0);\n\tg_atomic_int_set(&session->timedout, 0);\n\tg_atomic_int_set(&session->transport_gone, 0);\n\tsession->last_activity = janus_get_monotonic_time();\n\tsession->ice_handles = NULL;\n\tjanus_mutex_init(&session->mutex);\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, janus_uint64_dup(session->session_id), session);\n\tg_atomic_int_inc(&sessions_num);\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn session;\n}\n\njanus_session *janus_session_find(guint64 session_id) {\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_session *session = g_hash_table_lookup(sessions, &session_id);\n\tif(session != NULL) {\n\t\t/* A successful find automatically increases the reference counter:\n\t\t * it's up to the caller to decrease it again when done */\n\t\tjanus_refcount_increase(&session->ref);\n\t}\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn session;\n}\n\nvoid janus_session_notify_event(janus_session *session, json_t *event) {\n\tif(session != NULL && !g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_request *source = janus_session_get_request(session);\n\t\tif(source != NULL && source->transport != NULL) {\n\t\t\t/* Send this to the transport client */\n\t\t\tJANUS_LOG(LOG_HUGE, \"Sending event to %s (%p)\\n\", source->transport->get_package(), source->instance);\n\t\t\tsource->transport->send_message(source->instance, NULL, FALSE, event);\n\t\t} else {\n\t\t\t/* No transport, free the event */\n\t\t\tjson_decref(event);\n\t\t}\n\t\tjanus_request_unref(source);\n\t} else {\n\t\t/* No session, free the event */\n\t\tjson_decref(event);\n\t}\n}\n\n\n/* Destroys a session but does not remove it from the sessions hash table. */\ngint janus_session_destroy(janus_session *session) {\n\tguint64 session_id = session->session_id;\n\tJANUS_LOG(LOG_INFO, \"Destroying session %\"SCNu64\"; %p\\n\", session_id, session);\n\tif(!g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\treturn 0;\n\tjanus_session_handles_clear(session);\n\t/* The session will actually be destroyed when the counter gets to 0 */\n\tjanus_refcount_decrease(&session->ref);\n\n\treturn 0;\n}\n\njanus_ice_handle *janus_session_handles_find(janus_session *session, guint64 handle_id) {\n\tif(session == NULL)\n\t\treturn NULL;\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_ice_handle *handle = session->ice_handles ? g_hash_table_lookup(session->ice_handles, &handle_id) : NULL;\n\tif(handle != NULL) {\n\t\t/* A successful find automatically increases the reference counter:\n\t\t * it's up to the caller to decrease it again when done */\n\t\tjanus_refcount_increase(&handle->ref);\n\t}\n\tjanus_mutex_unlock(&session->mutex);\n\treturn handle;\n}\n\nvoid janus_session_handles_insert(janus_session *session, janus_ice_handle *handle) {\n\tjanus_mutex_lock(&session->mutex);\n\tif(session->ice_handles == NULL)\n\t\tsession->ice_handles = g_hash_table_new_full(g_int64_hash, g_int64_equal, (GDestroyNotify)g_free, (GDestroyNotify)janus_ice_handle_dereference);\n\tjanus_refcount_increase(&handle->ref);\n\tg_hash_table_insert(session->ice_handles, janus_uint64_dup(handle->handle_id), handle);\n\tg_atomic_int_inc(&handles_num);\n\tjanus_mutex_unlock(&session->mutex);\n}\n\ngint janus_session_handles_remove(janus_session *session, janus_ice_handle *handle) {\n\tjanus_mutex_lock(&session->mutex);\n\tgint error = janus_ice_handle_destroy(session, handle);\n\tif(g_hash_table_remove(session->ice_handles, &handle->handle_id))\n\t\tg_atomic_int_dec_and_test(&handles_num);\n\tjanus_mutex_unlock(&session->mutex);\n\treturn error;\n}\n\nvoid janus_session_handles_clear(janus_session *session) {\n\tjanus_mutex_lock(&session->mutex);\n\tif(session->ice_handles != NULL && g_hash_table_size(session->ice_handles) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\t/* Remove all handles */\n\t\tg_hash_table_iter_init(&iter, session->ice_handles);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_ice_handle *handle = value;\n\t\t\tif(!handle)\n\t\t\t\tcontinue;\n\t\t\tjanus_ice_handle_destroy(session, handle);\n\t\t\tg_hash_table_iter_remove(&iter);\n\t\t\tg_atomic_int_dec_and_test(&handles_num);\n\t\t}\n\t}\n\tjanus_mutex_unlock(&session->mutex);\n}\n\njson_t *janus_session_handles_list_json(janus_session *session) {\n\tjson_t *list = json_array();\n\tjanus_mutex_lock(&session->mutex);\n\tif(session->ice_handles != NULL && g_hash_table_size(session->ice_handles) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, session->ice_handles);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_ice_handle *handle = value;\n\t\t\tif(!handle)\n\t\t\t\tcontinue;\n\t\t\tjson_array_append_new(list, json_integer(handle->handle_id));\n\t\t}\n\t}\n\tjanus_mutex_unlock(&session->mutex);\n\treturn list;\n}\n\n/* Requests management */\nstatic void janus_request_free(const janus_refcount *request_ref) {\n\tjanus_request *request = janus_refcount_containerof(request_ref, janus_request, ref);\n\t/* This request can be destroyed, free all the resources */\n\trequest->transport = NULL;\n\tif(request->instance)\n\t\tjanus_refcount_decrease(&request->instance->ref);\n\trequest->instance = NULL;\n\trequest->request_id = NULL;\n\tif(request->message) {\n\t\tjson_decref(request->message);\n\t\trequest->message = NULL;\n\t}\n\tif(request->error) {\n\t\tg_free(request->error);\n\t\trequest->error = NULL;\n\t}\n\tg_free(request);\n}\n\njanus_request *janus_request_new(janus_transport *transport, janus_transport_session *instance, void *request_id, gboolean admin, json_t *message, json_error_t *error) {\n\tjanus_request *request = g_malloc(sizeof(janus_request));\n\trequest->transport = transport;\n\trequest->instance = instance;\n\tjanus_refcount_increase(&instance->ref);\n\trequest->request_id = request_id;\n\trequest->admin = admin;\n\trequest->message = message;\n\tif(error) {\n\t\trequest->error = (json_error_t*)g_malloc(sizeof(json_error_t));\n\t\t*request->error = *error;\n\t} else {\n\t\trequest->error = NULL;\n\t}\n\tg_atomic_int_set(&request->destroyed, 0);\n\tjanus_refcount_init(&request->ref, janus_request_free);\n\treturn request;\n}\n\nvoid janus_request_destroy(janus_request *request) {\n\tif(request == NULL || request == &exit_message || !g_atomic_int_compare_and_exchange(&request->destroyed, 0, 1))\n\t\treturn;\n\tjanus_refcount_decrease(&request->ref);\n}\n\nstatic int janus_request_check_secret(janus_request *request, guint64 session_id, const gchar *transaction_text) {\n\tgboolean secret_authorized = FALSE, token_authorized = FALSE;\n\tif(api_secret == NULL && !janus_auth_is_enabled()) {\n\t\t/* Nothing to check */\n\t\tsecret_authorized = TRUE;\n\t\ttoken_authorized = TRUE;\n\t} else {\n\t\tjson_t *root = request->message;\n\t\tif(api_secret != NULL) {\n\t\t\t/* There's an API secret, check that the client provided it */\n\t\t\tjson_t *secret = json_object_get(root, \"apisecret\");\n\t\t\tif(secret && json_is_string(secret) && janus_strcmp_const_time(json_string_value(secret), api_secret)) {\n\t\t\t\tsecret_authorized = TRUE;\n\t\t\t}\n\t\t}\n\t\tif(janus_auth_is_enabled()) {\n\t\t\t/* The token based authentication mechanism is enabled, check that the client provided it */\n\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\tif(token && json_is_string(token) && janus_auth_check_token(json_string_value(token))) {\n\t\t\t\ttoken_authorized = TRUE;\n\t\t\t}\n\t\t}\n\t\t/* We consider a request authorized if either the proper API secret or a valid token has been provided */\n\t\tif(!(api_secret != NULL && secret_authorized) && !(janus_auth_is_enabled() && token_authorized))\n\t\t\treturn JANUS_ERROR_UNAUTHORIZED;\n\t}\n\treturn 0;\n}\n\nstatic void janus_request_ice_handle_answer(janus_ice_handle *handle, char *jsep_sdp) {\n\t/* We got our answer */\n\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t/* Any pending trickles? */\n\tif(handle->pending_trickles) {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- Processing %d pending trickle candidates\\n\", handle->handle_id, g_list_length(handle->pending_trickles));\n\t\tGList *temp = NULL;\n\t\twhile(handle->pending_trickles) {\n\t\t\ttemp = g_list_first(handle->pending_trickles);\n\t\t\thandle->pending_trickles = g_list_remove_link(handle->pending_trickles, temp);\n\t\t\tjanus_ice_trickle *trickle = (janus_ice_trickle *)temp->data;\n\t\t\tg_list_free(temp);\n\t\t\tif(trickle == NULL)\n\t\t\t\tcontinue;\n\t\t\tif((janus_get_monotonic_time() - trickle->received) > candidates_timeout*G_USEC_PER_SEC) {\n\t\t\t\t/* FIXME Candidate is too old, discard it */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Discarding candidate (too old)\\n\", handle->handle_id);\n\t\t\t\tjanus_ice_trickle_destroy(trickle);\n\t\t\t\t/* FIXME We should report that */\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *candidate = trickle->candidate;\n\t\t\tif(candidate == NULL) {\n\t\t\t\tjanus_ice_trickle_destroy(trickle);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(json_is_object(candidate)) {\n\t\t\t\t/* We got a single candidate */\n\t\t\t\tint error = 0;\n\t\t\t\tconst char *error_string = NULL;\n\t\t\t\tif((error = janus_ice_trickle_parse(handle, candidate, &error_string)) != 0) {\n\t\t\t\t\t/* FIXME We should report the error parsing the trickle candidate */\n\t\t\t\t}\n\t\t\t} else if(json_is_array(candidate)) {\n\t\t\t\t/* We got multiple candidates in an array */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Got multiple candidates (%zu)\\n\", handle->handle_id, json_array_size(candidate));\n\t\t\t\tif(json_array_size(candidate) > 0) {\n\t\t\t\t\t/* Handle remote candidates */\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(candidate); i++) {\n\t\t\t\t\t\tjson_t *c = json_array_get(candidate, i);\n\t\t\t\t\t\t/* FIXME We don't care if any trickle fails to parse */\n\t\t\t\t\t\tjanus_ice_trickle_parse(handle, c, NULL);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Done, free candidate */\n\t\t\tjanus_ice_trickle_destroy(trickle);\n\t\t}\n\t}\n\n\tgboolean candidates_found = (handle->pc && g_slist_length(handle->pc->candidates) > 0);\n\t/* This was an answer, check if it's time to start ICE */\n\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE) && !candidates_found) {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- ICE Trickling is supported by the browser, waiting for remote candidates...\\n\", handle->handle_id);\n\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_START);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Done! Sending connectivity checks...\\n\", handle->handle_id);\n\t\tjanus_ice_setup_remote_candidates(handle, handle->stream_id, 1);\n\t}\n}\n\nint janus_process_incoming_request(janus_request *request) {\n\tint ret = -1;\n\tif(request == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Missing request or payload to process, giving up...\\n\");\n\t\treturn ret;\n\t}\n\tjson_t *root = request->message;\n\tif(root == NULL) {\n\t\tjson_error_t *error = request->error;\n\t\tif(error != NULL) {\n\t\t\tret = janus_process_error(request, 0, NULL, JANUS_ERROR_INVALID_JSON,\n\t\t\t\t\"Invalid Janus API request - %s(%d,%d): %s\", error->source, error->line, error->column, error->text);\n\t\t} else {\n\t\t\tret = janus_process_error_string(request, 0, NULL, JANUS_ERROR_INVALID_JSON, (char *)\"Invalid Janus API request\");\n\t\t}\n\t\treturn ret;\n\t}\n\t/* Ok, let's start with the ids */\n\tguint64 session_id = 0, handle_id = 0;\n\tjson_t *s = json_object_get(root, \"session_id\");\n\tif(json_is_null(s))\n\t\ts = NULL;\n\tif(s && json_is_integer(s))\n\t\tsession_id = json_integer_value(s);\n\tjson_t *h = json_object_get(root, \"handle_id\");\n\tif(json_is_null(h))\n\t\th = NULL;\n\tif(h && json_is_integer(h))\n\t\thandle_id = json_integer_value(h);\n\n\tjanus_session *session = NULL;\n\tjanus_ice_handle *handle = NULL;\n\n\tint error_code = 0;\n\tchar error_cause[100];\n\t/* Get transaction and message request */\n\tJANUS_VALIDATE_JSON_OBJECT(root, incoming_request_parameters,\n\t\terror_code, error_cause, FALSE,\n\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\tif(error_code != 0) {\n\t\tret = janus_process_error_string(request, session_id, NULL, error_code, error_cause);\n\t\tgoto jsondone;\n\t}\n\tjson_t *transaction = json_object_get(root, \"transaction\");\n\tconst gchar *transaction_text = json_string_value(transaction);\n\tjson_t *message = json_object_get(root, \"janus\");\n\tconst gchar *message_text = json_string_value(message);\n\n\tif(session_id == 0 && handle_id == 0) {\n\t\t/* Can only be a 'Create new session', a 'Get info' or a 'Ping/Pong' request */\n\t\tif(!strcasecmp(message_text, \"info\")) {\n\t\t\tret = janus_process_success(request, janus_info(transaction_text));\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(!strcasecmp(message_text, \"ping\")) {\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"pong\", 0, transaction_text);\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(strcasecmp(message_text, \"create\")) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* Make sure we're accepting new sessions */\n\t\tif(!accept_new_sessions) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_NOT_ACCEPTING_SESSIONS, NULL);\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* Any secret/token to check? */\n\t\tret = janus_request_check_secret(request, session_id, transaction_text);\n\t\tif(ret != 0) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNAUTHORIZED, NULL);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tsession_id = 0;\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tif(id != NULL) {\n\t\t\t/* The application provided the session ID to use */\n\t\t\tsession_id = json_integer_value(id);\n\t\t\tif(session_id > 0 && (session = janus_session_find(session_id)) != NULL) {\n\t\t\t\t/* Session ID already taken */\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_SESSION_CONFLICT, \"Session ID already in use\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t}\n\n\t\t/* Handle it */\n\t\tsession = janus_session_create(session_id);\n\t\tif(session == NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Memory error\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tsession_id = session->session_id;\n\t\t/* We increase the counter as this request is using the session */\n\t\tjanus_refcount_increase(&session->ref);\n\t\t/* Take note of the request source that originated this session (HTTP, WebSockets, RabbitMQ?) */\n\t\tsession->source = janus_request_new(request->transport, request->instance, NULL, FALSE, NULL, NULL);\n\t\t/* Notify the source that a new session has been created */\n\t\trequest->transport->session_created(request->instance, session->session_id);\n\t\t/* Notify event handlers */\n\t\tif(janus_events_is_enabled()) {\n\t\t\t/* Session created, add info on the transport that originated it */\n\t\t\tjson_t *transport = json_object();\n\t\t\tjson_object_set_new(transport, \"transport\", json_string(session->source->transport->get_package()));\n\t\t\tchar id[32];\n\t\t\tmemset(id, 0, sizeof(id));\n\t\t\t/* To avoid sending a stringified version of the transport pointer\n\t\t\t * around, we convert it to a number and hash it instead */\n\t\t\tuint64_t p = janus_uint64_hash(GPOINTER_TO_UINT(session->source->instance));\n\t\t\tg_snprintf(id, sizeof(id), \"%\"SCNu64, p);\n\t\t\tjson_object_set_new(transport, \"id\", json_string(id));\n\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_SESSION, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\tsession_id, \"created\", transport);\n\t\t}\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\tjson_t *data = json_object();\n\t\tjson_object_set_new(data, \"id\", json_integer(session_id));\n\t\tjson_object_set_new(reply, \"data\", data);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t\tgoto jsondone;\n\t}\n\tif(session_id < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid session\\n\");\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_SESSION_NOT_FOUND, NULL);\n\t\tgoto jsondone;\n\t}\n\tif(h && handle_id < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid handle\\n\");\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_HANDLE_NOT_FOUND, NULL);\n\t\tgoto jsondone;\n\t}\n\n\t/* Go on with the processing */\n\tret = janus_request_check_secret(request, session_id, transaction_text);\n\tif(ret != 0) {\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNAUTHORIZED, NULL);\n\t\tgoto jsondone;\n\t}\n\n\t/* If we got here, make sure we have a session (and/or a handle) */\n\tsession = janus_session_find(session_id);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't find any session %\"SCNu64\"...\\n\", session_id);\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_SESSION_NOT_FOUND, \"No such session %\"SCNu64\"\", session_id);\n\t\tgoto jsondone;\n\t}\n\t/* Update the last activity timer */\n\tsession->last_activity = janus_get_monotonic_time();\n\thandle = NULL;\n\tif(handle_id > 0) {\n\t\thandle = janus_session_handles_find(session, handle_id);\n\t\tif(!handle) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't find any handle %\"SCNu64\" in session %\"SCNu64\"...\\n\", handle_id, session_id);\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_HANDLE_NOT_FOUND, \"No such handle %\"SCNu64\" in session %\"SCNu64\"\", handle_id, session_id);\n\t\t\tgoto jsondone;\n\t\t}\n\t}\n\n\t/* What is this? */\n\tif(!strcasecmp(message_text, \"keepalive\")) {\n\t\t/* Just a keep-alive message, reply with an ack */\n\t\tJANUS_LOG(LOG_VERB, \"Got a keep-alive on session %\"SCNu64\"\\n\", session_id);\n\t\tjson_t *reply = janus_create_message(\"ack\", session_id, transaction_text);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t} else if(!strcasecmp(message_text, \"attach\")) {\n\t\tif(handle != NULL) {\n\t\t\t/* Attach is a session-level command */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, attach_parameters,\n\t\t\terror_code, error_cause, FALSE,\n\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\tif(error_code != 0) {\n\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjson_t *plugin = json_object_get(root, \"plugin\");\n\t\tconst gchar *plugin_text = json_string_value(plugin);\n\t\tjanus_plugin *plugin_t = janus_plugin_find(plugin_text);\n\t\tif(plugin_t == NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_NOT_FOUND, \"No such plugin '%s'\", plugin_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* If the auth token mechanism is enabled, we should check if this token can access this plugin */\n\t\tconst char *token_value = NULL;\n\t\tif(janus_auth_is_enabled()) {\n\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\tif(token != NULL) {\n\t\t\t\ttoken_value = json_string_value(token);\n\t\t\t\tif(token_value && !janus_auth_check_plugin(token_value, plugin_t)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Token '%s' can't access plugin '%s'\\n\", token_value, plugin_text);\n\t\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNAUTHORIZED_PLUGIN, \"Provided token can't access plugin '%s'\", plugin_text);\n\t\t\t\t\tgoto jsondone;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjson_t *opaque = json_object_get(root, \"opaque_id\");\n\t\tconst char *opaque_id = opaque ? json_string_value(opaque) : NULL;\n\t\tjson_t *loop = json_object_get(root, \"loop_index\");\n\t\tint loop_index = loop ? json_integer_value(loop) : -1;\n\t\t/* Create handle */\n\t\thandle = janus_ice_handle_create(session, opaque_id, token_value);\n\t\tif(handle == NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Memory error\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\thandle_id = handle->handle_id;\n\t\t/* We increase the counter as this request is using the handle */\n\t\tjanus_refcount_increase(&handle->ref);\n\t\t/* Attach to the plugin */\n\t\tint error = 0;\n\t\tif((error = janus_ice_handle_attach_plugin(session, handle, plugin_t, loop_index)) != 0) {\n\t\t\t/* TODO Make error struct to pass verbose information */\n\t\t\tjanus_session_handles_remove(session, handle);\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't attach to plugin '%s', error '%d'\\n\", plugin_text, error);\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_ATTACH, \"Couldn't attach to plugin: error '%d'\", error);\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\tjson_t *data = json_object();\n\t\tjson_object_set_new(data, \"id\", json_integer(handle_id));\n\t\tjson_object_set_new(reply, \"data\", data);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t} else if(!strcasecmp(message_text, \"destroy\")) {\n\t\tif(handle != NULL) {\n\t\t\t/* Query is a session-level command */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tif(g_hash_table_remove(sessions, &session->session_id))\n\t\t\tg_atomic_int_dec_and_test(&sessions_num);\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Notify the source that the session has been destroyed */\n\t\tjanus_request *source = janus_session_get_request(session);\n\t\tif(source && source->transport)\n\t\t\tsource->transport->session_over(source->instance, session->session_id, FALSE, FALSE);\n\t\tjanus_request_unref(source);\n\n\t\t/* Schedule the session for deletion */\n\t\tjanus_session_destroy(session);\n\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t\t/* Notify event handlers as well */\n\t\tif(janus_events_is_enabled())\n\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_SESSION, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\tsession_id, \"destroyed\", NULL);\n\t} else if(!strcasecmp(message_text, \"detach\")) {\n\t\tif(handle == NULL) {\n\t\t\t/* Query is an handle-level command */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(handle->app == NULL || handle->app_handle == NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_DETACH, \"No plugin to detach from\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tint error = janus_session_handles_remove(session, handle);\n\t\tif(error != 0) {\n\t\t\t/* TODO Make error struct to pass verbose information */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_DETACH, \"Couldn't detach from plugin: error '%d'\", error);\n\t\t\t/* TODO Delete handle instance */\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t} else if(!strcasecmp(message_text, \"hangup\")) {\n\t\tif(handle == NULL) {\n\t\t\t/* Query is an handle-level command */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(handle->app == NULL || handle->app_handle == NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_DETACH, \"No plugin attached\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjanus_ice_webrtc_hangup(handle, \"Janus API\");\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t} else if(!strcasecmp(message_text, \"claim\")) {\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tif(session->source != NULL) {\n\t\t\t/* If we're claiming from the same transport, ignore */\n\t\t\tif(session->source->instance == request->instance) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t/* Prepare JSON reply */\n\t\t\t\tjson_t *reply = json_object();\n\t\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\t\tjson_object_set_new(reply, \"session_id\", json_integer(session_id));\n\t\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\t\t/* Send the success reply */\n\t\t\t\tret = janus_process_success(request, reply);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\t/* Notify the old transport that this session is over for them, but has been reclaimed */\n\t\t\tsession->source->transport->session_over(session->source->instance, session->session_id, FALSE, TRUE);\n\t\t\tjanus_request_destroy(session->source);\n\t\t\tsession->source = NULL;\n\t\t}\n\t\tsession->source = janus_request_new(request->transport, request->instance, NULL, FALSE, NULL, NULL);\n\t\t/* Notify the new transport that it has claimed a session */\n\t\tsession->source->transport->session_claimed(session->source->instance, session->session_id);\n\t\t/* Previous transport may be gone, clear flag */\n\t\tg_atomic_int_set(&session->transport_gone, 0);\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = json_object();\n\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\tjson_object_set_new(reply, \"session_id\", json_integer(session_id));\n\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t} else if(!strcasecmp(message_text, \"message\")) {\n\t\tif(handle == NULL) {\n\t\t\t/* Query is an handle-level command */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(handle->app == NULL || handle->app_handle == NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_MESSAGE, \"No plugin to handle this message\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjanus_plugin *plugin_t = (janus_plugin *)handle->app;\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] There's a message for %s\\n\", handle->handle_id, plugin_t->get_name());\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, body_parameters,\n\t\t\terror_code, error_cause, FALSE,\n\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\tif(error_code != 0) {\n\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjson_t *body = json_object_get(root, \"body\");\n\t\t/* Is there an SDP attached? */\n\t\tjson_t *jsep = json_object_get(root, \"jsep\");\n\t\tchar *jsep_type = NULL;\n\t\tchar *jsep_sdp = NULL, *jsep_sdp_stripped = NULL;\n\t\tgboolean renegotiation = FALSE;\n\t\tif(jsep != NULL) {\n\t\t\tif(!json_is_object(jsep)) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_JSON_OBJECT, \"Invalid jsep object\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tJANUS_VALIDATE_JSON_OBJECT_FORMAT(\"JSEP error: missing mandatory element (%s)\",\n\t\t\t\t\"JSEP error: invalid element type (%s should be %s)\",\n\t\t\t\tjsep, jsep_parameters, error_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *type = json_object_get(jsep, \"type\");\n\t\t\tjsep_type = g_strdup(json_string_value(type));\n\t\t\ttype = NULL;\n\t\t\tjson_t *jsep_trickle = json_object_get(jsep, \"trickle\");\n\t\t\tgboolean do_trickle = jsep_trickle ? json_is_true(jsep_trickle) : TRUE;\n\t\t\tjson_t *jsep_rids = json_object_get(jsep, \"rid_order\");\n\t\t\tgboolean rids_hml = TRUE;\n\t\t\tif(jsep_rids != NULL) {\n\t\t\t\tconst char *jsep_rids_value = json_string_value(jsep_rids);\n\t\t\t\tif(jsep_rids_value != NULL) {\n\t\t\t\t\tif(!strcasecmp(jsep_rids_value, \"hml\")) {\n\t\t\t\t\t\trids_hml = TRUE;\n\t\t\t\t\t} else if(!strcasecmp(jsep_rids_value, \"lmh\")) {\n\t\t\t\t\t\trids_hml = FALSE;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Invalid 'rid_order' value, falling back to 'hml'\\n\", handle->handle_id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjson_object_del(jsep, \"rid_order\");\n\t\t\t}\n\t\t\tjson_t *jsep_e2ee = json_object_get(jsep, \"e2ee\");\n\t\t\tgboolean e2ee = jsep_e2ee ? json_is_true(jsep_e2ee) : FALSE;\n\t\t\tjson_t *svc = json_object_get(jsep, \"svc\");\n\t\t\tif(svc) {\n\t\t\t\t/* SVC properties were passed as well, make sure the format is correct */\n\t\t\t\tgboolean svc_valid = TRUE;\n\t\t\t\tif(!json_is_array(svc) || json_array_size(svc) < 1) {\n\t\t\t\t\tsvc_valid = FALSE;\n\t\t\t\t} else {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(svc); i++) {\n\t\t\t\t\t\tjson_t *s = json_array_get(svc, i);\n\t\t\t\t\t\tif(!json_is_object(s)) {\n\t\t\t\t\t\t\tsvc_valid = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *s_mindex = json_object_get(s, \"mindex\");\n\t\t\t\t\t\tjson_t *s_mid = json_object_get(s, \"mid\");\n\t\t\t\t\t\tjson_t *s_svc = json_object_get(s, \"svc\");\n\t\t\t\t\t\tif(!s_mindex && !s_mid) {\n\t\t\t\t\t\t\tsvc_valid = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif((s_mindex && !json_is_integer(s_mindex)) ||\n\t\t\t\t\t\t\t\t(s_mid && !json_is_string(s_mid)) ||\n\t\t\t\t\t\t\t\t!s_svc || !json_is_string(s_svc)) {\n\t\t\t\t\t\t\tsvc_valid = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!svc_valid) {\n\t\t\t\t\tsvc = NULL;\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Invalid 'svc' value, ignoring\\n\", handle->handle_id);\n\t\t\t\t\tjson_object_del(jsep, \"svc\");\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Are we still cleaning up from a previous media session? */\n\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Still cleaning up from a previous media session, let's wait a bit...\\n\", handle->handle_id);\n\t\t\t\tgint64 waited = 0;\n\t\t\t\twhile(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {\n\t\t\t\t\tg_usleep(100000);\n\t\t\t\t\twaited += 100000;\n\t\t\t\t\tif(waited >= 3*G_USEC_PER_SEC) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- Waited 3 seconds, that's enough!\\n\", handle->handle_id);\n\t\t\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_WEBRTC_STATE, \"Still cleaning a previous session\");\n\t\t\t\t\t\tgoto jsondone;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Check if we're renegotiating (if we have an answer, we did an offer/answer round already) */\n\t\t\trenegotiation = janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEGOTIATED);\n\t\t\t/* Check the JSEP type */\n\t\t\tjanus_mutex_lock(&handle->mutex);\n\t\t\tgboolean offer = FALSE;\n\t\t\tif(!strcasecmp(jsep_type, \"offer\")) {\n\t\t\t\toffer = TRUE;\n\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER);\n\t\t\t} else if(!strcasecmp(jsep_type, \"answer\")) {\n\t\t\t\toffer = FALSE;\n\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER);\n\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER))\n\t\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEGOTIATED);\n\t\t\t} else {\n\t\t\t\t/* TODO Handle other message types as well */\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_JSEP_UNKNOWN_TYPE, \"JSEP error: unknown message type '%s'\", jsep_type);\n\t\t\t\tg_free(jsep_type);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *sdp = json_object_get(jsep, \"sdp\");\n\t\t\tjsep_sdp = (char *)json_string_value(sdp);\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Remote SDP:\\n%s\", handle->handle_id, jsep_sdp);\n\t\t\t/* Is this valid SDP? */\n\t\t\tchar error_str[512];\n\t\t\terror_str[0] = '\\0';\n\t\t\tjanus_dtls_role peer_dtls_role = JANUS_DTLS_ROLE_ACTPASS;\n\t\t\tint audio = 0, video = 0, data = 0;\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_preparse(handle, jsep_sdp, error_str, sizeof(error_str),\n\t\t\t\t(offer && !renegotiation ? &peer_dtls_role : NULL), &audio, &video, &data);\n\t\t\tif(parsed_sdp == NULL) {\n\t\t\t\t/* Invalid SDP */\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_JSEP_INVALID_SDP, error_str);\n\t\t\t\tg_free(jsep_type);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\t/* Notify event handlers */\n\t\t\tif(janus_events_is_enabled()) {\n\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_JSEP, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\t\tsession_id, handle_id, handle->opaque_id, \"remote\", jsep_type, jsep_sdp);\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] There are %d audio, %d video and %d data m-lines\\n\",\n\t\t\t\thandle->handle_id, audio, video, data);\n\t\t\t/* We behave differently if it's a new session or an update... */\n\t\t\tif(!renegotiation) {\n\t\t\t\t/* New session */\n\t\t\t\tif(offer) {\n\t\t\t\t\t/* Check which DTLS role we should take */\n\t\t\t\t\tjanus_dtls_role dtls_role = JANUS_DTLS_ROLE_CLIENT;\n\t\t\t\t\tif(peer_dtls_role == JANUS_DTLS_ROLE_CLIENT)\n\t\t\t\t\t\tdtls_role = JANUS_DTLS_ROLE_SERVER;\n\t\t\t\t\t/* Setup ICE locally (we received an offer) */\n\t\t\t\t\tif(janus_ice_setup_local(handle, offer, do_trickle, dtls_role) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error setting ICE locally\\n\");\n\t\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\t\tg_free(jsep_type);\n\t\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Error setting ICE locally\");\n\t\t\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\t\t\tgoto jsondone;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Make sure we're waiting for an ANSWER in the first place */\n\t\t\t\t\tif(!handle->agent) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unexpected ANSWER (did we offer?)\\n\");\n\t\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\t\tg_free(jsep_type);\n\t\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNEXPECTED_ANSWER, \"Unexpected ANSWER (did we offer?)\");\n\t\t\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\t\t\tgoto jsondone;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* If for some reason the user is asking us to force using a relay, do that. Notice\n\t\t\t\t * that this only works with libnice >= 0.1.14, and will cause the PeerConnection\n\t\t\t\t * to fail if Janus itself is not configured to use a TURN server. Don't use this\n\t\t\t\t * feature if you don't know what you're doing! You will almost always NOT want\n\t\t\t\t * Janus itself to use TURN: https://janus.conf.meetecho.com/docs/FAQ.html#turn */\n\t\t\t\tif(json_is_true(json_object_get(jsep, \"force_relay\"))) {\n\t\t\t\t\tif(!janus_ice_is_force_relay_allowed()) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Forcing Janus to use a TURN server is not allowed\\n\", handle->handle_id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Forcing Janus to use a TURN server\\n\", handle->handle_id);\n\t\t\t\t\t\tg_object_set(G_OBJECT(handle->agent), \"force-relay\", TRUE, NULL);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Process the remote SDP */\n\t\t\t\tif(janus_sdp_process_remote(handle, parsed_sdp, rids_hml, FALSE) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing SDP\\n\");\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\tg_free(jsep_type);\n\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_JSEP_INVALID_SDP, \"Error processing SDP\");\n\t\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\t\tgoto jsondone;\n\t\t\t\t}\n\t\t\t\tif(!offer) {\n\t\t\t\t\t/* Set remote candidates now (we received an answer) */\n\t\t\t\t\tif(do_trickle) {\n\t\t\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_request_ice_handle_answer(handle, jsep_sdp);\n\t\t\t\t\t/* Check if the answer does contain the mid/abs-send-time/twcc extmaps */\n\t\t\t\t\tgboolean do_mid = FALSE, do_twcc = FALSE, do_dd = FALSE, do_abs_send_time = FALSE,\n\t\t\t\t\t\tdo_abs_capture_time = FALSE, do_video_layers_alloc = FALSE;\n\t\t\t\t\tGList *temp = parsed_sdp->m_lines;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\t\t\tgboolean have_mid = FALSE, have_twcc = FALSE, have_dd = FALSE,\n\t\t\t\t\t\t\thave_abs_send_time = FALSE, have_abs_capture_time = FALSE,\n\t\t\t\t\t\t\thave_video_layers_alloc = FALSE;\n\t\t\t\t\t\tGList *tempA = m->attributes;\n\t\t\t\t\t\twhile(tempA) {\n\t\t\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\t\t\t\t\tif(a->name && a->value && !strcasecmp(a->name, \"extmap\")) {\n\t\t\t\t\t\t\t\tif(strstr(a->value, JANUS_RTP_EXTMAP_MID))\n\t\t\t\t\t\t\t\t\thave_mid = TRUE;\n\t\t\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC))\n\t\t\t\t\t\t\t\t\thave_twcc = TRUE;\n\t\t\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_DEPENDENCY_DESC))\n\t\t\t\t\t\t\t\t\thave_dd = TRUE;\n\t\t\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_ABS_SEND_TIME))\n\t\t\t\t\t\t\t\t\thave_abs_send_time = TRUE;\n\t\t\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME))\n\t\t\t\t\t\t\t\t\thave_abs_capture_time = TRUE;\n\t\t\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_VIDEO_LAYERS))\n\t\t\t\t\t\t\t\t\thave_video_layers_alloc = TRUE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdo_mid = do_mid || have_mid;\n\t\t\t\t\t\tdo_twcc = do_twcc || have_twcc;\n\t\t\t\t\t\tdo_dd = do_dd || have_dd;\n\t\t\t\t\t\tdo_abs_send_time = do_abs_send_time || have_abs_send_time;\n\t\t\t\t\t\tdo_abs_capture_time = do_abs_capture_time || have_abs_capture_time;\n\t\t\t\t\t\tdo_video_layers_alloc = do_video_layers_alloc || have_video_layers_alloc;\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t\tif(!do_mid && handle->pc)\n\t\t\t\t\t\thandle->pc->mid_ext_id = 0;\n\t\t\t\t\tif(!do_twcc && handle->pc) {\n\t\t\t\t\t\thandle->pc->do_transport_wide_cc = FALSE;\n\t\t\t\t\t\thandle->pc->transport_wide_cc_ext_id = 0;\n\t\t\t\t\t}\n\t\t\t\t\tif(!do_dd && handle->pc)\n\t\t\t\t\t\thandle->pc->dependencydesc_ext_id = 0;\n\t\t\t\t\tif(!do_abs_send_time && handle->pc)\n\t\t\t\t\t\thandle->pc->abs_send_time_ext_id = 0;\n\t\t\t\t\tif(!do_abs_capture_time && handle->pc)\n\t\t\t\t\t\thandle->pc->abs_capture_time_ext_id = 0;\n\t\t\t\t\tif(!do_video_layers_alloc && handle->pc)\n\t\t\t\t\t\thandle->pc->videolayers_ext_id = 0;\n\t\t\t\t} else {\n\t\t\t\t\t/* Check if the mid RTP extension is being negotiated */\n\t\t\t\t\thandle->pc->mid_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_MID);\n\t\t\t\t\t/* Check if the RTP Stream ID extension is being negotiated */\n\t\t\t\t\thandle->pc->rid_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_RID);\n\t\t\t\t\thandle->pc->ridrtx_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_REPAIRED_RID);\n\t\t\t\t\t/* Check if the audio level ID extension is being negotiated */\n\t\t\t\t\thandle->pc->audiolevel_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t\t\t/* Check if the video orientation ID extension is being negotiated */\n\t\t\t\t\thandle->pc->videoorientation_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t\t\t/* Check if the playout delay ID extension is being negotiated */\n\t\t\t\t\thandle->pc->playoutdelay_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_PLAYOUT_DELAY);\n\t\t\t\t\t/* Check if the abs-send-time ID extension is being negotiated */\n\t\t\t\t\thandle->pc->abs_send_time_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_ABS_SEND_TIME);\n\t\t\t\t\t/* Check if the abs-capture-time ID extension is being negotiated */\n\t\t\t\t\thandle->pc->abs_capture_time_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME);\n\t\t\t\t\t/* Check if the video-layers-allocation ID extension is being negotiated */\n\t\t\t\t\thandle->pc->videolayers_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_VIDEO_LAYERS);\n\t\t\t\t\t/* Check if transport wide CC is supported */\n\t\t\t\t\tint transport_wide_cc_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC);\n\t\t\t\t\thandle->pc->do_transport_wide_cc = transport_wide_cc_ext_id > 0 ? TRUE : FALSE;\n\t\t\t\t\thandle->pc->transport_wide_cc_ext_id = transport_wide_cc_ext_id;\n\t\t\t\t\t/* Check if the dependency descriptor ID extension is being negotiated */\n\t\t\t\t\thandle->pc->dependencydesc_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_DEPENDENCY_DESC);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* FIXME This is a renegotiation: we can currently only handle simple changes in media\n\t\t\t\t * direction and ICE restarts: anything more complex than that will result in an error */\n\t\t\t\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] Negotiation update, checking what changed...\\n\", handle->handle_id);\n\t\t\t\tif(janus_sdp_process_remote(handle, parsed_sdp, rids_hml, TRUE) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing SDP\\n\");\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\tg_free(jsep_type);\n\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNEXPECTED_ANSWER, \"Error processing SDP\");\n\t\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\t\tgoto jsondone;\n\t\t\t\t}\n\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ICE_RESTART)) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] Restarting ICE...\\n\", handle->handle_id);\n\t\t\t\t\t/* FIXME We only need to do that for offers: if it's an answer, we did that already */\n\t\t\t\t\tif(offer) {\n\t\t\t\t\t\tjanus_ice_restart(handle);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ICE_RESTART);\n\t\t\t\t\t}\n\t\t\t\t\t/* Update remote credentials for ICE */\n\t\t\t\t\tif(handle->pc) {\n\t\t\t\t\t\tnice_agent_set_remote_credentials(handle->agent, handle->pc->stream_id,\n\t\t\t\t\t\t\thandle->pc->ruser, handle->pc->rpass);\n\t\t\t\t\t}\n\t\t\t\t\t/* If we're full-trickling, we'll need to resend the candidates later */\n\t\t\t\t\tif(janus_ice_is_full_trickle_enabled()) {\n\t\t\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RESEND_TRICKLES);\n\t\t\t\t\t}\n\t\t\t\t}\n#ifdef HAVE_SCTP\n\t\t\t\tif(!offer) {\n\t\t\t\t\t/* Were datachannels just added? */\n\t\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)) {\n\t\t\t\t\t\tjanus_ice_peerconnection *pc = handle->pc;\n\t\t\t\t\t\tif(pc != NULL && pc->dtls != NULL && pc->dtls->sctp == NULL) {\n\t\t\t\t\t\t\t/* Create SCTP association as well */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Creating datachannels...\\n\", handle->handle_id);\n\t\t\t\t\t\t\tjanus_dtls_srtp_create_sctp(pc->dtls);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n#endif\n\t\t\t\t/* Check if renegotiating has added new RTP extensions */\n\t\t\t\tif(offer) {\n\t\t\t\t\t/* Check if the mid RTP extension is being negotiated */\n\t\t\t\t\thandle->pc->mid_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_MID);\n\t\t\t\t\t/* Check if the RTP Stream ID extension is being negotiated */\n\t\t\t\t\thandle->pc->rid_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_RID);\n\t\t\t\t\thandle->pc->ridrtx_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_REPAIRED_RID);\n\t\t\t\t\t/* Check if the audio level ID extension is being negotiated */\n\t\t\t\t\thandle->pc->audiolevel_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t\t\t/* Check if the video orientation ID extension is being negotiated */\n\t\t\t\t\thandle->pc->videoorientation_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t\t\t/* Check if the playout delay ID extension is being negotiated */\n\t\t\t\t\thandle->pc->playoutdelay_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_PLAYOUT_DELAY);\n\t\t\t\t\t/* Check if the abs-send-time ID extension is being negotiated */\n\t\t\t\t\thandle->pc->abs_send_time_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_ABS_SEND_TIME);\n\t\t\t\t\t/* Check if the abs-capture-time ID extension is being negotiated */\n\t\t\t\t\thandle->pc->abs_capture_time_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME);\n\t\t\t\t\t/* Check if the video-layers-alloc ID extension is being negotiated */\n\t\t\t\t\thandle->pc->videolayers_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_VIDEO_LAYERS);\n\t\t\t\t\t/* Check if transport wide CC is supported */\n\t\t\t\t\tint transport_wide_cc_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC);\n\t\t\t\t\thandle->pc->do_transport_wide_cc = transport_wide_cc_ext_id > 0 ? TRUE : FALSE;\n\t\t\t\t\thandle->pc->transport_wide_cc_ext_id = transport_wide_cc_ext_id;\n\t\t\t\t\t/* Check if the dependency descriptor ID extension is being negotiated */\n\t\t\t\t\thandle->pc->dependencydesc_ext_id = janus_rtp_header_extension_get_id(jsep_sdp, JANUS_RTP_EXTMAP_DEPENDENCY_DESC);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchar *tmp = handle->remote_sdp;\n\t\t\thandle->remote_sdp = g_strdup(jsep_sdp);\n\t\t\tg_free(tmp);\n\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t/* Anonymize SDP */\n\t\t\tif(janus_sdp_anonymize(parsed_sdp) < 0) {\n\t\t\t\t/* Invalid SDP */\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_JSEP_INVALID_SDP, \"JSEP error: invalid SDP\");\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tg_free(jsep_type);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjsep_sdp_stripped = janus_sdp_write(parsed_sdp);\n\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\tsdp = NULL;\n\t\t\tif(e2ee)\n\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_E2EE);\n\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t}\n\n\t\t/* Make sure the app handle is still valid */\n\t\tif(handle->app == NULL || !janus_plugin_session_is_alive(handle->app_handle)) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_MESSAGE, \"No plugin to handle this message\");\n\t\t\tif(jsep_sdp_stripped != NULL)\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\tg_free(jsep_type);\n\t\t\tg_free(jsep_sdp_stripped);\n\t\t\tgoto jsondone;\n\t\t}\n\n\t\t/* Send the message to the plugin (which must eventually free transaction_text and unref the two objects, body and jsep) */\n\t\tjson_incref(body);\n\t\tjson_t *body_jsep = NULL;\n\t\tif(jsep_sdp_stripped) {\n\t\t\tbody_jsep = json_pack(\"{ssss}\", \"type\", jsep_type, \"sdp\", jsep_sdp_stripped);\n\t\t\t/* Check if simulcasting is enabled in one of the media streams */\n\t\t\tjanus_mutex_lock(&handle->mutex);\n\t\t\tif(handle->pc == NULL) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_WEBRTC_STATE, \"Invalid PeerConnection\");\n\t\t\t\tjson_decref(body);\n\t\t\t\tjson_decref(body_jsep);\n\t\t\t\tg_free(jsep_type);\n\t\t\t\tg_free(jsep_sdp_stripped);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *simulcast = NULL;\n\t\t\tjanus_ice_peerconnection_medium *medium = NULL;\n\t\t\tuint mi=0;\n\t\t\tfor(mi=0; mi<g_hash_table_size(handle->pc->media); mi++) {\n\t\t\t\tmedium = g_hash_table_lookup(handle->pc->media, GUINT_TO_POINTER(mi));\n\t\t\t\tif(medium && (medium->ssrc_peer[1] || medium->rid[0])) {\n\t\t\t\t\tif(simulcast == NULL)\n\t\t\t\t\t\tsimulcast = json_array();\n\t\t\t\t\tjson_t *msc = json_object();\n\t\t\t\t\tjson_object_set_new(msc, \"mindex\", json_integer(medium->mindex));\n\t\t\t\t\tif(medium->mid != NULL)\n\t\t\t\t\t\tjson_object_set_new(msc, \"mid\", json_string(medium->mid));\n\t\t\t\t\t/* If we have rids, pass those, otherwise pass the SSRCs */\n\t\t\t\t\tif(medium->rid[0]) {\n\t\t\t\t\t\tjson_t *rids = json_array();\n\t\t\t\t\t\tif(medium->rid[2])\n\t\t\t\t\t\t\tjson_array_append_new(rids, json_string(medium->rid[2]));\n\t\t\t\t\t\tif(medium->rid[1])\n\t\t\t\t\t\t\tjson_array_append_new(rids, json_string(medium->rid[1]));\n\t\t\t\t\t\tjson_array_append_new(rids, json_string(medium->rid[0]));\n\t\t\t\t\t\tjson_object_set_new(msc, \"rids\", rids);\n\t\t\t\t\t\tif(medium->pc && medium->pc->rid_ext_id > 0)\n\t\t\t\t\t\t\tjson_object_set_new(msc, \"rid-ext\", json_integer(medium->pc->rid_ext_id));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjson_t *ssrcs = json_array();\n\t\t\t\t\t\tjson_array_append_new(ssrcs, json_integer(medium->ssrc_peer[0]));\n\t\t\t\t\t\tif(medium->ssrc_peer[1])\n\t\t\t\t\t\t\tjson_array_append_new(ssrcs, json_integer(medium->ssrc_peer[1]));\n\t\t\t\t\t\tif(medium->ssrc_peer[2])\n\t\t\t\t\t\t\tjson_array_append_new(ssrcs, json_integer(medium->ssrc_peer[2]));\n\t\t\t\t\t\tjson_object_set_new(msc, \"ssrcs\", ssrcs);\n\t\t\t\t\t}\n\t\t\t\t\tjson_array_append_new(simulcast, msc);\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\tif(simulcast)\n\t\t\t\tjson_object_set_new(body_jsep, \"simulcast\", simulcast);\n\t\t\tjson_t *svc = json_object_get(jsep, \"svc\");\n\t\t\tif(svc) {\n\t\t\t\tjson_incref(svc);\n\t\t\t\tjson_object_set_new(body_jsep, \"svc\", svc);\n\t\t\t}\n\t\t\t/* Check if this is a renegotiation or update */\n\t\t\tif(renegotiation)\n\t\t\t\tjson_object_set_new(body_jsep, \"update\", json_true());\n\t\t\t/* If media is encrypted end-to-end, the plugin may need to know */\n\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_E2EE))\n\t\t\t\tjson_object_set_new(body_jsep, \"e2ee\", json_true());\n\t\t}\n\t\tjanus_plugin_result *result = plugin_t->handle_message(handle->app_handle,\n\t\t\tg_strdup((char *)transaction_text), body, body_jsep);\n\t\tg_free(jsep_type);\n\t\tg_free(jsep_sdp_stripped);\n\t\tif(result == NULL) {\n\t\t\t/* Something went horribly wrong! */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_MESSAGE, \"Plugin didn't give a result\");\n\t\t\tif(body_jsep != NULL)\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(result->type == JANUS_PLUGIN_OK) {\n\t\t\t/* The plugin gave a result already (synchronous request/response) */\n\t\t\tif(result->content == NULL || !json_is_object(result->content)) {\n\t\t\t\t/* Missing content, or not a JSON object */\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_MESSAGE,\n\t\t\t\t\tresult->content == NULL ?\n\t\t\t\t\t\t\"Plugin didn't provide any content for this synchronous response\" :\n\t\t\t\t\t\t\"Plugin returned an invalid JSON response\");\n\t\t\t\tjanus_plugin_result_destroy(result);\n\t\t\t\tif(body_jsep != NULL)\n\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\t/* Reference the content, as destroying the result instance will decref it */\n\t\t\tjson_incref(result->content);\n\t\t\t/* Prepare JSON response */\n\t\t\tjson_t *reply = janus_create_message(\"success\", session->session_id, transaction_text);\n\t\t\tjson_object_set_new(reply, \"sender\", json_integer(handle->handle_id));\n\t\t\tif(janus_is_opaqueid_in_api_enabled() && handle->opaque_id != NULL)\n\t\t\t\tjson_object_set_new(reply, \"opaque_id\", json_string(handle->opaque_id));\n\t\t\tjson_t *plugin_data = json_object();\n\t\t\tjson_object_set_new(plugin_data, \"plugin\", json_string(plugin_t->get_package()));\n\t\t\tjson_object_set_new(plugin_data, \"data\", result->content);\n\t\t\tjson_object_set_new(reply, \"plugindata\", plugin_data);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t} else if(result->type == JANUS_PLUGIN_OK_WAIT) {\n\t\t\t/* The plugin received the request but didn't process it yet, send an ack (asynchronous notifications may follow) */\n\t\t\tjson_t *reply = janus_create_message(\"ack\", session_id, transaction_text);\n\t\t\tif(result->text)\n\t\t\t\tjson_object_set_new(reply, \"hint\", json_string(result->text));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t} else {\n\t\t\t/* Something went horribly wrong! */\n\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_MESSAGE,\n\t\t\t\t(char *)(result->text ? result->text : \"Plugin returned a severe (unknown) error\"));\n\t\t\tjanus_plugin_result_destroy(result);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjanus_plugin_result_destroy(result);\n\t} else if(!strcasecmp(message_text, \"trickle\")) {\n\t\tif(handle == NULL) {\n\t\t\t/* Trickle is an handle-level command */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(handle->app == NULL || !janus_plugin_session_is_alive(handle->app_handle)) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_MESSAGE, \"No plugin to handle this trickle candidate\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjson_t *candidate = json_object_get(root, \"candidate\");\n\t\tjson_t *candidates = json_object_get(root, \"candidates\");\n\t\tif(candidate == NULL && candidates == NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_MISSING_MANDATORY_ELEMENT, \"Missing mandatory element (candidate|candidates)\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(candidate != NULL && candidates != NULL) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_JSON, \"Can't have both candidate and candidates\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Received a trickle, but still cleaning a previous session\\n\", handle->handle_id);\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_WEBRTC_STATE, \"Still cleaning a previous session\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\tjanus_mutex_lock(&handle->mutex);\n\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE)) {\n\t\t\t/* It looks like this peer supports Trickle, after all */\n\t\t\tJANUS_LOG(LOG_VERB, \"Handle %\"SCNu64\" supports trickle even if it didn't negotiate it...\\n\", handle->handle_id);\n\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE);\n\t\t}\n\t\t/* Is there any stream ready? this trickle may get here before the SDP it relates to */\n\t\tif(handle->pc == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] No stream, queueing this trickle as it got here before the SDP...\\n\", handle->handle_id);\n\t\t\t/* Enqueue this trickle candidate(s), we'll process this later */\n\t\t\tjanus_ice_trickle *early_trickle = janus_ice_trickle_new(transaction_text, candidate ? candidate : candidates);\n\t\t\thandle->pending_trickles = g_list_append(handle->pending_trickles, early_trickle);\n\t\t\t/* Send the ack right away, an event will tell the application if the candidate(s) failed */\n\t\t\tgoto trickledone;\n\t\t}\n\t\t/* Is the ICE stack ready already? */\n\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER) ||\n\t\t\t\t!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER) ||\n\t\t\t\t!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER)) {\n\t\t\tconst char *cause = NULL;\n\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER))\n\t\t\t\tcause = \"processing the offer\";\n\t\t\telse if(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER))\n\t\t\t\tcause = \"waiting for the answer\";\n\t\t\telse if(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER))\n\t\t\t\tcause = \"waiting for the offer\";\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Still %s, queueing this trickle to wait until we're done there...\\n\",\n\t\t\t\thandle->handle_id, cause);\n\t\t\t/* Enqueue this trickle candidate(s), we'll process this later */\n\t\t\tjanus_ice_trickle *early_trickle = janus_ice_trickle_new(transaction_text, candidate ? candidate : candidates);\n\t\t\thandle->pending_trickles = g_list_append(handle->pending_trickles, early_trickle);\n\t\t\t/* Send the ack right away, an event will tell the application if the candidate(s) failed */\n\t\t\tgoto trickledone;\n\t\t}\n\t\tif(candidate != NULL) {\n\t\t\t/* We got a single candidate */\n\t\t\tint error = 0;\n\t\t\tconst char *error_string = NULL;\n\t\t\tif((error = janus_ice_trickle_parse(handle, candidate, &error_string)) != 0) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, error, \"%s\", error_string);\n\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t} else {\n\t\t\t/* We got multiple candidates in an array */\n\t\t\tif(!json_is_array(candidates)) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_ELEMENT_TYPE, \"candidates is not an array\");\n\t\t\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Got multiple candidates (%zu)\\n\", json_array_size(candidates));\n\t\t\tif(json_array_size(candidates) > 0) {\n\t\t\t\t/* Handle remote candidates */\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(candidates); i++) {\n\t\t\t\t\tjson_t *c = json_array_get(candidates, i);\n\t\t\t\t\t/* FIXME We don't care if any trickle fails to parse */\n\t\t\t\t\tjanus_ice_trickle_parse(handle, c, NULL);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\ntrickledone:\n\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t/* We reply right away, not to block the web server... */\n\t\tjson_t *reply = janus_create_message(\"ack\", session_id, transaction_text);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t} else {\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN_REQUEST, \"Unknown request '%s'\", message_text);\n\t}\n\njsondone:\n\t/* Done processing */\n\tif(handle != NULL)\n\t\tjanus_refcount_decrease(&handle->ref);\n\tif(session != NULL)\n\t\tjanus_refcount_decrease(&session->ref);\n\treturn ret;\n}\n\nstatic json_t *janus_json_token_plugin_array(const char *token_value) {\n\tjson_t *plugins_list = json_array();\n\tGList *plugins = janus_auth_list_plugins(token_value);\n\tif(plugins != NULL) {\n\t\tGList *tmp = plugins;\n\t\twhile(tmp) {\n\t\t\tjanus_plugin *p = (janus_plugin *)tmp->data;\n\t\t\tif(p != NULL)\n\t\t\t\tjson_array_append_new(plugins_list, json_string(p->get_package()));\n\t\t\ttmp = tmp->next;\n\t\t}\n\t\tg_list_free(plugins);\n\t\tplugins = NULL;\n\t}\n\treturn plugins_list;\n}\n\nstatic json_t *janus_json_list_token_plugins(const char *token_value, const gchar *transaction_text) {\n\tjson_t *plugins_list = janus_json_token_plugin_array(token_value);\n\t/* Prepare JSON reply */\n\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\tjson_t *data = json_object();\n\tjson_object_set_new(data, \"plugins\", plugins_list);\n\tjson_object_set_new(reply, \"data\", data);\n\treturn reply;\n}\n\nstatic int janus_request_allow_token(janus_request *request, guint64 session_id, const gchar *transaction_text, gboolean allow, gboolean add) {\n\t/* Allow/disallow a valid token valid to access a plugin */\n\tint ret = -1;\n\tint error_code = 0;\n\tchar error_cause[100];\n\tjson_t *root = request->message;\n\tif(!janus_auth_is_stored_mode()) {\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Stored-Token based authentication disabled\");\n\t\tgoto jsondone;\n\t}\n\tJANUS_VALIDATE_JSON_OBJECT(root, add_token_parameters,\n\t\terror_code, error_cause, FALSE,\n\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t/* Any plugin this token should be limited to? */\n\tjson_t *allowed = json_object_get(root, \"plugins\");\n\tif(error_code == 0 && !add && (!allowed || json_array_size(allowed) == 0)) {\n\t\terror_code = JANUS_ERROR_INVALID_ELEMENT_TYPE;\n\t\tg_strlcpy(error_cause, \"Invalid element type (plugins should be a non-empty array)\", sizeof(error_cause));\n\t}\n\tif(error_code != 0) {\n\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\tgoto jsondone;\n\t}\n\tjson_t *token = json_object_get(root, \"token\");\n\tconst char *token_value = json_string_value(token);\n\tif(add) {\n\t\t/* First of all, add the new token */\n\t\tif(!janus_auth_add_token(token_value)) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Error adding token\");\n\t\t\tgoto jsondone;\n\t\t}\n\t} else {\n\t\t/* Check if the token is valid, first */\n\t\tif(!janus_auth_check_token(token_value)) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_TOKEN_NOT_FOUND, \"Token %s not found\", token_value);\n\t\t\tgoto jsondone;\n\t\t}\n\t}\n\tif(allowed && json_array_size(allowed) > 0) {\n\t\t/* Specify which plugins this token has access to */\n\t\tsize_t i = 0;\n\t\tgboolean ok = TRUE;\n\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\tjson_t *p = json_array_get(allowed, i);\n\t\t\tif(!p || !json_is_string(p)) {\n\t\t\t\t/* FIXME Should we fail here? */\n\t\t\t\tif(add){\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid plugin passed to the new token request, skipping...\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid plugin passed to the new token request...\\n\");\n\t\t\t\t\tok = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst gchar *plugin_text = json_string_value(p);\n\t\t\tjanus_plugin *plugin_t = janus_plugin_find(plugin_text);\n\t\t\tif(plugin_t == NULL) {\n\t\t\t\t/* FIXME Should we fail here? */\n\t\t\t\tif(add) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No such plugin '%s' passed to the new token request, skipping...\\n\", plugin_text);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such plugin '%s' passed to the new token request...\\n\", plugin_text);\n\t\t\t\t\tok = FALSE;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif(!ok) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_ELEMENT_TYPE, \"Invalid element type (some of the provided plugins are invalid)\");\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* Take care of the plugins access limitations */\n\t\ti = 0;\n\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\tjson_t *p = json_array_get(allowed, i);\n\t\t\tconst gchar *plugin_text = json_string_value(p);\n\t\t\tjanus_plugin *plugin_t = janus_plugin_find(plugin_text);\n\t\t\tif(!(allow ? janus_auth_allow_plugin(token_value, plugin_t) : janus_auth_disallow_plugin(token_value, plugin_t))) {\n\t\t\t\t/* FIXME Should we notify individual failures? */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Error allowing access to '%s' to the new token, bad things may happen...\\n\", plugin_text);\n\t\t\t}\n\t\t}\n\t} else {\n\t\t/* No plugin limitation specified, allow all plugins */\n\t\tif(plugins && g_hash_table_size(plugins) > 0) {\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, plugins);\n\t\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_plugin *plugin_t = value;\n\t\t\t\tif(plugin_t == NULL)\n\t\t\t\t\tcontinue;\n\t\t\t\tif(!janus_auth_allow_plugin(token_value, plugin_t)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error allowing access to '%s' to the new token, bad things may happen...\\n\", plugin_t->get_package());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* Get the list of plugins this new token can now access */\n\tjson_t *reply = janus_json_list_token_plugins(token_value, transaction_text);\n\t/* Send the success reply */\n\tret = janus_process_success(request, reply);\njsondone:\n\treturn ret;\n}\n\n/* Admin/monitor WebServer requests handler */\nint janus_process_incoming_admin_request(janus_request *request) {\n\tint ret = -1;\n\tint error_code = 0;\n\tchar error_cause[100];\n\tif(request == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Missing request or payload to process, giving up...\\n\");\n\t\treturn ret;\n\t}\n\tjson_t *root = request->message;\n\t/* Ok, let's start with the ids */\n\tguint64 session_id = 0, handle_id = 0;\n\tjson_t *s = json_object_get(root, \"session_id\");\n\tif(s && json_is_integer(s))\n\t\tsession_id = json_integer_value(s);\n\tjson_t *h = json_object_get(root, \"handle_id\");\n\tif(h && json_is_integer(h))\n\t\thandle_id = json_integer_value(h);\n\n\tjanus_session *session = NULL;\n\tjanus_ice_handle *handle = NULL;\n\n\t/* Get transaction and message request */\n\tJANUS_VALIDATE_JSON_OBJECT(root, admin_parameters,\n\t\terror_code, error_cause, FALSE,\n\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\tif(error_code != 0) {\n\t\tret = janus_process_error_string(request, session_id, NULL, error_code, error_cause);\n\t\tgoto jsondone;\n\t}\n\tjson_t *transaction = json_object_get(root, \"transaction\");\n\tconst gchar *transaction_text = json_string_value(transaction);\n\tjson_t *message = json_object_get(root, \"janus\");\n\tconst gchar *message_text = json_string_value(message);\n\n\tif(session_id == 0 && handle_id == 0) {\n\t\t/* Can only be a 'Get all sessions' or some general setting manipulation request */\n\t\tif(!strcasecmp(message_text, \"info\")) {\n\t\t\t/* The generic info request */\n\t\t\tret = janus_process_success(request, janus_info(transaction_text));\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(!strcasecmp(message_text, \"ping\")) {\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"pong\", 0, transaction_text);\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tif(admin_api_secret != NULL) {\n\t\t\t/* There's an admin/monitor secret, check that the client provided it */\n\t\t\tjson_t *secret = json_object_get(root, \"admin_secret\");\n\t\t\tif(!secret || !json_is_string(secret) || !janus_strcmp_const_time(json_string_value(secret), admin_api_secret)) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNAUTHORIZED, NULL);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t}\n\t\tif(!strcasecmp(message_text, \"get_status\")) {\n\t\t\t/* Return some info on the settings (mostly debug-related, at the moment) */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_t *status = json_object();\n\t\t\tjson_object_set_new(status, \"token_auth\", janus_auth_is_enabled() ? json_true() : json_false());\n\t\t\tjson_object_set_new(status, \"session_timeout\", json_integer(global_session_timeout));\n\t\t\tjson_object_set_new(status, \"reclaim_session_timeout\", json_integer(reclaim_session_timeout));\n\t\t\tjson_object_set_new(status, \"candidates_timeout\", json_integer(candidates_timeout));\n\t\t\tjson_object_set_new(status, \"log_level\", json_integer(janus_log_level));\n\t\t\tjson_object_set_new(status, \"log_timestamps\", janus_log_timestamps ? json_true() : json_false());\n\t\t\tjson_object_set_new(status, \"log_colors\", janus_log_colors ? json_true() : json_false());\n\t\t\tjson_object_set_new(status, \"log_rotate_sig\", json_integer(janus_log_rotate_sig));\n\t\t\tjson_object_set_new(status, \"locking_debug\", lock_debug ? json_true() : json_false());\n\t\t\tjson_object_set_new(status, \"refcount_debug\", refcount_debug ? json_true() : json_false());\n\t\t\tjson_object_set_new(status, \"min_nack_queue\", json_integer(janus_get_min_nack_queue()));\n\t\t\tjson_object_set_new(status, \"nack-optimizations\", janus_is_nack_optimizations_enabled() ? json_true() : json_false());\n\t\t\tjson_object_set_new(status, \"no_media_timer\", json_integer(janus_get_no_media_timer()));\n\t\t\tjson_object_set_new(status, \"slowlink_threshold\", json_integer(janus_get_slowlink_threshold()));\n\t\t\tjson_object_set_new(reply, \"status\", status);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_session_timeout\")) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, timeout_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *timeout = json_object_get(root, \"timeout\");\n\t\t\tint timeout_num = json_integer_value(timeout);\n\t\t\tif(timeout_num < 0) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_ELEMENT_TYPE, \"Invalid element type (timeout should be a positive integer)\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\n\t\t\t/* Set global session timeout */\n\t\t\tglobal_session_timeout = timeout_num;\n\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"timeout\", json_integer(timeout_num));\n\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_log_level\")) {\n\t\t\t/* Change the debug logging level */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, level_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *level = json_object_get(root, \"level\");\n\t\t\tint level_num = json_integer_value(level);\n\t\t\tif(level_num < LOG_NONE || level_num > LOG_MAX) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_ELEMENT_TYPE, \"Invalid element type (level should be between %d and %d)\", LOG_NONE, LOG_MAX);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjanus_log_level = level_num;\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"level\", json_integer(janus_log_level));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_locking_debug\")) {\n\t\t\t/* Enable/disable the locking debug (would show a message on the console for every lock attempt) */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, debug_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *debug = json_object_get(root, \"debug\");\n\t\t\tlock_debug = json_is_true(debug);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"locking_debug\", lock_debug ? json_true() : json_false());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_refcount_debug\")) {\n\t\t\t/* Enable/disable the reference counter debug (would show a message on the console for every increase/decrease) */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, debug_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *debug = json_object_get(root, \"debug\");\n\t\t\tif(json_is_true(debug)) {\n\t\t\t\trefcount_debug = TRUE;\n\t\t\t} else {\n\t\t\t\trefcount_debug = FALSE;\n\t\t\t}\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"refcount_debug\", refcount_debug ? json_true() : json_false());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_log_timestamps\")) {\n\t\t\t/* Enable/disable the log timestamps */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, timestamps_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *timestamps = json_object_get(root, \"timestamps\");\n\t\t\tjanus_log_timestamps = json_is_true(timestamps);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"log_timestamps\", janus_log_timestamps ? json_true() : json_false());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_log_colors\")) {\n\t\t\t/* Enable/disable the log colors */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, colors_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *colors = json_object_get(root, \"colors\");\n\t\t\tjanus_log_colors = json_is_true(colors);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"log_colors\", janus_log_colors ? json_true() : json_false());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_min_nack_queue\")) {\n\t\t\t/* Change the current value for the min NACK queue */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, mnq_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *mnq = json_object_get(root, \"min_nack_queue\");\n\t\t\tint mnq_num = json_integer_value(mnq);\n\t\t\tjanus_set_min_nack_queue(mnq_num);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"min_nack_queue\", json_integer(janus_get_min_nack_queue()));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_nack_optimizations\")) {\n\t\t\t/* Enable/disable NACK optimizations */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, nopt_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tgboolean optimize = json_is_true(json_object_get(root, \"nack_optimizations\"));\n\t\t\tjanus_set_nack_optimizations_enabled(optimize);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"nack-optimizations\", janus_is_nack_optimizations_enabled() ? json_true() : json_false());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_no_media_timer\")) {\n\t\t\t/* Change the current value for the no-media timer */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, nmt_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *nmt = json_object_get(root, \"no_media_timer\");\n\t\t\tint nmt_num = json_integer_value(nmt);\n\t\t\tjanus_set_no_media_timer(nmt_num);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"no_media_timer\", json_integer(janus_get_no_media_timer()));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"set_slowlink_threshold\")) {\n\t\t\t/* Change the current value for the slowlink-threshold value */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, st_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *nmt = json_object_get(root, \"slowlink_threshold\");\n\t\t\tint nmt_num = json_integer_value(nmt);\n\t\t\tjanus_set_slowlink_threshold(nmt_num);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"slowlink_threshold\", json_integer(janus_get_slowlink_threshold()));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"accept_new_sessions\")) {\n\t\t\t/* Configure whether we should accept new incoming sessions or not:\n\t\t\t * this can be particularly useful whenever, e.g., we want to stop\n\t\t\t * accepting new sessions because we're draining this server */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, ans_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *accept = json_object_get(root, \"accept\");\n\t\t\taccept_new_sessions = json_is_true(accept);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"accept\", accept_new_sessions ? json_true() : json_false());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"message_plugin\")) {\n\t\t\t/* Contact a plugin and expect a response */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, messageplugin_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *plugin = json_object_get(root, \"plugin\");\n\t\t\tconst char *plugin_value = json_string_value(plugin);\n\t\t\tjanus_plugin *p = janus_plugin_find(plugin_value);\n\t\t\tif(p == NULL) {\n\t\t\t\t/* No such handler... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Invalid plugin\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_NOT_FOUND, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tif(p->handle_admin_message == NULL) {\n\t\t\t\t/* Handler doesn't implement the hook... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Plugin doesn't support Admin API messages\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *query = json_object_get(root, \"request\");\n\t\t\tjson_t *response = p->handle_admin_message(query);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"response\", response ? response : json_object());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"query_transport\")) {\n\t\t\t/* Contact a transport and expect a response */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, querytransport_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *transport = json_object_get(root, \"transport\");\n\t\t\tconst char *transport_value = json_string_value(transport);\n\t\t\tjanus_transport *t = g_hash_table_lookup(transports, transport_value);\n\t\t\tif(t == NULL) {\n\t\t\t\t/* No such transport... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Invalid transport\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_NOT_FOUND, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tif(t->query_transport == NULL) {\n\t\t\t\t/* Transport doesn't implement the hook... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Transport plugin doesn't support queries\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *query = json_object_get(root, \"request\");\n\t\t\tjson_t *response = t->query_transport(query);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"response\", response ? response : json_object());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"query_eventhandler\")) {\n\t\t\t/* Contact an event handler and expect a response */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, queryhandler_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *handler = json_object_get(root, \"handler\");\n\t\t\tconst char *handler_value = json_string_value(handler);\n\t\t\tjanus_eventhandler *evh = g_hash_table_lookup(eventhandlers, handler_value);\n\t\t\tif(evh == NULL) {\n\t\t\t\t/* No such handler... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Invalid event handler\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_NOT_FOUND, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tif(evh->handle_request == NULL) {\n\t\t\t\t/* Handler doesn't implement the hook... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Event handler doesn't support queries\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *query = json_object_get(root, \"request\");\n\t\t\tjson_t *response = evh->handle_request(query);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"response\", response ? response : json_object());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"query_logger\")) {\n\t\t\t/* Contact a logger and expect a response */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, querylogger_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *logger = json_object_get(root, \"logger\");\n\t\t\tconst char *logger_value = json_string_value(logger);\n\t\t\tjanus_logger *l = g_hash_table_lookup(loggers, logger_value);\n\t\t\tif(l == NULL) {\n\t\t\t\t/* No such handler... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Invalid logger\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_NOT_FOUND, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tif(l->handle_request == NULL) {\n\t\t\t\t/* Handler doesn't implement the hook... */\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"%s\", \"Logger doesn't support queries\");\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *query = json_object_get(root, \"request\");\n\t\t\tjson_t *response = l->handle_request(query);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"response\", response ? response : json_object());\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"custom_event\")) {\n\t\t\t/* Enqueue a custom \"external\" event to notify via event handlers */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, customevent_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *schema = json_object_get(root, \"schema\");\n\t\t\tconst char *schema_value = json_string_value(schema);\n\t\t\tjson_t *data = json_object_get(root, \"data\");\n\t\t\tif(janus_events_is_enabled()) {\n\t\t\t\tjson_incref(data);\n\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_EXTERNAL, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\t\t0, schema_value, data);\n\t\t\t}\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"custom_logline\")) {\n\t\t\t/* Print something custom on the logs, using the specified debug level */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, customlogline_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *line = json_object_get(root, \"line\");\n\t\t\tconst char *log_line = json_string_value(line);\n\t\t\tjson_t *level = json_object_get(root, \"level\");\n\t\t\tint log_level = LOG_INFO;\n\t\t\tif(level) {\n\t\t\t\tlog_level = json_integer_value(level);\n\t\t\t\tif(log_level < LOG_NONE || log_level > LOG_MAX)\n\t\t\t\t\tlog_level = LOG_INFO;\n\t\t\t}\n\t\t\t/* Print the log line on the log */\n\t\t\tJANUS_LOG(log_level, \"%s\\n\", log_line);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"list_sessions\")) {\n\t\t\t/* List sessions */\n\t\t\tsession_id = 0;\n\t\t\tjson_t *list = json_array();\n\t\t\tif(sessions != NULL && g_hash_table_size(sessions) > 0) {\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tg_hash_table_iter_init(&iter, sessions);\n\t\t\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\tjanus_session *session = value;\n\t\t\t\t\tif(session == NULL) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjson_array_append_new(list, json_integer(session->session_id));\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t}\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"sessions\", list);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"add_token\")) {\n\t\t\t/* Add a token valid for authentication */\n\t\t\tret = janus_request_allow_token(request, session_id, transaction_text, TRUE, TRUE);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"list_tokens\")) {\n\t\t\t/* List all the valid tokens */\n\t\t\tif(!janus_auth_is_stored_mode()) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Stored-Token based authentication disabled\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *tokens_list = json_array();\n\t\t\tGList *list = janus_auth_list_tokens();\n\t\t\tif(list != NULL) {\n\t\t\t\tGList *tmp = list;\n\t\t\t\twhile(tmp) {\n\t\t\t\t\tchar *token = (char *)tmp->data;\n\t\t\t\t\tif(token != NULL) {\n\t\t\t\t\t\tjson_t *plugins_list = janus_json_token_plugin_array(token);\n\t\t\t\t\t\tif(json_array_size(plugins_list) > 0) {\n\t\t\t\t\t\t\tjson_t *t = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(t, \"token\", json_string(token));\n\t\t\t\t\t\t\tjson_object_set_new(t, \"allowed_plugins\", plugins_list);\n\t\t\t\t\t\t\tjson_array_append_new(tokens_list, t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tjson_decref(plugins_list);\n\t\t\t\t\t\ttmp->data = NULL;\n\t\t\t\t\t\tg_free(token);\n\t\t\t\t\t}\n\t\t\t\t\ttmp = tmp->next;\n\t\t\t\t}\n\t\t\t\tg_list_free(list);\n\t\t\t}\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_t *data = json_object();\n\t\t\tjson_object_set_new(data, \"tokens\", tokens_list);\n\t\t\tjson_object_set_new(reply, \"data\", data);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"allow_token\")) {\n\t\t\t/* Allow a valid token valid to access a plugin */\n\t\t\tret = janus_request_allow_token(request, session_id, transaction_text, TRUE, FALSE);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"disallow_token\")) {\n\t\t\t/* Disallow a valid token valid from accessing a plugin */\n\t\t\tret = janus_request_allow_token(request, session_id, transaction_text, FALSE, FALSE);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"remove_token\")) {\n\t\t\t/* Invalidate a token for authentication purposes */\n\t\t\tif(!janus_auth_is_stored_mode()) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Stored-Token based authentication disabled\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, token_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\tconst char *token_value = json_string_value(token);\n\t\t\tif(!janus_auth_remove_token(token_value)) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN, \"Error removing token\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"resolve_address\")) {\n\t\t\t/* Helper method to evaluate whether this instance can resolve an address, and how soon */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, resaddr_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tconst char *address = json_string_value(json_object_get(root, \"address\"));\n\t\t\t/* Resolve the address */\n\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\tstruct addrinfo *res = NULL;\n\t\t\tjanus_network_address addr;\n\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\tif(getaddrinfo(address, NULL, NULL, &res) != 0 ||\n\t\t\t\t\tjanus_network_address_from_sockaddr(res->ai_addr, &addr) != 0 ||\n\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) != 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve %s...\\n\", address);\n\t\t\t\tif(res)\n\t\t\t\t\tfreeaddrinfo(res);\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text,\n\t\t\t\t\tJANUS_ERROR_UNKNOWN, (char *)\"Could not resolve address\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tgint64 end = janus_get_monotonic_time();\n\t\t\tfreeaddrinfo(res);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"ip\", json_string(janus_network_address_string_from_buffer(&addr_buf)));\n\t\t\tjson_object_set_new(reply, \"elapsed\", json_integer(end-start));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"test_stun\")) {\n\t\t\t/* Helper method to evaluate whether this instance can use STUN with a specific server */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, teststun_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tconst char *address = json_string_value(json_object_get(root, \"address\"));\n\t\t\tuint16_t port = json_integer_value(json_object_get(root, \"port\"));\n\t\t\tuint16_t local_port = json_integer_value(json_object_get(root, \"localport\"));\n\t\t\t/* Resolve the address */\n\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\tstruct addrinfo *res = NULL;\n\t\t\tjanus_network_address addr;\n\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\tif(getaddrinfo(address, NULL, NULL, &res) != 0 ||\n\t\t\t\t\tjanus_network_address_from_sockaddr(res->ai_addr, &addr) != 0 ||\n\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) != 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve %s...\\n\", address);\n\t\t\t\tif(res)\n\t\t\t\t\tfreeaddrinfo(res);\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text,\n\t\t\t\t\tJANUS_ERROR_UNKNOWN, (char *)\"Could not resolve address\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tfreeaddrinfo(res);\n\t\t\t/* Test the STUN server */\n\t\t\tjanus_network_address public_addr = { 0 };\n\t\t\tuint16_t public_port = 0;\n\t\t\tif(janus_ice_test_stun_server(&addr, port, local_port, &public_addr, &public_port) < 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text,\n\t\t\t\t\tJANUS_ERROR_UNKNOWN, (char *)\"STUN request failed\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tif(janus_network_address_to_string_buffer(&public_addr, &addr_buf) != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text,\n\t\t\t\t\tJANUS_ERROR_UNKNOWN, (char *)\"Could not resolve public address\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tconst char *public_ip_addr = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\tgint64 end = janus_get_monotonic_time();\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"public_ip\", json_string(public_ip_addr));\n\t\t\tjson_object_set_new(reply, \"public_port\", json_integer(public_port));\n\t\t\tjson_object_set_new(reply, \"elapsed\", json_integer(end-start));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"loops_info\")) {\n\t\t\t/* Query the Janus core to see how many handles each static loop is\n\t\t\t * handling: will return an empty list if static loops are disabled */\n\t\t\tjson_t *list = janus_ice_static_event_loops_info();\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", 0, transaction_text);\n\t\t\tjson_object_set_new(reply, \"loops\", list);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else {\n\t\t\t/* No message we know of */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t}\n\tif(session_id < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid session\\n\");\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_SESSION_NOT_FOUND, NULL);\n\t\tgoto jsondone;\n\t}\n\tif(h && handle_id < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid handle\\n\");\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_SESSION_NOT_FOUND, NULL);\n\t\tgoto jsondone;\n\t}\n\n\t/* Go on with the processing */\n\tif(admin_api_secret != NULL) {\n\t\t/* There's an API secret, check that the client provided it */\n\t\tjson_t *secret = json_object_get(root, \"admin_secret\");\n\t\tif(!secret || !json_is_string(secret) || !janus_strcmp_const_time(json_string_value(secret), admin_api_secret)) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNAUTHORIZED, NULL);\n\t\t\tgoto jsondone;\n\t\t}\n\t}\n\n\t/* If we got here, make sure we have a session (and/or a handle) */\n\tsession = janus_session_find(session_id);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't find any session %\"SCNu64\"...\\n\", session_id);\n\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_SESSION_NOT_FOUND, \"No such session %\"SCNu64\"\", session_id);\n\t\tgoto jsondone;\n\t}\n\thandle = NULL;\n\tif(handle_id > 0) {\n\t\thandle = janus_session_handles_find(session, handle_id);\n\t\tif(!handle) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't find any handle %\"SCNu64\" in session %\"SCNu64\"...\\n\", handle_id, session_id);\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_HANDLE_NOT_FOUND, \"No such handle %\"SCNu64\" in session %\"SCNu64\"\", handle_id, session_id);\n\t\t\tgoto jsondone;\n\t\t}\n\t}\n\n\t/* What is this? */\n\tif(handle == NULL) {\n\t\t/* Session-related */\n\t\tif(!strcasecmp(message_text, \"destroy_session\")) {\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tif(g_hash_table_remove(sessions, &session->session_id))\n\t\t\t\tg_atomic_int_dec_and_test(&sessions_num);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* Notify the source that the session has been destroyed */\n\t\t\tjanus_request *source = janus_session_get_request(session);\n\t\t\tif(source && source->transport)\n\t\t\t\tsource->transport->session_over(source->instance, session->session_id, FALSE, FALSE);\n\t\t\tjanus_request_unref(source);\n\t\t\t/* Schedule the session for deletion */\n\t\t\tjanus_session_destroy(session);\n\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\t/* Notify event handlers as well */\n\t\t\tif(janus_events_is_enabled())\n\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_SESSION, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\t\tsession_id, \"destroyed\", NULL);\n\t\t\tgoto jsondone;\n\t\t} else if (!strcasecmp(message_text, \"set_session_timeout\")) {\n\t\t\t/* Specific session timeout setting. */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, session_timeout_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\n\t\t\t/* Positive timeout is seconds, 0 is unlimited, -1 is global session timeout */\n\t\t\tjson_t *timeout = json_object_get(root, \"timeout\");\n\t\t\tint timeout_num = json_integer_value(timeout);\n\t\t\tif(timeout_num < -1) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_ELEMENT_TYPE, \"Invalid element type (timeout should be a non-negative integer or -1)\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\n\t\t\t/* Set specific session timeout */\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tsession->timeout = timeout_num;\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\tjson_object_set_new(reply, \"timeout\", json_integer(timeout_num));\n\t\t\tjson_object_set_new(reply, \"session_id\", json_integer(session_id));\n\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(strcasecmp(message_text, \"list_handles\")) {\n\t\t\t/* If this is not a request to destroy a session, it must be a request to list the handles */\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\n\t\t/* List handles */\n\t\tjson_t *list = janus_session_handles_list_json(session);\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\tjson_object_set_new(reply, \"handles\", list);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t\tgoto jsondone;\n\t} else {\n\t\t/* Handle-related */\n\t\tif(!strcasecmp(message_text, \"detach_handle\")) {\n\t\t\tif(handle->app == NULL || handle->app_handle == NULL) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_DETACH, \"No plugin to detach from\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tint error = janus_session_handles_remove(session, handle);\n\t\t\tif(error != 0) {\n\t\t\t\t/* TODO Make error struct to pass verbose information */\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_DETACH, \"Couldn't detach from plugin: error '%d'\", error);\n\t\t\t\t/* TODO Delete handle instance */\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"hangup_webrtc\")) {\n\t\t\tif(handle->app == NULL || handle->app_handle == NULL) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_PLUGIN_DETACH, \"No plugin attached\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tjanus_ice_webrtc_hangup(handle, \"Admin API\");\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"start_pcap\") || !strcasecmp(message_text, \"start_text2pcap\")) {\n\t\t\t/* Start dumping RTP and RTCP packets to a pcap or text2pcap file */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, text2pcap_parameters,\n\t\t\t\terror_code, error_cause, FALSE,\n\t\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\t\tif(error_code != 0) {\n\t\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tgboolean text = !strcasecmp(message_text, \"start_text2pcap\");\n\t\t\tconst char *folder = json_string_value(json_object_get(root, \"folder\"));\n\t\t\tconst char *filename = json_string_value(json_object_get(root, \"filename\"));\n\t\t\tint truncate = json_integer_value(json_object_get(root, \"truncate\"));\n\t\t\tif(handle->text2pcap != NULL) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN,\n\t\t\t\t\ttext ? \"text2pcap already started\" : \"pcap already started\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\thandle->text2pcap = janus_text2pcap_create(folder, filename, truncate, text);\n\t\t\tif(handle->text2pcap == NULL) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN,\n\t\t\t\t\ttext ? \"Error starting text2pcap dump\" : \"Error starting pcap dump\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tg_atomic_int_set(&handle->dump_packets, 1);\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t} else if(!strcasecmp(message_text, \"stop_pcap\") || !strcasecmp(message_text, \"stop_text2pcap\")) {\n\t\t\t/* Stop dumping RTP and RTCP packets to a pcap or text2pcap file */\n\t\t\tif(handle->text2pcap == NULL) {\n\t\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_UNKNOWN,\n\t\t\t\t\t\"Capture not started\");\n\t\t\t\tgoto jsondone;\n\t\t\t}\n\t\t\tif(g_atomic_int_compare_and_exchange(&handle->dump_packets, 1, 0)) {\n\t\t\t\tjanus_text2pcap_close(handle->text2pcap);\n\t\t\t\tg_clear_pointer(&handle->text2pcap, janus_text2pcap_free);\n\t\t\t}\n\t\t\t/* Prepare JSON reply */\n\t\t\tjson_t *reply = json_object();\n\t\t\tjson_object_set_new(reply, \"janus\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\t/* Send the success reply */\n\t\t\tret = janus_process_success(request, reply);\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* If this is not a request to start/stop debugging to text2pcap, it must be a handle_info */\n\t\tif(strcasecmp(message_text, \"handle_info\")) {\n\t\t\tret = janus_process_error(request, session_id, transaction_text, JANUS_ERROR_INVALID_REQUEST_PATH, \"Unhandled request '%s' at this path\", message_text);\n\t\t\tgoto jsondone;\n\t\t}\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, handleinfo_parameters,\n\t\t\terror_code, error_cause, FALSE,\n\t\t\tJANUS_ERROR_MISSING_MANDATORY_ELEMENT, JANUS_ERROR_INVALID_ELEMENT_TYPE);\n\t\tif(error_code != 0) {\n\t\t\tret = janus_process_error_string(request, session_id, transaction_text, error_code, error_cause);\n\t\t\tgoto jsondone;\n\t\t}\n\t\t/* Check if we should limit the response to the plugin-specific info */\n\t\tgboolean plugin_only = json_is_true(json_object_get(root, \"plugin_only\"));\n\t\t/* Prepare info */\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"session_id\", json_integer(session_id));\n\t\tjson_object_set_new(info, \"session_last_activity\", json_integer(session->last_activity));\n\t\tjson_object_set_new(info, \"session_timeout\", json_integer(session->timeout == -1 ? global_session_timeout : (guint)session->timeout));\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tif(session->source && session->source->transport)\n\t\t\tjson_object_set_new(info, \"session_transport\", json_string(session->source->transport->get_package()));\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\tjanus_mutex_lock(&handle->mutex);\n\t\tjson_object_set_new(info, \"handle_id\", json_integer(handle_id));\n\t\tif(handle->opaque_id)\n\t\t\tjson_object_set_new(info, \"opaque_id\", json_string(handle->opaque_id));\n\t\tif(handle->token)\n\t\t\tjson_object_set_new(info, \"token\", json_string(handle->token));\n\t\tjson_object_set_new(info, \"loop-running\", (handle->mainloop != NULL &&\n\t\t\tg_main_loop_is_running(handle->mainloop)) ? json_true() : json_false());\n\t\tjson_object_set_new(info, \"created\", json_integer(handle->created));\n\t\tjson_object_set_new(info, \"current_time\", json_integer(janus_get_monotonic_time()));\n\t\tif(handle->app && janus_plugin_session_is_alive(handle->app_handle)) {\n\t\t\tjanus_plugin *plugin = (janus_plugin *)handle->app;\n\t\t\tjson_object_set_new(info, \"plugin\", json_string(plugin->get_package()));\n\t\t\tif(plugin->query_session) {\n\t\t\t\t/* FIXME This check will NOT work with legacy plugins that were compiled BEFORE the method was specified in plugin.h */\n\t\t\t\tjson_t *query = plugin->query_session(handle->app_handle);\n\t\t\t\tif(query != NULL) {\n\t\t\t\t\t/* Make sure this is a JSON object */\n\t\t\t\t\tif(!json_is_object(query)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring invalid query response from the plugin (not an object)\\n\");\n\t\t\t\t\t\tjson_decref(query);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjson_object_set_new(info, \"plugin_specific\", query);\n\t\t\t\t\t}\n\t\t\t\t\tquery = NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(plugin_only)\n\t\t\tgoto info_done;\n\t\tjson_t *flags = json_object();\n\t\tjson_object_set_new(flags, \"got-offer\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"got-answer\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"negotiated\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEGOTIATED) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"processing-offer\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"starting\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_START) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"ice-restart\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ICE_RESTART) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"ready\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"stopped\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"alert\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"trickle\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"all-trickles\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"resend-trickles\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RESEND_TRICKLES) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"trickle-synced\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE_SYNCED) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"data-channels\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"has-audio\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_AUDIO) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"has-video\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_VIDEO) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"new-datachan-sdp\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"rfc4588-rtx\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"cleaning\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING) ? json_true() : json_false());\n\t\tjson_object_set_new(flags, \"e2ee\", janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_E2EE) ? json_true() : json_false());\n\t\tjson_object_set_new(info, \"flags\", flags);\n\t\tif(handle->agent) {\n\t\t\tjson_object_set_new(info, \"agent-created\", json_integer(handle->agent_created));\n\t\t\tif(handle->agent_started > 0)\n\t\t\t\tjson_object_set_new(info, \"agent-started\", json_integer(handle->agent_started));\n\t\t\tjson_object_set_new(info, \"ice-mode\", json_string(janus_ice_is_ice_lite_enabled() ? \"lite\" : \"full\"));\n\t\t\tjson_object_set_new(info, \"ice-role\", json_string(handle->controlling ? \"controlling\" : \"controlled\"));\n\t\t}\n\t\tjson_t *sdps = json_object();\n\t\tif(handle->rtp_profile)\n\t\t\tjson_object_set_new(sdps, \"profile\", json_string(handle->rtp_profile));\n\t\tif(handle->local_sdp)\n\t\t\tjson_object_set_new(sdps, \"local\", json_string(handle->local_sdp));\n\t\tif(handle->remote_sdp)\n\t\t\tjson_object_set_new(sdps, \"remote\", json_string(handle->remote_sdp));\n\t\tjson_object_set_new(info, \"sdps\", sdps);\n\t\tif(handle->pending_trickles)\n\t\t\tjson_object_set_new(info, \"pending-trickles\", json_integer(g_list_length(handle->pending_trickles)));\n\t\tif(handle->queued_packets)\n\t\t\tjson_object_set_new(info, \"queued-packets\", json_integer(g_async_queue_length(handle->queued_packets)));\n\t\tif(g_atomic_int_get(&handle->dump_packets) && handle->text2pcap) {\n\t\t\tif(handle->text2pcap->text) {\n\t\t\t\tjson_object_set_new(info, \"dump-to-text2pcap\", json_true());\n\t\t\t\tjson_object_set_new(info, \"text2pcap-file\", json_string(handle->text2pcap->filename));\n\t\t\t} else {\n\t\t\t\tjson_object_set_new(info, \"dump-to-pcap\", json_true());\n\t\t\t\tjson_object_set_new(info, \"pcap-file\", json_string(handle->text2pcap->filename));\n\t\t\t}\n\t\t}\n\t\tif(handle->pc) {\n\t\t\tjson_t *p = janus_admin_peerconnection_summary(handle->pc);\n\t\t\tif(p)\n\t\t\t\tjson_object_set_new(info, \"webrtc\", p);\n\t\t}\ninfo_done:\n\t\tjanus_mutex_unlock(&handle->mutex);\n\t\t/* Prepare JSON reply */\n\t\tjson_t *reply = janus_create_message(\"success\", session_id, transaction_text);\n\t\tjson_object_set_new(reply, \"handle_id\", json_integer(handle_id));\n\t\tjson_object_set_new(reply, \"info\", info);\n\t\t/* Send the success reply */\n\t\tret = janus_process_success(request, reply);\n\t\tgoto jsondone;\n\t}\n\njsondone:\n\t/* Done processing */\n\tif(handle != NULL)\n\t\tjanus_refcount_decrease(&handle->ref);\n\tif(session != NULL)\n\t\tjanus_refcount_decrease(&session->ref);\n\treturn ret;\n}\n\nint janus_process_success(janus_request *request, json_t *payload)\n{\n\tif(!request || !payload)\n\t\treturn -1;\n\t/* Pass to the right transport plugin */\n\tJANUS_LOG(LOG_HUGE, \"Sending %s API response to %s (%p)\\n\", request->admin ? \"admin\" : \"Janus\", request->transport->get_package(), request->instance);\n\treturn request->transport->send_message(request->instance, request->request_id, request->admin, payload);\n}\n\nstatic int janus_process_error_string(janus_request *request, uint64_t session_id, const char *transaction, gint error, gchar *error_string)\n{\n\tif(!request)\n\t\treturn -1;\n\t/* Done preparing error */\n\tJANUS_LOG(LOG_VERB, \"[%s] Returning %s API error %d (%s)\\n\", transaction, request->admin ? \"admin\" : \"Janus\", error, error_string);\n\t/* Prepare JSON error */\n\tjson_t *reply = janus_create_message(\"error\", session_id, transaction);\n\tjson_t *error_data = json_object();\n\tjson_object_set_new(error_data, \"code\", json_integer(error));\n\tjson_object_set_new(error_data, \"reason\", json_string(error_string));\n\tjson_object_set_new(reply, \"error\", error_data);\n\t/* Pass to the right transport plugin */\n\treturn request->transport->send_message(request->instance, request->request_id, request->admin, reply);\n}\n\nint janus_process_error(janus_request *request, uint64_t session_id, const char *transaction, gint error, const char *format, ...)\n{\n\tif(!request)\n\t\treturn -1;\n\tgchar *error_string = NULL;\n\tgchar error_buf[512];\n\tif(format == NULL) {\n\t\t/* No error string provided, use the default one */\n\t\terror_string = (gchar *)janus_get_api_error(error);\n\t} else {\n\t\t/* This callback has variable arguments (error string) */\n\t\tva_list ap;\n\t\tva_start(ap, format);\n\t\tg_vsnprintf(error_buf, sizeof(error_buf), format, ap);\n\t\tva_end(ap);\n\t\terror_string = error_buf;\n\t}\n\treturn janus_process_error_string(request, session_id, transaction, error, error_string);\n}\n\n/* Admin/monitor helpers */\njson_t *janus_admin_peerconnection_summary(janus_ice_peerconnection *pc) {\n\tif(pc == NULL)\n\t\treturn NULL;\n\tjson_t *w = json_object();\n\tjson_t *i = json_object();\n\tjson_object_set_new(i, \"stream_id\", json_integer(pc->stream_id));\n\tjson_object_set_new(i, \"component_id\", json_integer(pc->component_id));\n\tjson_object_set_new(i, \"state\", json_string(janus_get_ice_state_name(pc->state)));\n\tif(pc->icefailed_detected) {\n\t\tjson_object_set_new(i, \"failed-detected\", json_integer(pc->icefailed_detected));\n\t\tjson_object_set_new(i, \"icetimer-started\", pc->icestate_source ? json_true() : json_false());\n\t}\n\tif(pc->gathered > 0)\n\t\tjson_object_set_new(i, \"gathered\", json_integer(pc->gathered));\n\tif(pc->connected > 0)\n\t\tjson_object_set_new(i, \"connected\", json_integer(pc->connected));\n\tif(pc->local_candidates) {\n\t\tjson_t *cs = json_array();\n\t\tGSList *candidates = pc->local_candidates, *c = NULL;\n\t\tfor (c = candidates; c; c = c->next) {\n\t\t\tgchar *lc = (gchar *) c->data;\n\t\t\tif(lc)\n\t\t\t\tjson_array_append_new(cs, json_string(lc));\n\t\t}\n\t\tjson_object_set_new(i, \"local-candidates\", cs);\n\t}\n\tif(pc->remote_candidates) {\n\t\tjson_t *cs = json_array();\n\t\tGSList *candidates = pc->remote_candidates, *c = NULL;\n\t\tfor (c = candidates; c; c = c->next) {\n\t\t\tgchar *rc = (gchar *) c->data;\n\t\t\tif(rc)\n\t\t\t\tjson_array_append_new(cs, json_string(rc));\n\t\t}\n\t\tjson_object_set_new(i, \"remote-candidates\", cs);\n\t}\n\tif(pc->selected_pair) {\n\t\tjson_object_set_new(i, \"selected-pair\", json_string(pc->selected_pair));\n\t}\n\tjson_object_set_new(i, \"ready\", json_integer(pc->cdone));\n\tjson_object_set_new(w, \"ice\", i);\n\tjson_t *d = json_object();\n\tif(pc->dtls) {\n\t\tjanus_dtls_srtp *dtls = pc->dtls;\n\t\tjson_object_set_new(d, \"fingerprint\", json_string(janus_dtls_get_local_fingerprint()));\n\t\tif(pc->remote_fingerprint)\n\t\t\tjson_object_set_new(d, \"remote-fingerprint\", json_string(pc->remote_fingerprint));\n\t\tif(pc->remote_hashing)\n\t\t\tjson_object_set_new(d, \"remote-fingerprint-hash\", json_string(pc->remote_hashing));\n\t\tjson_object_set_new(d, \"dtls-role\", json_string(janus_get_dtls_srtp_role(pc->dtls_role)));\n\t\tjson_object_set_new(d, \"dtls-state\", json_string(janus_get_dtls_srtp_state(dtls->dtls_state)));\n\t\tjson_object_set_new(d, \"retransmissions\", json_integer(dtls->retransmissions));\n\t\tjson_object_set_new(d, \"valid\", dtls->srtp_valid ? json_true() : json_false());\n\t\tconst char *srtp_profile = janus_get_dtls_srtp_profile(dtls->srtp_profile);\n\t\tjson_object_set_new(d, \"srtp-profile\", json_string(srtp_profile ? srtp_profile : \"none\"));\n\t\tjson_object_set_new(d, \"ready\", dtls->ready ? json_true() : json_false());\n\t\tif(dtls->dtls_started > 0)\n\t\t\tjson_object_set_new(d, \"handshake-started\", json_integer(dtls->dtls_started));\n\t\tif(dtls->dtls_connected > 0)\n\t\t\tjson_object_set_new(d, \"connected\", json_integer(dtls->dtls_connected));\n#ifdef HAVE_SCTP\n\t\t/* FIXME Actually check if this succeeded? */\n\t\tjson_object_set_new(d, \"sctp-association\", dtls->sctp ? json_true() : json_false());\n#endif\n\t\tjson_t *stats = json_object();\n\t\tjson_t *in_stats = json_object();\n\t\tjson_object_set_new(in_stats, \"packets\", json_integer(pc->dtls_in_stats.info[0].packets));\n\t\tjson_object_set_new(in_stats, \"bytes\", json_integer(pc->dtls_in_stats.info[0].bytes));\n\t\tjson_t *out_stats = json_object();\n\t\tjson_object_set_new(out_stats, \"packets\", json_integer(pc->dtls_out_stats.info[0].packets));\n\t\tjson_object_set_new(out_stats, \"bytes\", json_integer(pc->dtls_out_stats.info[0].bytes));\n\t\tjson_object_set_new(stats, \"in\", in_stats);\n\t\tjson_object_set_new(stats, \"out\", out_stats);\n\t\tjson_object_set_new(d, \"stats\", stats);\n\t}\n\tjson_object_set_new(w, \"dtls\", d);\n\tjson_t *se = json_object();\n\tif(pc->mid_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_MID, json_integer(pc->mid_ext_id));\n\tif(pc->rid_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_RID, json_integer(pc->rid_ext_id));\n\tif(pc->ridrtx_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_REPAIRED_RID, json_integer(pc->ridrtx_ext_id));\n\tif(pc->abs_send_time_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_ABS_SEND_TIME, json_integer(pc->abs_send_time_ext_id));\n\tif(pc->abs_capture_time_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_ABS_SEND_TIME, json_integer(pc->abs_capture_time_ext_id));\n\tif(pc->transport_wide_cc_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC, json_integer(pc->transport_wide_cc_ext_id));\n\tif(pc->audiolevel_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_AUDIO_LEVEL, json_integer(pc->audiolevel_ext_id));\n\tif(pc->videoorientation_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION, json_integer(pc->videoorientation_ext_id));\n\tif(pc->playoutdelay_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_PLAYOUT_DELAY, json_integer(pc->playoutdelay_ext_id));\n\tif(pc->dependencydesc_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_DEPENDENCY_DESC, json_integer(pc->dependencydesc_ext_id));\n\tif(pc->videolayers_ext_id > 0)\n\t\tjson_object_set_new(se, JANUS_RTP_EXTMAP_VIDEO_LAYERS, json_integer(pc->videolayers_ext_id));\n\tjson_object_set_new(w, \"extensions\", se);\n\tjson_t *bwe = json_object();\n\tjson_object_set_new(bwe, \"twcc\", pc->do_transport_wide_cc ? json_true() : json_false());\n\tif(pc->transport_wide_cc_ext_id >= 0)\n\t\tjson_object_set_new(bwe, \"twcc-ext-id\", json_integer(pc->transport_wide_cc_ext_id));\n\tjson_object_set_new(w, \"bwe\", bwe);\n\tjson_t *media = json_object();\n\t/* Iterate on all media */\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tuint mi=0;\n\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\tjson_t *m = janus_admin_peerconnection_medium_summary(medium);\n\t\tif(m)\n\t\t\tjson_object_set_new(media, medium->mid, m);\n\t}\n\tjson_object_set_new(w, \"media\", media);\n\treturn w;\n}\n\njson_t *janus_admin_peerconnection_medium_summary(janus_ice_peerconnection_medium *medium) {\n\tif(medium == NULL)\n\t\treturn NULL;\n\t/* SSRCs */\n\tjson_t *m = json_object();\n\tif(medium->type == JANUS_MEDIA_AUDIO)\n\t\tjson_object_set_new(m, \"type\", json_string(\"audio\"));\n\telse if(medium->type == JANUS_MEDIA_VIDEO)\n\t\tjson_object_set_new(m, \"type\", json_string(\"video\"));\n\telse if(medium->type == JANUS_MEDIA_DATA)\n\t\tjson_object_set_new(m, \"type\", json_string(\"data\"));\n\tjson_object_set_new(m, \"mindex\", json_integer(medium->mindex));\n\tjson_object_set_new(m, \"mid\", json_string(medium->mid));\n\tif(medium->msid || medium->remote_msid) {\n\t\tjson_t *mm = json_object();\n\t\tif(medium->msid) {\n\t\t\tjson_object_set_new(mm, \"local-stream\", json_string(medium->msid));\n\t\t\tif(medium->mstid)\n\t\t\t\tjson_object_set_new(mm, \"local-track\", json_string(medium->mstid));\n\t\t}\n\t\tif(medium->remote_msid) {\n\t\t\tjson_object_set_new(mm, \"remote-stream\", json_string(medium->remote_msid));\n\t\t\tif(medium->remote_mstid)\n\t\t\t\tjson_object_set_new(mm, \"remote-track\", json_string(medium->remote_mstid));\n\t\t}\n\t\tjson_object_set_new(m, \"msid\", mm);\n\t}\n\tif(medium->type != JANUS_MEDIA_DATA) {\n\t\tjson_object_set_new(m, \"do_nacks\", medium->do_nacks ? json_true() : json_false());\n\t\tjson_object_set_new(m, \"nack-queue-ms\", json_integer(medium->nack_queue_ms));\n\t}\n\tif(medium->type != JANUS_MEDIA_DATA) {\n\t\tjson_t *ms = json_object();\n\t\tif(medium->ssrc)\n\t\t\tjson_object_set_new(ms, \"ssrc\", json_integer(medium->ssrc));\n\t\tif(medium->ssrc_rtx)\n\t\t\tjson_object_set_new(ms, \"ssrc-rtx\", json_integer(medium->ssrc_rtx));\n\t\tif(medium->ssrc_peer[0])\n\t\t\tjson_object_set_new(ms, \"ssrc-peer\", json_integer(medium->ssrc_peer[0]));\n\t\tif(medium->ssrc_peer[1])\n\t\t\tjson_object_set_new(ms, \"ssrc-peer-sim-1\", json_integer(medium->ssrc_peer[1]));\n\t\tif(medium->ssrc_peer[2])\n\t\t\tjson_object_set_new(ms, \"ssrc-peer-sim-2\", json_integer(medium->ssrc_peer[2]));\n\t\tif(medium->ssrc_peer_rtx[0])\n\t\t\tjson_object_set_new(ms, \"ssrc-peer-rtx\", json_integer(medium->ssrc_peer_rtx[0]));\n\t\tif(medium->ssrc_peer_rtx[1])\n\t\t\tjson_object_set_new(ms, \"ssrc-peer-sim-1-rtx\", json_integer(medium->ssrc_peer_rtx[1]));\n\t\tif(medium->ssrc_peer_rtx[2])\n\t\t\tjson_object_set_new(ms, \"ssrc-peer-sim-2-rtx\", json_integer(medium->ssrc_peer_rtx[2]));\n\t\tjson_object_set_new(m, \"ssrc\", ms);\n\t\tif(medium->rid[0] && medium->pc->rid_ext_id > 0) {\n\t\t\tjson_t *mr = json_object();\n\t\t\tjson_t *rid = json_array();\n\t\t\tif(medium->rid[2])\n\t\t\t\tjson_array_append_new(rid, json_string(medium->rid[2]));\n\t\t\tif(medium->rid[1])\n\t\t\t\tjson_array_append_new(rid, json_string(medium->rid[1]));\n\t\t\tjson_array_append_new(rid, json_string(medium->rid[0]));\n\t\t\tjson_object_set_new(mr, \"rid\", rid);\n\t\t\tjson_object_set_new(mr, \"rid-ext-id\", json_integer(medium->pc->rid_ext_id));\n\t\t\tif(medium->pc->ridrtx_ext_id > 0)\n\t\t\t\tjson_object_set_new(mr, \"ridrtx-ext-id\", json_integer(medium->pc->ridrtx_ext_id));\n\t\t\tjson_object_set_new(mr, \"rid-order\", json_string(medium->rids_hml ? \"hml\" : \"lmh\"));\n\t\t\tif(medium->legacy_rid)\n\t\t\t\tjson_object_set_new(mr, \"rid-syntax\", json_string(\"legacy\"));\n\t\t\tjson_object_set_new(m, \"rid-simulcast\", mr);\n\t\t}\n\t}\n\t/* Media direction */\n\tif(medium->type != JANUS_MEDIA_DATA) {\n\t\tjson_t *md = json_object();\n\t\tjson_object_set_new(md, \"send\", medium->send ? json_true() : json_false());\n\t\tjson_object_set_new(md, \"recv\", medium->recv ? json_true() : json_false());\n\t\tjson_object_set_new(m, \"direction\", md);\n\t}\n\t/* Payload type and codec */\n\tif(medium->type != JANUS_MEDIA_DATA && (medium->payload_type > -1 || medium->rtx_payload_type > -1)) {\n\t\tjson_t *sc = json_object();\n\t\tif(medium->payload_type > -1)\n\t\t\tjson_object_set_new(sc, \"pt\", json_integer(medium->payload_type));\n\t\tif(medium->opusred_pt > 0)\n\t\t\tjson_object_set_new(sc, \"opus-red-pt\", json_integer(medium->opusred_pt));\n\t\tif(medium->rtx_payload_type > -1)\n\t\t\tjson_object_set_new(sc, \"rtx-pt\", json_integer(medium->rtx_payload_type));\n\t\tif(medium->codec != NULL)\n\t\t\tjson_object_set_new(sc, \"codec\", json_string(medium->codec));\n\t\tjson_object_set_new(m, \"codecs\", sc);\n\t}\n\t/* RTCP stats */\n\tint vindex=0;\n\tif(medium->type != JANUS_MEDIA_DATA) {\n\t\tjson_t *rtcp_stats = NULL;\n\t\tfor(vindex=0; vindex<3; vindex++) {\n\t\t\tif(medium->rtcp_ctx[vindex] != NULL) {\n\t\t\t\tif(rtcp_stats == NULL)\n\t\t\t\t\trtcp_stats = json_object();\n\t\t\t\tjson_t *medium_rtcp_stats = json_object();\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"base\", json_integer(medium->rtcp_ctx[vindex]->tb));\n\t\t\t\tif(vindex == 0)\n\t\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"rtt\", json_integer(janus_rtcp_context_get_rtt(medium->rtcp_ctx[vindex])));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"lost\", json_integer(janus_rtcp_context_get_lost_all(medium->rtcp_ctx[vindex], FALSE)));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"lost-by-remote\", json_integer(janus_rtcp_context_get_lost_all(medium->rtcp_ctx[vindex], TRUE)));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"jitter-local\", json_integer(janus_rtcp_context_get_jitter(medium->rtcp_ctx[vindex], FALSE)));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"jitter-remote\", json_integer(janus_rtcp_context_get_jitter(medium->rtcp_ctx[vindex], TRUE)));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"in-link-quality\", json_integer(janus_rtcp_context_get_in_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"in-media-link-quality\", json_integer(janus_rtcp_context_get_in_media_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"out-link-quality\", json_integer(janus_rtcp_context_get_out_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\tjson_object_set_new(medium_rtcp_stats, \"out-media-link-quality\", json_integer(janus_rtcp_context_get_out_media_link_quality(medium->rtcp_ctx[vindex])));\n\t\t\t\tif(vindex == 0)\n\t\t\t\t\tjson_object_set_new(rtcp_stats, \"main\", medium_rtcp_stats);\n\t\t\t\telse if(vindex == 1)\n\t\t\t\t\tjson_object_set_new(rtcp_stats, \"sim1\", medium_rtcp_stats);\n\t\t\t\telse\n\t\t\t\t\tjson_object_set_new(rtcp_stats, \"sim2\", medium_rtcp_stats);\n\t\t\t}\n\t\t}\n\t\tif(rtcp_stats != NULL)\n\t\t\tjson_object_set_new(m, \"rtcp\", rtcp_stats);\n\t}\n\t/* Media stats */\n\tjson_t *stats = json_object();\n\tjson_t *in_stats = json_object();\n\tjson_t *out_stats = json_object();\n\tfor(vindex=0; vindex<3; vindex++) {\n\t\tif(vindex > 0 && medium->ssrc_peer[vindex] == 0)\n\t\t\tcontinue;\n\t\tjson_t *container = (vindex == 0 ? in_stats : json_object());\n\t\tjson_object_set_new(container, \"packets\", json_integer(medium->in_stats.info[vindex].packets));\n\t\tjson_object_set_new(container, \"bytes\", json_integer(medium->in_stats.info[vindex].bytes));\n\t\tif(medium->type != JANUS_MEDIA_DATA) {\n\t\t\tjson_object_set_new(container, \"bytes_lastsec\", json_integer(medium->in_stats.info[vindex].bytes_lastsec));\n\t\t\tif(medium->do_nacks) {\n\t\t\t\tjson_object_set_new(container, \"nacks\", json_integer(medium->in_stats.info[vindex].nacks));\n\t\t\t\tif(medium->rtcp_ctx[vindex])\n\t\t\t\t\tjson_object_set_new(in_stats, \"retransmissions\", json_integer(medium->rtcp_ctx[vindex]->retransmitted));\n\t\t\t}\n\t\t\tif(vindex == 1)\n\t\t\t\tjson_object_set_new(in_stats, \"video-simulcast-1\", container);\n\t\t\telse if(vindex == 2)\n\t\t\t\tjson_object_set_new(in_stats, \"video-simulcast-2\", container);\n\t\t}\n\t}\n\tjson_object_set_new(out_stats, \"packets\", json_integer(medium->out_stats.info[0].packets));\n\tjson_object_set_new(out_stats, \"bytes\", json_integer(medium->out_stats.info[0].bytes));\n\tif(medium->type != JANUS_MEDIA_DATA) {\n\t\tjson_object_set_new(out_stats, \"bytes_lastsec\", json_integer(medium->out_stats.info[0].bytes_lastsec));\n\t\tjson_object_set_new(out_stats, \"nacks\", json_integer(medium->out_stats.info[0].nacks));\n\t}\n\tjson_object_set_new(stats, \"in\", in_stats);\n\tjson_object_set_new(stats, \"out\", out_stats);\n\tjson_object_set_new(m, \"stats\", stats);\n\treturn m;\n}\n\n\n/* Transports */\nvoid janus_transport_close(gpointer key, gpointer value, gpointer user_data) {\n\tjanus_transport *transport = (janus_transport *)value;\n\tif(!transport)\n\t\treturn;\n\ttransport->destroy();\n}\n\nvoid janus_transportso_close(gpointer key, gpointer value, gpointer user_data) {\n\tvoid *transport = value;\n\tif(!transport)\n\t\treturn;\n\t/* FIXME We don't dlclose transports to be sure we can detect leaks */\n\t//~ dlclose(transport);\n}\n\n/* Transport callback interface */\nvoid janus_transport_incoming_request(janus_transport *plugin, janus_transport_session *transport, void *request_id, gboolean admin, json_t *message, json_error_t *error) {\n\tJANUS_LOG(LOG_VERB, \"Got %s API request from %s (%p)\\n\", admin ? \"an admin\" : \"a Janus\", plugin->get_package(), transport);\n\t/* Create a janus_request instance to handle the request */\n\tjanus_request *request = janus_request_new(plugin, transport, request_id, admin, message, message ? NULL : error);\n\t/* Enqueue the request, the thread will pick it up */\n\tg_async_queue_push(requests, request);\n}\n\nvoid janus_transport_gone(janus_transport *plugin, janus_transport_session *transport) {\n\t/* Get rid of sessions this transport was handling */\n\tJANUS_LOG(LOG_VERB, \"A %s transport instance has gone away (%p)\\n\", plugin->get_package(), transport);\n\tjanus_mutex_lock(&sessions_mutex);\n\tif(sessions && g_hash_table_size(sessions) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, sessions);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_session *session = (janus_session *) value;\n\t\t\tif(!session || g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->timedout) || session->last_activity == 0)\n\t\t\t\tcontinue;\n\t\t\tif(session->source && session->source->instance == transport) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Session %\"SCNu64\" will be over if not reclaimed\\n\", session->session_id);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Marking Session %\"SCNu64\" as over\\n\", session->session_id);\n\t\t\t\tif(reclaim_session_timeout < 1) { /* Reclaim session timeouts are disabled */\n\t\t\t\t\t/* Notify event handlers, if needed */\n\t\t\t\t\tif(janus_events_is_enabled())\n\t\t\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_SESSION, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\t\t\t\tsession->session_id, \"destroyed\", NULL);\n\t\t\t\t\t/* Mark the session as destroyed */\n\t\t\t\t\tjanus_session_destroy(session);\n\t\t\t\t\tg_atomic_int_dec_and_test(&sessions_num);\n\t\t\t\t\tg_hash_table_iter_remove(&iter);\n\t\t\t\t} else {\n\t\t\t\t\t/* Set flag for transport_gone. The Janus sessions watchdog will clean this up if not reclaimed */\n\t\t\t\t\tg_atomic_int_set(&session->transport_gone, 1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\ngboolean janus_transport_is_api_secret_needed(janus_transport *plugin) {\n\treturn api_secret != NULL;\n}\n\ngboolean janus_transport_is_api_secret_valid(janus_transport *plugin, const char *apisecret) {\n\tif(api_secret == NULL)\n\t\treturn TRUE;\n\treturn apisecret && janus_strcmp_const_time(apisecret, api_secret);\n}\n\ngboolean janus_transport_is_auth_token_needed(janus_transport *plugin) {\n\treturn janus_auth_is_enabled();\n}\n\ngboolean janus_transport_is_auth_token_valid(janus_transport *plugin, const char *token) {\n\tif(!janus_auth_is_enabled())\n\t\treturn TRUE;\n\treturn token && janus_auth_check_token(token);\n}\n\nvoid janus_transport_notify_event(janus_transport *plugin, void *transport, json_t *event) {\n\t/* A plugin asked to notify an event to the handlers */\n\tif(!plugin || !event || !json_is_object(event))\n\t\treturn;\n\t/* Notify event handlers */\n\tif(janus_events_is_enabled()) {\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_TRANSPORT, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t0, plugin->get_package(), transport, event);\n\t} else {\n\t\tjson_decref(event);\n\t}\n}\n\nvoid janus_transport_task(gpointer data, gpointer user_data) {\n\tJANUS_LOG(LOG_VERB, \"Transport task pool, serving request\\n\");\n\tjanus_request *request = (janus_request *)data;\n\tif(request == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Missing request\\n\");\n\t\treturn;\n\t}\n\tif(!request->admin)\n\t\tjanus_process_incoming_request(request);\n\telse\n\t\tjanus_process_incoming_admin_request(request);\n\t/* Done */\n\tjanus_request_destroy(request);\n}\n\n\n/* Thread to handle incoming requests: may involve an asynchronous task for plugin messaging */\nstatic void *janus_transport_requests(void *data) {\n\tJANUS_LOG(LOG_INFO, \"Joining Janus requests handler thread\\n\");\n\tjanus_request *request = NULL;\n\tgboolean destroy = FALSE;\n\twhile(!g_atomic_int_get(&stop)) {\n\t\trequest = g_async_queue_pop(requests);\n\t\tif(request == &exit_message)\n\t\t\tbreak;\n\t\t/* Should we process the request synchronously or with a task from the thread pool? */\n\t\tdestroy = TRUE;\n\t\t/* Process the request synchronously only it's not a message for a plugin */\n\t\tjson_t *message = json_object_get(request->message, \"janus\");\n\t\tconst gchar *message_text = json_string_value(message);\n\t\tif(message_text && !strcasecmp(message_text, request->admin ? \"message_plugin\" : \"message\")) {\n\t\t\t/* Spawn a task thread */\n\t\t\tGError *tperror = NULL;\n\t\t\tg_thread_pool_push(tasks, request, &tperror);\n\t\t\tif(tperror != NULL) {\n\t\t\t\t/* Something went wrong... */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to push task in thread pool...\\n\",\n\t\t\t\t\ttperror->code, tperror->message ? tperror->message : \"??\");\n\t\t\t\tg_error_free(tperror);\n\t\t\t\tjson_t *transaction = json_object_get(message, \"transaction\");\n\t\t\t\tconst char *transaction_text = json_is_string(transaction) ? json_string_value(transaction) : NULL;\n\t\t\t\tjanus_process_error(request, 0, transaction_text, JANUS_ERROR_UNKNOWN, \"Thread pool error\");\n\t\t\t} else {\n\t\t\t\t/* Don't destroy the request now, the task will take care of that */\n\t\t\t\tdestroy = FALSE;\n\t\t\t}\n\t\t} else {\n\t\t\tif(!request->admin)\n\t\t\t\tjanus_process_incoming_request(request);\n\t\t\telse\n\t\t\t\tjanus_process_incoming_admin_request(request);\n\t\t}\n\t\t/* Done */\n\t\tif(destroy)\n\t\t\tjanus_request_destroy(request);\n\t}\n\tJANUS_LOG(LOG_INFO, \"Leaving Janus requests handler thread\\n\");\n\treturn NULL;\n}\n\n\n/* Event handlers */\nvoid janus_eventhandler_close(gpointer key, gpointer value, gpointer user_data) {\n\tjanus_eventhandler *eventhandler = (janus_eventhandler *)value;\n\tif(!eventhandler)\n\t\treturn;\n\teventhandler->destroy();\n}\n\nvoid janus_eventhandlerso_close(gpointer key, gpointer value, gpointer user_data) {\n\tvoid *eventhandler = (janus_eventhandler *)value;\n\tif(!eventhandler)\n\t\treturn;\n\t//~ dlclose(eventhandler);\n}\n\n\n/* Loggers */\nvoid janus_logger_close(gpointer key, gpointer value, gpointer user_data) {\n\tjanus_logger *logger = (janus_logger *)value;\n\tif(!logger)\n\t\treturn;\n\tlogger->destroy();\n}\n\nvoid janus_loggerso_close(gpointer key, gpointer value, gpointer user_data) {\n\tvoid *logger = (janus_logger *)value;\n\tif(!logger)\n\t\treturn;\n\t//~ dlclose(logger);\n}\n\n\n/* Plugins */\nvoid janus_plugin_close(gpointer key, gpointer value, gpointer user_data) {\n\tjanus_plugin *plugin = (janus_plugin *)value;\n\tif(!plugin)\n\t\treturn;\n\tplugin->destroy();\n}\n\nvoid janus_pluginso_close(gpointer key, gpointer value, gpointer user_data) {\n\tvoid *plugin = value;\n\tif(!plugin)\n\t\treturn;\n\t/* FIXME We don't dlclose plugins to be sure we can detect leaks */\n\t//~ dlclose(plugin);\n}\n\njanus_plugin *janus_plugin_find(const gchar *package) {\n\tif(package != NULL && plugins != NULL)\t/* FIXME Do we need to fix the key pointer? */\n\t\treturn g_hash_table_lookup(plugins, package);\n\treturn NULL;\n}\n\n\n/* Plugin callback interface */\nint janus_plugin_push_event(janus_plugin_session *plugin_session, janus_plugin *plugin, const char *transaction, json_t *message, json_t *jsep) {\n\tif(!plugin || !message)\n\t\treturn -1;\n\tif(!janus_plugin_session_is_alive(plugin_session))\n\t\treturn -2;\n\tjanus_refcount_increase(&plugin_session->ref);\n\tjanus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!ice_handle || janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)) {\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\treturn JANUS_ERROR_SESSION_NOT_FOUND;\n\t}\n\tjanus_refcount_increase(&ice_handle->ref);\n\tjanus_session *session = ice_handle->session;\n\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\tjanus_refcount_decrease(&ice_handle->ref);\n\t\treturn JANUS_ERROR_SESSION_NOT_FOUND;\n\t}\n\t/* Make sure this is a JSON object */\n\tif(!json_is_object(message)) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Cannot push event (JSON error: not an object)\\n\", ice_handle->handle_id);\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\tjanus_refcount_decrease(&ice_handle->ref);\n\t\treturn JANUS_ERROR_INVALID_JSON_OBJECT;\n\t}\n\t/* Attach JSEP if possible? */\n\tconst char *sdp_type = json_string_value(json_object_get(jsep, \"type\"));\n\tconst char *sdp = json_string_value(json_object_get(jsep, \"sdp\"));\n\tgboolean restart = json_object_get(jsep, \"sdp\") ? json_is_true(json_object_get(jsep, \"restart\")) : FALSE;\n\tgboolean e2ee = json_object_get(jsep, \"sdp\") ? json_is_true(json_object_get(jsep, \"e2ee\")) : FALSE;\n\tjson_t *merged_jsep = NULL;\n\tif(sdp_type != NULL && sdp != NULL) {\n\t\tmerged_jsep = janus_plugin_handle_sdp(plugin_session, plugin, sdp_type, sdp, restart);\n\t\tif(merged_jsep == NULL) {\n\t\t\tif(ice_handle == NULL || janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t\t\t|| janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Cannot push event (handle not available anymore or negotiation stopped)\\n\", ice_handle->handle_id);\n\t\t\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\t\t\tjanus_refcount_decrease(&ice_handle->ref);\n\t\t\t\treturn JANUS_ERROR_HANDLE_NOT_FOUND;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Cannot push event (JSON error: problem with the SDP)\\n\", ice_handle->handle_id);\n\t\t\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\t\t\tjanus_refcount_decrease(&ice_handle->ref);\n\t\t\t\treturn JANUS_ERROR_JSEP_INVALID_SDP;\n\t\t\t}\n\t\t}\n\t}\n\t/* Reference the payload, as the plugin may still need it and will do a decref itself */\n\tjson_incref(message);\n\t/* Prepare JSON event */\n\tjson_t *event = janus_create_message(\"event\", session->session_id, transaction);\n\tjson_object_set_new(event, \"sender\", json_integer(ice_handle->handle_id));\n\tif(janus_is_opaqueid_in_api_enabled() && ice_handle->opaque_id != NULL)\n\t\tjson_object_set_new(event, \"opaque_id\", json_string(ice_handle->opaque_id));\n\tjson_t *plugin_data = json_object();\n\tjson_object_set_new(plugin_data, \"plugin\", json_string(plugin->get_package()));\n\tjson_object_set_new(plugin_data, \"data\", message);\n\tjson_object_set_new(event, \"plugindata\", plugin_data);\n\tif(merged_jsep != NULL) {\n\t\tif(e2ee)\n\t\t\tjanus_flags_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_E2EE);\n\t\tif(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_E2EE))\n\t\t\tjson_object_set_new(merged_jsep, \"e2ee\", json_true());\n\t\tjson_object_set_new(event, \"jsep\", merged_jsep);\n\t\t/* In case event handlers are enabled, push the local SDP to all handlers */\n\t\tif(janus_events_is_enabled()) {\n\t\t\tconst char *merged_sdp_type = json_string_value(json_object_get(merged_jsep, \"type\"));\n\t\t\tconst char *merged_sdp = json_string_value(json_object_get(merged_jsep, \"sdp\"));\n\t\t\t/* Notify event handlers as well */\n\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_JSEP, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\t\tsession->session_id, ice_handle->handle_id, ice_handle->opaque_id, \"local\", merged_sdp_type, merged_sdp);\n\t\t}\n\t}\n\t/* Send the event */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending event to transport...\\n\", ice_handle->handle_id);\n\tjanus_session_notify_event(session, event);\n\n\tif((restart || janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RESEND_TRICKLES))\n\t\t\t&& janus_ice_is_full_trickle_enabled()) {\n\t\t/* We're restarting ICE, send our trickle candidates again */\n\t\tjanus_ice_resend_trickles(ice_handle);\n\t}\n\n\tjanus_refcount_decrease(&plugin_session->ref);\n\tjanus_refcount_decrease(&ice_handle->ref);\n\treturn JANUS_OK;\n}\n\njson_t *janus_plugin_handle_sdp(janus_plugin_session *plugin_session, janus_plugin *plugin, const char *sdp_type, const char *sdp, gboolean restart) {\n\tif(!janus_plugin_session_is_alive(plugin_session) ||\n\t\t\tplugin == NULL || sdp_type == NULL || sdp == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid arguments\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\t//~ if(ice_handle == NULL || janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY)) {\n\tif(ice_handle == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid ICE handle\\n\");\n\t\treturn NULL;\n\t}\n\tint offer = 0;\n\tif(!strcasecmp(sdp_type, \"offer\")) {\n\t\t/* This is an offer from a plugin */\n\t\toffer = 1;\n\t\tjanus_flags_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER);\n\t\tjanus_flags_clear(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER);\n\t} else if(!strcasecmp(sdp_type, \"answer\")) {\n\t\t/* This is an answer from a plugin */\n\t\tjanus_flags_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_ANSWER);\n\t\tif(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_GOT_OFFER))\n\t\t\tjanus_flags_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEGOTIATED);\n\t} else {\n\t\t/* TODO Handle other messages */\n\t\tJANUS_LOG(LOG_ERR, \"Unknown type '%s'\\n\", sdp_type);\n\t\treturn NULL;\n\t}\n\t/* Is this valid SDP? */\n\tchar error_str[512];\n\terror_str[0] = '\\0';\n\tint audio = 0, video = 0, data = 0;\n\tjanus_sdp *parsed_sdp = janus_sdp_preparse(ice_handle, sdp, error_str, sizeof(error_str), NULL, &audio, &video, &data);\n\tif(parsed_sdp == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Couldn't parse SDP... %s\\n\", ice_handle->handle_id, error_str);\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] There are %d audio, %d video and %d data m-lines\\n\",\n\t\tice_handle->handle_id, audio, video, data);\n\tgboolean updating = FALSE;\n\tif(offer) {\n\t\t/* Are we still cleaning up from a previous media session? */\n\t\tif(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Still cleaning up from a previous media session, let's wait a bit...\\n\", ice_handle->handle_id);\n\t\t\tgint64 waited = 0;\n\t\t\twhile(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Still cleaning up from a previous media session, let's wait a bit...\\n\", ice_handle->handle_id);\n\t\t\t\tg_usleep(100000);\n\t\t\t\twaited += 100000;\n\t\t\t\tif(waited >= 3*G_USEC_PER_SEC) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- Waited 3 seconds, that's enough!\\n\", ice_handle->handle_id);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Still cleaning a previous session\\n\", ice_handle->handle_id);\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_lock(&ice_handle->mutex);\n\t\tif(ice_handle->agent == NULL) {\n\t\t\t/* We still need to configure the WebRTC stuff: negotiate RFC4588 by default */\n\t\t\tjanus_flags_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX);\n\t\t\t/* Process SDP in order to setup ICE locally (this is going to result in an answer from the browser) */\n\t\t\tif(janus_ice_setup_local(ice_handle, FALSE, TRUE, JANUS_DTLS_ROLE_ACTPASS) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error setting ICE locally\\n\", ice_handle->handle_id);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\t/* Create medium instances */\n\t\t\tif(janus_sdp_process_local(ice_handle, parsed_sdp, FALSE) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error processing SDP\\n\", ice_handle->handle_id);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t} else {\n\t\t\tupdating = TRUE;\n\t\t\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] Updating existing session\\n\", ice_handle->handle_id);\n\t\t\tif(offer && ice_handle->pc) {\n\t\t\t\t/* Check what changed */\n\t\t\t\tif(janus_sdp_process_local(ice_handle, parsed_sdp, TRUE) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error processing SDP\\n\", ice_handle->handle_id);\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Make sure we don't send the rid/repaired-rid attributes when offering ourselves */\n\t\tint mindex = 0;\n\t\tint mid_ext_id = 0, transport_wide_cc_ext_id = 0, abs_send_time_ext_id = 0,\n\t\t\tabs_capture_time_ext_id = 0, audiolevel_ext_id = 0, videoorientation_ext_id = 0,\n\t\t\tplayoutdelay_ext_id = 0, dependencydesc_ext_id = 0, videolayers_ext_id = 0;\n\t\tGList *temp = parsed_sdp->m_lines;\n\t\twhile(temp) {\n\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\tjanus_ice_peerconnection_medium *medium = ice_handle->pc ?\n\t\t\t\tg_hash_table_lookup(ice_handle->pc->media, GUINT_TO_POINTER(mindex)) : NULL;\n\t\t\tGList *tempA = m->attributes;\n\t\t\twhile(tempA) {\n\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\t\tif(a->name && a->value) {\n\t\t\t\t\tif(!strcasecmp(a->name, \"extmap\")) {\n\t\t\t\t\t\tif(strstr(a->value, JANUS_RTP_EXTMAP_MID))\n\t\t\t\t\t\t\tmid_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC))\n\t\t\t\t\t\t\ttransport_wide_cc_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_ABS_SEND_TIME))\n\t\t\t\t\t\t\tabs_send_time_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME))\n\t\t\t\t\t\t\tabs_capture_time_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_AUDIO_LEVEL))\n\t\t\t\t\t\t\taudiolevel_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION))\n\t\t\t\t\t\t\tvideoorientation_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_PLAYOUT_DELAY))\n\t\t\t\t\t\t\tplayoutdelay_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_DEPENDENCY_DESC))\n\t\t\t\t\t\t\tdependencydesc_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_VIDEO_LAYERS))\n\t\t\t\t\t\t\tvideolayers_ext_id = atoi(a->value);\n\t\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_RID) ||\n\t\t\t\t\t\t\t\tstrstr(a->value, JANUS_RTP_EXTMAP_REPAIRED_RID)) {\n\t\t\t\t\t\t\tm->attributes = g_list_remove(m->attributes, a);\n\t\t\t\t\t\t\ttempA = m->attributes;\n\t\t\t\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(m->type == JANUS_SDP_AUDIO && !strcasecmp(a->name, \"rtpmap\") &&\n\t\t\t\t\t\t\tstrstr(a->value, \"red/48000/2\")) {\n\t\t\t\t\t\t/* If the plugin offered RED, take note of it */\n\t\t\t\t\t\tint opusred_pt = atoi(a->value);\n\t\t\t\t\t\tif(medium != NULL)\n\t\t\t\t\t\t\tmedium->opusred_pt = opusred_pt;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttempA = tempA->next;\n\t\t\t}\n\t\t\tmindex++;\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tif(ice_handle->pc && ice_handle->pc->mid_ext_id != mid_ext_id)\n\t\t\tice_handle->pc->mid_ext_id = mid_ext_id;\n\t\tif(ice_handle->pc && ice_handle->pc->transport_wide_cc_ext_id != transport_wide_cc_ext_id) {\n\t\t\tice_handle->pc->do_transport_wide_cc = transport_wide_cc_ext_id > 0 ? TRUE : FALSE;\n\t\t\tice_handle->pc->transport_wide_cc_ext_id = transport_wide_cc_ext_id;\n\t\t}\n\t\tif(ice_handle->pc && ice_handle->pc->abs_send_time_ext_id != abs_send_time_ext_id)\n\t\t\tice_handle->pc->abs_send_time_ext_id = abs_send_time_ext_id;\n\t\tif(ice_handle->pc && ice_handle->pc->abs_capture_time_ext_id != abs_capture_time_ext_id)\n\t\t\tice_handle->pc->abs_capture_time_ext_id = abs_capture_time_ext_id;\n\t\tif(ice_handle->pc && ice_handle->pc->audiolevel_ext_id != audiolevel_ext_id)\n\t\t\tice_handle->pc->audiolevel_ext_id = audiolevel_ext_id;\n\t\tif(ice_handle->pc && ice_handle->pc->videoorientation_ext_id != videoorientation_ext_id)\n\t\t\tice_handle->pc->videoorientation_ext_id = videoorientation_ext_id;\n\t\tif(ice_handle->pc && ice_handle->pc->playoutdelay_ext_id != playoutdelay_ext_id)\n\t\t\tice_handle->pc->playoutdelay_ext_id = playoutdelay_ext_id;\n\t\tif(ice_handle->pc && ice_handle->pc->dependencydesc_ext_id != dependencydesc_ext_id)\n\t\t\tice_handle->pc->dependencydesc_ext_id = dependencydesc_ext_id;\n\t\tif(ice_handle->pc && ice_handle->pc->videolayers_ext_id != videolayers_ext_id)\n\t\t\tice_handle->pc->videolayers_ext_id = videolayers_ext_id;\n\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t} else {\n\t\t/* Check if the answer does contain the mid/rid/repaired-rid/abs-send-time/twcc extmaps */\n\t\tint mindex = 0;\n\t\tgboolean do_mid = FALSE, do_rid = FALSE, do_repaired_rid = FALSE,\n\t\t\tdo_dd = FALSE, do_twcc = FALSE, do_abs_send_time = FALSE,\n\t\t\tdo_abs_capture_time = FALSE, do_video_layers_alloc = FALSE;\n\t\tGList *temp = parsed_sdp->m_lines;\n\t\tjanus_mutex_lock(&ice_handle->mutex);\n\t\twhile(temp) {\n\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\tjanus_ice_peerconnection_medium *medium = ice_handle->pc ?\n\t\t\t\tg_hash_table_lookup(ice_handle->pc->media, GUINT_TO_POINTER(mindex)) : NULL;\n\t\t\tgboolean have_mid = FALSE, have_rid = FALSE, have_repaired_rid = FALSE,\n\t\t\t\thave_twcc = FALSE, have_dd = FALSE, have_abs_send_time = FALSE,\n\t\t\t\thave_abs_capture_time = FALSE, have_video_layers_alloc = FALSE, have_msid = FALSE;\n\t\t\tint opusred_pt = -1;\n\t\t\tGList *tempA = m->attributes;\n\t\t\twhile(tempA) {\n\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\t\tif(a->name && a->value && !strcasecmp(a->name, \"msid\")) {\n\t\t\t\t\t/* Found msid attribute */\n\t\t\t\t\thave_msid = TRUE;\n\t\t\t\t\tchar msid[65], mstid[65];\n\t\t\t\t\tmsid[0] = '\\0';\n\t\t\t\t\tmstid[0] = '\\0';\n\t\t\t\t\tif(sscanf(a->value, \"%64s %64s\", msid, mstid) != 2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid msid on m-line #%d\\n\",\n\t\t\t\t\t\t\tice_handle->handle_id, m->index);\n\t\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t\tif(medium != NULL && (medium->msid == NULL || strcasecmp(medium->msid, msid))) {\n\t\t\t\t\t\tchar *old_msid = medium->msid;\n\t\t\t\t\t\tmedium->msid = g_strdup(msid);\n\t\t\t\t\t\tg_free(old_msid);\n\t\t\t\t\t}\n\t\t\t\t\tif(medium != NULL && (medium->mstid == NULL || strcasecmp(medium->mstid, mstid))) {\n\t\t\t\t\t\tchar *old_mstid = medium->mstid;\n\t\t\t\t\t\tmedium->mstid = g_strdup(mstid);\n\t\t\t\t\t\tg_free(old_mstid);\n\t\t\t\t\t}\n\t\t\t\t\t/* Remove this msid attribute, the core will add it again later */\n\t\t\t\t\tGList *msid_attr = tempA;\n\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\tm->attributes = g_list_remove_link(m->attributes, msid_attr);\n\t\t\t\t\tg_list_free_full(msid_attr, (GDestroyNotify)janus_sdp_attribute_destroy);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(a->name && a->value && !strcasecmp(a->name, \"extmap\")) {\n\t\t\t\t\tif(strstr(a->value, JANUS_RTP_EXTMAP_MID))\n\t\t\t\t\t\thave_mid = TRUE;\n\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_RID))\n\t\t\t\t\t\thave_rid = TRUE;\n\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_REPAIRED_RID))\n\t\t\t\t\t\thave_repaired_rid = TRUE;\n\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC))\n\t\t\t\t\t\thave_twcc = TRUE;\n\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_DEPENDENCY_DESC))\n\t\t\t\t\t\tdo_dd = TRUE;\n\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_ABS_SEND_TIME))\n\t\t\t\t\t\thave_abs_send_time = TRUE;\n\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME))\n\t\t\t\t\t\thave_abs_capture_time = TRUE;\n\t\t\t\t\telse if(strstr(a->value, JANUS_RTP_EXTMAP_VIDEO_LAYERS))\n\t\t\t\t\t\thave_video_layers_alloc = TRUE;\n\t\t\t\t} else if(m->type == JANUS_SDP_AUDIO && medium != NULL && medium->opusred_pt > 0 &&\n\t\t\t\t\t\ta->name && a->value && !strcasecmp(a->name, \"rtpmap\") && strstr(a->value, \"red/48000/2\")) {\n\t\t\t\t\topusred_pt = atoi(a->value);\n\t\t\t\t}\n\t\t\t\ttempA = tempA->next;\n\t\t\t}\n\t\t\t/* If the user offered RED but the plugin rejected it, disable it */\n\t\t\tif(opusred_pt < 0 && medium != NULL && medium->opusred_pt > 0)\n\t\t\t\tmedium->opusred_pt = 0;\n\t\t\tif(!have_msid && medium != NULL) {\n\t\t\t\tg_free(medium->msid);\n\t\t\t\tmedium->msid = NULL;\n\t\t\t\tg_free(medium->mstid);\n\t\t\t\tmedium->mstid = NULL;\n\t\t\t}\n\t\t\t/* Check if rid-based simulcasting is available */\n\t\t\tif(!have_rid && medium != NULL) {\n\t\t\t\tg_free(medium->rid[0]);\n\t\t\t\tmedium->rid[0] = NULL;\n\t\t\t\tg_free(medium->rid[1]);\n\t\t\t\tmedium->rid[1] = NULL;\n\t\t\t\tg_free(medium->rid[2]);\n\t\t\t\tmedium->rid[2] = NULL;\n\t\t\t\tif(medium->ssrc_peer_temp > 0) {\n\t\t\t\t\tmedium->ssrc_peer[0] = medium->ssrc_peer_temp;\n\t\t\t\t\tmedium->ssrc_peer_temp = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tdo_mid = do_mid || have_mid;\n\t\t\tdo_rid = do_rid || have_rid;\n\t\t\tdo_repaired_rid = do_repaired_rid || have_repaired_rid;\n\t\t\tdo_twcc = do_twcc || have_twcc;\n\t\t\tdo_dd = do_dd || have_dd;\n\t\t\tdo_abs_send_time = do_abs_send_time || have_abs_send_time;\n\t\t\tdo_abs_capture_time = do_abs_capture_time || have_abs_capture_time;\n\t\t\tdo_video_layers_alloc = do_video_layers_alloc || have_video_layers_alloc;\n\t\t\tmindex++;\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tif(!do_mid && ice_handle->pc)\n\t\t\tice_handle->pc->mid_ext_id = 0;\n\t\tif(!do_rid && ice_handle->pc) {\n\t\t\tice_handle->pc->rid_ext_id = 0;\n\t\t\tice_handle->pc->ridrtx_ext_id = 0;\n\t\t}\n\t\tif(!do_repaired_rid && ice_handle->pc)\n\t\t\tice_handle->pc->ridrtx_ext_id = 0;\n\t\tif(!do_twcc && ice_handle->pc) {\n\t\t\tice_handle->pc->do_transport_wide_cc = FALSE;\n\t\t\tice_handle->pc->transport_wide_cc_ext_id = 0;\n\t\t}\n\t\tif(!do_dd && ice_handle->pc)\n\t\t\tice_handle->pc->dependencydesc_ext_id = 0;\n\t\tif(!do_abs_send_time && ice_handle->pc)\n\t\t\tice_handle->pc->abs_send_time_ext_id = 0;\n\t\tif(!do_abs_capture_time && ice_handle->pc)\n\t\t\tice_handle->pc->abs_capture_time_ext_id = 0;\n\t\tif(!do_video_layers_alloc && ice_handle->pc)\n\t\t\tice_handle->pc->videolayers_ext_id = 0;\n\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t}\n\tif(!updating && !janus_ice_is_full_trickle_enabled()) {\n\t\t/* Wait for candidates-done callback */\n\t\tint waiting = 0;\n\t\twhile(ice_handle->cdone < 1) {\n\t\t\tif(ice_handle == NULL || janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t\t\t|| janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Handle detached or PC closed, giving up...!\\n\", ice_handle ? ice_handle->handle_id : 0);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\tif(ice_handle->cdone < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error gathering candidates!\\n\", ice_handle->handle_id);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\tif(waiting && (waiting % 5000) == 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Waited 5s for candidates, that's way too much... going on with what we have (WebRTC setup might fail)\\n\", ice_handle->handle_id);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(waiting && (waiting % 1000) == 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] %s for candidates-done callback... (slow gathering, are you using STUN or TURN for Janus too, instead of just for users? Consider enabling full-trickle instead)\\n\",\n\t\t\t\t\tice_handle->handle_id, (waiting == 1000 ? \"Waiting\" : \"Still waiting\"));\n\t\t\t}\n\t\t\twaiting++;\n\t\t\tg_usleep(1000);\n\t\t}\n\t}\n\t/* Anonymize SDP */\n\tif(janus_sdp_anonymize(parsed_sdp) < 0) {\n\t\t/* Invalid SDP */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid SDP\\n\", ice_handle->handle_id);\n\t\tjanus_sdp_destroy(parsed_sdp);\n\t\treturn NULL;\n\t}\n\n\t/* Check if this is a renegotiation and we need an ICE restart */\n\tif(offer && restart)\n\t\tjanus_ice_restart(ice_handle);\n\t/* Add our details */\n\tjanus_mutex_lock(&ice_handle->mutex);\n\tjanus_ice_peerconnection *pc = ice_handle->pc;\n\tif(pc == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No WebRTC PeerConnection\\n\", ice_handle->handle_id);\n\t\tjanus_sdp_destroy(parsed_sdp);\n\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t\treturn NULL;\n\t}\n\t/* Iterate on all media */\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tuint mi=0;\n\t/* Let's build a list of payload types first */\n\tif(pc->payload_types == NULL)\n\t\tpc->payload_types = g_hash_table_new(NULL, NULL);\n\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\tif(medium && medium->type != JANUS_MEDIA_DATA) {\n\t\t\tjanus_sdp_mline *m = janus_sdp_mline_find_by_index(parsed_sdp, medium->mindex);\n\t\t\tif(m && m->ptypes) {\n\t\t\t\tGList *tpt = m->ptypes;\n\t\t\t\twhile(tpt) {\n\t\t\t\t\tg_hash_table_insert(pc->payload_types, tpt->data, tpt->data);\n\t\t\t\t\ttpt = tpt->next;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* Now let's iterate on media */\n\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\tif(medium && medium->type == JANUS_MEDIA_VIDEO &&\n\t\t\t\tjanus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX) &&\n\t\t\t\tmedium->rtx_payload_types == NULL) {\n\t\t\t/* Make sure we have a list of rtx payload types to generate, if needed */\n\t\t\tjanus_sdp_mline *m = janus_sdp_mline_find_by_index(parsed_sdp, medium->mindex);\n\t\t\tif(m && m->ptypes) {\n\t\t\t\tmedium->rtx_payload_types = g_hash_table_new(NULL, NULL);\n\t\t\t\tif(pc->rtx_payload_types == NULL)\n\t\t\t\t\tpc->rtx_payload_types = g_hash_table_new(NULL, NULL);\n\t\t\t\tif(pc->rtx_payload_types_rev == NULL)\n\t\t\t\t\tpc->rtx_payload_types_rev = g_hash_table_new(NULL, NULL);\n\t\t\t\tGList *ptypes = m->ptypes;\n\t\t\t\twhile(ptypes) {\n\t\t\t\t\tint ptype = GPOINTER_TO_INT(ptypes->data);\n\t\t\t\t\tif(g_hash_table_lookup(pc->rtx_payload_types_rev, GINT_TO_POINTER(ptype))) {\n\t\t\t\t\t\t/* This is an RTX for an existing payload type, skip */\n\t\t\t\t\t\tptypes = ptypes->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Let's check if a mapping exists already */\n\t\t\t\t\tint rtx_ptype = GPOINTER_TO_INT(g_hash_table_lookup(pc->rtx_payload_types, GINT_TO_POINTER(ptype)));\n\t\t\t\t\tif(rtx_ptype == 0) {\n\t\t\t\t\t\t/* No mapping yet, find one now */\n\t\t\t\t\t\trtx_ptype = ptype+1;\n\t\t\t\t\t\tif(rtx_ptype > 127)\n\t\t\t\t\t\t\trtx_ptype = 96;\n\t\t\t\t\t\twhile(g_hash_table_lookup(pc->payload_types, GINT_TO_POINTER(rtx_ptype)) ||\n\t\t\t\t\t\t\t\tg_hash_table_lookup(pc->rtx_payload_types_rev, GINT_TO_POINTER(rtx_ptype))) {\n\t\t\t\t\t\t\trtx_ptype++;\n\t\t\t\t\t\t\tif(rtx_ptype > 127)\n\t\t\t\t\t\t\t\trtx_ptype = 96;\n\t\t\t\t\t\t\tif(rtx_ptype == ptype) {\n\t\t\t\t\t\t\t\t/* We did a whole round? should never happen... */\n\t\t\t\t\t\t\t\trtx_ptype = -1;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(rtx_ptype > 0) {\n\t\t\t\t\t\tg_hash_table_insert(pc->payload_types, GINT_TO_POINTER(rtx_ptype), GINT_TO_POINTER(rtx_ptype));\n\t\t\t\t\t\tg_hash_table_insert(pc->rtx_payload_types, GINT_TO_POINTER(ptype), GINT_TO_POINTER(rtx_ptype));\n\t\t\t\t\t\tg_hash_table_insert(pc->rtx_payload_types_rev, GINT_TO_POINTER(rtx_ptype), GINT_TO_POINTER(ptype));\n\t\t\t\t\t\tg_hash_table_insert(medium->rtx_payload_types, GINT_TO_POINTER(ptype), GINT_TO_POINTER(rtx_ptype));\n\t\t\t\t\t}\n\t\t\t\t\tmedium->do_nacks = TRUE;\n\t\t\t\t\tptypes = ptypes->next;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(medium && medium->type == JANUS_MEDIA_VIDEO &&\n\t\t\t\tjanus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX) &&\n\t\t\t\tmedium->rtx_payload_types != NULL) {\n\t\t\t/* Check if there are new payload types that conflict with our rtx additions */\n\t\t\tjanus_sdp_mline *m = janus_sdp_mline_find_by_index(parsed_sdp, medium->mindex);\n\t\t\tif(m && m->ptypes) {\n\t\t\t\tGList *ptypes = g_list_copy(m->ptypes), *tempP = ptypes;\n\t\t\t\tGList *rtx_ptypes = g_hash_table_get_values(medium->rtx_payload_types);\n\t\t\t\twhile(tempP) {\n\t\t\t\t\tint ptype = GPOINTER_TO_INT(tempP->data);\n\t\t\t\t\tif(g_hash_table_lookup(medium->clock_rates, GINT_TO_POINTER(ptype)) &&\n\t\t\t\t\t\t\tg_list_find(rtx_ptypes, GINT_TO_POINTER(ptype))) {\n\t\t\t\t\t\t/* We have a payload type that is both a codec and rtx, get rid of it */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Removing duplicate payload type %d\\n\", ice_handle->handle_id, ptype);\n\t\t\t\t\t\tjanus_sdp_remove_payload_type(parsed_sdp, medium->mindex, ptype);\n\t\t\t\t\t\tg_hash_table_remove(medium->clock_rates, GINT_TO_POINTER(ptype));\n\t\t\t\t\t}\n\t\t\t\t\ttempP = tempP->next;\n\t\t\t\t}\n\t\t\t\tg_list_free(ptypes);\n\t\t\t\tg_list_free(rtx_ptypes);\n\t\t\t}\n\t\t}\n\t}\n\t/* Enrich the SDP the plugin gave us with all the WebRTC related stuff */\n\tchar *sdp_merged = janus_sdp_merge(ice_handle, parsed_sdp, offer ? TRUE : FALSE);\n\tif(sdp_merged == NULL) {\n\t\t/* Couldn't merge SDP */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error merging SDP\\n\", ice_handle->handle_id);\n\t\tjanus_sdp_destroy(parsed_sdp);\n\t\tjanus_mutex_unlock(&ice_handle->mutex);\n\t\treturn NULL;\n\t}\n\tjanus_sdp_destroy(parsed_sdp);\n\n\tif(!updating) {\n\t\tif(offer) {\n\t\t\t/* We set the flag to wait for an answer before handling trickle candidates */\n\t\t\tjanus_flags_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Sending answer, ready to setup remote candidates and send connectivity checks...\\n\", ice_handle->handle_id);\n\t\t\tjanus_request_ice_handle_answer(ice_handle, NULL);\n\t\t}\n\t}\n#ifdef HAVE_SCTP\n\tif(!offer && janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY)) {\n\t\t/* Renegotiation: check if datachannels were just added on an existing PeerConnection */\n\t\tif(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)) {\n\t\t\tjanus_ice_peerconnection *pc = ice_handle->pc;\n\t\t\tif(pc != NULL && pc->dtls != NULL && pc->dtls->sctp == NULL) {\n\t\t\t\t/* Create SCTP association as well */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Creating datachannels...\\n\", ice_handle->handle_id);\n\t\t\t\tjanus_dtls_srtp_create_sctp(pc->dtls);\n\t\t\t}\n\t\t}\n\t}\n#endif\n\n\t/* Prepare JSON event */\n\tjson_t *jsep = json_object();\n\tjson_object_set_new(jsep, \"type\", json_string(sdp_type));\n\tjson_object_set_new(jsep, \"sdp\", json_string(sdp_merged));\n\tchar *tmp = ice_handle->local_sdp;\n\tice_handle->local_sdp = sdp_merged;\n\tjanus_mutex_unlock(&ice_handle->mutex);\n\tg_free(tmp);\n\treturn jsep;\n}\n\nvoid janus_plugin_relay_rtp(janus_plugin_session *plugin_session, janus_plugin_rtp *packet) {\n\tif((plugin_session < (janus_plugin_session *)0x1000) || g_atomic_int_get(&plugin_session->stopped) ||\n\t\t\tpacket == NULL || packet->buffer == NULL || packet->length < 1)\n\t\treturn;\n\tjanus_ice_handle *handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!handle || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t|| janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn;\n\tjanus_ice_relay_rtp(handle, packet);\n}\n\nvoid janus_plugin_relay_rtcp(janus_plugin_session *plugin_session, janus_plugin_rtcp *packet) {\n\tif((plugin_session < (janus_plugin_session *)0x1000) || g_atomic_int_get(&plugin_session->stopped) ||\n\t\t\tpacket == NULL || packet->buffer == NULL || packet->length < 1)\n\t\treturn;\n\tjanus_ice_handle *handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!handle || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t|| janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn;\n\tjanus_ice_relay_rtcp(handle, packet);\n}\n\nvoid janus_plugin_relay_data(janus_plugin_session *plugin_session, janus_plugin_data *packet) {\n\tif((plugin_session < (janus_plugin_session *)0x1000) || g_atomic_int_get(&plugin_session->stopped) ||\n\t\t\tpacket == NULL || packet->buffer == NULL || packet->length < 1)\n\t\treturn;\n\tjanus_ice_handle *handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!handle || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t|| janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn;\n#ifdef HAVE_SCTP\n\tjanus_ice_relay_data(handle, packet);\n#else\n\tJANUS_LOG(LOG_WARN, \"Asked to relay data, but Data Channels support has not been compiled...\\n\");\n#endif\n}\n\nvoid janus_plugin_send_pli(janus_plugin_session *plugin_session) {\n\tif((plugin_session < (janus_plugin_session *)0x1000) || g_atomic_int_get(&plugin_session->stopped))\n\t\treturn;\n\tjanus_ice_handle *handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!handle || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t|| janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn;\n\tjanus_ice_send_pli(handle);\n}\n\nvoid janus_plugin_send_pli_stream(janus_plugin_session *plugin_session, int mindex) {\n\tif((plugin_session < (janus_plugin_session *)0x1000) || g_atomic_int_get(&plugin_session->stopped))\n\t\treturn;\n\tjanus_ice_handle *handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!handle || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t|| janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn;\n\tjanus_ice_send_pli_stream(handle, mindex);\n}\n\nvoid janus_plugin_send_remb(janus_plugin_session *plugin_session, uint32_t bitrate) {\n\tif((plugin_session < (janus_plugin_session *)0x1000) || g_atomic_int_get(&plugin_session->stopped))\n\t\treturn;\n\tjanus_ice_handle *handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!handle || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t|| janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT))\n\t\treturn;\n\tjanus_ice_send_remb(handle, bitrate);\n}\n\nstatic gboolean janus_plugin_close_pc_internal(gpointer user_data) {\n\t/* We actually enforce the close_pc here */\n\tjanus_plugin_session *plugin_session = (janus_plugin_session *) user_data;\n\tjanus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!ice_handle || !g_atomic_int_compare_and_exchange(&ice_handle->closepc, 1, 0)) {\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\treturn G_SOURCE_REMOVE;\n\t}\n\tjanus_refcount_increase(&ice_handle->ref);\n\tif(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)\n\t\t\t|| janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)) {\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\tjanus_refcount_decrease(&ice_handle->ref);\n\t\treturn G_SOURCE_REMOVE;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Plugin asked to hangup PeerConnection: sending alert\\n\", ice_handle->handle_id);\n\t/* Send an alert on all the DTLS connections */\n\tjanus_ice_webrtc_hangup(ice_handle, \"Close PC\");\n\tjanus_refcount_decrease(&plugin_session->ref);\n\tjanus_refcount_decrease(&ice_handle->ref);\n\n\treturn G_SOURCE_REMOVE;\n}\n\nvoid janus_plugin_close_pc(janus_plugin_session *plugin_session) {\n\t/* A plugin asked to get rid of a PeerConnection: enqueue it as a timed source */\n\tif(!janus_plugin_session_is_alive(plugin_session))\n\t\treturn;\n\tjanus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!ice_handle || !g_atomic_int_compare_and_exchange(&ice_handle->closepc, 0, 1))\n\t\treturn;\n\tjanus_refcount_increase(&plugin_session->ref);\n\tGSource *timeout_source = g_timeout_source_new_seconds(0);\n\tg_source_set_callback(timeout_source, janus_plugin_close_pc_internal, plugin_session, NULL);\n\tg_source_attach(timeout_source, sessions_watchdog_context);\n\tg_source_unref(timeout_source);\n}\n\nstatic gboolean janus_plugin_end_session_internal(gpointer user_data) {\n\t/* We actually enforce the end_session here */\n\tjanus_plugin_session *plugin_session = (janus_plugin_session *) user_data;\n\tjanus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\tif(!ice_handle) {\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\treturn G_SOURCE_REMOVE;\n\t}\n\tjanus_refcount_increase(&ice_handle->ref);\n\tif(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)) {\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\tjanus_refcount_decrease(&ice_handle->ref);\n\t\treturn G_SOURCE_REMOVE;\n\t}\n\tjanus_session *session = (janus_session *)ice_handle->session;\n\tif(!session) {\n\t\tjanus_refcount_decrease(&plugin_session->ref);\n\t\tjanus_refcount_decrease(&ice_handle->ref);\n\t\treturn G_SOURCE_REMOVE;\n\t}\n\t/* Destroy the handle */\n\tjanus_session_handles_remove(session, ice_handle);\n\n\tjanus_refcount_decrease(&plugin_session->ref);\n\tjanus_refcount_decrease(&ice_handle->ref);\n\treturn G_SOURCE_REMOVE;\n}\n\nvoid janus_plugin_end_session(janus_plugin_session *plugin_session) {\n\t/* A plugin asked to get rid of a handle: enqueue it as a timed source */\n\tif(!janus_plugin_session_is_alive(plugin_session))\n\t\treturn;\n\tjanus_refcount_increase(&plugin_session->ref);\n\tGSource *timeout_source = g_timeout_source_new_seconds(0);\n\tg_source_set_callback(timeout_source, janus_plugin_end_session_internal, plugin_session, NULL);\n\tg_source_attach(timeout_source, sessions_watchdog_context);\n\tg_source_unref(timeout_source);\n}\n\nvoid janus_plugin_notify_event(janus_plugin *plugin, janus_plugin_session *plugin_session, json_t *event) {\n\t/* A plugin asked to notify an event to the handlers */\n\tif(!plugin || !event || !json_is_object(event))\n\t\treturn;\n\tguint64 session_id = 0, handle_id = 0;\n\tchar *opaque_id = NULL;\n\tif(plugin_session != NULL) {\n\t\tif(!janus_plugin_session_is_alive(plugin_session)) {\n\t\t\tjson_decref(event);\n\t\t\treturn;\n\t\t}\n\t\tjanus_ice_handle *ice_handle = (janus_ice_handle *)plugin_session->gateway_handle;\n\t\tif(!ice_handle) {\n\t\t\tjson_decref(event);\n\t\t\treturn;\n\t\t}\n\t\thandle_id = ice_handle->handle_id;\n\t\topaque_id = ice_handle->opaque_id;\n\t\tjanus_session *session = (janus_session *)ice_handle->session;\n\t\tif(!session) {\n\t\t\tjson_decref(event);\n\t\t\treturn;\n\t\t}\n\t\tsession_id = session->session_id;\n\t}\n\t/* Notify event handlers */\n\tif(janus_events_is_enabled()) {\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_PLUGIN, JANUS_EVENT_SUBTYPE_NONE,\n\t\t\tsession_id, handle_id, opaque_id, plugin->get_package(), event);\n\t} else {\n\t\tjson_decref(event);\n\t}\n}\n\ngboolean janus_plugin_auth_is_signed(void) {\n\treturn janus_auth_is_signed_mode();\n}\n\ngboolean janus_plugin_auth_is_signature_valid(janus_plugin *plugin, const char *token) {\n\treturn janus_auth_check_signature(token, plugin->get_package());\n}\n\ngboolean janus_plugin_auth_signature_contains(janus_plugin *plugin, const char *token, const char *descriptor) {\n\treturn janus_auth_check_signature_contains(token, plugin->get_package(), descriptor);\n}\n\n\n/* Main */\ngint main(int argc, char *argv[]) {\n\t/* Core dumps may be disallowed by parent of this process; change that */\n\tstruct rlimit core_limits;\n\tcore_limits.rlim_cur = core_limits.rlim_max = RLIM_INFINITY;\n\tsetrlimit(RLIMIT_CORE, &core_limits);\n\n\tjanus_mark_started();\n\n\t/* Handle SIGINT (CTRL-C), SIGTERM (from service managers) */\n\tsignal(SIGINT, janus_handle_signal);\n\tsignal(SIGTERM, janus_handle_signal);\n\tatexit(janus_termination_handler);\n\n\tJANUS_PRINT(\"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\tJANUS_PRINT(\"Janus commit: %s\\n\", janus_build_git_sha);\n\tJANUS_PRINT(\"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\n\t/* Initialize some command line options defaults */\n\toptions.debug_level = -1;\n\toptions.session_timeout = -1;\n\toptions.reclaim_session_timeout = -1;\n\toptions.min_nack_queue = -1;\n\toptions.no_media_timer = -1;\n\toptions.slowlink_threshold = -1;\n\toptions.twcc_period = -1;\n\t/* Let's call our cmdline parser */\n\tif(!janus_options_parse(&options, argc, argv))\n\t\texit(1);\n\n\tif(options.print_version) {\n\t\tjanus_options_destroy();\n\t\texit(0);\n\t}\n\n\t/* Any configuration to open? */\n\tif(options.config_file) {\n\t\tconfig_file = g_strdup(options.config_file);\n\t}\n\tif(options.configs_folder) {\n\t\tconfigs_folder = g_strdup(options.configs_folder);\n\t} else {\n\t\tconfigs_folder = g_strdup(CONFDIR);\n\t}\n\tif(config_file == NULL) {\n\t\tchar file[255];\n\t\tg_snprintf(file, 255, \"%s/janus.jcfg\", configs_folder);\n\t\tconfig_file = g_strdup(file);\n\t}\n\tif((config = janus_config_parse(config_file)) == NULL) {\n\t\t/* We failed to load the libconfig configuration file, let's try the INI */\n\t\tJANUS_PRINT(\"Failed to load %s, trying the INI instead...\\n\", config_file);\n\t\tg_free(config_file);\n\t\tchar file[255];\n\t\tg_snprintf(file, 255, \"%s/janus.cfg\", configs_folder);\n\t\tconfig_file = g_strdup(file);\n\t\tif((config = janus_config_parse(config_file)) == NULL) {\n\t\t\tif(options.config_file) {\n\t\t\t\t/* We only give up if the configuration file was explicitly provided */\n\t\t\t\tJANUS_PRINT(\"Error reading configuration from %s\\n\", config_file);\n\t\t\t\tjanus_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t\tJANUS_PRINT(\"Error reading/parsing the configuration file in %s, going on with the defaults and the command line arguments\\n\",\n\t\t\t\tconfigs_folder);\n\t\t\tconfig = janus_config_create(\"janus.cfg\");\n\t\t\tif(config == NULL) {\n\t\t\t\t/* If we can't even create an empty configuration, something's definitely wrong */\n\t\t\t\tjanus_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t}\n\t}\n\t/* Pre-fetch some categories (creates them if they don't exist) */\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\tjanus_config_category *config_certs = janus_config_get_create(config, NULL, janus_config_type_category, \"certificates\");\n\tjanus_config_category *config_nat = janus_config_get_create(config, NULL, janus_config_type_category, \"nat\");\n\tjanus_config_category *config_media = janus_config_get_create(config, NULL, janus_config_type_category, \"media\");\n\tjanus_config_category *config_transports = janus_config_get_create(config, NULL, janus_config_type_category, \"transports\");\n\tjanus_config_category *config_plugins = janus_config_get_create(config, NULL, janus_config_type_category, \"plugins\");\n\tjanus_config_category *config_events = janus_config_get_create(config, NULL, janus_config_type_category, \"events\");\n\tjanus_config_category *config_loggers = janus_config_get_create(config, NULL, janus_config_type_category, \"loggers\");\n\n\t/* Any log prefix? */\n\tjanus_config_array *lp = janus_config_get(config, config_general, janus_config_type_item, \"log_prefix\");\n\tif(lp && lp->value)\n\t\tjanus_log_global_prefix = g_strdup(lp->value);\n\n\t/* Check if there are folders to protect */\n\tjanus_config_array *pfs = janus_config_get(config, config_general, janus_config_type_array, \"protected_folders\");\n\tif(pfs && pfs->list) {\n\t\tGList *item = pfs->list;\n\t\twhile(item) {\n\t\t\tjanus_config_item *pf = (janus_config_item *)item->data;\n\t\t\tif(pf && pf->type == janus_config_type_item && pf->name == NULL && pf->value != NULL)\n\t\t\t\tjanus_protected_folder_add(pf->value);\n\t\t\titem = item->next;\n\t\t}\n\t}\n\n\t/* Check if we need to log to console and/or file */\n\tgboolean use_stdout = TRUE;\n\tif(options.disable_stdout) {\n\t\tuse_stdout = FALSE;\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"log_to_stdout\", \"no\"));\n\t} else if(!options.log_stdout) {\n\t\t/* Check if the configuration file is saying anything about this */\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"log_to_stdout\");\n\t\tif(item && item->value && !janus_is_true(item->value))\n\t\t\tuse_stdout = FALSE;\n\t}\n\tconst char *logfile = NULL;\n\tif(options.log_file) {\n\t\tlogfile = options.log_file;\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"log_to_file\", \"no\"));\n\t} else {\n\t\t/* Check if the configuration file is saying anything about this */\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"log_to_file\");\n\t\tif(item && item->value)\n\t\t\tlogfile = item->value;\n\t}\n\n\t/* Set an optional signal handler for log rotation */\n\tconst char *log_rotate_sig = NULL;\n\tif(options.log_rotate_sig) {\n\t\tlog_rotate_sig = options.log_rotate_sig;\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"log_rotate_sig\", log_rotate_sig));\n\t} else {\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"log_rotate_sig\");\n\t\tif(item && item->value)\n\t\t\tlog_rotate_sig = item->value;\n\t}\n\tif(log_rotate_sig != NULL) {\n\t\tif(!strcasecmp(log_rotate_sig, \"SIGUSR1\")) {\n\t\t\tjanus_log_rotate_sig = SIGUSR1;\n\t\t} else if(!strcasecmp(log_rotate_sig, \"SIGHUP\")) {\n\t\t\tjanus_log_rotate_sig = SIGHUP;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported signal for log rotation: %s\\n\", log_rotate_sig);\n\t\t}\n\t\tif(janus_log_rotate_sig > 0) {\n\t\t\tJANUS_LOG(LOG_INFO, \"Setting signal for log rotation: %s (%d)\\n\", log_rotate_sig, janus_log_rotate_sig);\n\t\t\tsignal(janus_log_rotate_sig, janus_handle_signal);\n\t\t}\n\t}\n\n\t/* Check if we're going to daemonize Janus */\n\tif(options.daemon) {\n\t\tdaemonize = TRUE;\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"daemonize\", \"yes\"));\n\t} else {\n\t\t/* Check if the configuration file is saying anything about this */\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"daemonize\");\n\t\tif(item && item->value && janus_is_true(item->value))\n\t\t\tdaemonize = TRUE;\n\t}\n\t/* If we're going to daemonize, make sure logging to stdout is disabled and a log file has been specified */\n\tif(daemonize && use_stdout && !options.log_stdout) {\n\t\tuse_stdout = FALSE;\n\t}\n\t/* Daemonize now, if we need to */\n\tif(daemonize) {\n\t\tJANUS_PRINT(\"Running Janus as a daemon\\n\");\n\n\t\t/* Create a pipe for parent<->child communication during the startup phase */\n\t\tif(pipe(pipefd) == -1) {\n\t\t\tJANUS_PRINT(\"pipe error!\\n\");\n\t\t\texit(1);\n\t\t}\n\n\t\t/* Fork off the parent process */\n\t\tpid_t pid = fork();\n\t\tif(pid < 0) {\n\t\t\tJANUS_PRINT(\"Fork error!\\n\");\n\t\t\texit(1);\n\t\t}\n\t\tif(pid > 0) {\n\t\t\t/* Ok, we're the parent: let's wait for the child to tell us everything started fine */\n\t\t\tclose(pipefd[1]);\n\t\t\tint code = -1;\n\t\t\tstruct pollfd pollfds;\n\n\t\t\twhile(code < 0) {\n\t\t\t\tpollfds.fd = pipefd[0];\n\t\t\t\tpollfds.events = POLLIN;\n\t\t\t\tint res = poll(&pollfds, 1, -1);\n\t\t\t\tif(res < 0)\n\t\t\t\t\tbreak;\n\t\t\t\tif(res == 0)\n\t\t\t\t\tcontinue;\n\t\t\t\tif(pollfds.revents & POLLERR || pollfds.revents & POLLHUP)\n\t\t\t\t\tbreak;\n\t\t\t\tif(pollfds.revents & POLLIN) {\n\t\t\t\t\tres = read(pipefd[0], &code, sizeof(int));\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(code < 0)\n\t\t\t\tcode = 1;\n\n\t\t\t/* Leave the parent and return the exit code we received from the child */\n\t\t\tif(code)\n\t\t\t\tJANUS_PRINT(\"Error launching Janus (error code %d), check the logs for more details\\n\", code);\n\t\t\texit(code);\n\t\t}\n\t\t/* Child here */\n\t\tclose(pipefd[0]);\n\n\t\t/* Change the file mode mask */\n\t\tumask(0);\n\n\t\t/* Create a new SID for the child process */\n\t\tpid_t sid = setsid();\n\t\tif(sid < 0) {\n\t\t\tJANUS_PRINT(\"Error setting SID!\\n\");\n\t\t\texit(1);\n\t\t}\n\t\t/* Change the current working directory */\n\t\tconst char *cwd = options.cwd_path ? options.cwd_path : \"/\";\n\t\tif((chdir(cwd)) < 0) {\n\t\t\tJANUS_PRINT(\"Error changing the current working directory!\\n\");\n\t\t\texit(1);\n\t\t}\n\t\t/* We close stdin/stdout/stderr when initializing the logger */\n\t}\n\n\t/* Was a custom instance name provided? */\n\tif(options.server_name)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"server_name\", options.server_name));\n\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"server_name\");\n\tif(item && item->value)\n\t\tserver_name = g_strdup(item->value);\n\n\t/* Check if we should exit immediately on dlopen or dlsym errors */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"exit_on_dl_error\");\n\tif(item && item->value && janus_is_true(item->value))\n\t\texit_on_dl_error = TRUE;\n\n\t/* Check if there are external loggers we need to load */\n\tconst char *path = NULL;\n\tDIR *dir = NULL;\n\t/* External loggers are usually disabled by default: they need to be enabled in the configuration */\n\tgchar **disabled_loggers = NULL;\n\tpath = LOGGERDIR;\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"loggers_folder\");\n\tif(item && item->value)\n\t\tpath = (char *)item->value;\n\tJANUS_LOG(LOG_INFO, \"Logger plugins folder: %s\\n\", path);\n\tdir = opendir(path);\n\tif(!dir) {\n\t\t/* Not really fatal, we don't care and go on anyway: loggers are not fundamental */\n\t\tJANUS_LOG(LOG_WARN, \"\\tCouldn't access logger plugins folder...\\n\");\n\t} else {\n\t\t/* Any loggers to ignore? */\n\t\titem = janus_config_get(config, config_loggers, janus_config_type_item, \"disable\");\n\t\tif(item && item->value)\n\t\t\tdisabled_loggers = g_strsplit(item->value, \",\", -1);\n\t\t/* Open the shared objects */\n\t\tstruct dirent *eventent = NULL;\n\t\tchar eventpath[1024];\n\t\twhile((eventent = readdir(dir))) {\n\t\t\tint len = strlen(eventent->d_name);\n\t\t\tif (len < 4) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (strcasecmp(eventent->d_name+len-strlen(SHLIB_EXT), SHLIB_EXT)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* Check if this logger has been disabled in the configuration file */\n\t\t\tif(disabled_loggers != NULL) {\n\t\t\t\tgchar *index = disabled_loggers[0];\n\t\t\t\tif(index != NULL) {\n\t\t\t\t\tint i=0;\n\t\t\t\t\tgboolean skip = FALSE;\n\t\t\t\t\twhile(index != NULL) {\n\t\t\t\t\t\twhile(isspace(*index))\n\t\t\t\t\t\t\tindex++;\n\t\t\t\t\t\tif(strlen(index) && !strcmp(index, eventent->d_name)) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Logger plugin '%s' has been disabled, skipping...\\n\", eventent->d_name);\n\t\t\t\t\t\t\tskip = TRUE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tindex = disabled_loggers[i];\n\t\t\t\t\t}\n\t\t\t\t\tif(skip)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_INFO, \"Loading logger plugin '%s'...\\n\", eventent->d_name);\n\t\t\tmemset(eventpath, 0, 1024);\n\t\t\tg_snprintf(eventpath, 1024, \"%s/%s\", path, eventent->d_name);\n\t\t\tvoid *event = dlopen(eventpath, RTLD_NOW | RTLD_GLOBAL);\n\t\t\tif(!event) {\n\t\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load logger plugin '%s': %s\\n\", eventent->d_name, dlerror());\n\t\t\t\tif(exit_on_dl_error)\n\t\t\t\t\texit(1);\n\t\t\t} else {\n\t\t\t\tcreate_l *create = (create_l*) dlsym(event, \"create\");\n\t\t\t\tconst char *dlsym_error = dlerror();\n\t\t\t\tif(dlsym_error) {\n\t\t\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load symbol 'create': %s\\n\", dlsym_error);\n\t\t\t\t\tif(exit_on_dl_error)\n\t\t\t\t\t\texit(1);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjanus_logger *janus_logger = create();\n\t\t\t\tif(!janus_logger) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tCouldn't use function 'create'...\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Are all the mandatory methods and callbacks implemented? */\n\t\t\t\tif(!janus_logger->init || !janus_logger->destroy ||\n\t\t\t\t\t\t!janus_logger->get_api_compatibility ||\n\t\t\t\t\t\t!janus_logger->get_version ||\n\t\t\t\t\t\t!janus_logger->get_version_string ||\n\t\t\t\t\t\t!janus_logger->get_description ||\n\t\t\t\t\t\t!janus_logger->get_package ||\n\t\t\t\t\t\t!janus_logger->get_name ||\n\t\t\t\t\t\t!janus_logger->incoming_logline) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tMissing some mandatory methods/callbacks, skipping this logger plugin...\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(janus_logger->get_api_compatibility() < JANUS_LOGGER_API_VERSION) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"The '%s' logger plugin was compiled against an older version of the API (%d < %d), skipping it: update it to enable it again\\n\",\n\t\t\t\t\t\tjanus_logger->get_package(), janus_logger->get_api_compatibility(), JANUS_LOGGER_API_VERSION);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjanus_logger->init(server_name ? server_name : JANUS_SERVER_NAME, configs_folder);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\tVersion: %d (%s)\\n\", janus_logger->get_version(), janus_logger->get_version_string());\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t   [%s] %s\\n\", janus_logger->get_package(), janus_logger->get_name());\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t   %s\\n\", janus_logger->get_description());\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t   Plugin API version: %d\\n\", janus_logger->get_api_compatibility());\n\t\t\t\tif(loggers == NULL)\n\t\t\t\t\tloggers = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\t\tg_hash_table_insert(loggers, (gpointer)janus_logger->get_package(), janus_logger);\n\t\t\t\tif(loggers_so == NULL)\n\t\t\t\t\tloggers_so = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\t\tg_hash_table_insert(loggers_so, (gpointer)janus_logger->get_package(), event);\n\t\t\t}\n\t\t}\n\t\tclosedir(dir);\n\t}\n\tif(disabled_loggers != NULL)\n\t\tg_strfreev(disabled_loggers);\n\tdisabled_loggers = NULL;\n\n\t/* Initialize logger */\n\tif(janus_log_init(daemonize, use_stdout, logfile, loggers) < 0)\n\t\texit(1);\n\n\tJANUS_PRINT(\"---------------------------------------------------\\n\");\n\tJANUS_PRINT(\"  Starting Meetecho Janus (WebRTC Server) v%s\\n\", janus_version_string);\n\tJANUS_PRINT(\"---------------------------------------------------\\n\\n\");\n\n\t/* Setup Glib */\n#if !GLIB_CHECK_VERSION(2, 36, 0)\n\tg_type_init();\n#endif\n\n\t/* Logging level: default is info and no timestamps */\n\tjanus_log_level = LOG_INFO;\n\tjanus_log_timestamps = FALSE;\n\tjanus_log_colors = TRUE;\n\tif(options.debug_level > -1) {\n\t\tif(options.debug_level < LOG_NONE)\n\t\t\toptions.debug_level = 0;\n\t\telse if(options.debug_level > LOG_MAX)\n\t\t\toptions.debug_level = LOG_MAX;\n\t\tjanus_log_level = options.debug_level;\n\t}\n\n\t/* Any PID we need to create? */\n\tconst char *pidfile = NULL;\n\tif(options.pid_file) {\n\t\tpidfile = options.pid_file;\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"pid_file\", pidfile));\n\t} else {\n\t\t/* Check if the configuration file is saying anything about this */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"pid_file\");\n\t\tif(item && item->value)\n\t\t\tpidfile = item->value;\n\t}\n\tif(janus_pidfile_create(pidfile) < 0)\n\t\texit(1);\n\n\t/* Proceed with the rest of the configuration */\n\tjanus_config_print(config);\n\tif(options.debug_level > -1) {\n\t\tchar debug[5];\n\t\tg_snprintf(debug, 5, \"%d\", options.debug_level);\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"debug_level\", debug));\n\t} else {\n\t\t/* No command line directive on logging, try the configuration file */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"debug_level\");\n\t\tif(item && item->value) {\n\t\t\tint temp_level = atoi(item->value);\n\t\t\tif(temp_level == 0 && strcmp(item->value, \"0\")) {\n\t\t\t\tJANUS_PRINT(\"Invalid debug level %s (configuration), using default (info=4)\\n\", item->value);\n\t\t\t} else {\n\t\t\t\tjanus_log_level = temp_level;\n\t\t\t\tif(janus_log_level < LOG_NONE)\n\t\t\t\t\tjanus_log_level = 0;\n\t\t\t\telse if(janus_log_level > LOG_MAX)\n\t\t\t\t\tjanus_log_level = LOG_MAX;\n\t\t\t}\n\t\t}\n\t}\n\t/* Any command line argument that should overwrite the configuration? */\n\tJANUS_PRINT(\"Checking command line arguments...\\n\");\n\tif(options.debug_timestamps)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"debug_timestamps\", \"yes\"));\n\tif(options.disable_colors)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"debug_colors\", \"no\"));\n\tif(options.debug_locks)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"debug_locks\", \"yes\"));\n\tif(options.session_timeout > -1) {\n\t\tchar st[20];\n\t\tg_snprintf(st, 20, \"%d\", options.session_timeout);\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"session_timeout\", st));\n\t}\n\tif(options.reclaim_session_timeout > -1) {\n\t\tchar st[20];\n\t\tg_snprintf(st, 20, \"%d\", options.reclaim_session_timeout);\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"reclaim_session_timeout\", st));\n\t}\n \tif(options.interface)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"interface\", options.interface));\n\tif(options.configs_folder)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"configs_folder\", options.configs_folder));\n\tif(options.plugins_folder)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"plugins_folder\", options.plugins_folder));\n\tif(options.apisecret)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"api_secret\", options.apisecret));\n\tif(options.token_auth)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"token_auth\", \"true\"));\n\tif(options.token_auth_secret)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"token_auth_secret\", options.token_auth_secret));\n\tif(options.no_webrtc_encryption)\n\t\tjanus_config_add(config, config_general, janus_config_item_create(\"no_webrtc_encryption\", \"true\"));\n\tif(options.cert_pem)\n\t\tjanus_config_add(config, config_certs, janus_config_item_create(\"cert_pem\", options.cert_pem));\n\tif(options.cert_key)\n\t\tjanus_config_add(config, config_certs, janus_config_item_create(\"cert_key\", options.cert_key));\n\tif(options.cert_pwd)\n\t\tjanus_config_add(config, config_certs, janus_config_item_create(\"cert_pwd\", options.cert_pwd));\n\tif(options.stun_server) {\n\t\t/* Split in server and port (if port missing, use 3478 as default) */\n\t\tchar *stunport = strrchr(options.stun_server, ':');\n\t\tif(stunport != NULL) {\n\t\t\t*stunport = '\\0';\n\t\t\tstunport++;\n\t\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"stun_server\", options.stun_server));\n\t\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"stun_port\", stunport));\n\t\t} else {\n\t\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"stun_server\", options.stun_server));\n\t\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"stun_port\", \"3478\"));\n\t\t}\n\t}\n\tif(options.nat_1_1)\n\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"nat_1_1_mapping\", options.nat_1_1));\n\tif(options.keep_private_host)\n\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"keep_private_host\", \"true\"));\n\tif(options.ice_enforce_list)\n\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"ice_enforce_list\", options.ice_enforce_list));\n\tif(options.ice_ignore_list)\n\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"ice_ignore_list\", options.ice_ignore_list));\n\tif(options.full_trickle)\n\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"full_trickle\", \"true\"));\n\tif(options.ice_lite)\n\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"ice_lite\", \"true\"));\n\tif(options.ice_tcp)\n\t\tjanus_config_add(config, config_nat, janus_config_item_create(\"ice_tcp\", \"true\"));\n\tif(options.ipv6_candidates)\n\t\tjanus_config_add(config, config_media, janus_config_item_create(\"ipv6\", \"true\"));\n\tif(options.ipv6_link_local)\n\t\tjanus_config_add(config, config_media, janus_config_item_create(\"ipv6_linklocal\", \"true\"));\n\tif(options.min_nack_queue > -1) {\n\t\tchar mnq[20];\n\t\tg_snprintf(mnq, 20, \"%d\", options.min_nack_queue);\n\t\tjanus_config_add(config, config_media, janus_config_item_create(\"min_nack_queue\", mnq));\n\t}\n\tif(options.no_media_timer > -1) {\n\t\tchar nmt[20];\n\t\tg_snprintf(nmt, 20, \"%d\", options.no_media_timer);\n\t\tjanus_config_add(config, config_media, janus_config_item_create(\"no_media_timer\", nmt));\n\t}\n\tif(options.slowlink_threshold > -1) {\n\t\tchar st[20];\n\t\tg_snprintf(st, 20, \"%d\", options.slowlink_threshold);\n\t\tjanus_config_add(config, config_media, janus_config_item_create(\"slowlink_threshold\", st));\n\t}\n\tif(options.twcc_period > -1) {\n\t\tchar tp[20];\n\t\tg_snprintf(tp, 20, \"%d\", options.twcc_period);\n\t\tjanus_config_add(config, config_media, janus_config_item_create(\"twcc_period\", tp));\n\t}\n\tif(options.rtp_port_range) {\n\t\tjanus_config_add(config, config_media, janus_config_item_create(\"rtp_port_range\", options.rtp_port_range));\n\t}\n\tif(options.event_handlers)\n\t\tjanus_config_add(config, config_events, janus_config_item_create(\"broadcast\", \"yes\"));\n\tjanus_config_print(config);\n\n\t/* Logging/debugging */\n\tJANUS_PRINT(\"Debug/log level is %d\\n\", janus_log_level);\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"debug_timestamps\");\n\tif(item && item->value)\n\t\tjanus_log_timestamps = janus_is_true(item->value);\n\tJANUS_PRINT(\"Debug/log timestamps are %s\\n\", janus_log_timestamps ? \"enabled\" : \"disabled\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"debug_colors\");\n\tif(item && item->value)\n\t\tjanus_log_colors = janus_is_true(item->value);\n\tJANUS_PRINT(\"Debug/log colors are %s\\n\", janus_log_colors ? \"enabled\" : \"disabled\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"debug_locks\");\n\tif(item && item->value)\n\t\tlock_debug = janus_is_true(item->value);\n\tif(lock_debug) {\n\t\tJANUS_PRINT(\"Lock/mutex debugging is enabled\\n\");\n\t}\n\n\t/* First of all, let's check if we're disabling WebRTC encryption for debugging purposes */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"no_webrtc_encryption\");\n\tif(item && item->value && janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_WARN, \"Disabling WebRTC encryption: *THIS IS ONLY ACCEPTABLE WHEN DEBUGGING!*\\n\");\n\t\twebrtc_encryption = FALSE;\n\t}\n\n\t/* Any IP/interface to enforce/ignore? */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ice_enforce_list\");\n\tif(item && item->value) {\n\t\tgchar **list = g_strsplit(item->value, \",\", -1);\n\t\tgchar *index = list[0];\n\t\tif(index != NULL) {\n\t\t\tint i=0;\n\t\t\twhile(index != NULL) {\n\t\t\t\tif(strlen(index) > 0) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Adding '%s' to the ICE enforce list...\\n\", index);\n\t\t\t\t\tjanus_ice_enforce_interface(g_strdup(index));\n\t\t\t\t}\n\t\t\t\ti++;\n\t\t\t\tindex = list[i];\n\t\t\t}\n\t\t}\n\t\tg_clear_pointer(&list, g_strfreev);\n\t}\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ice_ignore_list\");\n\tif(item && item->value) {\n\t\tgchar **list = g_strsplit(item->value, \",\", -1);\n\t\tgchar *index = list[0];\n\t\tif(index != NULL) {\n\t\t\tint i=0;\n\t\t\twhile(index != NULL) {\n\t\t\t\tif(strlen(index) > 0) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Adding '%s' to the ICE ignore list...\\n\", index);\n\t\t\t\t\tjanus_ice_ignore_interface(g_strdup(index));\n\t\t\t\t}\n\t\t\t\ti++;\n\t\t\t\tindex = list[i];\n\t\t\t}\n\t\t}\n\t\tg_clear_pointer(&list, g_strfreev);\n\t}\n\t/* What is the local IP? */\n\tJANUS_LOG(LOG_VERB, \"Selecting local IP address...\\n\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"interface\");\n\tif(item && item->value) {\n\t\tJANUS_LOG(LOG_VERB, \"  -- Will try to use %s\\n\", item->value);\n\t\t/* Verify that the address is valid */\n\t\tstruct ifaddrs *ifas = NULL;\n\t\tjanus_network_address iface;\n\t\tjanus_network_address_string_buffer ibuf;\n\t\tif(getifaddrs(&ifas) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unable to acquire list of network devices/interfaces; some configurations may not work as expected... %d (%s)\\n\",\n\t\t\t\terrno, g_strerror(errno));\n\t\t} else {\n\t\t\tif(janus_network_lookup_interface(ifas, item->value, &iface) != 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting local IP address to %s, falling back to detecting IP address...\\n\", item->value);\n\t\t\t} else {\n\t\t\t\tif(janus_network_address_to_string_buffer(&iface, &ibuf) != 0 || janus_network_address_string_buffer_is_null(&ibuf)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error getting local IP address from %s, falling back to detecting IP address...\\n\", item->value);\n\t\t\t\t} else {\n\t\t\t\t\tlocal_ip = g_strdup(janus_network_address_string_from_buffer(&ibuf));\n\t\t\t\t}\n\t\t\t}\n\t\t\tfreeifaddrs(ifas);\n\t\t}\n\t}\n\tif(local_ip == NULL) {\n\t\tlocal_ip = janus_network_detect_local_ip_as_string(janus_network_query_options_any_ip);\n\t\tif(local_ip == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find any address! using 127.0.0.1 as the local IP... (which is NOT going to work out of your machine)\\n\");\n\t\t\tlocal_ip = g_strdup(\"127.0.0.1\");\n\t\t}\n\t}\n\tJANUS_LOG(LOG_INFO, \"Using %s as local IP...\\n\", local_ip);\n\n\t/* Check if a custom session timeout value was specified */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"session_timeout\");\n\tif(item && item->value) {\n\t\tint st = atoi(item->value);\n\t\tif(st < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring session_timeout value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tif(st == 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Session timeouts have been disabled (note, may result in orphaned sessions)\\n\");\n\t\t\t}\n\t\t\tglobal_session_timeout = st;\n\t\t}\n\t}\n\n\t/* Check if a custom reclaim session timeout value was specified */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"reclaim_session_timeout\");\n\tif(item && item->value) {\n\t\tint rst = atoi(item->value);\n\t\tif(rst < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring reclaim_session_timeout value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tif(rst == 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Reclaim session timeouts have been disabled, will cleanup immediately\\n\");\n\t\t\t}\n\t\t\treclaim_session_timeout = rst;\n\t\t}\n\t}\n\n\t/* Check if a custom candidates timeout value was specified */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"candidates_timeout\");\n\tif(item && item->value) {\n\t\tint ct = atoi(item->value);\n\t\tif(ct <= 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring candidates_timeout value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tcandidates_timeout = ct;\n\t\t}\n\t}\n\n\t/* Is there any API secret to consider? */\n\tapi_secret = NULL;\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"api_secret\");\n\tif(item && item->value) {\n\t\tapi_secret = g_strdup(item->value);\n\t}\n\t/* Is there any API secret to consider? */\n\tadmin_api_secret = NULL;\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"admin_secret\");\n\tif(item && item->value) {\n\t\tadmin_api_secret = g_strdup(item->value);\n\t}\n\t/* Also check if the token based authentication mechanism needs to be enabled */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"token_auth\");\n\tgboolean auth_enabled = item && item->value && janus_is_true(item->value);\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"token_auth_secret\");\n\tconst char *auth_secret = NULL;\n\tif (item && item->value)\n\t\tauth_secret = item->value;\n\tjanus_auth_init(auth_enabled, auth_secret);\n\n\t/* Check if opaque IDs should be sent back in the Janus API too */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"opaqueid_in_api\");\n\tif(item && item->value && janus_is_true(item->value))\n\t\tjanus_enable_opaqueid_in_api();\n\n\t/* Initialize the recorder code */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"recordings_tmp_ext\");\n\tif(item && item->value) {\n\t\tjanus_recorder_init(TRUE, item->value);\n\t} else {\n\t\tjanus_recorder_init(FALSE, NULL);\n\t}\n\n\t/* Check if we should hide dependencies in \"info\" requests */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"hide_dependencies\");\n\tif(item && item->value && janus_is_true(item->value))\n\t\thide_dependencies = TRUE;\n\n\t/* Setup ICE stuff (e.g., checking if the provided STUN server is correct) */\n\tchar *stun_server = NULL, *turn_server = NULL;\n\tuint16_t stun_port = 0, turn_port = 0;\n\tchar *turn_type = NULL, *turn_user = NULL, *turn_pwd = NULL;\n\tchar *turn_rest_api = NULL, *turn_rest_api_key = NULL;\n#ifdef HAVE_TURNRESTAPI\n\tchar *turn_rest_api_method = NULL;\n\tuint turn_rest_api_timeout = 10;\n#endif\n\tuint16_t rtp_min_port = 0, rtp_max_port = 0;\n\tgboolean ice_lite = FALSE, ice_tcp = FALSE, full_trickle = FALSE, ipv6 = FALSE,\n\t\tipv6_linklocal = FALSE, ignore_mdns = FALSE, ignore_unreachable_ice_server = FALSE;\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"ipv6\");\n\tipv6 = (item && item->value) ? janus_is_true(item->value) : FALSE;\n\tif(ipv6) {\n\t\titem = janus_config_get(config, config_media, janus_config_type_item, \"ipv6_linklocal\");\n\t\tipv6_linklocal = (item && item->value) ? janus_is_true(item->value) : FALSE;\n\t}\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"rtp_port_range\");\n\tif(item && item->value) {\n\t\t/* Split in min and max port */\n\t\tchar *maxport = strrchr(item->value, '-');\n\t\tif(maxport != NULL) {\n\t\t\t*maxport = '\\0';\n\t\t\tmaxport++;\n\t\t\tif(janus_string_to_uint16(item->value, &rtp_min_port) < 0)\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP min port value: %s (assuming 0)\\n\", item->value);\n\t\t\tif(janus_string_to_uint16(maxport, &rtp_max_port) < 0)\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP max port value: %s (assuming 0)\\n\", maxport);\n\t\t\tmaxport--;\n\t\t\t*maxport = '-';\n\t\t}\n\t\tif(rtp_min_port > rtp_max_port) {\n\t\t\tuint16_t temp_port = rtp_min_port;\n\t\t\trtp_min_port = rtp_max_port;\n\t\t\trtp_max_port = temp_port;\n\t\t}\n\t\tif(rtp_max_port == 0)\n\t\t\trtp_max_port = 65535;\n\t\tJANUS_LOG(LOG_INFO, \"RTP port range: %u -- %u\\n\", rtp_min_port, rtp_max_port);\n\t}\n\t/* Check if we need to enable the ICE Lite mode */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ice_lite\");\n\tice_lite = (item && item->value) ? janus_is_true(item->value) : FALSE;\n\t/* Check if we need to enable ICE-TCP support (warning: still broken, for debugging only) */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ice_tcp\");\n\tice_tcp = (item && item->value) ? janus_is_true(item->value) : FALSE;\n\t/* Check if we need to do full-trickle instead of half-trickle */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"full_trickle\");\n\tfull_trickle = (item && item->value) ? janus_is_true(item->value) : FALSE;\n\t/* Check if we should exit if a STUN or TURN server is unreachable */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ignore_unreachable_ice_server\");\n\tignore_unreachable_ice_server = (item && item->value) ? janus_is_true(item->value) : FALSE;\n\t/* Any STUN server to use in Janus? */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"stun_server\");\n\tif(item && item->value)\n\t\tstun_server = (char *)item->value;\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"stun_port\");\n\tif(item && item->value && janus_string_to_uint16(item->value, &stun_port) < 0) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid STUN port: %s (disabling STUN)\\n\", item->value);\n\t\tstun_server = NULL;\n\t}\n\t/* Check if we should drop mDNS candidates */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ignore_mdns\");\n\tignore_mdns = (item && item->value) ? janus_is_true(item->value) : FALSE;\n\t/* Any 1:1 NAT mapping to take into account? */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"nat_1_1_mapping\");\n\tif(item && item->value) {\n\t\tJANUS_LOG(LOG_INFO, \"Using nat_1_1_mapping for public IP: %s\\n\", item->value);\n\t\tchar **list = g_strsplit(item->value, \",\", -1);\n\t\tchar *index = list[0];\n\t\tif(index != NULL) {\n\t\t\tint i=0;\n\t\t\twhile(index != NULL) {\n\t\t\t\tif(strlen(index) > 0) {\n\t\t\t\t\tif(!janus_network_string_is_valid_address(janus_network_query_options_any_ip, index)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid nat_1_1_mapping address %s, skipping...\\n\", index);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_add_public_ip(index);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti++;\n\t\t\t\tindex = list[i];\n\t\t\t}\n\t\t}\n\t\tg_strfreev(list);\n\t\tif(janus_get_public_ip_count() > 0) {\n\t\t\t/* Check if we should replace the private host, or advertise both candidates */\n\t\t\tgboolean keep_private_host = FALSE;\n\t\t\titem = janus_config_get(config, config_nat, janus_config_type_item, \"keep_private_host\");\n\t\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- Going to keep the private host too (separate candidates)\\n\");\n\t\t\t\tkeep_private_host = TRUE;\n\t\t\t}\n\t\t\tjanus_ice_enable_nat_1_1(keep_private_host);\n\t\t}\n\t}\n\t/* Any TURN server to use in Janus? */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_server\");\n\tif(item && item->value)\n\t\tturn_server = (char *)item->value;\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_port\");\n\tif(item && item->value && janus_string_to_uint16(item->value, &turn_port) < 0) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid TURN port: %s (disabling TURN)\\n\", item->value);\n\t\tturn_server = NULL;\n\t}\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_type\");\n\tif(item && item->value)\n\t\tturn_type = (char *)item->value;\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_user\");\n\tif(item && item->value)\n\t\tturn_user = (char *)item->value;\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_pwd\");\n\tif(item && item->value)\n\t\tturn_pwd = (char *)item->value;\n\t/* Check if there's any TURN REST API backend to use */\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_rest_api\");\n\tif(item && item->value)\n\t\tturn_rest_api = (char *)item->value;\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_rest_api_key\");\n\tif(item && item->value)\n\t\tturn_rest_api_key = (char *)item->value;\n#ifdef HAVE_TURNRESTAPI\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_rest_api_method\");\n\tif(item && item->value)\n\t\tturn_rest_api_method = (char *)item->value;\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"turn_rest_api_timeout\");\n\tif(item && item->value) {\n\t\tint rst = atoi(item->value);\n\t\tif(rst <= 0) { /* Don't allow user to set 0 seconds i.e., infinite wait */\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring turn_rest_api_timeout as it's not a positive integer, leaving at default (10 seconds)\\n\");\n\t\t} else {\n\t\t\tturn_rest_api_timeout = rst;\n\t\t}\n\t}\n#endif\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"allow_force_relay\");\n\tif(item && item->value && janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_WARN, \"Note: applications/users will be allowed to force Janus to use TURN. Make sure you know what you're doing!\\n\");\n\t\tjanus_ice_allow_force_relay();\n\t}\n\t/* Do we need a limited number of static event loops, or is it ok to have one per handle (the default)? */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"event_loops\");\n\tif(item && item->value) {\n\t\tint loops = atoi(item->value);\n\t\t/* Check if we should allow API calls to specify which loops to use for new handles */\n\t\tgboolean loops_api = FALSE;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"allow_loop_indication\");\n\t\tif(item && item->value)\n\t\t\tloops_api = janus_is_true(item->value);\n\t\tjanus_ice_set_static_event_loops(loops, loops_api);\n\t}\n\t/* Also check if we need a cap on the size of the task pool (default is no limit) */\n\tint task_pool_size = -1;\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"task_pool_size\");\n\tif(item && item->value) {\n\t\ttask_pool_size = atoi(item->value);\n\t\tif(task_pool_size <= 0)\n\t\t\ttask_pool_size = -1;\n\t}\n\t/* Initialize the ICE stack now */\n\tjanus_ice_init(ice_lite, ice_tcp, full_trickle, ignore_mdns, ipv6, ipv6_linklocal, rtp_min_port, rtp_max_port);\n\tif(janus_ice_set_stun_server(stun_server, stun_port) < 0) {\n\t\tif(!ignore_unreachable_ice_server) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Invalid STUN address %s:%u\\n\", stun_server, stun_port);\n\t\t\tjanus_options_destroy();\n\t\t\texit(1);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid STUN address %s:%u. STUN will be disabled\\n\", stun_server, stun_port);\n\t\t}\n\t}\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ice_nomination\");\n\tif(item && item->value) {\n#ifndef HAVE_ICE_NOMINATION\n\t\tJANUS_LOG(LOG_WARN, \"This version of libnice doesn't support setting the ICE nomination mode, ignoring '%s'\\n\", item->value);\n#else\n\t\tjanus_ice_set_nomination_mode(item->value);\n#endif\n\t}\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ice_consent_freshness\");\n\tif(item && item->value)\n\t\tjanus_ice_set_consent_freshness_enabled(janus_is_true(item->value));\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"ice_keepalive_conncheck\");\n\tif(item && item->value)\n\t\tjanus_ice_set_keepalive_conncheck_enabled(janus_is_true(item->value));\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"hangup_on_failed\");\n\tif(item && item->value)\n\t\tjanus_ice_set_hangup_on_failed_enabled(janus_is_true(item->value));\n\tif(janus_ice_set_turn_server(turn_server, turn_port, turn_type, turn_user, turn_pwd) < 0) {\n\t\tif(!ignore_unreachable_ice_server) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Invalid TURN address %s:%u\\n\", turn_server, turn_port);\n\t\t\tjanus_options_destroy();\n\t\t\texit(1);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid TURN address %s:%u. TURN will be disabled\\n\", turn_server, turn_port);\n\t\t}\n\t}\n#ifndef HAVE_TURNRESTAPI\n\tif(turn_rest_api != NULL || turn_rest_api_key != NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"A TURN REST API backend specified in the settings, but libcurl support has not been built\\n\");\n\t}\n#else\n\tif(janus_ice_set_turn_rest_api(turn_rest_api, turn_rest_api_key, turn_rest_api_method, turn_rest_api_timeout) < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Invalid TURN REST API configuration: %s (%s, %s)\\n\", turn_rest_api, turn_rest_api_key, turn_rest_api_method);\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n#endif\n\titem = janus_config_get(config, config_nat, janus_config_type_item, \"nice_debug\");\n\tif(item && item->value && janus_is_true(item->value)) {\n\t\t/* Enable libnice debugging */\n\t\tJANUS_LOG(LOG_WARN,\"nice_debug option is NOT SUPPORTED ANYMORE! Please set NICE_DEBUG and G_MESSAGES_DEBUG env vars when starting Janus\\n\");\n\t}\n\n\tif(stun_server == NULL && turn_server == NULL) {\n\t\t/* No STUN and TURN server provided for Janus: make sure it isn't on a private address */\n\t\tint num_ips = janus_get_public_ip_count();\n\t\tif(num_ips == 0) {\n\t\t\t/* If nat_1_1_mapping is off, the first (and only) public IP is the local_ip */\n\t\t\tnum_ips++;\n\t\t}\n\t\t/* Check each public IP */\n\t\tint i = 0;\n\t\tfor(i = 0; i < num_ips; i++) {\n\t\t\tgboolean private_address = FALSE;\n\t\t\tconst gchar *test_ip = janus_get_public_ip(i);\n\t\t\tjanus_network_address addr;\n\t\t\tif(janus_network_string_to_address(janus_network_query_options_any_ip, test_ip, &addr) != 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid address %s..?\\n\", test_ip);\n\t\t\t} else {\n\t\t\t\tif(addr.family == AF_INET) {\n\t\t\t\t\tunsigned short int ip[4];\n\t\t\t\t\tsscanf(test_ip, \"%hu.%hu.%hu.%hu\", &ip[0], &ip[1], &ip[2], &ip[3]);\n\t\t\t\t\tif(ip[0] == 10) {\n\t\t\t\t\t\t/* Class A private address */\n\t\t\t\t\t\tprivate_address = TRUE;\n\t\t\t\t\t} else if(ip[0] == 172 && (ip[1] >= 16 && ip[1] <= 31)) {\n\t\t\t\t\t\t/* Class B private address */\n\t\t\t\t\t\tprivate_address = TRUE;\n\t\t\t\t\t} else if(ip[0] == 192 && ip[1] == 168) {\n\t\t\t\t\t\t/* Class C private address */\n\t\t\t\t\t\tprivate_address = TRUE;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* TODO Similar check for IPv6... */\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(private_address) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Janus is deployed on a private address (%s) but you didn't specify any STUN server!\"\n\t\t\t                    \" Expect trouble if this is supposed to work over the internet and not just in a LAN...\\n\", test_ip);\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Is there any DSCP TOS to apply? */\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"dscp\");\n\tif(!item || !item->value)\t/* Just for backwards compatibility */\n\t\titem = janus_config_get(config, config_media, janus_config_type_item, \"dscp_tos\");\n\tif(item && item->value) {\n\t\tint dscp = atoi(item->value);\n\t\tif(dscp < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring dscp value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tjanus_set_dscp(dscp);\n\t\t}\n\t}\n\n\t/* NACK related stuff */\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"min_nack_queue\");\n\tif(item && item->value) {\n\t\tint mnq = atoi(item->value);\n\t\tif(mnq < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring min_nack_queue value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tjanus_set_min_nack_queue(mnq);\n\t\t}\n\t}\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"nack_optimizations\");\n\tif(item && item->value) {\n\t\tgboolean optimize = janus_is_true(item->value);\n\t\tjanus_set_nack_optimizations_enabled(optimize);\n\t}\n\t/* no-media timer */\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"no_media_timer\");\n\tif(item && item->value) {\n\t\tint nmt = atoi(item->value);\n\t\tif(nmt < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring no_media_timer value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tjanus_set_no_media_timer(nmt);\n\t\t}\n\t}\n\t/* slowlink-threshold value */\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"slowlink_threshold\");\n\tif(item && item->value) {\n\t\tint st = atoi(item->value);\n\t\tif(st < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring slowlink_threshold value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tjanus_set_slowlink_threshold(st);\n\t\t}\n\t}\n\t/* TWCC period */\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"twcc_period\");\n\tif(item && item->value) {\n\t\tint tp = atoi(item->value);\n\t\tif(tp <= 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring twcc_period value as it's not a positive integer\\n\");\n\t\t} else {\n\t\t\tjanus_set_twcc_period(tp);\n\t\t}\n\t}\n\n\t/* Setup OpenSSL stuff */\n\tconst char *server_pem;\n\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_pem\");\n\tif(!item || !item->value) {\n\t\tserver_pem = NULL;\n\t} else {\n\t\tserver_pem = item->value;\n\t}\n\tconst char *server_key;\n\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_key\");\n\tif(!item || !item->value) {\n\t\tserver_key = NULL;\n\t} else {\n\t\tserver_key = item->value;\n\t}\n\tconst char *password;\n\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_pwd\");\n\tif(!item || !item->value) {\n\t\tpassword = NULL;\n\t} else {\n\t\tpassword = item->value;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Using certificates:\\n\\t%s\\n\\t%s\\n\", server_pem, server_key);\n\n\tSSL_library_init();\n\tSSL_load_error_strings();\n\tOpenSSL_add_all_algorithms();\n\t/* Check if random pool looks ok (this does not give any guarantees for later, though) */\n\tif(RAND_status() != 1) {\n\t\tJANUS_LOG(LOG_FATAL, \"Random pool is not properly seeded, cannot generate random numbers\\n\");\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\t/* ... and DTLS-SRTP in particular */\n\tconst char *dtls_ciphers = NULL;\n\titem = janus_config_get(config, config_certs, janus_config_type_item, \"dtls_ciphers\");\n\tif(item && item->value)\n\t\tdtls_ciphers = item->value;\n\tguint16 dtls_timeout = 1000;\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"dtls_timeout\");\n\tif(item && item->value && janus_string_to_uint16(item->value, &dtls_timeout) < 0) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid DTLS timeout: %s (falling back to default)\\n\", item->value);\n\t\tdtls_timeout = 1000;\n\t}\n\tgboolean rsa_private_key = FALSE;\n\titem = janus_config_get(config, config_certs, janus_config_type_item, \"rsa_private_key\");\n\tif(item && item->value)\n\t\trsa_private_key = janus_is_true(item->value);\n\tgboolean dtls_accept_selfsigned = TRUE;\n\titem = janus_config_get(config, config_certs, janus_config_type_item, \"dtls_accept_selfsigned\");\n\tif(item && item->value)\n\t\tdtls_accept_selfsigned = janus_is_true(item->value);\n\tif(janus_dtls_srtp_init(server_pem, server_key, password, dtls_ciphers, dtls_timeout, rsa_private_key, dtls_accept_selfsigned) < 0) {\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\t/* Check if there's any custom value for the starting MTU to use in the BIO filter */\n\titem = janus_config_get(config, config_media, janus_config_type_item, \"dtls_mtu\");\n\tif(item && item->value)\n\t\tjanus_dtls_bio_agent_set_mtu(atoi(item->value));\n\n#ifdef HAVE_SCTP\n\t/* Initialize SCTP for DataChannels */\n\tif(janus_sctp_init() < 0) {\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n#else\n\tJANUS_LOG(LOG_WARN, \"Data Channels support not compiled\\n\");\n#endif\n\n\t/* Initialize the RTP forwarders functionality */\n\tif(janus_rtp_forwarders_init() < 0) {\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\n\t/* Sessions */\n\tsessions = g_hash_table_new_full(g_int64_hash, g_int64_equal, (GDestroyNotify)g_free, NULL);\n\t/* Start the sessions timeout watchdog */\n\tsessions_watchdog_context = g_main_context_new();\n\tGMainLoop *watchdog_loop = g_main_loop_new(sessions_watchdog_context, FALSE);\n\tGError *error = NULL;\n\tGThread *watchdog = g_thread_try_new(\"timeout watchdog\", &janus_sessions_watchdog, watchdog_loop, &error);\n\tif(error != NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to start sessions timeout watchdog...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\t/* Start the thread that will dispatch incoming requests */\n\trequests = g_async_queue_new_full((GDestroyNotify)janus_request_destroy);\n\tGThread *requests_thread = g_thread_try_new(\"sessions requests\", &janus_transport_requests, NULL, &error);\n\tif(error != NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to start requests thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\t/* Create a thread pool to handle asynchronous requests, no matter what the transport */\n\terror = NULL;\n\ttasks = g_thread_pool_new(janus_transport_task, NULL, task_pool_size, FALSE, &error);\n\tif(error != NULL) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to launch the request pool task thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\t/* Wait 120 seconds before stopping idle threads to avoid the creation of too many threads for AddressSanitizer. */\n\tg_thread_pool_set_max_idle_time(120 * 1000);\n\n\t/* Load event handlers */\n\tpath = NULL;\n\tdir = NULL;\n\t/* Event handlers are disabled by default, though: they need to be enabled in the configuration */\n\titem = janus_config_get(config, config_events, janus_config_type_item, \"broadcast\");\n\tgboolean enable_events = FALSE;\n\tif(item && item->value)\n\t\tenable_events = janus_is_true(item->value);\n\tif(!enable_events) {\n\t\tJANUS_LOG(LOG_INFO, \"Event handlers support disabled\\n\");\n\t} else {\n\t\tgchar **disabled_eventhandlers = NULL;\n\t\tpath = EVENTDIR;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"events_folder\");\n\t\tif(item && item->value)\n\t\t\tpath = (char *)item->value;\n\t\tJANUS_LOG(LOG_INFO, \"Event handler plugins folder: %s\\n\", path);\n\t\tdir = opendir(path);\n\t\tif(!dir) {\n\t\t\t/* Not really fatal, we don't care and go on anyway: event handlers are not fundamental */\n\t\t\tJANUS_LOG(LOG_WARN, \"\\tCouldn't access event handler plugins folder...\\n\");\n\t\t} else {\n\t\t\titem = janus_config_get(config, config_events, janus_config_type_item, \"stats_period\");\n\t\t\tif(item && item->value) {\n\t\t\t\t/* Check if we need to use a larger period for pushing statistics to event handlers */\n\t\t\t\tint period = atoi(item->value);\n\t\t\t\tif(period < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid event handlers statistics period, using default value (1 second)\\n\");\n\t\t\t\t} else if(period == 0) {\n\t\t\t\t\tjanus_ice_set_event_stats_period(0);\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Disabling event handlers statistics period, no media statistics will be pushed to event handlers\\n\");\n\t\t\t\t} else {\n\t\t\t\t\tjanus_ice_set_event_stats_period(period);\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Setting event handlers statistics period to %d seconds\\n\", period);\n\t\t\t\t}\n\t\t\t}\n\t\t\titem = janus_config_get(config, config_events, janus_config_type_item, \"combine_media_stats\");\n\t\t\tif(item && item->value) {\n\t\t\t\tgboolean combine = janus_is_true(item->value);\n\t\t\t\tjanus_ice_event_set_combine_media_stats(combine);\n\t\t\t\tif(combine)\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Event handler configured to send media stats combined in a single event\\n\");\n\t\t\t}\n\t\t\t/* Any event handlers to ignore? */\n\t\t\titem = janus_config_get(config, config_events, janus_config_type_item, \"disable\");\n\t\t\tif(item && item->value)\n\t\t\t\tdisabled_eventhandlers = g_strsplit(item->value, \",\", -1);\n\t\t\t/* Open the shared objects */\n\t\t\tstruct dirent *eventent = NULL;\n\t\t\tchar eventpath[1024];\n\t\t\twhile((eventent = readdir(dir))) {\n\t\t\t\tint len = strlen(eventent->d_name);\n\t\t\t\tif (len < 4) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (strcasecmp(eventent->d_name+len-strlen(SHLIB_EXT), SHLIB_EXT)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Check if this event handler has been disabled in the configuration file */\n\t\t\t\tif(disabled_eventhandlers != NULL) {\n\t\t\t\t\tgchar *index = disabled_eventhandlers[0];\n\t\t\t\t\tif(index != NULL) {\n\t\t\t\t\t\tint i=0;\n\t\t\t\t\t\tgboolean skip = FALSE;\n\t\t\t\t\t\twhile(index != NULL) {\n\t\t\t\t\t\t\twhile(isspace(*index))\n\t\t\t\t\t\t\t\tindex++;\n\t\t\t\t\t\t\tif(strlen(index) && !strcmp(index, eventent->d_name)) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Event handler plugin '%s' has been disabled, skipping...\\n\", eventent->d_name);\n\t\t\t\t\t\t\t\tskip = TRUE;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ti++;\n\t\t\t\t\t\t\tindex = disabled_eventhandlers[i];\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(skip)\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Loading event handler plugin '%s'...\\n\", eventent->d_name);\n\t\t\t\tmemset(eventpath, 0, 1024);\n\t\t\t\tg_snprintf(eventpath, 1024, \"%s/%s\", path, eventent->d_name);\n\t\t\t\tvoid *event = dlopen(eventpath, RTLD_NOW | RTLD_GLOBAL);\n\t\t\t\tif(!event) {\n\t\t\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load event handler plugin '%s': %s\\n\", eventent->d_name, dlerror());\n\t\t\t\t\tif(exit_on_dl_error) {\n\t\t\t\t\t\tjanus_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcreate_e *create = (create_e*) dlsym(event, \"create\");\n\t\t\t\t\tconst char *dlsym_error = dlerror();\n\t\t\t\t\tif(dlsym_error) {\n\t\t\t\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load symbol 'create': %s\\n\", dlsym_error);\n\t\t\t\t\t\tif(exit_on_dl_error) {\n\t\t\t\t\t\t\tjanus_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_eventhandler *janus_eventhandler = create();\n\t\t\t\t\tif(!janus_eventhandler) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tCouldn't use function 'create'...\\n\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Are all the mandatory methods and callbacks implemented? */\n\t\t\t\t\tif(!janus_eventhandler->init || !janus_eventhandler->destroy ||\n\t\t\t\t\t\t\t!janus_eventhandler->get_api_compatibility ||\n\t\t\t\t\t\t\t!janus_eventhandler->get_version ||\n\t\t\t\t\t\t\t!janus_eventhandler->get_version_string ||\n\t\t\t\t\t\t\t!janus_eventhandler->get_description ||\n\t\t\t\t\t\t\t!janus_eventhandler->get_package ||\n\t\t\t\t\t\t\t!janus_eventhandler->get_name ||\n\t\t\t\t\t\t\t!janus_eventhandler->incoming_event) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tMissing some mandatory methods/callbacks, skipping this event handler plugin...\\n\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(janus_eventhandler->get_api_compatibility() < JANUS_EVENTHANDLER_API_VERSION) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"The '%s' event handler plugin was compiled against an older version of the API (%d < %d), skipping it: update it to enable it again\\n\",\n\t\t\t\t\t\t\tjanus_eventhandler->get_package(), janus_eventhandler->get_api_compatibility(), JANUS_EVENTHANDLER_API_VERSION);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_eventhandler->init(configs_folder);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"\\tVersion: %d (%s)\\n\", janus_eventhandler->get_version(), janus_eventhandler->get_version_string());\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t   [%s] %s\\n\", janus_eventhandler->get_package(), janus_eventhandler->get_name());\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t   %s\\n\", janus_eventhandler->get_description());\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t   Plugin API version: %d\\n\", janus_eventhandler->get_api_compatibility());\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t   Subscriptions:\");\n\t\t\t\t\tif(janus_eventhandler->events_mask == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" none\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif(janus_flags_is_set(&janus_eventhandler->events_mask, JANUS_EVENT_TYPE_SESSION))\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" sessions\");\n\t\t\t\t\t\tif(janus_flags_is_set(&janus_eventhandler->events_mask, JANUS_EVENT_TYPE_HANDLE))\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" handles\");\n\t\t\t\t\t\tif(janus_flags_is_set(&janus_eventhandler->events_mask, JANUS_EVENT_TYPE_JSEP))\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" jsep\");\n\t\t\t\t\t\tif(janus_flags_is_set(&janus_eventhandler->events_mask, JANUS_EVENT_TYPE_WEBRTC))\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" webrtc\");\n\t\t\t\t\t\tif(janus_flags_is_set(&janus_eventhandler->events_mask, JANUS_EVENT_TYPE_MEDIA))\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" media\");\n\t\t\t\t\t\tif(janus_flags_is_set(&janus_eventhandler->events_mask, JANUS_EVENT_TYPE_PLUGIN))\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" plugins\");\n\t\t\t\t\t\tif(janus_flags_is_set(&janus_eventhandler->events_mask, JANUS_EVENT_TYPE_TRANSPORT))\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \" transports\");\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"\\n\");\n\t\t\t\t\tif(eventhandlers == NULL)\n\t\t\t\t\t\teventhandlers = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\t\t\tg_hash_table_insert(eventhandlers, (gpointer)janus_eventhandler->get_package(), janus_eventhandler);\n\t\t\t\t\tif(eventhandlers_so == NULL)\n\t\t\t\t\t\teventhandlers_so = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\t\t\tg_hash_table_insert(eventhandlers_so, (gpointer)janus_eventhandler->get_package(), event);\n\t\t\t\t}\n\t\t\t}\n\t\t\tclosedir(dir);\n\t\t}\n\t\tif(disabled_eventhandlers != NULL)\n\t\t\tg_strfreev(disabled_eventhandlers);\n\t\tdisabled_eventhandlers = NULL;\n\t\t/* Initialize the event broadcaster */\n\t\tif(janus_events_init(enable_events, (server_name ? server_name : (char *)JANUS_SERVER_NAME), eventhandlers) < 0) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Error initializing the Event handlers mechanism...\\n\");\n\t\t\tjanus_options_destroy();\n\t\t\texit(1);\n\t\t}\n\t\t/* Send a status event every once in a while */\n\t\tGSource *timeout_source = g_timeout_source_new_seconds(15);\n\t\tg_source_set_callback(timeout_source, janus_status_sessions, sessions_watchdog_context, NULL);\n\t\tg_source_attach(timeout_source, sessions_watchdog_context);\n\t\tg_source_unref(timeout_source);\n\t}\n\n\t/* Load plugins */\n\tpath = PLUGINDIR;\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"plugins_folder\");\n\tif(item && item->value)\n\t\tpath = (char *)item->value;\n\tJANUS_LOG(LOG_INFO, \"Plugins folder: %s\\n\", path);\n\tdir = opendir(path);\n\tif(!dir) {\n\t\tJANUS_LOG(LOG_FATAL, \"\\tCouldn't access plugins folder...\\n\");\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\t/* Any plugin to ignore? */\n\tgchar **disabled_plugins = NULL;\n\titem = janus_config_get(config, config_plugins, janus_config_type_item, \"disable\");\n\tif(item && item->value)\n\t\tdisabled_plugins = g_strsplit(item->value, \",\", -1);\n\t/* Open the shared objects */\n\tstruct dirent *pluginent = NULL;\n\tchar pluginpath[1024];\n\twhile((pluginent = readdir(dir))) {\n\t\tint len = strlen(pluginent->d_name);\n\t\tif (len < 4) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (strcasecmp(pluginent->d_name+len-strlen(SHLIB_EXT), SHLIB_EXT)) {\n\t\t\tcontinue;\n\t\t}\n\t\t/* Check if this plugins has been disabled in the configuration file */\n\t\tif(disabled_plugins != NULL) {\n\t\t\tgchar *index = disabled_plugins[0];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\tgboolean skip = FALSE;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\twhile(isspace(*index))\n\t\t\t\t\t\tindex++;\n\t\t\t\t\tif(strlen(index) && !strcmp(index, pluginent->d_name)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Plugin '%s' has been disabled, skipping...\\n\", pluginent->d_name);\n\t\t\t\t\t\tskip = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = disabled_plugins[i];\n\t\t\t\t}\n\t\t\t\tif(skip)\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tJANUS_LOG(LOG_INFO, \"Loading plugin '%s'...\\n\", pluginent->d_name);\n\t\tmemset(pluginpath, 0, 1024);\n\t\tg_snprintf(pluginpath, 1024, \"%s/%s\", path, pluginent->d_name);\n\t\tvoid *plugin = dlopen(pluginpath, RTLD_NOW | RTLD_GLOBAL);\n\t\tif(!plugin) {\n\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load plugin '%s': %s\\n\", pluginent->d_name, dlerror());\n\t\t\tif(exit_on_dl_error) {\n\t\t\t\tjanus_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else {\n\t\t\tcreate_p *create = (create_p*) dlsym(plugin, \"create\");\n\t\t\tconst char *dlsym_error = dlerror();\n\t\t\tif(dlsym_error) {\n\t\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load symbol 'create': %s\\n\", dlsym_error);\n\t\t\t\tif(exit_on_dl_error) {\n\t\t\t\t\tjanus_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_plugin *janus_plugin = create();\n\t\t\tif(!janus_plugin) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tCouldn't use function 'create'...\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* Are all the mandatory methods and callbacks implemented? */\n\t\t\tif(!janus_plugin->init || !janus_plugin->destroy ||\n\t\t\t\t\t!janus_plugin->get_api_compatibility ||\n\t\t\t\t\t!janus_plugin->get_version ||\n\t\t\t\t\t!janus_plugin->get_version_string ||\n\t\t\t\t\t!janus_plugin->get_description ||\n\t\t\t\t\t!janus_plugin->get_package ||\n\t\t\t\t\t!janus_plugin->get_name ||\n\t\t\t\t\t!janus_plugin->create_session ||\n\t\t\t\t\t!janus_plugin->query_session ||\n\t\t\t\t\t!janus_plugin->destroy_session ||\n\t\t\t\t\t!janus_plugin->handle_message ||\n\t\t\t\t\t!janus_plugin->setup_media ||\n\t\t\t\t\t!janus_plugin->hangup_media) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tMissing some mandatory methods/callbacks, skipping this plugin...\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(janus_plugin->get_api_compatibility() < JANUS_PLUGIN_API_VERSION) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"The '%s' plugin was compiled against an older version of the API (%d < %d), skipping it: update it to enable it again\\n\",\n\t\t\t\t\tjanus_plugin->get_package(), janus_plugin->get_api_compatibility(), JANUS_PLUGIN_API_VERSION);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(janus_plugin->init(&janus_handler_plugin, configs_folder) < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"The '%s' plugin could not be initialized\\n\", janus_plugin->get_package());\n\t\t\t\tdlclose(plugin);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"\\tVersion: %d (%s)\\n\", janus_plugin->get_version(), janus_plugin->get_version_string());\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   [%s] %s\\n\", janus_plugin->get_package(), janus_plugin->get_name());\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   %s\\n\", janus_plugin->get_description());\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   Plugin API version: %d\\n\", janus_plugin->get_api_compatibility());\n\t\t\tif(!janus_plugin->incoming_rtp && !janus_plugin->incoming_rtcp && !janus_plugin->incoming_data) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"The '%s' plugin doesn't implement any callback for RTP/RTCP/data... is this on purpose?\\n\",\n\t\t\t\t\tjanus_plugin->get_package());\n\t\t\t}\n\t\t\tif(!janus_plugin->incoming_rtp && !janus_plugin->incoming_rtcp && janus_plugin->incoming_data) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"The '%s' plugin will only handle data channels (no RTP/RTCP)... is this on purpose?\\n\",\n\t\t\t\t\tjanus_plugin->get_package());\n\t\t\t}\n\t\t\tif(plugins == NULL)\n\t\t\t\tplugins = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\tg_hash_table_insert(plugins, (gpointer)janus_plugin->get_package(), janus_plugin);\n\t\t\tif(plugins_so == NULL)\n\t\t\t\tplugins_so = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\tg_hash_table_insert(plugins_so, (gpointer)janus_plugin->get_package(), plugin);\n\t\t}\n\t}\n\tclosedir(dir);\n\tif(disabled_plugins != NULL)\n\t\tg_strfreev(disabled_plugins);\n\tdisabled_plugins = NULL;\n\n\t/* Load transports */\n\tgboolean janus_api_enabled = FALSE, admin_api_enabled = FALSE;\n\tpath = TRANSPORTDIR;\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"transports_folder\");\n\tif(item && item->value)\n\t\tpath = (char *)item->value;\n\tJANUS_LOG(LOG_INFO, \"Transport plugins folder: %s\\n\", path);\n\tdir = opendir(path);\n\tif(!dir) {\n\t\tJANUS_LOG(LOG_FATAL, \"\\tCouldn't access transport plugins folder...\\n\");\n\t\tjanus_options_destroy();\n\t\texit(1);\n\t}\n\t/* Any transport to ignore? */\n\tgchar **disabled_transports = NULL;\n\titem = janus_config_get(config, config_transports, janus_config_type_item, \"disable\");\n\tif(item && item->value)\n\t\tdisabled_transports = g_strsplit(item->value, \",\", -1);\n\t/* Open the shared objects */\n\tstruct dirent *transportent = NULL;\n\tchar transportpath[1024];\n\twhile((transportent = readdir(dir))) {\n\t\tint len = strlen(transportent->d_name);\n\t\tif (len < 4) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (strcasecmp(transportent->d_name+len-strlen(SHLIB_EXT), SHLIB_EXT)) {\n\t\t\tcontinue;\n\t\t}\n\t\t/* Check if this transports has been disabled in the configuration file */\n\t\tif(disabled_transports != NULL) {\n\t\t\tgchar *index = disabled_transports[0];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\tgboolean skip = FALSE;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\twhile(isspace(*index))\n\t\t\t\t\t\tindex++;\n\t\t\t\t\tif(strlen(index) && !strcmp(index, transportent->d_name)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Transport plugin '%s' has been disabled, skipping...\\n\", transportent->d_name);\n\t\t\t\t\t\tskip = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = disabled_transports[i];\n\t\t\t\t}\n\t\t\t\tif(skip)\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tJANUS_LOG(LOG_INFO, \"Loading transport plugin '%s'...\\n\", transportent->d_name);\n\t\tmemset(transportpath, 0, 1024);\n\t\tg_snprintf(transportpath, 1024, \"%s/%s\", path, transportent->d_name);\n\t\tvoid *transport = dlopen(transportpath, RTLD_NOW | RTLD_GLOBAL);\n\t\tif(!transport) {\n\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load transport plugin '%s': %s\\n\", transportent->d_name, dlerror());\n\t\t\tif(exit_on_dl_error) {\n\t\t\t\tjanus_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else {\n\t\t\tcreate_t *create = (create_t*) dlsym(transport, \"create\");\n\t\t\tconst char *dlsym_error = dlerror();\n\t\t\tif(dlsym_error) {\n\t\t\t\tJANUS_LOG(exit_on_dl_error ? LOG_FATAL : LOG_ERR, \"\\tCouldn't load symbol 'create': %s\\n\", dlsym_error);\n\t\t\t\tif(exit_on_dl_error) {\n\t\t\t\t\tjanus_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_transport *janus_transport = create();\n\t\t\tif(!janus_transport) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tCouldn't use function 'create'...\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* Are all the mandatory methods and callbacks implemented? */\n\t\t\tif(!janus_transport->init || !janus_transport->destroy ||\n\t\t\t\t\t!janus_transport->get_api_compatibility ||\n\t\t\t\t\t!janus_transport->get_version ||\n\t\t\t\t\t!janus_transport->get_version_string ||\n\t\t\t\t\t!janus_transport->get_description ||\n\t\t\t\t\t!janus_transport->get_package ||\n\t\t\t\t\t!janus_transport->get_name ||\n\t\t\t\t\t!janus_transport->send_message ||\n\t\t\t\t\t!janus_transport->is_janus_api_enabled ||\n\t\t\t\t\t!janus_transport->is_admin_api_enabled ||\n\t\t\t\t\t!janus_transport->session_created ||\n\t\t\t\t\t!janus_transport->session_over ||\n\t\t\t\t\t!janus_transport->session_claimed) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tMissing some mandatory methods/callbacks, skipping this transport plugin...\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(janus_transport->get_api_compatibility() < JANUS_TRANSPORT_API_VERSION) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"The '%s' transport plugin was compiled against an older version of the API (%d < %d), skipping it: update it to enable it again\\n\",\n\t\t\t\t\tjanus_transport->get_package(), janus_transport->get_api_compatibility(), JANUS_TRANSPORT_API_VERSION);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(janus_transport->init(&janus_handler_transport, configs_folder) < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"The '%s' plugin could not be initialized\\n\", janus_transport->get_package());\n\t\t\t\tdlclose(transport);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"\\tVersion: %d (%s)\\n\", janus_transport->get_version(), janus_transport->get_version_string());\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   [%s] %s\\n\", janus_transport->get_package(), janus_transport->get_name());\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   %s\\n\", janus_transport->get_description());\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   Plugin API version: %d\\n\", janus_transport->get_api_compatibility());\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   Janus API: %s\\n\", janus_transport->is_janus_api_enabled() ? \"enabled\" : \"disabled\");\n\t\t\tJANUS_LOG(LOG_VERB, \"\\t   Admin API: %s\\n\", janus_transport->is_admin_api_enabled() ? \"enabled\" : \"disabled\");\n\t\t\tjanus_api_enabled = janus_api_enabled || janus_transport->is_janus_api_enabled();\n\t\t\tadmin_api_enabled = admin_api_enabled || janus_transport->is_admin_api_enabled();\n\t\t\tif(transports == NULL)\n\t\t\t\ttransports = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\tg_hash_table_insert(transports, (gpointer)janus_transport->get_package(), janus_transport);\n\t\t\tif(transports_so == NULL)\n\t\t\t\ttransports_so = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\tg_hash_table_insert(transports_so, (gpointer)janus_transport->get_package(), transport);\n\t\t}\n\t}\n\tclosedir(dir);\n\tif(disabled_transports != NULL)\n\t\tg_strfreev(disabled_transports);\n\tdisabled_transports = NULL;\n\t/* Make sure at least a Janus API transport is available */\n\tif(!janus_api_enabled) {\n\t\tJANUS_LOG(LOG_FATAL, \"No Janus API transport is available... enable at least one and restart Janus\\n\");\n\t\tjanus_options_destroy();\n\t\texit(1);\t/* FIXME Should we really give up? */\n\t}\n\t/* Make sure at least an admin API transport is available, if the auth mechanism is enabled */\n\tif(!admin_api_enabled && janus_auth_is_stored_mode()) {\n\t\tJANUS_LOG(LOG_FATAL, \"No Admin/monitor transport is available, but the stored token based authentication mechanism is enabled... this will cause all requests to fail, giving up! If you want to use tokens, enable the Admin/monitor API or set the token auth secret.\\n\");\n\t\tjanus_options_destroy();\n\t\texit(1);\t/* FIXME Should we really give up? */\n\t}\n\n\t/* Make sure libnice is recent enough, otherwise print a warning */\n\tint libnice_version = 0;\n\tif(libnice_version_string != NULL && sscanf(libnice_version_string, \"%*d.%*d.%d\", &libnice_version) == 1) {\n\t\tif(libnice_version < 16) {\n\t\t\tJANUS_LOG(LOG_WARN, \"libnice version outdated: %s installed, at least 0.1.16 recommended. Notice the installed version was checked at build time: if you updated libnice in the meanwhile, re-configure and recompile to get rid of this warning\\n\",\n\t\t\t\tlibnice_version_string);\n\t\t}\n\t}\n\n\t/* Ok, Janus has started! Let the parent now about this if we're daemonizing */\n\tif(daemonize) {\n\t\tint code = 0;\n\t\tssize_t res = 0;\n\t\tdo {\n\t\t\tres = write(pipefd[1], &code, sizeof(int));\n\t\t} while(res == -1 && errno == EINTR);\n\t}\n\n\t/* If the Event Handlers mechanism is enabled, notify handlers that Janus just started */\n\tif(janus_events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"status\", json_string(\"started\"));\n\t\tjson_object_set_new(info, \"info\", janus_info(NULL));\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_CORE, JANUS_EVENT_SUBTYPE_CORE_STARTUP, 0, info);\n\t}\n\n\t/* Loop until we have to stop */\n\tmainloop = g_main_loop_new (NULL, TRUE);\n\tg_main_loop_run(mainloop);\n\n\t/* If the Event Handlers mechanism is enabled, notify handlers that Janus is hanging up */\n\tif(janus_events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"status\", json_string(\"shutdown\"));\n\t\tjson_object_set_new(info, \"signum\", json_integer(stop_signal));\n\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_CORE, JANUS_EVENT_SUBTYPE_CORE_SHUTDOWN, 0, info);\n\t}\n\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"Ending sessions timeout watchdog...\\n\");\n\tg_main_loop_quit(watchdog_loop);\n\tg_thread_join(watchdog);\n\twatchdog = NULL;\n\tg_main_loop_unref(watchdog_loop);\n\tg_main_context_unref(sessions_watchdog_context);\n\tsessions_watchdog_context = NULL;\n\n\tif(config)\n\t\tjanus_config_destroy(config);\n\n\tJANUS_LOG(LOG_INFO, \"Closing transport plugins:\\n\");\n\tif(transports != NULL && g_hash_table_size(transports) > 0) {\n\t\tg_hash_table_foreach(transports, janus_transport_close, NULL);\n\t\tg_clear_pointer(&transports, g_hash_table_destroy);\n\t}\n\tif(transports_so != NULL && g_hash_table_size(transports_so) > 0) {\n\t\tg_hash_table_foreach(transports_so, janus_transportso_close, NULL);\n\t\tg_clear_pointer(&transports_so, g_hash_table_destroy);\n\t}\n\t/* Get rid of requests tasks and thread too */\n\tg_thread_pool_free(tasks, FALSE, FALSE);\n\tJANUS_LOG(LOG_INFO, \"Ending requests thread...\\n\");\n\tg_async_queue_push(requests, &exit_message);\n\tg_thread_join(requests_thread);\n\trequests_thread = NULL;\n\tg_async_queue_unref(requests);\n\n\tJANUS_LOG(LOG_INFO, \"Destroying sessions...\\n\");\n\tg_clear_pointer(&sessions, g_hash_table_destroy);\n\tjanus_ice_deinit();\n\tJANUS_LOG(LOG_INFO, \"Freeing crypto resources...\\n\");\n\tjanus_dtls_srtp_cleanup();\n\tEVP_cleanup();\n\tERR_free_strings();\n#ifdef HAVE_SCTP\n\tJANUS_LOG(LOG_INFO, \"De-initializing SCTP...\\n\");\n\tjanus_sctp_deinit();\n#endif\n\tjanus_rtp_forwarders_deinit();\n\tjanus_auth_deinit();\n\n\tJANUS_LOG(LOG_INFO, \"Closing plugins:\\n\");\n\tif(plugins != NULL && g_hash_table_size(plugins) > 0) {\n\t\tg_hash_table_foreach(plugins, janus_plugin_close, NULL);\n\t\tg_clear_pointer(&plugins, g_hash_table_destroy);\n\t}\n\tif(plugins_so != NULL && g_hash_table_size(plugins_so) > 0) {\n\t\tg_hash_table_foreach(plugins_so, janus_pluginso_close, NULL);\n\t\tg_clear_pointer(&plugins_so, g_hash_table_destroy);\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"Closing event handlers:\\n\");\n\tjanus_events_deinit();\n\tif(eventhandlers != NULL && g_hash_table_size(eventhandlers) > 0) {\n\t\tg_hash_table_foreach(eventhandlers, janus_eventhandler_close, NULL);\n\t\tg_clear_pointer(&eventhandlers, g_hash_table_destroy);\n\t}\n\tif(eventhandlers_so != NULL && g_hash_table_size(eventhandlers_so) > 0) {\n\t\tg_hash_table_foreach(eventhandlers_so, janus_eventhandlerso_close, NULL);\n\t\tg_clear_pointer(&eventhandlers_so, g_hash_table_destroy);\n\t}\n\n\tjanus_recorder_deinit();\n\tg_free(local_ip);\n\tif (public_ips) {\n\t\tg_list_free(public_ips);\n\t}\n\tif (public_ips_table) {\n\t\tg_clear_pointer(&public_ips_table, g_hash_table_destroy);\n\t}\n\n\tif(janus_ice_get_static_event_loops() > 0)\n\t\tjanus_ice_stop_static_event_loops();\n\n\tjanus_protected_folders_clear();\n\n#ifdef REFCOUNT_DEBUG\n\t/* Any reference counters that are still up while we're leaving? (debug-mode only) */\n\tjanus_mutex_lock(&counters_mutex);\n\tif(counters && g_hash_table_size(counters) > 0) {\n\t\tJANUS_PRINT(\"Debugging reference counters: %d still allocated\\n\", g_hash_table_size(counters));\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, counters);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tJANUS_PRINT(\"  -- %p\\n\", value);\n\t\t}\n\t} else {\n\t\tJANUS_PRINT(\"Debugging reference counters: 0 still allocated\\n\");\n\t}\n\tjanus_mutex_unlock(&counters_mutex);\n#endif\n\tg_clear_pointer(&janus_log_global_prefix, g_free);\n\n\tjanus_options_destroy();\n\tJANUS_PRINT(\"Bye!\\n\");\n\n\texit(0);\n}\n"
  },
  {
    "path": "src/janus.h",
    "content": "/*! \\file   janus.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus core (headers)\n * \\details Implementation of the Janus core. This code takes care of\n * the server initialization (command line/configuration) and setup,\n * and makes use of the available transport plugins (by default HTTP,\n * WebSockets, RabbitMQ, if compiled) and Janus protocol (a JSON-based\n * protocol) to interact with the applications, whether they're web based\n * or not. The core also takes care of bridging peers and plugins\n * accordingly, in terms of both messaging and real-time media transfer\n * via WebRTC.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_CORE_H\n#define JANUS_CORE_H\n\n#include <inttypes.h>\n#include <stdlib.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n#include <ctype.h>\n#include <unistd.h>\n\n#include <jansson.h>\n\n#include \"mutex.h\"\n#include \"ice.h\"\n#include \"refcount.h\"\n#include \"transports/transport.h\"\n#include \"events/eventhandler.h\"\n#include \"loggers/logger.h\"\n#include \"plugins/plugin.h\"\n\n\n/*! \\brief Helper to address requests and their sources (e.g., a specific HTTP connection, websocket, RabbitMQ or others) */\ntypedef struct janus_request janus_request;\n\n/*! \\brief Janus Core-Client session */\ntypedef struct janus_session {\n\t/*! \\brief Janus Core-Client session ID */\n\tguint64 session_id;\n\t/*! \\brief Map of handles this session is managing */\n\tGHashTable *ice_handles;\n\t/*! \\brief Time of the last activity on the session */\n\tgint64 last_activity;\n\t/*! \\brief Pointer to the request instance (and the transport that originated the session) */\n\tjanus_request *source;\n\t/*! \\brief Flag to notify there's been a session timeout */\n\tvolatile gint timedout;\n\t/*! \\brief Timeout value in seconds to use with this session, 0 is unlimited, -1 is global session timeout setting */\n\tgint timeout;\n\t/*! \\brief Flag to notify that transport is gone */\n\tvolatile gint transport_gone;\n\t/*! \\brief Mutex to lock/unlock this session */\n\tjanus_mutex mutex;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_session;\n\n\n/** @name Janus Core-Client session methods\n */\n///@{\n/*! \\brief Method to create a new Janus Core-Client session\n * @param[in] session_id The desired Janus Core-Client session ID, or 0 if it needs to be generated randomly\n * @returns The created Janus Core-Client session if successful, NULL otherwise */\njanus_session *janus_session_create(guint64 session_id);\n/*! \\brief Method to find an existing Janus Core-Client session from its ID\n * @param[in] session_id The Janus Core-Client session ID\n * @returns The created Janus Core-Client session if successful, NULL otherwise */\njanus_session *janus_session_find(guint64 session_id);\n/*! \\brief Method to add an event to notify to the queue of notifications for this session\n * @param[in] session The Janus Core-Client session this notification is related to\n * @param[in] event The event to notify as a Jansson JSON object */\nvoid janus_session_notify_event(janus_session *session, json_t *event);\n/*! \\brief Method to destroy a Janus Core-Client session\n * @param[in] session The Janus Core-Client session to destroy\n * @returns 0 in case of success, a negative integer otherwise */\ngint janus_session_destroy(janus_session *session);\n/*! \\brief Method to find an existing Janus ICE handle from its ID\n * @param[in] session The Janus Core-Client session this ICE handle belongs to\n * @param[in] handle_id The Janus ICE handle ID\n * @returns The Janus ICE handle if successful, NULL otherwise */\njanus_ice_handle *janus_session_handles_find(janus_session *session, guint64 handle_id);\n/*! \\brief Method to insert a Janus ICE handle in a session\n * @param[in] session The Janus Core-Client session\n * @param[in] handle The Janus ICE handle */\nvoid janus_session_handles_insert(janus_session *session, janus_ice_handle *handle);\n/*! \\brief Method to remove a Janus ICE handle from a session\n * @param[in] session The Janus Core-Client session\n * @param[in] handle The Janus ICE handle\n * @returns The error code of janus_ice_handle_destroy */\ngint janus_session_handles_remove(janus_session *session, janus_ice_handle *handle);\n/*! \\brief Method to remove all Janus ICE handles from a session\n * @param[in] session The Janus Core-Client session */\nvoid janus_session_handles_clear(janus_session *session);\n/*! \\brief Method to list the IDs of all Janus ICE handles of a session as JSON\n * @param[in] session The Janus Core-Client session\n * @returns The JSON array */\njson_t *janus_session_handles_list_json(janus_session *session);\n///@}\n\n\n/** @name Janus request processing\n * \\details Since messages may come from different sources (plain HTTP,\n * WebSockets, RabbitMQ and potentially even more in the future), we\n * have a shared way to process messages: a method to process a request,\n * and helper methods to return a success or an error message.\n */\n///@{\n/*! \\brief Helper to address requests and their sources (e.g., a specific HTTP connection, websocket, RabbitMQ or others) */\nstruct janus_request {\n\t/*! \\brief Pointer to the transport plugin */\n\tjanus_transport *transport;\n\t/*! \\brief Pointer to the transport-provided session instance */\n\tjanus_transport_session *instance;\n\t/*! \\brief Opaque pointer to the request ID, if available */\n\tvoid *request_id;\n\t/*! \\brief Whether this is a Janus API or admin API request */\n\tgboolean admin;\n\t/*! \\brief Pointer to the original request, if available */\n\tjson_t *message;\n\t/*! \\brief Pointer to any JSON errors parsing the original request */\n\tjson_error_t *error;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n};\n/*! \\brief Helper to allocate a janus_request instance\n * @param[in] transport Pointer to the transport\n * @param[in] instance Pointer to the transport-provided session instance\n * @param[in] request_id Opaque pointer to the request ID, if available\n * @param[in] admin Whether this is a Janus API or Admin API request\n * @param[in] message Opaque pointer to the original request, if available\n * @param[in] error If the transport detected a JSON parsing error, the error content\n * @returns A pointer to a janus_request instance if successful, NULL otherwise */\njanus_request *janus_request_new(janus_transport *transport, janus_transport_session *instance, void *request_id, gboolean admin, json_t *message, json_error_t *error);\n/*! \\brief Helper to destroy a janus_request instance\n * @param[in] request The janus_request instance to destroy\n * @note The opaque pointers in the instance are not destroyed, that's up to you */\nvoid janus_request_destroy(janus_request *request);\n/*! \\brief Helper to process an incoming request, no matter where it comes from\n * @param[in] request The JSON request\n * @returns 0 on success, a negative integer otherwise\n */\nint janus_process_incoming_request(janus_request *request);\n/*! \\brief Helper to process an incoming admin/monitor request, no matter where it comes from\n * @param[in] request The request instance and its source\n * @returns 0 on success, a negative integer otherwise\n */\nint janus_process_incoming_admin_request(janus_request *request);\n/*! \\brief Method to return a successful Janus response message (JSON) to the browser\n * @param[in] request The request instance and its source\n * @param[in] payload The payload to return as a JSON object\n * @returns 0 on success, a negative integer otherwise\n */\nint janus_process_success(janus_request *request, json_t *payload);\n/*! \\brief Method to return an error Janus response message (JSON) to the browser\n * @param[in] request The request instance and its source\n * @param[in] session_id Janus session identifier this error refers to\n * @param[in] transaction The Janus transaction identifier\n * @param[in] error The error code as defined in apierror.h\n * @param[in] format The printf format of the reason string, followed by a variable\n * number of arguments, if needed; if format is NULL, a pre-configured string\n * associated with the error code is used\n * @returns 0 on success, a negative integer otherwise\n */\nint janus_process_error(janus_request *request, uint64_t session_id, const char *transaction, gint error, const char *format, ...) G_GNUC_PRINTF(5, 6);\n///@}\n\n\n/** @name Janus transport plugin management\n * The core doesn't support any transport for the Janus API by default.\n * In order to be able to with external clients, transport plugins are\n * needed, e.g., to provide support for REST HTTP/HTTPS, WebSockets,\n * RabbitMQ or others. These transport plugins are shared objects that\n * need to implement the interfaces defined in transport.h and as such\n * are dynamically loaded by the server at startup, and unloaded when\n * the server closes.\n */\n///@{\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to destroy a transport instance\n * @param[in] key Key of the transports hash table (package name)\n * @param[in] value The janus_transport instance to destroy\n * @param[in] user_data User provided data (unused) */\nvoid janus_transport_close(void *key, void *value, void *user_data);\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to close a transport plugin\n * @param[in] key Key of the transports hash table (package name)\n * @param[in] value The janus_transport instance to close\n * @param[in] user_data User provided data (unused) */\nvoid janus_transportso_close(void *key, void *value, void *user_data);\n///@}\n\n/** @name Janus event handler plugin management\n * The core doesn't notify anyone, except session originators, and only\n * then only about stuff relevant to them. In order to allow for a more\n * apt management of core and plugin related events on a broader sense,\n * event handler plugins are needed. These event handler plugins are\n * shared objects that need to implement the interfaces defined in\n * eventhandler.h and as such are dynamically loaded by the server at\n * startup, and unloaded when the server closes.\n */\n///@{\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to destroy an eventhandler instance\n * @param[in] key Key of the events hash table (package name)\n * @param[in] value The janus_eventhandler instance to destroy\n * @param[in] user_data User provided data (unused) */\nvoid janus_eventhandler_close(void *key, void *value, void *user_data);\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to close an eventhandler plugin\n * @param[in] key Key of the events hash table (package name)\n * @param[in] value The janus_eventhandler instance to close\n * @param[in] user_data User provided data (unused) */\nvoid janus_eventhandlerso_close(void *key, void *value, void *user_data);\n///@}\n\n/** @name Janus external logger plugin management\n * By default, Janus has integrated support for logging to stdout and to\n * a static file. Custom and advanced logging can be accomplished using\n * additional logger plugins. These logger plugins are shared objects that\n * need to implement the interfaces defined in logger.h and as such are\n * dynamically loaded by the server at startup, and unloaded when the server closes.\n */\n///@{\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to destroy a logger instance\n * @param[in] key Key of the loggers hash table (package name)\n * @param[in] value The janus_logger instance to destroy\n * @param[in] user_data User provided data (unused) */\nvoid janus_logger_close(void *key, void *value, void *user_data);\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to close a logger plugin\n * @param[in] key Key of the events hash table (package name)\n * @param[in] value The janus_logger instance to close\n * @param[in] user_data User provided data (unused) */\nvoid janus_loggerso_close(void *key, void *value, void *user_data);\n///@}\n\n/** @name Janus plugin management\n * As anticipated, the server doesn't provide any specific feature: it takes\n * care of WebRTC-related stuff, and of sending and receiving JSON-based\n * messages. To implement applications based on these foundations, plugins\n * can be used. These plugins are shared objects that need to implement\n * the interfaces defined in plugin.h and as such are dynamically loaded\n * by the server at startup, and unloaded when the server closes.\n */\n///@{\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to destroy a plugin instance\n * @param[in] key Key of the plugins hash table (package name)\n * @param[in] value The janus_plugin plugin instance to destroy\n * @param[in] user_data User provided data (unused) */\nvoid janus_plugin_close(void *key, void *value, void *user_data);\n/*! \\brief Callback (g_hash_table_foreach) invoked when it's time to close a plugin\n * @param[in] key Key of the plugins hash table (package name)\n * @param[in] value The janus_plugin plugin instance to close\n * @param[in] user_data User provided data (unused) */\nvoid janus_pluginso_close(void *key, void *value, void *user_data);\n/*! \\brief Method to return a registered plugin instance out of its package name\n * @param[in] package The unique package name of the plugin\n * @returns The plugin instance */\njanus_plugin *janus_plugin_find(const gchar *package);\n///@}\n\n/*! \\brief Helper method to return the path to the provided server certificate */\ngchar *janus_get_server_pem(void);\n/*! \\brief Helper method to return the path to the provided server certificate key */\ngchar *janus_get_server_key(void);\n\n\n/*! \\brief Helper method to return the local IP address (autodetected by default) */\ngchar *janus_get_local_ip(void);\n/*! \\brief Helper method to return a given public IP address to use in the SDP (if multiple are configured for 1-1 NAT) */\ngchar *janus_get_public_ip(guint index);\n/*! \\brief Helper method to return the number of public IP addresses (if configured for 1-1 NAT) */\nguint janus_get_public_ip_count(void);\n/*! \\brief Helper method to add an IP address to use in the SDP */\nvoid janus_add_public_ip(const char *ip);\n/*! \\brief Helper method to check if we have at least one manually passed public IPv4 address (for 1-1 NAT management) */\ngboolean janus_has_public_ipv4_ip(void);\n/*! \\brief Helper method to check if we have at least one manually passed public IPv6 address (for 1-1 NAT management) */\ngboolean janus_has_public_ipv6_ip(void);\n\n/*! \\brief Helper method to check whether the server is being shut down */\ngint janus_is_stopping(void);\n\n/*! \\brief Helper method to check whether WebRTC encryption is (as it should) enabled\n * \\note This is required by the ICE and DTLS portions of the code to decide whether\n * to do the DTLS handshake, and whether SRTP encryption is required. Disabling the\n * WebRTC encryption is supposed to be a debugging tool you can use with the Chrome\n * setting with the same name, \\c --disable-webrtc-encryption , and you should\n * NEVER use it otherwise (it would simply not work with regular WebRTC endpoints).\n * @returns TRUE if WebRTC encryption is enabled (the default), and FALSE otherwise */\ngboolean janus_is_webrtc_encryption_enabled(void);\n\n#endif\n"
  },
  {
    "path": "src/log.c",
    "content": "/*! \\file     log.c\n * \\author    Jay Ridgeway <jayridge@gmail.com>\n * \\copyright GNU General Public License v3\n * \\brief     Buffered logging\n * \\details   Implementation of a simple buffered logger designed to remove\n * I/O wait from threads that may be sensitive to such delays. Each time\n * there's stuff to be written (to stdout, log files, or external loggers),\n * it's added to an async queue, which is consumed from a dedicated thread\n * that then actually takes care of I/O.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <errno.h>\n#include <string.h>\n#include <unistd.h>\n\n#include \"log.h\"\n#include \"debug.h\"\n#include \"utils.h\"\n#include \"loggers/logger.h\"\n\n#define THREAD_NAME \"log\"\n\ntypedef struct janus_log_buffer {\n\tint64_t timestamp;\n\tchar *str;\n} janus_log_buffer;\nstatic janus_log_buffer exit_message;\nstatic janus_log_buffer reload_message;\nstatic void janus_log_buffer_free(janus_log_buffer *b) {\n\tif(b == NULL || b == &exit_message)\n\t\treturn;\n\tg_free(b->str);\n\tg_free(b);\n}\n\nstatic GAsyncQueue *janus_log_queue = NULL;\nstatic GThread *log_thread = NULL;\n\nstatic gboolean janus_log_console = TRUE;\nstatic char *janus_log_filepath = NULL;\nstatic FILE *janus_log_file = NULL;\n\nstatic GHashTable *external_loggers = NULL;\n\nstatic volatile gint initialized = 0;\nstatic gint stopping = 0;\n\ngboolean janus_log_is_stdout_enabled(void) {\n\treturn janus_log_console;\n}\n\ngboolean janus_log_is_logfile_enabled(void) {\n\treturn janus_log_file != NULL;\n}\n\nchar *janus_log_get_logfile_path(void) {\n\treturn janus_log_filepath;\n}\n\nstatic void janus_log_print_buffer(janus_log_buffer *b) {\n\tif(janus_log_console)\n\t\tfputs(b->str, stdout);\n\tif(janus_log_file)\n\t\tfputs(b->str, janus_log_file);\n\tif(external_loggers != NULL) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, external_loggers);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_logger *l = value;\n\t\t\tif(l == NULL)\n\t\t\t\tcontinue;\n\t\t\tl->incoming_logline(b->timestamp, b->str);\n\t\t}\n\t}\n\t/* Flush the buffers */\n\tif(janus_log_console)\n\t\tfflush(stdout);\n\tif(janus_log_file)\n\t\tfflush(janus_log_file);\n}\n\nstatic void *janus_log_thread(void *ctx) {\n\tjanus_log_buffer *b = NULL;\n\n\twhile(!g_atomic_int_get(&stopping)) {\n\t\tb = g_async_queue_pop(janus_log_queue);\n\t\tif(b == NULL || b == &exit_message)\n\t\t\tbreak;\n\t\tif(b == &reload_message) {\n\t\t\tJANUS_PRINT(\"Got a log reload request.\\n\");\n\t\t\t/* Ensure everything in the buffer has been written before reopening the file */\n\t\t\twhile((b = g_async_queue_try_pop(janus_log_queue)) != NULL) {\n\t\t\t\tif(b->str != NULL)\n\t\t\t\t\tjanus_log_print_buffer(b);\n\t\t\t\tjanus_log_buffer_free(b);\n\t\t\t}\n\t\t\tfflush(janus_log_file);\n\t\t\tfclose(janus_log_file);\n\t\t\t/* Now let's start using the log file again */\n\t\t\tjanus_log_file = fopen(janus_log_filepath, \"awt\");\n\t\t\tif(janus_log_file == NULL) {\n\t\t\t\tJANUS_PRINT(\"Error opening log file %s: %s\\n\", janus_log_filepath, g_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif(b->str == NULL) {\n\t\t\tjanus_log_buffer_free(b);\n\t\t\tcontinue;\n\t\t}\n\t\t/* We have something to log */\n\t\tjanus_log_print_buffer(b);\n\t\t/* Done, get rid of this log line */\n\t\tjanus_log_buffer_free(b);\n\t}\n\t/* Print all that's left to print */\n\twhile((b = g_async_queue_try_pop(janus_log_queue)) != NULL) {\n\t\tif(b->str != NULL)\n\t\t\tjanus_log_print_buffer(b);\n\t\tjanus_log_buffer_free(b);\n\t}\n\tif(janus_log_console)\n\t\tfflush(stdout);\n\tif(janus_log_file)\n\t\tfflush(janus_log_file);\n\n\tif(janus_log_file)\n\t\tfclose(janus_log_file);\n\tjanus_log_file = NULL;\n\tg_free(janus_log_filepath);\n\tjanus_log_filepath = NULL;\n\n\treturn NULL;\n}\n\nvoid janus_vprintf(const char *format, ...) {\n\tif(g_atomic_int_get(&stopping))\n\t\treturn;\n\tif(janus_log_queue == NULL)\n\t\tjanus_log_queue = g_async_queue_new_full((GDestroyNotify)janus_log_buffer_free);\n\t/* Serialize it to a string */\n\tva_list ap;\n\tva_start(ap, format);\n\tchar *str = NULL;\n\tint len = g_vasprintf(&str, format, ap);\n\tva_end(ap);\n\tif(len < 0 || str == NULL)\n\t\treturn;\n\t/* Queue the new log buffer */\n\tjanus_log_buffer *b = g_malloc(sizeof(janus_log_buffer));\n\tb->timestamp = janus_get_real_time();\n\tb->str = str;\n\tg_async_queue_push(janus_log_queue, b);\n}\n\nint janus_log_init(gboolean daemon, gboolean console, const char *logfile, GHashTable *loggers) {\n\t/* Make sure we only initialize once */\n\tif(!g_atomic_int_compare_and_exchange(&initialized, 0, 1))\n\t\treturn 0;\n\tif(console) {\n\t\t/* Set stdout to block buffering, see BUFSIZ in stdio.h */\n\t\tsetvbuf(stdout, NULL, _IOFBF, 0);\n\t}\n\tjanus_log_console = console;\n\texternal_loggers = loggers;\n\tif(logfile != NULL) {\n\t\t/* Open a log file for writing (and append) */\n\t\tjanus_log_file = fopen(logfile, \"awt\");\n\t\tif(janus_log_file == NULL) {\n\t\t\tJANUS_PRINT(\"Error opening log file %s: %s\\n\", logfile, g_strerror(errno));\n\t\t\tgoto error;\n\t\t}\n\t\tjanus_log_filepath = g_strdup(logfile);\n\t}\n\tif(external_loggers != NULL)\n\t\tJANUS_PRINT(\"Adding %d external loggers\\n\", g_hash_table_size(external_loggers));\n\tif(!janus_log_console && logfile == NULL && external_loggers == NULL) {\n\t\tJANUS_PRINT(\"WARNING: logging completely disabled!\\n\");\n\t\tJANUS_PRINT(\"         (no stdout, no logfile and no external loggers, this may not be what you want...)\\n\");\n\t}\n\tif(daemon && !console) {\n\t\t/* Replace the standard file descriptors */\n\t\tif(freopen(\"/dev/null\", \"r\", stdin) == NULL) {\n\t\t\tJANUS_PRINT(\"Error replacing stdin with /dev/null\\n\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(freopen(\"/dev/null\", \"w\", stdout) == NULL) {\n\t\t\tJANUS_PRINT(\"Error replacing stdout with /dev/null\\n\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(freopen(\"/dev/null\", \"w\", stderr) == NULL) {\n\t\t\tJANUS_PRINT(\"Error replacing stderr with /dev/null\\n\");\n\t\t\tgoto error;\n\t\t}\n\t}\n\tif(janus_log_queue == NULL)\n\t\tjanus_log_queue = g_async_queue_new_full((GDestroyNotify)janus_log_buffer_free);\n\tlog_thread = g_thread_new(THREAD_NAME, &janus_log_thread, NULL);\n\treturn 0;\n\nerror:\n\tg_atomic_int_set(&initialized, 0);\n\tjanus_log_destroy();\n\treturn -1;\n}\n\nvoid janus_log_reload(void) {\n\tif(janus_log_file == NULL || log_thread == NULL)\n\t\treturn;\n\tg_async_queue_push(janus_log_queue, &reload_message);\n}\n\nvoid janus_log_destroy(void) {\n\tg_atomic_int_set(&stopping, 1);\n\tif(log_thread != NULL) {\n\t\tg_async_queue_push(janus_log_queue, &exit_message);\n\t\tg_thread_join(log_thread);\n\t} else if(!g_atomic_int_get(&initialized)) {\n\t\t/* Never initialized Print what was in the buffer to stdout */\n\t\tjanus_log_buffer *b = NULL;\n\t\twhile((b = g_async_queue_try_pop(janus_log_queue)) != NULL) {\n\t\t\tif(b->str != NULL)\n\t\t\t\tjanus_log_print_buffer(b);\n\t\t\tjanus_log_buffer_free(b);\n\t\t}\n\t}\n\tg_async_queue_unref(janus_log_queue);\n\tjanus_log_queue = NULL;\n}\n"
  },
  {
    "path": "src/log.h",
    "content": "/*! \\file    log.h\n * \\author   Jay Ridgeway <jayridge@gmail.com>\n * \\copyright GNU General Public License v3\n * \\brief    Buffered logging (headers)\n * \\details   Implementation of a simple buffered logger designed to remove\n * I/O wait from threads that may be sensitive to such delays. Each time\n * there's stuff to be written (to stdout, log files, or external loggers),\n * it's added to an async queue, which is consumed from a dedicated thread\n * that then actually takes care of I/O.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_LOG_H\n#define JANUS_LOG_H\n\n#include <stdio.h>\n#include <glib.h>\n\n/*! \\brief Buffered vprintf\n* @param[in] format Format string as defined by glib, followed by the\n* optional parameters to insert into formatted string (printf style)\n* \\note This output is buffered and may not appear immediately on stdout. */\nvoid janus_vprintf(const char *format, ...) G_GNUC_PRINTF(1, 2);\n\n/*! \\brief Log initialization\n* \\note This should be called before attempting to use the logger. A buffer\n* pool and processing thread are created.\n* @param daemon Whether the Janus is running as a daemon or not\n* @param console Whether the output should be printed on stdout or not\n* @param logfile Log file to save the output to, if any\n* @param loggers Hash table of external loggers registered in the core, if any\n* @returns 0 in case of success, a negative integer otherwise */\nint janus_log_init(gboolean daemon, gboolean console, const char *logfile, GHashTable *loggers);\n/*! \\brief Reopen log file (log rotation) */\nvoid janus_log_reload(void);\n/*! \\brief Log destruction */\nvoid janus_log_destroy(void);\n\n/*! \\brief Method to check whether stdout logging is enabled\n * @returns TRUE if stdout logging is enabled, FALSE otherwise */\ngboolean janus_log_is_stdout_enabled(void);\n/*! \\brief Method to check whether file-based logging is enabled\n * @returns TRUE if file-based logging is enabled, FALSE otherwise */\ngboolean janus_log_is_logfile_enabled(void);\n/*! \\brief Method to get the path to the log file\n * @returns The full path to the log file, or NULL otherwise */\nchar *janus_log_get_logfile_path(void);\n\n#endif\n"
  },
  {
    "path": "src/loggers/janus_jsonlog.c",
    "content": "/*! \\file   janus_jsonlog.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus JSON logger plugin\n * \\details  This is a trivial logger plugin for Janus, which is only\n * there to showcase how you can implement your own external logger for\n * log lines coming from the Janus core or one of the plugins. This\n * specific logger plugin serializes log lines to a JSON object and\n * saves them all to a configured local file.\n *\n * \\ingroup loggers\n * \\ref loggers\n */\n\n#include <errno.h>\n\n#include \"logger.h\"\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../utils.h\"\n\n\n/* Plugin information */\n#define JANUS_JSONLOG_VERSION\t\t\t1\n#define JANUS_JSONLOG_VERSION_STRING\t\"0.0.1\"\n#define JANUS_JSONLOG_DESCRIPTION\t\t\"This is a trivial sample logger plugin for Janus, which saves log lines to a local JSON file.\"\n#define JANUS_JSONLOG_NAME\t\t\t\t\"JANUS JSON logger plugin\"\n#define JANUS_JSONLOG_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_JSONLOG_PACKAGE\t\t\t\"janus.logger.jsonlog\"\n\n/* Plugin methods */\njanus_logger *create(void);\nint janus_jsonlog_init(const char *server_name, const char *config_path);\nvoid janus_jsonlog_destroy(void);\nint janus_jsonlog_get_api_compatibility(void);\nint janus_jsonlog_get_version(void);\nconst char *janus_jsonlog_get_version_string(void);\nconst char *janus_jsonlog_get_description(void);\nconst char *janus_jsonlog_get_name(void);\nconst char *janus_jsonlog_get_author(void);\nconst char *janus_jsonlog_get_package(void);\nvoid janus_jsonlog_incoming_logline(int64_t timestamp, const char *line);\njson_t *janus_jsonlog_handle_request(json_t *request);\n\n/* Logger setup */\nstatic janus_logger janus_jsonlog =\n\tJANUS_LOGGER_INIT (\n\t\t.init = janus_jsonlog_init,\n\t\t.destroy = janus_jsonlog_destroy,\n\n\t\t.get_api_compatibility = janus_jsonlog_get_api_compatibility,\n\t\t.get_version = janus_jsonlog_get_version,\n\t\t.get_version_string = janus_jsonlog_get_version_string,\n\t\t.get_description = janus_jsonlog_get_description,\n\t\t.get_name = janus_jsonlog_get_name,\n\t\t.get_author = janus_jsonlog_get_author,\n\t\t.get_package = janus_jsonlog_get_package,\n\n\t\t.incoming_logline = janus_jsonlog_incoming_logline,\n\t\t.handle_request = janus_jsonlog_handle_request,\n\t);\n\n/* Plugin creator */\njanus_logger *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_JSONLOG_NAME);\n\treturn &janus_jsonlog;\n}\n\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic GThread *logger_thread = NULL;\nstatic void *janus_jsonlog_thread(void *data);\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Queue of log lines to handle */\nstatic GAsyncQueue *loglines = NULL;\n\n/* Structure we use for queueing log lines */\ntypedef struct janus_jsonlog_line {\n\tint64_t timestamp;\t\t/* When the log line was printed */\n\tchar *line;\t\t\t\t/* Content of the log line */\n} janus_jsonlog_line;\nstatic janus_jsonlog_line exit_line;\nstatic void janus_jsonlog_line_free(janus_jsonlog_line *jline) {\n\tif(!jline || jline == &exit_line)\n\t\treturn;\n\tg_free(jline->line);\n\tg_free(jline);\n}\n\n/* File to save the log to */\nstatic FILE *logfile = NULL;\nstatic char *logfilename = NULL;\n\n\n/* Parameter validation (for querying or tweaking via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\n/* Error codes for the Admin API interaction */\n#define JANUS_JSONLOG_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_JSONLOG_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_JSONLOG_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_JSONLOG_ERROR_UNKNOWN_ERROR\t\t499\n\n\n/* Plugin implementation */\nint janus_jsonlog_init(const char *server_name, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tgboolean enabled = FALSE;\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_JSONLOG_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_JSONLOG_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_JSONLOG_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\t/* Handle configuration */\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\n\t\t/* Setup the logger, if required */\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"JSON logger disabled\\n\");\n\t\t} else {\n\t\t\t/* File to save log to */\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"filename\");\n\t\t\tif(!item || !item->value) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"No filename for the JSON logger specified\\n\");\n\t\t\t} else {\n\t\t\t\tlogfilename = g_strdup(item->value);\n\t\t\t\tlogfile = fopen(logfilename, \"a\");\n\t\t\t\tif(logfile == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_FATAL, \"Error opening file '%s' (%d, %s)\\n\",\n\t\t\t\t\t\tlogfilename, errno, g_strerror(errno));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* Check the JSON indentation */\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\t\t\tif(item && item->value) {\n\t\t\t\t/* Check how we need to format/serialize the JSON output */\n\t\t\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Done */\n\t\t\tenabled = (logfile != NULL);\n\t\t}\n\t}\n\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\tif(!enabled) {\n\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t}\n\tJANUS_LOG(LOG_VERB, \"JSON logger configured: %s\\n\", logfilename);\n\n\t/* Initialize the log queue */\n\tloglines = g_async_queue_new_full((GDestroyNotify) janus_jsonlog_line_free);\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming log lines */\n\tGError *error = NULL;\n\tlogger_thread = g_thread_try_new(\"janus jsonlog thread\", janus_jsonlog_thread, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the JSON logger thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_JSONLOG_NAME);\n\treturn 0;\n}\n\nvoid janus_jsonlog_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(loglines, &exit_line);\n\tif(logger_thread != NULL) {\n\t\tg_thread_join(logger_thread);\n\t\tlogger_thread = NULL;\n\t}\n\n\tg_async_queue_unref(loglines);\n\tloglines = NULL;\n\n\tif(logfile != NULL) {\n\t\tfflush(logfile);\n\t\tfclose(logfile);\n\t}\n\tg_free(logfilename);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_JSONLOG_NAME);\n}\n\nint janus_jsonlog_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_LOGGER_API_VERSION;\n}\n\nint janus_jsonlog_get_version(void) {\n\treturn JANUS_JSONLOG_VERSION;\n}\n\nconst char *janus_jsonlog_get_version_string(void) {\n\treturn JANUS_JSONLOG_VERSION_STRING;\n}\n\nconst char *janus_jsonlog_get_description(void) {\n\treturn JANUS_JSONLOG_DESCRIPTION;\n}\n\nconst char *janus_jsonlog_get_name(void) {\n\treturn JANUS_JSONLOG_NAME;\n}\n\nconst char *janus_jsonlog_get_author(void) {\n\treturn JANUS_JSONLOG_AUTHOR;\n}\n\nconst char *janus_jsonlog_get_package(void) {\n\treturn JANUS_JSONLOG_PACKAGE;\n}\n\nvoid janus_jsonlog_incoming_logline(int64_t timestamp, const char *line) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized) || line == NULL) {\n\t\t/* Janus is closing or the plugin is */\n\t\treturn;\n\t}\n\n\t/* Do NOT handle the log line here in this callback! Since Janus sends\n\t * log lines from its internal logger thread, performing I/O or network\n\t * operations in here could dangerously slow Janus down. Let's just\n\t * duplicate and enqueue the string containing the log line, and handle\n\t * it in our own thread: we have a monotonic time indicator of when the\n\t * log line was actually added on this machine, so that, if relevant, we can\n\t * compute any delay in the actual log line processing ourselves. */\n\tjanus_jsonlog_line *l = g_malloc(sizeof(janus_jsonlog_line));\n\tl->timestamp = timestamp;\n\tl->line = g_strdup(line);\n\tg_async_queue_push(loglines, l);\n\n}\n\njson_t *janus_jsonlog_handle_request(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this requests to query the plugin or apply tweaks to the logic */\n\tjson_t *response = json_object();\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_JSONLOG_ERROR_MISSING_ELEMENT, JANUS_JSONLOG_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"info\")) {\n\t\t/* We only support a request to get some info from the plugin */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_JSONLOG_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n/* Thread to handle incoming log lines */\nstatic void *janus_jsonlog_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining JSON logger thread\\n\");\n\n\tjanus_jsonlog_line *jline = NULL;\n\tjson_t *json = NULL;\n\tchar *json_text = NULL;\n\tsize_t json_len = 0, offset = 0, written = 0;\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\t/* Get a log line from the queue */\n\t\tjline = g_async_queue_pop(loglines);\n\t\tif(jline == &exit_line)\n\t\t\tbreak;\n\n\t\t/* Create a new JSON object with its contents */\n\t\tjson = json_object();\n\t\tjson_object_set_new(json, \"timestamp\", json_integer(jline->timestamp));\n\t\tif(jline->line != NULL)\n\t\t\tjson_object_set_new(json, \"line\", json_string(jline->line));\n\t\tjanus_jsonlog_line_free(jline);\n\n\t\t/* Convert the JSON object to string */\n\t\tjson_text = json_dumps(json, json_format);\n\t\tjson_decref(json);\n\n\t\t/* Save it to file */\n\t\tjson_len = strlen(json_text);\n\t\toffset = 0;\n\t\twhile(json_len > 0) {\n\t\t\twritten = fwrite(json_text + offset, sizeof(char), json_len, logfile);\n\t\t\tjson_len -= written;\n\t\t\toffset += written;\n\t\t}\n\t\tconst char *lf = \"\\n\";\n\t\tfwrite(lf, sizeof(char), strlen(lf), logfile);\n\t\tfflush(logfile);\n\t\tfree(json_text);\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving JSON logger thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/loggers/logger.h",
    "content": "/*! \\file   logger.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Modular Janus loggers (headers)\n * \\details  This header contains the definition of the callbacks all\n * the custom loggers need to implement to interact with the Janus core.\n * In fact, a custom logger is basically a module that receives log lines\n * from the Janus core and plugins, so that they can be handled somehow\n * (e.g., aggregated or forwarded elsewhere).\n *\n * An logger plugin that wants to register at the Janus core needs to\n * implement the \\c janus_logger interface. This includes callbacks\n * the Janus core can use to receive log lines to process. Besides, as a\n * logger plugin is a shared object, and as such external to the\n * core itself, in order to be dynamically loaded at startup it needs\n * to implement the \\c create_l() hook as well, that should return a\n * pointer to the plugin instance. This is an example of such a step:\n *\n\\verbatim\nstatic janus_logger mylogger = {\n\t[..]\n};\n\njanus_logger *create(void) {\n\tJANUS_LOG(LOG_VERB, , \"%s created!\\n\", MY_LOGGER_NAME);\n\treturn &mylogger;\n}\n\\endverbatim\n *\n * This will make sure that your logger plugin is loaded at startup\n * by the Janus core, if it is deployed in the proper folder.\n *\n * As anticipated and described in the above example, a logger plugin\n * must basically be an instance of the \\c janus_logger type. As such,\n * it must implement the following methods and callbacks for the core:\n *\n * - \\c init(): this is called by the Janus core as soon as your logger\n * plugin is started; this is where you should setup your logger plugin\n * (e.g., static stuff and reading the configuration file);\n * - \\c destroy(): on the other hand, this is called by the core when it\n * is shutting down, and your logger plugin should too;\n * - \\c get_api_compatibility(): this method MUST return JANUS_LOGGER_API_VERSION;\n * - \\c get_version(): this method should return a numeric version identifier (e.g., 3);\n * - \\c get_version_string(): this method should return a verbose version identifier (e.g., \"v1.0.1\");\n * - \\c get_description(): this method should return a verbose description of your logger plugin (e.g., \"This is a logger that saves log lines on a database\");\n * - \\c get_name(): this method should return a short display name for your logger plugin (e.g., \"My Amazing Logger\");\n * - \\c get_package(): this method should return a unique package identifier for your logger plugin (e.g., \"janus.logger.mylogger\");\n * - \\c incoming_logline(): this callback informs the logger that a log line is available for consumption.\n *\n * All the above methods and callbacks are mandatory: the Janus core will\n * reject al logger plugin that doesn't implement any of the mandatory callbacks.\n *\n * Unlike other kind of modules (transports, plugins), the \\c init() method\n * here only passes the path to the configurations files folder, as loggers\n * never need to contact the Janus core themselves. This path can be used to\n * read and parse a configuration file for the logger plugin: the logger\n * plugins we made available out of the box use the package name as a\n * name for the file (e.g., \\c janus.logger.json.jcfg for the sample\n * logger plugin), but you're free to use a different one, as long\n * as it doesn't collide with existing ones. Besides, the existing logger\n * plugins use the same libconfig format for configuration files the core\n * uses (relying on the \\c janus_config helpers for the purpose) but\n * again, if you prefer a different format (XML, JSON, etc.) that's up to you.\n *\n * \\ingroup loggerapi\n * \\ref loggerapi\n */\n\n#ifndef JANUS_LOGGER_H\n#define JANUS_LOGGER_H\n\n#include <stdlib.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n#include <ctype.h>\n#include <unistd.h>\n#include <inttypes.h>\n\n#include <glib.h>\n#include <jansson.h>\n\n#include \"../utils.h\"\n\n\n/*! \\brief Version of the API, to match the one logger plugins were compiled against */\n#define JANUS_LOGGER_API_VERSION\t3\n\n/*! \\brief Initialization of all logger plugin properties to NULL\n *\n * \\note All logger plugins MUST add this as the FIRST line when initializing\n * their logger plugin structure, e.g.:\n *\n\\verbatim\nstatic janus_logger janus_fake_logger_plugin =\n\t{\n\t\tJANUS_LOGGER_INIT,\n\n\t\t.init = janus_fake_init,\n\t\t[..]\n\\endverbatim\n */\n\n#define JANUS_LOGGER_INIT(...) {\t\t\t\\\n\t\t.init = NULL,\t\t\t\t\t\t\t\\\n\t\t.destroy = NULL,\t\t\t\t\t\t\\\n\t\t.get_api_compatibility = NULL,\t\t\t\\\n\t\t.get_version = NULL,\t\t\t\t\t\\\n\t\t.get_version_string = NULL,\t\t\t\t\\\n\t\t.get_description = NULL,\t\t\t\t\\\n\t\t.get_name = NULL,\t\t\t\t\t\t\\\n\t\t.get_author = NULL,\t\t\t\t\t\t\\\n\t\t.get_package = NULL,\t\t\t\t\t\\\n\t\t.incoming_logline = NULL,\t\t\t\t\\\n\t\t## __VA_ARGS__ }\n\n\n/*! \\brief The logger plugin session and callbacks interface */\ntypedef struct janus_logger janus_logger;\n\n\n/*! \\brief The logger plugin session and callbacks interface */\nstruct janus_logger {\n\t/*! \\brief Logger plugin initialization/constructor\n\t * @param[in] server_name Name of the Janus instance generating the logs\n\t * @param[in] config_path Path of the folder where the configuration for this logger plugin can be found\n\t * @returns 0 in case of success, a negative integer in case of error */\n\tint (* const init)(const char *server_name, const char *config_path);\n\t/*! \\brief Logger plugin deinitialization/destructor */\n\tvoid (* const destroy)(void);\n\n\t/*! \\brief Informative method to request the API version this logger plugin was compiled against\n\t *  \\note All logger plugins MUST implement this method and return JANUS_LOGGER_API_VERSION\n\t * to make this work, or they will be rejected by the core. */\n\tint (* const get_api_compatibility)(void);\n\t/*! \\brief Informative method to request the numeric version of the logger plugin */\n\tint (* const get_version)(void);\n\t/*! \\brief Informative method to request the string version of the logger plugin */\n\tconst char *(* const get_version_string)(void);\n\t/*! \\brief Informative method to request a description of the logger plugin */\n\tconst char *(* const get_description)(void);\n\t/*! \\brief Informative method to request the name of the logger plugin */\n\tconst char *(* const get_name)(void);\n\t/*! \\brief Informative method to request the author of the logger plugin */\n\tconst char *(* const get_author)(void);\n\t/*! \\brief Informative method to request the package name of the logger plugin (what will be used in web applications to refer to it) */\n\tconst char *(* const get_package)(void);\n\n\t/*! \\brief Method to notify the logger plugin that a new log line is available\n\t * \\details All log lines are notified as a string\n\t * \\note Do NOT handle the log line directly in this method. Janus sends\n\t * log lines from its internal logger thread, so any I/O or blocking thing\n\t * you may be doing here would most likely end up slowing it down. Just take\n\t * note of it and handle it somewhere else. It's your responsibility to\n\t * duplicate the string to use it later: the string you get in the callback\n\t * is NOT a copy, and MUST NOT be modified.\n\t * @param[in] timestamp Monotonic timestamp of when the log line was printed\n\t * @param[in] line String containing the log line */\n\tvoid (* const incoming_logline)(int64_t timestamp, const char *line);\n\n\t/*! \\brief Method to send a request to this specific logger plugin\n\t * \\details The method takes a Jansson json_t, that contains all the info related\n\t * to the request. This object will come from an Admin API request, and is\n\t * meant to represent a synchronous request. Since each logger can have\n\t * its own bells and whistles, there's no constraint on what this object should\n\t * contain, which is entirely logger-specific. A json_t object needs to be\n\t * returned as a response, which will be sent in response to the Admin API call.\n\t * This can be useful to tweak settings in real-time, or to probe the internals\n\t * of the logger plugin for monitoring purposes.\n\t * @param[in] request Jansson object containing the request\n\t * @returns A Jansson object containing the response for the client */\n\tjson_t *(* const handle_request)(json_t *request);\n};\n\n/*! \\brief The hook that logger plugins need to implement to be created from the Janus core */\ntypedef janus_logger* create_l(void);\n\n#endif\n"
  },
  {
    "path": "src/mach_gettime.h",
    "content": "#ifndef MACH_GETTIME_H\n#define MACH_GETTIME_H\n\n#include <sys/types.h>\n#include <sys/_types/_timespec.h>\n#include <mach/mach.h>\n#include <mach/clock.h>\n#include <mach/mach_time.h>\n\n/* The opengroup spec isn't clear on the mapping from REALTIME to CALENDAR\n being appropriate or not.\n http://pubs.opengroup.org/onlinepubs/009695299/basedefs/time.h.html */\n\n// XXX only supports a single timer\n#define TIMER_ABSTIME -1\n#define CLOCK_REALTIME CALENDAR_CLOCK\n#define CLOCK_MONOTONIC SYSTEM_CLOCK\n\n#if !defined(__DARWIN_C_LEVEL) || __DARWIN_C_LEVEL < 199309L\ntypedef int clockid_t;\n#endif\n\n/* the mach kernel uses struct mach_timespec, so struct timespec\n    is loaded from <sys/_types/_timespec.h> for compatibility */\n// struct timespec { time_t tv_sec; long tv_nsec; };\n\nint clock_gettime(clockid_t clk_id, struct timespec *tp);\n\n#include <mach/mach_time.h>\n\n#define MT_NANO (+1.0E-9)\n#define MT_GIGA UINT64_C(1000000000)\n\n// TODO create a list of timers,\nstatic double mt_timebase = 0.0;\nstatic uint64_t mt_timestart = 0;\n\n// TODO be more careful in a multithreaded environment\nint clock_gettime(clockid_t clk_id, struct timespec *tp)\n{\n    kern_return_t retval = KERN_SUCCESS;\n    if( clk_id == TIMER_ABSTIME)\n    {\n        if (!mt_timestart) { // only one timer, initialized on the first call to the TIMER\n            mach_timebase_info_data_t tb = { 0 };\n            mach_timebase_info(&tb);\n            mt_timebase = tb.numer;\n            mt_timebase /= tb.denom;\n            mt_timestart = mach_absolute_time();\n        }\n\n        double diff = (mach_absolute_time() - mt_timestart) * mt_timebase;\n        tp->tv_sec = diff * MT_NANO;\n        tp->tv_nsec = diff - (tp->tv_sec * MT_GIGA);\n    }\n    else // other clk_ids are mapped to the corresponding mach clock_service\n    {\n        clock_serv_t cclock;\n        mach_timespec_t mts;\n\n        host_get_clock_service(mach_host_self(), clk_id, &cclock);\n        retval = clock_get_time(cclock, &mts);\n        mach_port_deallocate(mach_task_self(), cclock);\n\n        tp->tv_sec = mts.tv_sec;\n        tp->tv_nsec = mts.tv_nsec;\n    }\n\n    return retval;\n}\n#endif\n"
  },
  {
    "path": "src/mainpage.dox",
    "content": "/*!\n * \\mainpage Janus - General purpose WebRTC server\n *\n * <div class=\"alert alert-warning\">\n *\t\t<b>Note Well:</b> these are the demos and documentation for the <code>multistream</code> version\n *\t\tof Janus, which is a new version. If you want to check the previous version of\n *\t\tJanus instead (i.e., <code>0.x</code>, a.k.a. \"legacy\") click\n *\t\t<a href=\"https://janus-legacy.conf.meetecho.com\">here</a> instead.\n * </div>\n *\n * \\par Developer Documentation for the Janus WebRTC server\n * This is the main developer documentation for the Janus WebRTC\n * Server, generated with the help of\n * <a href=\"http://www.doxygen.org\">Doxygen</a>. Make sure you\n * check the \\ref DEPS before attempting a compilation. If you are\n * interested in how to compile, install and use Janus,\n * checkout the \\ref README information. A \\ref FAQ page is also available,\n * as well as an overview on \\ref CHANGELOG.\n *\n * \\par A general purpose WebRTC server\n * The Janus WebRTC Server has been conceived as a <tt>general purpose</tt>\n * server. As such, it doesn't provide any functionality per se\n * other than implementing the means to set up a WebRTC media communication\n * with a browser, exchanging JSON messages with it, and relaying RTP/RTCP\n * and messages between browsers and the server-side application logic they're attached to. Any specific\n * feature/application needs to be implemented in server side plugins,\n * that browsers can then contact via the Janus core to take advantage of\n * the functionality they provide. Example of such plugins can be\n * implementations of applications like echo tests, conference bridges,\n * media recorders, SIP gateways and the like.\n *\n * The reason for this is simple: we wanted something that would have a\n * <tt>small footprint</tt> (hence a C implementation) and that we could only\n * equip with what was <tt>really needed</tt> (hence pluggable modules). That is,\n * something that would allow us to deploy either a full-fledged WebRTC\n * server on the cloud, or a small nettop/box to handle a specific use case.\n *\n * \\par Architecture and APIs\n * The core of the server is specified in the \\ref core section. The protocols\n * implemented in the Janus core are listed in the \\ref protocols group\n * instead. A list of plugins provided out of the box by Meetecho are\n * documented in the \\ref pluginslist page: these plugins can be changed\n * or extended to match your requirements, or just used as a simple\n * reference should you be interested in writing a new plugin from\n * scratch (and you're definitely welcome to!). A \\ref pluginapi to\n * create new plugins, or understand how they're conceived, is documented\n * as well. A documentation on the available API transports and the\n * HTTP/WebSocket JavaScript API to use Janus and the plugins it\n * makes available in your web application can be found in the \\ref JS\n * and \\ref rest pages. New API transports can be created referring to\n * the \\ref transportapi page. If you're interested in monitoring Janus\n * resources, you can refer to the \\ref admin page. Event handler\n * plugins can also be used for the purpose: refer to the \\ref eventhandlers\n * and, for more developer oriented info, to the \\ref eventhandlerapi\n * page if you're interested in creating your own. For what concerns logging,\n * out of the box Janus supports printing the output to \\c stdout and saving\n * to a local file, but logging can also be extended via additional plugins:\n * refer to the \\ref loggerapi for more details, and to learn how to write\n * your own. You can check the \\ref ide page for some details about how\n * to setup your local development environment for Janus..\n *\n * Janus also supports \\ref recordings out of the box, so check the\n * related documentation to know more about how that works.\n *\n * Finally, some information on how to deploy Janus and your web\n * applications that want to make use of it are provided in the \\ref deploy\n * page. If you're interested in starting Janus as a service/daemon rather\n * than launching it normally, check the information provided in the\n * \\ref service page. Some additional \\ref resources are also listed in\n * case you're interested in talking to Janus from different languages\n * and platforms.\n *\n * To conclude, the \\ref debug page contains info on how you can help us\n * fixing issues you might encounter along the road.\n *\n * \\section copyright Copyright and author\n *\n * Janus WebRTC Server © 2014-2026 <a href=\"https://www.meetecho.com/\">Meetecho</a> (https://www.meetecho.com/)\n *\n * \\author Lorenzo Miniero <lorenzo@meetecho.com> ( \\ref CREDITS )\n *\n * \\section license License\n * This program is free software, distributed under the terms of\n * the GNU General Public License Version 3. For more details and licensing\n * options, including a commercial license, see the \\ref COPYING page.\n *\n*/\n\n/*! \\page DEPS Dependencies\n *\n * The application and the plugins depend on the following open source\n * software and libraries, so make sure you install the related development\n * versions before attempting a compilation:\n *\n * - \\b GLib: http://library.gnome.org/devel/glib/\n * - \\b pkg-config: http://www.freedesktop.org/wiki/Software/pkg-config/\n * - \\b Jansson: http://www.digip.org/jansson/ (JSON)\n * - \\b libconfig: https://hyperrealm.github.io/libconfig/ (configuration files)\n * - \\b libnice: https://libnice.freedesktop.org/ (ICE/STUN/TURN, at least v0.1.16 suggested, v0.1.18 recommended)\n * - \\b OpenSSL: http://www.openssl.org/ (DTLS, at least v1.0.1e)\n * - \\b libsrtp: https://github.com/cisco/libsrtp (SRTP, at least v2.x suggested)\n * - \\b usrsctp: https://github.com/sctplab/usrsctp (\\c optional, Data Channels)\n * - \\b libmicrohttpd: http://www.gnu.org/software/libmicrohttpd/ (\\c optional, v0.9.59, Web server)\n * - \\b libwebsockets: https://libwebsockets.org/ (\\c optional, at least v4.x suggested, WebSockets)\n * - \\b rabbitmq-c: https://github.com/alanxz/rabbitmq-c (\\c optional, v1.0.4, RabbitMQ)\n * - \\b paho.mqtt.c: https://eclipse.org/paho/clients/c (\\c optional, v1.1.0 for MQTT v3.1 & v3.1.1 or v1.3.0 for MQTT v5)\n * - \\b nanomsg: https://nanomsg.org/ (\\c optional, Nanomsg)\n * - \\b Sofia-SIP: https://github.com/freeswitch/sofia-sip (\\c optional, only needed for the SIP plugin)\n * - \\b libopus: http://opus-codec.org/ (\\c optional, only needed for the AudioBridge plugin)\n * - \\b libogg: http://xiph.org/ogg/ (\\c optional, only needed for the AudioBridge and Streaming plugins)\n * - \\b libcurl: https://curl.haxx.se/libcurl/ (\\c optional, only needed for the TURN REST API,\n * RTSP support in the Streaming plugin and the sample Event Handler plugin)\n * - \\b zlib: https://zlib.net/ (gzip compression utility)\n * - \\b Lua: https://www.lua.org/download.html (\\c optional, only needed for the Lua plugin)\n * - \\b Duktape: https://duktape.org/ (\\c optional, only needed for the Duktape plugin)\n * - \\b npm: https://docs.npmjs.com/ (\\c optional, used during build for generating JavaScript modules)\n *\n */\n\n/*! \\page JS JavaScript API\n * Janus exposes, assuming the HTTP transport has been compiled, a\n * pseudo-RESTful interface, and optionally also WebSocket/RabbitMQ/MQTT/Nanomsg/UnixSockets\n * interfaces as well, all of which based on JSON messages. These\n * interfaces are described in more detail in the \\ref plainhttp \\ref WS\n * \\ref rabbit \\ref apimqtt \\ref apinanomsg and \\ref unix documentation respectively, and all allow clients to\n * take advantage of the features provided by Janus and the functionality\n * made available by its plugins. Considering most clients will be web browsers,\n * a common choice will be to rely on either the REST or the WebSockets\n * interface for the purpose. To make things easier for web\n * developers, a JavaScript library (\\c janus.js) is available that can\n * make use of both interfaces using exactly the same API. This library\n * eases the task of creating sessions with the Janus core, attaching WebRTC\n * users to plugins, send and receive requests and events to the plugins\n * themselves and so on. For real examples of how this library can be\n * used, check the demos in the \\b html folder of this package. Notice\n * that the \\c janus.js library makes use of the features made available\n * by the <a href=\"https://github.com/webrtc/adapter\">webrtc-adapter</a>\n * shim, which means that your web application should always include it\n * as a dependency. For instance, all the demos link to it externally via\n * <a href=\"https://cdnjs.com/\">cdnjs.com</a>.\n *\n * \\note The current \\c janus.js library allows you to provide custom implementations of\n * certain dependencies, in order to make it easier to integrate with other JavaScript\n * libraries and frameworks. Using this feature you can ensure \\c janus.js does not (implicitly)\n * depend on certain global variables. Two implementations are included in \\c janus.js itself:\n *\n *  -# \\ref js-default-deps which relies on native browser APIs,\n *     which in turn require somewhat more modern browsers\n *  -# \\ref js-old-deps which uses jQuery (http://jquery.com/) instead,\n *     and should provide equivalent behaviour to previous versions of \\c janus.js\n *\n * By default \\ref js-default-deps will be used, but you can override this\n * when initialising the Janus library and pass a custom dependencies object instead.\n * For details, refer to: \\ref js-dependencies\n *\n * In general, when using the Janus features, you would normally do the following:\n *\n * -# include the Janus JavaScript library in your web page;\n * -# initialize the Janus JavaScript library and (optionally) passing its dependencies;\n * -# connect to the server and create a session;\n * -# create one or more handles to attach to a plugin (e.g., echo test and/or streaming);\n * -# interact with the plugin (sending/receiving messages, negotiating a PeerConnection);\n * -# eventually, close all the handles and shutdown the related PeerConnections;\n * -# destroy the session.\n *\n * The above steps will be presented in order, describing how you can use\n * the low level API to accomplish them. Consider that in the future we might\n * provide higher level wrappers to this API to address specific needs, e.g.,\n * a higher level API for each plugin: this would make it even easier to use\n * the server features, as a high level API for the streaming plugin, for\n * instance, may just ask you to provide the server address and the ID of\n * the \\c &lt;video&gt; element to display the stream in, and would take care of all the\n * above mentioned steps on your behalf. Needless to say, you're very welcome\n * to provide wrapper APIs yourself, if you feel a sudden urge to do so! :-)\n *\n * \\section janusjs Using janus.js\n *\n * As a first step, you should include the Janus library in your project.\n * Depending on your needs you can either use \\c janus.js or one of the generated\n * JavaScript module variants of it. For available module syntaxes and how to build the\n * corresponding variants, see: \\ref js-modules\n *\n\\verbatim\n<script type=\"text/javascript\" src=\"janus.js\" ></script>\n\\endverbatim\n *\n * The core of the JavaScript API is the \\c Janus object. This object needs\n * to be initialized the first time it is used in a page. This can be done\n * using the static \\c init method of the object, which accepts the\n * following options:\n *\n * - \\c debug: whether debug should be enabled on the JavaScript console, and what levels\n *   - \\c true or \\c \"all\": all debuggers enabled (Janus.trace, Janus.debug, Janus.log, Janus.warn, Janus.error)\n *   - array (e.g., <code>[\"trace\", \"warn\"]</code>): only enable selected debuggers (allowed tokens: trace, debug, log, warn, error)\n *   - \\c false: disable all debuggers\n * - \\c callback: a user provided function that is invoked when the initialization is complete\n * - \\c dependencies: a user provided implementation of Janus library dependencies\n *\n * Here's an example:\n *\n *\n \\verbatim\nJanus.init({\n   debug: true,\n   dependencies: Janus.useDefaultDependencies(), // or: Janus.useOldDependencies() to get the behaviour of previous Janus versions\n   callback: function() {\n\t   // Done!\n   }\n});\n \\endverbatim\n *\n * \\note When using one of the JavaScript module variants of \\c janus.js, you\n * will need to import the \\c Janus symbol from the module first. See also: \\ref js-modules\n * For example, using the ECMAScript module variant, the above example should be altered to:\n *\n *\n \\verbatim\nimport * as Janus from './janus.es.js'\n\nJanus.init({\n   debug: true,\n   dependencies: Janus.useDefaultDependencies(), // or: Janus.useOldDependencies() to get the behaviour of previous Janus versions\n   callback: function() {\n\t   // Done!\n   }\n});\n \\endverbatim\n *\n * Once the library has been initialized, you can start creating sessions.\n * Normally, each browser tab will need a single session with the server: in\n * fact, each Janus session can contain several different plugin handles\n * at the same time, meaning you can start several different WebRTC sessions\n * with the same or different plugins for the same user using the same\n * Janus session. That said, you're free to set up different Janus\n * sessions in the same page, should you prefer so.\n *\n * Creating a session is quite easy. You just need to use the \\c new constructor\n * to create a new \\c Janus object that will handle your interaction with the\n * server. Considering the dynamic and asynchronous nature of Janus sessions\n * (events may occur at any time), there are several properties and callbacks you\n * can configure when creating a session:\n *\n * - \\c server: the address of the server as a specific address (e.g.,\n * http://yourserver:8088/janus to use the plain HTTP API or ws://yourserver:8188/\n * for WebSockets) or as an array of addresses to try sequentially to allow\n * automatic for fallback/failover during setup;\n * - \\c iceServers: a list of STUN/TURN servers to use (a default STUN server\n * will be used if you skip this property);\n * - \\c withCredentials: whether the \\c withCredentials property of XHR requests\n * should be enabled or not (false by default, and only valid when using HTTP\n * as a transport, ignored for WebSockets);\n * - \\c max_poll_events: the number of events that should be returned when polling;\n * the default is 1 (polling returns an object), passing a higher number will\n * have the backend return an array of objects instead (again, only valid for\n HTTP usage as this is strictly related to long polling, ignored for WebSockets);\n * - \\c destroyOnUnload: whether we should destroy automatically try and\n * destroy this session via Janus API when \\c onbeforeunload is called (true by default);\n * - \\c token , \\c apisecret: optional parameters only needed in case you're \\ref auth ;\n * - a set of callbacks to be notified about events, namely:\n * \t\t- \\c success: the session was successfully created and is ready to be used;\n * \t\t- \\c error: the session was NOT successfully created;\n * \t\t- \\c destroyed: the session was destroyed and can't be used any more.\n *\n * These properties and callbacks are passed to the method as properties\n * of a single parameter object: that is, the \\c Janus constructor takes a\n * single parameter, which although acts as a container for all the available\n * options. The \\c success callback is where you typically start your application\n * logic, e.g., attaching the peer to a plugin and start a media session.\n *\n * Here's an example:\n *\n \\verbatim\nvar janus = new Janus(\n    {\n        server: 'http://yourserver:8088/janus',\n        success: function() {\n            // Done! attach to plugin XYZ\n        },\n        error: function(cause) {\n            // Error, can't go on...\n        },\n        destroyed: function() {\n            // I should get rid of this\n        }\n    });\n \\endverbatim\n *\n * As anticipated, the server may be a specific address, e.g.:\n *\n \\verbatim\nvar janus = new Janus(\n    {\n        server: 'http://yourserver:8088/janus',\n                // or\n        server: 'ws://yourserver:8188/',\n        [..]\n \\endverbatim\n *\n * or an array of addresses. Such an array can be especially useful if\n * you want the library to first check if the WebSockets server is\n * reachable and, if not, fallback to plain HTTP, or just to provide\n * a link multiple instances to try for failover. This is an example of\n * how to pass a 'try websockets and fallback to HTTP' array:\n *\n \\verbatim\nvar janus = new Janus(\n    {\n        server: ['ws://yourserver:8188/','http://yourserver:8088/janus'],\n        [..]\n \\endverbatim\n *\n * Once created, this object represents your session with the server.\n * you can interact with a \\c Janus object in several different ways.\n * In particular, the following properties and methods are defined:\n *\n * - \\c getServer(): returns the address of the server;\n * - \\c isConnected(): returns \\c true if the Janus instance is connected\n * to the server, \\c false otherwise;\n * - \\c getSessionId(): returns the unique Janus session identifier;\n * - \\c attach(parameters): attaches the session to a plugin, creating a handle;\n * more handles to the same or different plugins can be created at the same time;\n * - \\c destroy(parameters): destroys the session with the server, and closes\n * all the handles (and related PeerConnections) the session may have with any plugin as well.\n *\n * The most important property is obviously the \\c attach() method, as\n * it's what will allow you to exploit the features of a plugin to manipulate\n * the media sent and/or received by a PeerConnection in your web page.\n * This method will create a plugin handle you can use for the purpose,\n * for which you can configure properties and callbacks when calling the\n * \\c attach() method itself. As for the \\c Janus constructor, the \\c attach()\n * method takes a single parameter that can contain any of the following\n * properties and callbacks:\n *\n * - \\c plugin: the unique package name of the plugin (e.g., \\c janus.plugin.echotest );\n * - \\c opaqueId: an optional opaque string meaningful to your application (e.g., to map all the handles of the same user);\n * - a set of callbacks to be notified about events, namely:\n * \t\t- \\c success: the handle was successfully created and is ready to be used;\n * \t\t- \\c error: the handle was NOT successfully created;\n * \t\t- \\c consentDialog: this callback is triggered just before \\c getUserMedia is called\n * (parameter=<b>true</b>) and after it is completed (parameter=<b>false</b>); this means it can\n * be used to modify the UI accordingly, e.g., to prompt the user about the need to accept the device access consent requests;\n * \t\t- \\c webrtcState: this callback is triggered with a <b>true</b> value\n * when the PeerConnection associated to a handle becomes active (so ICE, DTLS and\n * everything else succeeded) from the Janus perspective, while <b>false</b> is\n * triggered when the PeerConnection goes down instead; useful to figure out\n * when WebRTC is actually up and running between you and Janus (e.g., to notify\n * a user they're actually now active in a conference); notice that in case\n * of <b>false</b> a reason string may be present as an optional parameter;\n * \t\t- \\c connectionState: this callback is triggered when the connection state for the\n * PeerConnection associated to the handle changes: the argument of the callback\n * is the new state as a string (e.g., \"connected\" or \"failed\");\n * \t\t- \\c iceState: this callback is triggered when the ICE state for the\n * PeerConnection associated to the handle changes: the argument of the callback\n * is the new state as a string (e.g., \"connected\" or \"failed\");\n * \t\t- \\c mediaState: this callback is triggered when Janus starts or stops\n * receiving your media: for instance, a \\c mediaState with mid=<b>0</b>, type=<b>audio</b>\n * and on=<b>true</b> means Janus started receiving the audio stream identified\n * by mid \\c b in the offer/answer exchange and transceivers (or started\n * getting them again after a pause of more than a second); a \\c mediaState with\n * type=<b>video</b> and on=<b>false</b> means Janus hasn't received any video\n * from you in the last second, after a start was detected before; useful to\n * figure out when Janus actually started handling your media, or to detect\n * problems on the media path (e.g., media never started, or stopped at some time);\n * \t\t- \\c slowLink: this callback is triggered when Janus reports trouble\n * either sending or receiving media on the specified PeerConnection, typically\n * as a consequence of too lost packets detected to/from the user in the\n * last second: for instance, a \\c slowLink with uplink=<b>true</b> means\n * you notified several missing packets from Janus, while uplink=<b>false</b>\n * means Janus is not receiving all your packets; useful to figure out when\n * there are problems on the media path (e.g., excessive loss), in order to\n * possibly react accordingly (e.g., decrease the bitrate if most of our\n * packets are getting lost);\n * \t\t- \\c onmessage: a message/event has been received from the plugin;\n * \t\t- \\c onlocaltrack: a local \\c MediaStreamTrack is available and ready to be displayed;\n * \t\t- \\c onremotetrack: a remote \\c MediaStreamTrack is available and ready to be displayed;\n * \t\t- \\c ondataopen: a Data Channel is available and ready to be used;\n * \t\t- \\c ondata: data has been received through the Data Channel;\n * \t\t- \\c oncleanup: the WebRTC PeerConnection with the plugin was closed;\n * \t\t- \\c detached: the plugin handle has been detached by the plugin itself,\n * and so should not be used anymore.\n *\n * Here's an example:\n *\n \\verbatim\n// Attach to echo test plugin, using the previously created janus instance\njanus.attach(\n    {\n        plugin: \"janus.plugin.echotest\",\n        success: function(pluginHandle) {\n            // Plugin attached! 'pluginHandle' is our handle\n        },\n        error: function(cause) {\n            // Couldn't attach to the plugin\n        },\n        consentDialog: function(on) {\n            // e.g., Darken the screen if on=true (getUserMedia incoming), restore it otherwise\n        },\n        onmessage: function(msg, jsep) {\n            // We got a message/event (msg) from the plugin\n            // If jsep is not null, this involves a WebRTC negotiation\n        },\n        onlocaltrack: function(track, added) {\n            // A local track to display has just been added (getUserMedia worked!) or removed\n        },\n        onremotetrack: function(track, mid, added, metadata) {\n            // A remote track (working PeerConnection!) with a specific mid has just been added or removed\n            // You can query metadata to get some more information on why track was added or removed\n            // metadata fields:\n            //   - reason: 'created' | 'ended' | 'mute' | 'unmute'\n        },\n        oncleanup: function() {\n            // PeerConnection with the plugin closed, clean the UI\n            // The plugin handle is still valid so we can create a new one\n        },\n        detached: function() {\n            // Connection with the plugin closed, get rid of its features\n            // The plugin handle is not valid anymore\n        }\n    });\n \\endverbatim\n *\n * So the \\c attach() method allows you to attach to a plugin, and specify\n * the callbacks to invoke when anything relevant happens in this interaction.\n * To actively interact with the plugin, you can use the \\c Handle object\n * that is returned by the \\c success callback (pluginHandle in the example).\n *\n * This \\c Handle object has several methods you can use to interact with\n * the plugin or check the state of the session handle:\n *\n * - \\c getId(): returns the unique handle identifier;\n * - \\c getPlugin(): returns the unique package name of the attached plugin;\n * - \\c send(parameters): sends a message (with or without a jsep to\n * negotiate a PeerConnection) to the plugin;\n * - \\c createOffer(callbacks): asks the library to create a WebRTC compliant OFFER;\n * - \\c createAnswer(callbacks): asks the library to create a WebRTC compliant ANSWER;\n * - \\c handleRemoteJsep(callbacks): asks the library to handle an incoming WebRTC compliant session description;\n * - \\c replaceTracks(tracks, callbacks): asks the library to replace local tracks without renegotiating (no other offer/answer);\n * - \\c dtmf(parameters): sends a DTMF tone on the PeerConnection;\n * - \\c data(parameters): sends data through the Data Channel, if available;\n * - \\c getBitrate(mid): gets a verbose description of the currently received\n * video stream bitrate (optional mid to specify the stream, first video stream if missing);\n * - \\c getLocalTracks(): returns an array of the tracks that are currently being sent\n * on this PeerConnection, as basic objects including, e.g., type, \\c mid and \\c label of the track;\n * - \\c getRemoteTracks(): returns an array of the tracks that are currently being received\n * on this PeerConnection, as basic objects including, e.g., type, \\c mid and \\c label of the track;\n * - \\c muteAudio , \\c unmuteAudio , \\c isAudioMuted , \\c muteVideo , \\c unmuteVideo , \\c isVideoMuted: a\n set of helper functions to mute, unmute or check the muted status of a specific audio or video track\n * that is currently being sent (optional mid to specify the stream, first audio/video stream if missing);\n * - \\c getLocalVolume , \\c getRemoteVolume: a couple of helper functions to get the current volume of\n * the local or remote track; notice that this isn't supported on Firefox at the moment\n * (optional mid to specify the stream, first audio stream if missing);\n * - \\c hangup(sendRequest): tells the library to close the PeerConnection; if the optional \\c sendRequest argument is\n * set to \\c true, then a \\c hangup Janus API request is sent to Janus as well (disabled by default, Janus can usually\n * figure this out via DTLS alerts and the like but it may be useful to enable it sometimes);\n * - \\c detach(parameters): detaches from the plugin and destroys the handle, tearing\n * down the related PeerConnection if it exists.\n *\n * While the \\c Handle API may look complex, it's actually quite straightforward\n * once you get the concept. The only step that may require a little more\n * effort to understand is the PeerConnection negotiation, but again, if\n * you're familiar with the WebRTC API, the \\c Handle actually makes it\n * a lot easier.\n *\n * The idea behind it's usage is the following:\n *\n * -# you use \\c attach() to create a \\c Handle object;\n * -# in the \\c success callback, your application logic can kick in: you may\n * want to send a message to the plugin (<code>send({msg})</code>), negotiate\n * a PeerConnection with the plugin right away ( \\c createOffer followed\n * by a <code>send({msg, jsep})</code>) or wait for something to happen to do anything;\n * -# the \\c onmessage callback tells you when you've got messages from the plugin;\n * if the \\c jsep parameter is not null, just pass it to the library, which will take\n * care of it for you; if it's an \\b OFFER use \\c createAnswer (followed by a\n * <code>send({msg, jsep})</code> to close the loop with the plugin), otherwise use\n * \\c handleRemoteJsep ;\n * -# whether you took the initiative to set up a PeerConnection or the plugin did,\n * the \\c onlocaltrack and/or the \\c onremotetrack callbacks will provide\n * you with info on media tracks you can display or play in your page;\n * -# each plugin may allow you to manipulate what should flow through the\n * PeerConnection channel: the \\c send method and \\c onmessage callback\n * will allow you to handle this interaction (e.g., to tell the plugin\n * to mute your stream, or to be notified about someone joining a virtual room),\n * while the \\c ondata callback is triggered whenever data is received\n * on the Data Channel, if available (and the \\c ondataopen callback\n * will tell you when a Data Channel is actually available).\n *\n * The following paragraphs will delve a bit deeper in the negotiation\n * mechanism provided by the \\c Handle API, in particular describing\n * the properties and callbacks that may be involved. To follow the approach\n * outlined by the W3C WebRTC API, this negotiation mechanism is heavily\n * based on asynchronous methods as well. Notice that the following paragraphs\n * address the first negotiation step, that is the one to create a new\n * PeerConnection from scratch: to know how to originate or handle a\n * renegotiation instead (e.g., to add/remove/replace a media source, or\n * force an ICE restart) check the \\ref renegotiation section instead.\n *\n * - \\c createOffer takes a single parameter, that can contain any of the\n * following properties and callbacks:\n *   - \\c tracks: you can use this property to tell the library which media (audio/video/data)\n * you're interested in, and whether you're going to send and/or receive any of them; by default\n * no device is captured, and Data Channels are disabled as well; incoming audio and video\n * is instead autoaccepted unless you tell the library otherwise; the same property can also\n * be used to update sessions (e.g., to add/remove/replace tracks); this option is an array of\n * objects, where each object can take any of the following properties:\n *     - \\c type: mandatory, must be one of \"audio\", \"video\", \"screen\" and \"data\";\n *     - \\c mid: to address existing tracks (e.g., when answering or updating sessions),\n * the \\c mid property specifies that the info in the related track object\n * are specific to that target; notice that this is ignored in a first \\c createOffer ,\n * as mid values are created by the browser automatically and cannot be forced;\n *     - \\c capture: in case something must be captured (e.g., a microphone for\n * \"audio\" or a \"webcam\" for video), the \\c capture property can be used to\n * dictate what and how; passing \\c true asks for the default device, but\n * \\c getUserMedia (for audio/video) or \\c getDisplayMedia (for screen sharing)\n * constraints can be passed as well as objects, and in case those will be\n * used instead; passing a \\c MediaStreamTrack instance will tell the library\n * not to capture anything, but use the provided track as a source instead;\n *     - \\c simulcast: \\c true/false, for video, whether simulcast should be used for this track;\n *     - \\c svc: for video, the scalability mode to enable, in case SVC needs to be\n * used for this track; notice that SVC support is experimental in Janus, and not\n * fully supported in all browsers either;\n *     - \\c recv: \\c true/false , whether audio or video should be received\n * as well; in case \\c capture is set but \\c recv is false, then this means\n * you're asking for a \"sendonly\" track;\n *     - \\c add: \\c true/false , whether a new track should be added (default is true for offers);\n *     - \\c replace: \\c true/false , whether a provided \\c capture should replace what\n * is being captured for the specified track;\n *     - \\c remove: \\c true/false , whether the local track that's currently captured\n * should be removed, meaning nothing will be sent for that specific stream;\n *     - \\c dontStop: \\c true/false , whether a track that is being added right\n * now should \\b NOT be stopped when the track is removed; this is helpful whenever,\n * for instance, an external track is being provided to \\c capture , and the application\n * wants to keep control on the life cycle of the \\c MediaStreamTrack instance;\n *     - \\c transforms: in case Insertable Streams need to be used (e.g., for end-to-end\n * encryption), the \\c sender and/or \\c receiver transform functions for this track can\n * be provided in the \\c transforms property;\n *   - \\c trickle: \\c true/false, to tell the library whether you want\n * Trickle ICE to be used (true, the default) or not (false);\n *   - a set of callbacks to be notified about the result, namely:\n *     - \\c success: the session description was created (attached as a parameter) and is ready to be sent to the plugin;\n *     - \\c error: the session description was NOT successfully created;\n *     - \\c customizeSdp: you can modify the sdp generated by the webrtc engine if you need;\n * - \\c createAnswer takes the same options as createOffer, but requires\n * an additional one as part of the single parameter argument:\n *   - \\c jsep: the session description sent by the plugin (e.g., as received\n * in an \\c onmessage callback) as its OFFER.\n *\n * Whether you use \\c createOffer or \\c createAnswer depending on the scenario,\n * you should end up with a valid \\c jsep object returned in the \\c success\n * callback. You can attach this \\c jsep object to a message in a \\c send request\n * to pass it to the plugin, and have Janus negotiate a PeerConnection\n * with your application.\n *\n * Here's an example of how to use \\c createOffer, taken from the Echo Test demo page:\n *\n \\verbatim\n// Attach to echo test plugin\njanus.attach(\n    {\n        plugin: \"janus.plugin.echotest\",\n        success: function(pluginHandle) {\n            // Negotiate WebRTC\n            echotest = pluginHandle;\n            echotest.createOffer(\n                {\n                    // We want bidirectional audio and video, plus data channels\n                    tracks: [\n                        { type: 'audio', capture: true, recv: true },\n                        { type: 'video', capture: true, recv: true },\n                        { type: 'data' },\n                    ],\n                    success: function(jsep) {\n                        // Got our SDP! Send our OFFER to the plugin\n                        echotest.send({ message: body, jsep: jsep });\n                    },\n                    error: function(error) {\n                        // An error occurred...\n                    },\n                    customizeSdp: function(jsep) {\n                        // if you want to modify the original sdp, do as the following\n                        // oldSdp = jsep.sdp;\n                        // jsep.sdp = yourNewSdp;\n                    }\n                });\n        },\n        [..]\n        onmessage: function(msg, jsep) {\n            // Handle msg, if needed, and check jsep\n            if(jsep) {\n                // We have the ANSWER from the plugin\n                echotest.handleRemoteJsep({jsep: jsep});\n            }\n        },\n        [..]\n        onlocaltrack: function(track, added) {\n            // Invoked after createOffer\n            // This is info on a local track: when added, we can choose to render\n        },\n        onremotetrack: function(track, mid, added, metadata) {\n            // Invoked after handleRemoteJsep has got us a PeerConnection\n            // This is info on a remote track: when added, we can choose to render\n            // You can query metadata to get some more information on why track was added or removed\n            // metadata fields:\n            //   - reason: 'created' | 'ended' | 'mute' | 'unmute'\n        },\n        [..]\n  \\endverbatim\n *\n * This, instead, is an example of how to use \\c createAnswer, taken from the Streaming demo page:\n *\n \\verbatim\n// Attach to the Streaming plugin\njanus.attach(\n    {\n        plugin: \"janus.plugin.streaming\",\n        success: function(pluginHandle) {\n            // Handle created\n            streaming = pluginHandle;\n            [..]\n        },\n        [..]\n        onmessage: function(msg, jsep) {\n            // Handle msg, if needed, and check jsep\n            if(jsep) {\n                // We have an OFFER from the plugin\n                streaming.createAnswer(\n                    {\n                        // We attach the remote OFFER\n                        jsep: jsep,\n                        // We only specify data channels here, as this way in\n                        // case they were offered we'll enable them. Since we\n                        // don't mention audio or video tracks, we autoaccept them\n                        // as recvonly (since we won't capture anything ourselves)\n                        tracks: [\n                            { type: 'data' }\n                        ],\n                        success: function(ourjsep) {\n                            // Got our SDP! Send our ANSWER to the plugin\n                            var body = { request: \"start\" };\n                            streaming.send({ message: body, jsep: ourjsep });\n                        },\n                        error: function(error) {\n                            // An error occurred...\n                        }\n                    });\n            }\n        },\n        [..]\n        onlocaltrack: function(track, added) {\n            // This will NOT be invoked, we chose recvonly\n        },\n        onremotetrack: function(track, mid, added, metadata) {\n            // Invoked after send has got us a PeerConnection\n            // This is info on a remote track: when added, we can choose to render\n            // This is info on a remote track: when added, we can choose to render\n            // You can query metadata to get some more information on why track was added or removed\n            // metadata fields:\n            //   - reason: 'created' | 'ended' | 'mute' | 'unmute'\n        },\n        [..]\n  \\endverbatim\n *\n * Of course, these are just a couple of examples where the scenarios\n * assumed that one plugin would only receive (Echo Test) or generate\n * (Streaming) offers. A more complex example (e.g., a call using the\n * Video Call, SIP or NoSIP plugin, or a videoconfecence using the Video Room)\n * would involve both, allowing you to either send offers to a plugin,\n * or receive some from them. Handling this is just a matter of checking\n * the \\c type of the \\c jsep object and reacting accordingly.\n *\n * \\section renegotiation Updating an existing PeerConnection (with or without renegotiations)\n * While the JavaScript APIs described above will suffice for most of the\n * common scenarios, there are cases when updates on a PeerConnection may\n * be needed. This can happen whenever, for instance, you want to add a\n * new media source (e.g., add video to an audio only call), replace an\n * existing one (e.g., switch from capturing the camera to sharing your\n * screen), or trigger an ICE restart because of a network change. While\n * adding or removing tracks requires a renegotiation, which means a new SDP\n * offer/answer round to update the existing PeerConnection, just replacing\n * a track can be done without any further SDP exchange. In both cases,\n * the \\c tracks property introduced before can be used to update a session.\n *\n * To just replace an existing track (e.g., changing the camera used to\n * capture the video we're sending) the \\c replaceTracks handle method can\n * be used. Using it is quite trivial, since you simply need to provide a\n * list of tracks you want to update, identify them somehow (e.g., via\n * their \\c mid ), and provide the new \\c capture property. A simple\n * example that shows how to replace a video track with a specific camera\n * identified by its \\c deviceId is the following:\n *\n \\verbatim\nechotest.replaceTracks({\n    tracks: [\n        {\n            type: 'video',\n            mid: '1',    // We assume mid 1 is video\n            capture: { deviceId: { exact: videoDeviceId } }\n        }\n    ]\n});\n \\endverbatim\n *\n * In case a new track needs to be added to a session, or an existing\n * track be removed, instead, a renegotiation is indeed needed, because\n * it requires changes to be signalled to the other party. In that case,\n * a renegotiation happens exactly as new sessions do, meaning that you\n * either perform a new \\c createOffer followed by a \\c handleRemoteJsep\n * with the updated answer, or you handle an incoming updated offer and\n * provide an updated answer via \\c createAnswer : the library will\n * automatically understand from context if you're creating a new session\n * or updating an existing one.\n *\n * When updating a session to add or remove tracks, you need to provide\n * an updated \\c tracks property as well. It's important to only include\n * in this \\c tracks property a list of tracks you want to change: omitting\n * existing tracks will result in the library not making any change to\n * them. This means that a \\c createOffer without a \\c tracks property,\n * for instance, will simply generate a new offer without applying any\n * change (which, as we'll see later, can be useful in some cases, e.g.,\n * ICE restarts).\n *\n * Depending on what you want to update, you'll need to set one of the\n * following properties to \\c true in the related track object:\n *\n * - \\c add: adds a new track to the session;\n * - \\c replace: replaces an existing track in the session (similar to\n * what \\c replaceTracks does, but within the context of a renegotiation);\n * - \\c remove: removes an existing track from a session (the direction of\n * the related m-line will be changed to either \"recvonly\" or \"inactive\",\n * depending on whether there's incoming media as well or not).\n *\n * Notice that these properties are only processed when you're trying a\n * renegotiation, and will be ignored when creating a new PeerConnection.\n * These properties don't replace the existing \\c track properties, but go\n * along with them. For instance, when adding a new video stream, or\n * replacing an existing one, you can still use the video related properties\n * as before, e.g., to pass a specific device ID or asking for a screenshare\n * instead of a camera. As anticipated, omitting info on existing tracks\n * will leave them untouched.\n *\n * It's important to point out that, as for negotiations that result in\n * the creation of a new PeerConnection in the first place, how to perform\n * a renegotiation in practice will typically vary depending on the plugin\n * that you're trying to do it for. Some plugins may allow you to offer\n * a renegotiation, others may require you to send a different request\n * instead in order to trigger a renegotiation from the plugin. As it\n * will be clearer later, this is especially true for ICE restarts. As\n * such, apart from the generic and core-related definitions introduced\n * in this section, please refer to the documentation for each individual\n * plugin for more information about how to perform renegotiations in\n * specific use cases.\n *\n * Here's a simple example of how you can remove the local video capture\n * in a session, e.g., in the EchoTest demo:\n *\n \\verbatim\n// Remove local video\nechotest.createOffer(\n    {\n        tracks: [{ type: 'video', mid: '1', remove: true }],\n        success: function(jsep) {\n            echotest.send({ message: { video: true }, jsep: jsep })\n        },\n        error: function(error) {\n            bootbox.alert('WebRTC error... ' + error.message);\n        }\n    });\n \\endverbatim\n *\n * This other example shows how you can add a new video stream, e.g.,\n * a screen share, to an existing PeerConnection instead:\n *\n \\verbatim\n// Add local video\nechotest.createOffer(\n    {\n        tracks: [{ type: 'screen', add: true, capture: true }],\n        success: function(jsep) {\n            echotest.send({ message: { video: true }, jsep: jsep })\n        },\n        error: function(error) {\n            bootbox.alert('WebRTC error... ' + error.message);\n        }\n    });\n \\endverbatim\n *\n * Notice that renegotiations involving media changes (both local and remote)\n * will likely result in new calls to the \\c onlocaltrack and \\c onremotetrack\n * application callbacks: as such, be prepared to see those callbacks called\n * for the same PeerConnection more than once during the course of a media session.\n *\n * \\section restarts ICE restarts\n * While ICE restarts can be achieved with a renegotiation, they're complex\n * enough to deserve a specific subsection. In fact, ICE restarts don't\n * address changes in the media, but in the underlying transport itself.\n * They're used, for instance, when there's a network change (e.g., the\n * IP address changed, or the user switched from WiFi to 4G). In order for\n * this to work, new candidates must be exchanged, and connectivity checks\n * must be restarted in order to find the new optimal path.\n *\n * With \\c janus.js, you can only force an ICE restart when sending a new\n * offer. In order to do so, all you need to do is add `iceRestart :true`\n * to your `createOffer` call, and an ICE restart will be requested. The\n * following example shows how this can be done with the EchoTest:\n *\n \\verbatim\nechotest.createOffer(\n    {\n        iceRestart: true,\n        success: function(jsep) {\n            echotest.send({ message: { audio: true, video: true }, jsep: jsep});\n        }\n    });\n \\endverbatim\n *\n * Notice how, in this particular example, we're not asking for any change\n * on the media streams, but just an ICE restart, which is why we don't\n * provide a \\c tracks property at all: as a consequence, the library won't\n * make any changes to the existing streams, but only generate a new offer.\n * If successful, as soon as the answer is received, the client and Janus\n * will restart the ICE process and find a new path for the media packets.\n *\n * Notice that, with Janus and its plugins, you won't always be able to\n * force an ICE restart by sending a new SDP offer yourself: some plugins,\n * like the Streaming plugin for instance, will want to always send an\n * offer themselves, which means they'll be the ones actually forcing the\n * ICE restart from a negotiation perspective. In order to still allow\n * users to actually originate the process, all the stock Janus plugins\n * that assume they'll be sending offers for some or all of their media\n * streams also expose APIs to force an ICE restart from the server side.\n * You can learn more about this on a plugin level basis\n * <a target=\"_blank\" href=\"https://github.com/meetecho/janus-gateway/pull/753\">here</a> and\n * <a target=\"_blank\" href=\"https://github.com/meetecho/janus-gateway/pull/1099\">here</a>.\n * Besides, make sure you read the documentation for each of the plugins\n * you're interested in using ICE restarts for, as the details for how\n * to perform it properly are typically provided there.\n *\n * <hr>\n *\n * This is it! For more information about the API, have a look at the\n * demo pages that are available in the \\b html folder in this package.\n *\n */\n\n/*!\\page js-modules Using janus.js as JavaScript module\n *\n * To facilitate integration of \\c janus.js within modular JavaScript code bases,\n * you can instruct the build system(s) to generate a modular variants of \\c janus.js.\n * Generated modules may then be copied to your own JavaScript projects and seamlessly integrated with your own project's build system.\n *\n * Building the modules can be done in two ways:\n *\n * -# As part of a regular build of the Janus WebRTC Server, using \\c make, by enabling the integrated support via \\c configure\n * -# By running NPM commands manually. This may be useful if you are looking to build just the JavaScript modules without\n *    incurring the overhead of a full build of Janus.\n *\n * As an alternative to generating the module and copying it to your project, you can also tweak\n * your module bundler to use \\c janus.js directly from the official Janus repository. See more\n * details (including concrete instructions for Webpack): \\ref js-webpack\n *\n * \\section auto-build-js-modules Building modules using make\n * Each supported variant may be enabled by passing a corresponding \\c --enable-javascript-*-module flag\n * (with or without a \\c =yes directive) to \\c configure before invoking \\c make to build Janus.\n * Please note: if you do not pass any such flag, by default no modules will be built.\n *\n * The following table provides a summary of available module formats and their corresponding \\c configure options:\n *\n * <table class=\"table table-striped\">\n * <tr><th>Module format (syntax)</th><th>File name</th><th>configure flag to pass</th></tr>\n * <tr><td>ECMAScript</td><td>janus.es.js</td><td>\\c --enable-javascript-es-module</td></tr>\n * <tr><td>Universal Module Definition (UMD)</td><td>janus.umd.js</td><td>\\c --enable-javascript-umd-module</td></tr>\n * <tr><td>CommonJS</td><td>janus.cjs.js</td><td>\\c --enable-javascript-common-js-module</td></tr>\n * <tr><td>Immediately Invoked Function Expression (IIFE)</td><td>janus.iife.js</td><td>\\c --enable-javascript-iffe-module</td></tr>\n * </table>\n *\n * The \\c --enable-all-js-modules shortcut is available, in case you want to enable and build them all.\n *\n * When built and installed, these module variants may be found in the \\c $PREFIX/share/janus/javascript\n * folder, alongside the \\c janus.js file itself (assuming \\c $PREFIX the installation directory passed to \\c configure).\n *\n * \\note Building the JavaScript modules still requires NPM and may involve an \\c install which means \\c npm must be able\n * to download dependencies. By default \\c configure will attempt to auto-detect available \\c npm on your PATH, but\n * if you have installed NPM outside the PATH you can override this by passing the (full) path to your \\c npm executable, e.g.:\n *\n \\verbatim\n ./configure NPM=/path/to/my/custom/npm --enable-javascript-es-module=yes\n \\endverbatim\n *\n * \\section manual-build-modules Building modules manually with NPM\n * You can also opt to build modules by invoking \\c npm manually. The project root folder contains the necessary\n * configuration files to get you started:\n *\n \\verbatim\n npm install\n npm run rollup -- --o /path/to/desired/output/file-name.js --f cjs # or es, iffe, umd, amd, ...\n \\endverbatim\n *\n * Using \\c npm directly is useful if you want to build the JavaScript modules only, without building Janus itself\n * or if you are looking for advanced customisation options or alternative formats which are not integrated in \\c configure yet.\n * As you may have surmised from the example command, the actual build consists mostly of invoking \\c rollup with the\n * correct parameters. For more information on available parameters, please refer to the \\c rollup documentation:\n *\n * -# https://rollupjs.org/#command-line-flags\n * -# https://rollupjs.org/#configuration-files\n *\n * \\section js-webpack Using janus.js directly with Webpack and other bundlers\n *\n * Generating a converted version of \\c janus.js and copying it to your project is not always the best\n * solution. In many situations it may be preferred to let your JavaScript module bundler (e.g.\n * <a href=\"https://webpack.js.org\">Webpack</a>) grab the file directly from the official Janus repository.\n * Doing that you can manage \\c janus.js just like any other dependency coming from Github or the npm Registry,\n * getting rid of the manual copy step and letting the bundler take care of version management, updates\n * and downloads.\n *\n * Of course, the first step is to include the official Janus repository as a dependency in your\n * project by adding it to your \\c packages.json file.\n *\n \\verbatim\n {\n   \"dependencies\": {\n     \"janus-gateway\": \"git://github.com/meetecho/janus-gateway.git\"\n   }\n }\n \\endverbatim\n *\n * That will automatically drag in the dependency on the appropriate version of the\n * <a href=\"https://www.npmjs.com/package/webrtc-adapter\">WebRTC Adapter</a> from the npm Registry.\n * But \\c janus.js expects such adapter to be available as a global variable.\n * So we have to make that happen for it to work properly. In the case of Webpack, is as easy as\n * adding this to your \\c webpack.config.js file.\n \\verbatim\n const webpack = require('webpack');\n\n module.exports = {\n   plugins: [\n     // janus.js does not use 'import' to access to the functionality of webrtc-adapter,\n     // instead it expects a global object called 'adapter' for that.\n     // Let's make that object available.\n     new webpack.ProvidePlugin({ adapter: ['webrtc-adapter', 'default'] })\n   ]\n };\n \\endverbatim\n *\n * On the other hand, \\c janus.js defines the \\c Janus object globally. So a small tweak is also\n * needed to serve such object via \\c export. That can be done using\n * <a href=\"https://webpack.js.org/loaders/exports-loader/\">exports-loader</a> and adding this to\n * \\c webpack.config.js\n \\verbatim\n module.exports = {\n   module: {\n     rules: [\n       // janus.js does not use 'export' to provide its functionality to others, instead\n       // it creates a global variable called 'Janus' and expects consumers to use it.\n       // Let's use 'exports-loader' to simulate it uses 'export'.\n       {\n         test: require.resolve('janus-gateway'),\n         loader: 'exports-loader',\n          options: {\n            exports: 'Janus',\n          },\n       }\n     ]\n   }\n };\n \\endverbatim\n *\n * With that extra configuration, the official \\c janus.js can be used directly in any modular\n * JavaScript code base without any previous transformation of the file. That means you can simply\n * do this to access to the Janus API from your modular code.\n \\verbatim\n import { Janus } from 'janus-gateway';\n \\endverbatim\n\n * For more detailed or updated documentation check the\n * <a href=\"https://webpack.js.org/guides/shimming/\">Webpack shimming guide</a> or the equivalent\n * documentation for your bundler of choice.\n */\n\n/*!\\page js-dependencies Working with custom janus.js dependencies\n *\n * Certain dependencies of \\c janus.js may be passed during library initialization as\n * a property list containing the following keys:\n *\n * -# \\c newWebSocket: a function which given WebSockets server and protocol arguments\n * should return a new WebSocket (or something that acts like it)\n * -# \\c webRTCAdapter: an \\c adapter object such as provided by the\n * <a href=\"https://github.com/webrtc/adapter\">webrtc-adapter</a> library\n * -# \\c isArray: a function which tests if a given argument is a JavaScript array\n * -# \\c checkJanusExtension: a function which tests if the Janus Screensharing extension\n * for Chrome is installed/available. This can be done by testing whether or not an element\n * with an \\c id attribute value of \\c janus-extension-installed is present.\n * -# \\c httpAPICall: a function which given an url and options argument performs an\n * HTTP API request to Janus. This function is not as straightforward to implement,\n * see the section on \\ref js-http-apicall below for details.\n *\n * Depending on your needs you do not have to provide all these dependencies, e.g.\n * you do not need to implement the \\c httpAPICall function if your application relies\n * exclusively on WebSockets to access the Janus API.\n *\n * Two implementations of the dependencies object are provided by \\c janus.js:\n *\n * -# \\c Janus.useDefaultDependencies\n * -# \\c Janus.useOldDependencies\n *\n * In turn, each of these implementations accept their dependencies as arguments or fallback on\n * certain global variables. Below follows an overview:\n *\n * \\section js-default-deps Janus.useDefaultDependencies\n * The \\c Janus.useDefaultDependencies method relies on the following native browser APIs:\n *\n * -# \\c Promise: support for \\c Promises as standardised in ES 6 (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)\n * -# \\c fetch: support for the \\c fetch API (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)\n * -# \\c WebSocket: support for the \\c WebSocket API (https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)\n * -# \\c document.querySelector: support for the \\c document.querySelector API (https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)\n *\n * Additionally the \\c adapter object from the <a href=\"https://github.com/webrtc/adapter\">webrtc-adapter</a> library is also required.\n * These dependencies may either be passed explicitly to the function as a property list with keys of the same name, or\n * if omitted the function will fallback to relying on global variables of that name instead.\n *\n * Example:\n\\verbatim\n\tvar customDependencies = Janus.useDefaultDependencies({\n\t\tfetch: myCustomFetchImplementation // myCustomFetchImplementation should provide a compatible fetch() API\n\t});\n\n\tvar relyingOnGlobalsEntirely = Janus.useDefaultDependencies();\n\\endverbatim\n *\n * Being able to passe dependencies like this is especially useful in the context of modern ES modules:\n *\n\\verbatim\nimport adapter from 'webrtc-adapter';\n//  other imports elided\n\nconst setupDeps = () => Janus.useDefaultDependencies({\n\tadapter,\n\t// other dependencies elided\n});\n\nexport const initialiseJanusLibrary = () => Janus.init({dependencies: setupDeps()});\n\\endverbatim\n *\n * \\section js-old-deps Janus.useOldDependencies\n * The \\c Janus.useOldDependencies method relies on:\n *\n * -# \\c jQuery: the JQuery library (http://jquery.com/)\n * -# \\c WebSocket: support for the \\c WebSocket API (https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)\n * -# \\c adapter: the \\c adapter object from the <a href=\"https://github.com/webrtc/adapter\">webrtc-adapter</a> library\n *\n * This function provides a simple upgrade path for existing applications which are heavily\n * tied to jQuery (especially since previous versions of \\c janus.js depended on it).\n *\n * \\section js-http-apicall httpAPICall\n * The \\c httpAPICall function is used to issue API calls to the Janus HTTP(S) interfaces.\n * It will be passed two arguments:\n *\n * -# \\c url: a string which refers to the (server) URL of the API endpoint to contact\n * -# \\c options a property list (see below)\n *\n * Any return values from the \\c httpAPICall function will be ignored.\n *\n * When working with HTTP request or response bodies, the \\c httpAPICall is responsible for\n * serialisation to, and deserialisation from the 'wire format' (JSON).\n * That is: the \\c httpAPICall must transform objects to JSON or parse JSON as and when required.\n * Similarly, the \\c httpAPICall is also responsible for setting appropriate HTTP\n * \\c Content-Type (application/json) and/or \\c Accept headers.\n *\n * The \\c options argument may contain the following keys:\n *\n * -# \\c timeout: a timeout in milliseconds which should be imposed on the request.\n *    The \\c httpAPICall implementation is required to implement support for imposing timeouts\n *    on HTTP API requests.\n * -# \\c body: payload to include as body of the outgoing HTTP request.\n      The \\c httpAPICall must encode it in the 'wire format' (JSON).\n * -# \\c withCredentials: a boolean indicating whether or not HTTP credentials should be sent\n * -# \\c success: a callback which should be dispatched when an API request was successful.\n * -# \\c error: a callback which should be dispatched when an API request was unsuccessful, or timed out\n * -# \\c async: a boolean hint which indicates whether or not asynchronous requests are desirable.\n *    This hint is a primarily a remnant for backwards compatible behaviour when working with jQuery.\n *\n * The \\c success callback should be passed the deserialised API response body.\n * The \\c error callback accepts two arguments: a descriptive status text string and the raw error object\n * which caused the \\c error callback to be invoked.\n *\n * \\note The \\c httpAPICall represents the primary way to intercept HTTP(S) API calls issued from within the\n * \\c janus.js library. You can use this mechanism to augment outgoing requests with additional headers\n * or to intercept responses. For example:\n *\n * -# You can support authentication schemes based on the HTTP \\c Authorization header by\n * injecting it into outgoing API requests and routing them through a proxy.\n * -# You can intercept incoming responses and extract data from custom header values generated by a proxy.\n * -# You can combine both to implement a robust defence against <a href=\"https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)\">CSRF</a>\n * -# You can reroute the control flow entirely, and e.g. use \\c httpAPICall as an action creator in your\n * <a href=\"http://redux.js.org/\">Redux</a> application.\n *\n * \\section js-extension Custom Screensharing Extension for Chrome\n * To use a different extension for screensharing permissions in Chrome you can pass a \\c extension object\n * to \\c Janus.useDefaultDependencies and \\c Janus.useOldDependencies.\n * The object should provide the following methods:\n *\n * -# \\c init(): Do any setup work here. Will be called once when the dependencies are loaded.\n * -# \\c isInstalled(): should return a boolean indicating whether the Extension was detected and is ready to use.\n * -# \\c getScreen(callback): make a call to the extension to get a \\c streamId here. The streamId can be obtained\n *    from chrome using <a href=\"https://developer.chrome.com/extensions/desktopCapture\">chrome.desktopCapture.chooseDesktopMedia()</a>.\n *    When the request is successful pass the \\c streamId back using <tt>callback(null, streamId)</tt>,\n *    otherwise pass an \\c Error object like \\c callback(error)\n */\n\n/*! \\page rest RESTful, WebSockets, RabbitMQ, MQTT, Nanomsg and UnixSockets API\n *\n * Since version \\c 0.0.6, there are different ways to interact with a\n * Janus instance: a \\ref plainhttp (the default), a \\ref WS, a \\ref rabbit, \\ref apimqtt, \\ref apinanomsg\n * and a \\ref unix (both optional, need an external library to be available). All of\n * the interfaces use the same messages (in terms of requests, responses\n * and notifications), so almost all the concepts described in the\n * \\ref plainhttp section apply to the WebSocket/RabbitMQ/MQTT/Nanomsg/UnixSockets interfaces as well.\n * Besides, since version \\c 0.1.0 the transport mechanism for the Janus API\n * has been made modular, which means other protocols for transporting\n * Janus API messages might become available in the future: considering the\n * Janus protocol is supposed to be mostly agnostic to the protocol it is\n * transported on, the concepts explained in the following sections should\n * apply to those as well.\n *\n * As it will be explained later in the \\ref WS, \\ref rabbit, \\ref apimqtt, \\ref apinanomsg and \\ref unix sections\n * below, the only differences come when addressing specific sessions/handles\n * and in part in how you handle notifications using something different than\n * the REST interface: in fact, since with WebSockets, RabbitMQ, MQTT, Nanomsg and UnixSockets\n * (and, as anticipated, with other protocols that may be added in the future too)\n * there's no REST-based path involved, you'll need a couple of additional\n * identifiers to bridge the gap.\n * Some details are also provided in case you're interested in \\ref auth.\n *\n * \\section plainhttp Plain HTTP REST Interface\n * As anticipated in the \\ref JS documentation, the server deploys a\n * RESTful interface that clients can exploit. The \\c janus.js library\n * makes use of it in a transparent way, but if you're interested in\n * more details about it (e.g., because you want to talk to the server\n * your own way), this page described the interface and the protocol\n * the API exposes and uses.\n *\n * There are basically three types/levels of endpoints you can meet:\n *\n * -# \\ref root (\\c /janus by default, but configurable), which\n * you only \\b POST to in order to create a Janus session;\n * -# \\ref sessions (e.g., \\c /janus/12345678, using the\n * identifier retrieved with a previous create), which you either send\n * a \\b GET to (long poll for events and messages from plugins) or a \\b POST\n * (to create plugin handles or manipulate the session);\n * -# \\ref handles (e.g., \\c /janus/12345678/98765432, appending\n * the handle identifier to the session one) which you only send \\b POST\n * messages to (messages/negotiations for a plugin, handle manipulation),\n * as all events related to this handle would be received in the session\n * endpoint \\b GET (the \\c janus.js library would redirect the incoming\n * messages to the right handle internally).\n *\n * Messages and requests you can send to and receive from any of the\n * above mentioned endpoints are described in the following chapters.\n * In general, all messages share at least two fields:\n *\n * - \\c janus: the request/event (e.g., \"create\", \"attach\", \"message\", etc.);\n * - \\c transaction: a random string that the client can use to match incoming\n * messages from the server (since, as explained in the \\ref plugins\n * documentation, all messages are asynchronous).\n *\n * Different messages will of course add different information to this\n * base syntax. Error message, instead, usually have these fields:\n *\n * - \\c janus: this would be \"error\";\n * - \\c transaction: this would be the transaction identifier of the request\n * that failed;\n * - \\c error: a JSON object containing two fields:\n *   - \\c code: a numeric error code, as defined in apierror.h;\n *   - \\c reason: a verbose string describing the cause of the failure.\n *\n * An example of an error is presented here:\n *\n\\verbatim\n{\n\t\"janus\" : \"error\",\n\t\"transaction\" : \"a1b2c3d4\"\n\t\"error\" : {\n\t\t\"code\" : 458\n\t\t\"reason\" : \"Could not find session 12345678\"\n\t}\n}\n\\endverbatim\n *\n *\n * \\section info Getting info about the Janus instance\n * The API exposes an \\c info endpoint you can query to get information\n * about the Janus instance you're talking to. Specifically, it returns\n * information about the version of the Janus server, whether some of the\n * optional features (e.g., Data Channels or IPv6) are supported or not,\n * and which transports and plugins are available.\n *\n * To get this information, just send an HTTP \\b GET message to the \\c info\n * endpoint (e.g., http://yourserver:8088/janus/info), which will return\n * something like this:\n *\n\\verbatim\n{\n\t\"janus\": \"server_info\",\n\t\"transaction\": \"i1bzIL341Kl2\",\n\t\"name\": \"Janus WebRTC Server\",\n\t\"version\": 73,\n\t\"version_string\": \"0.7.3\",\n\t\"author\": \"Meetecho s.r.l.\",\n\t\"data_channels\": \"true\",\t// Data channels are supported\n\t\"ipv6\": \"false\",\t\t\t// IPv6 is not configured\n\t\"ice-tcp\": \"false\",\t\t\t// ICE-TCP support is disabled,\n\t[..]\n\t\"transports\": {\n\t\t\"janus.transport.http\": {\n\t\t\t\"name\": \"JANUS REST (HTTP/HTTPS) transport plugin\",\n\t\t\t\"author\": \"Meetecho s.r.l.\",\n\t\t\t\"description\": \"This transport plugin adds REST (HTTP/HTTPS) support to the Janus API via libmicrohttpd.\",\n\t\t\t\"version_string\": \"0.0.2\",\n\t\t\t\"version\": 2\n\t\t},\n\t\t[..]\t// Other transport plugins\n\t},\n\t\"plugins\": {\n\t\t\"janus.plugin.sip\": {\t\t// The SIP plugin is available\n\t\t\t\"version_string\": \"0.0.7\",\n\t\t\t\"description\": \"This is a simple SIP plugin for Janus, allowing WebRTC peers to register at a SIP server and call SIP user agents through Janus.\",\n\t\t\t\"author\": \"Meetecho s.r.l.\",\n\t\t\t\"name\": \"JANUS SIP plugin\",\n\t\t\t\"version\": 7\n\t\t},\n\t\t\"janus.plugin.videoroom\": {\t// The Video SFU plugin is available\n\t\t\t\"version_string\": \"0.0.3\",\n\t\t\t\"description\": \"This is a plugin implementing a videoconferencing SFU (Selective Forwarding Unit) for Janus, that is an audio/video router.\",\n\t\t\t\"author\": \"Meetecho s.r.l.\",\n\t\t\t\"name\": \"JANUS VideoRoom plugin\",\n\t\t\t\"version\": 3\n\t\t},\n\t\t[..]\t// Other plugins\n\t}\n\\endverbatim\n *\n * You can use this information to selectively enable or disable features\n * in your application according to what's available in the Janus instance\n * you're trying to contact.\n *\n *\n * \\section root The server root\n * The server root is \\c /janus by default but, as anticipated, it is\n * configurable, either via command line or in the \\c janus.jcfg configuration.\n *\n * You can only contact the server root when you want to create a new\n * session with the server. To do so, you need to \\b POST the a \\c janus \"create\"\n * JSON message to the server:\n *\n\\verbatim\n{\n\t\"janus\" : \"create\",\n\t\"transaction\" : \"<random alphanumeric string>\"\n}\n\\endverbatim\n *\n * If the request is successful, you'll receive the unique session identifier\n * in a response formatted like this:\n *\n\\verbatim\n{\n\t\"janus\" : \"success\",\n\t\"transaction\" : \"<same as the request>\",\n\t\"data\" : {\n\t\t\"id\" : <unique integer session ID>\n\t}\n}\n\\endverbatim\n *\n * In case of an error, you'll receive an error message as the one introduced\n * before. This request, if issued with a POST to the server root, can only\n * fail if you miss any of the required fields in the request.\n *\n *\n * \\section sessions The session endpoint\n * Once you've created a session, a new endpoint you can use is created\n * in the server. Specifically, the new endpoint is constructed by\n * concatenating the server root and the session identifier you've been\n * returned (\\c e.g., \\c /janus/12345678).\n *\n * This endpoint can be used in two different ways:\n *\n * -# using a parameter-less \\b GET request to the endpoint, you'll\n * issue a long-poll request to be notified about events and incoming\n * messages from this session;\n * -# using a \\b POST request to send JSON messages, you'll interact\n * with the session itself.\n *\n * <hr>\n *\n * \\par Long-poll requests\n * The long-poll will only trigger events related to messages you're\n * being sent from plugins, and as such will be clearer to understand\n * once you read the \\ref handles section. That said, the events are formatted\n * this way:\n *\n * - \\c janus: this would be \"event\";\n * - \\c sender: this would be the unique numeric plugin handle identifier;\n * - \\c transaction: this is optional: it is either related to a request\n * you sent to a plugin before, or it may be missing in case this is an\n * event the plugin sent on its own account;\n * - \\c plugindata: a JSON object containing the info coming from the plugin itself:\n *   - \\c plugin: the plugin's unique package name (e.g., \\c janus.plugin.echotest);\n *   - \\c data: an opaque JSON object that is plugin specific.\n * - \\c jsep: an optional JSON object containing the JSEP SDP (offer or\n * answer) the plugin may send to negotiate a WebRTC PeerConnection with\n * the client (check the \\ref handles section for more details).\n *\n * An example of such an event (in this case, sent by the janus_echotest.c\n * plugin in response to a request) is presented here:\n *\n\\verbatim\n{\n\t\"janus\" : \"event\",\n\t\"sender\" : 1815153248,\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"plugindata\" : {\n\t\t\"plugin\": \"janus.plugin.echotest\",\n\t\t\"data\" : {\n\t\t\t\"echotest\" : \"event\",\n\t\t\t\"result\" : \"ok\"\n\t\t}\n\t}\n}\n\\endverbatim\n *\n * The long-poll request has a 30 seconds timeout. If it has no event to\n * report, a simple \\em keep-alive message will be triggered:\n *\n\\verbatim\n{\n\t\"janus\" : \"keepalive\",\n}\n\\endverbatim\n *\n * As with all long-poll based approaches, it's up to your application\n * to send a new polling request as soon as an event or a keep-alive\n * has been received.\n *\n * Notice that, by default, the long poll returns a single event: that is,\n * as soon as a message becomes available in the session queue, that event\n * is returned and the long poll closes. If you want to receive more events\n * within the context of the same long poll, you can pass the \\c maxev\n * query string parameter to the GET, e.g.:\n *\n\\verbatim\nGET http://host:port/janus/<sessionid>?maxev=5\n\\endverbatim\n *\n\\verbatim\n[\n\t{\n\t\t// Event #1\n\t\t\"janus\" : \"event\",\n\t\t[..]\n\t},\n\t{\n\t\t// Event #2\n\t\t\"janus\" : \"event\",\n\t\t[..]\n\t},\n\t[..]\n]\n\\endverbatim\n *\n * This request will instruct the server to return at maximum 5 events\n * within the context of the same long poll, formatted as a JSON array\n * of events. Please beware that this does \\b NOT mean that you'll\n * always get 5 events this way: it only means that, if a message becomes\n * available in the queue and more events are present as well, Janus will\n * return more than one without needing you to send multiple long polls\n * immediately thereafter to get them. For this reason, don't be surprised\n * if even with a \\c maxev parameter set, you'll still get a single\n * event being notified as the sole object in the returned array.\n *\n * <hr>\n *\n * \\par Interacting with the session\n * To interact with the session, e.g., to create a new handle to attach\n * to a plugin or destroy the current session, you need to send a \\b POST\n * JSON message to the session endpoint.\n *\n * To attach to a plugin in order to exploit its features, you need to\n * \\b POST a \\c janus \"attach\" JSON message to the server; you'll need\n * of course to provide information on the plugin you want to attach to,\n * which can be done using the \\c plugin field:\n *\n\\verbatim\n{\n\t\"janus\" : \"attach\",\n\t\"plugin\" : \"<the plugin's unique package name>\",\n\t\"transaction\" : \"<random string>\"\n}\n\\endverbatim\n *\n * Notice that you can also provide an optional \\c opaque_id string\n * identifier (for more details on why this might be useful, read more\n * <a href=\"https://github.com/meetecho/janus-gateway/pull/748\">here</a>).\n * If the request is successful, you'll receive the unique plugin handle\n * identifier in a response formatted the same way as the session create\n * one, that is like this:\n *\n\\verbatim\n{\n\t\"janus\" : \"success\",\n\t\"transaction\" : \"<same as the request>\",\n\t\"data\" : {\n\t\t\"id\" : <unique integer plugin handle ID>\n\t}\n}\n\\endverbatim\n *\n * In case of an error, you'll receive an error message as the one introduced\n * before. This request, if issued with a POST to a valid session endpoint, can only\n * fail if you miss any of the required fields in the request or if the\n * plugin you requested is not available in the server.\n *\n * To destroy the current session, instead, just send a \"destroy\" \\c janus\n * request:\n *\n\\verbatim\n{\n\t\"janus\" : \"destroy\",\n\t\"transaction\" : \"<random string>\"\n}\n\\endverbatim\n *\n * This will also destroy the endpoint created for this session.\n * If your session is currently managing one or more plugin handles,\n * make sure you destroy them first (as explained in the next section).\n * The server tries to do this automatically when receiving a session\n * destroy request, but a cleaner approach on the client side would help\n * nonetheless avoid potential issues.\n *\n * Notice that a session may also be destroyed automatically in case of\n * inactivity. If Janus doesn't receive any activity (requests, long polls)\n * for a session for a time longer than the \\c session_timeout value\n * configured in \\c janus.jcfg then the session will timeout, and a\n * \\c timeout event will be fired. If a \\c reclaim_session_timeout value\n * is configured, you can still reclaim the session from the same or\n * a different transport using the \\c claim request, within a limited\n * amount of time. An unreclaimed session that has timed out will be\n * permanently destroyed, and will destroy all its handles as well.\n *\n * \\section handles The plugin handle endpoint\n * Once you've created a plugin handle, a new endpoint you can use is created\n * in the server. Specifically, the new endpoint is constructed by\n * concatenating the server root, the session identifier and the new\n * plugin handle identifier you've been returned (\\c e.g.,\n * \\c /janus/12345678/98765432).\n *\n * You can use this plugin handle for everything that is related to the\n * communication with a plugin, that is, send the plugin a message,\n * negotiate a WebRTC connection to attach to the plugin, and so on.\n *\n * To send a plugin a message/request, you need to \\b POST the handle\n * endpoint a \\c janus \"message\" JSON payload. The \\c body field will\n * have to contain a plugin-specific JSON payload. In case the message\n * also needs to convey WebRTC-related negotiation information, a \\c jsep\n * field containing the JSON-ified version of the JSEP object can be\n * attached as well.\n *\n * \\note If you attach a \\c jsep object, whether it's an offer or an answer,\n * you're stating your will to negotiate a PeerConnection. This means that\n * an empty or invalid \\c jsep object will trigger a validation and will\n * cause the whole request to fail, so make sure you exclude the field\n * completely from your request if all you're interested into is sending\n * a message to a plugin.\n *\n * Here's an example of a message you may send the janus_echotest.c plugin\n * to mute your audio:\n *\n\\verbatim\n{\n\t\"janus\" : \"message\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"body\" : {\n\t\t\"audio\" : false\n\t}\n}\n\\endverbatim\n *\n * The same message containing negotiation information as well, instead,\n * (an OFFER, in this example), is presented here:\n *\n\\verbatim\n{\n\t\"janus\" : \"message\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"body\" : {\n\t\t\"audio\" : false\n\t},\n\t\"jsep\" : {\n\t\t\"type\" : \"offer\",\n\t\t\"sdp\" : \"v=0\\r\\no=[..more sdp stuff..]\"\n\t}\n}\n\\endverbatim\n *\n * Please notice that, if for any reason you don't want to use the\n * trickling of ICE candidates from your application (which means you'll\n * include them all in the SDP OFFER or ANSWER, which is usually not\n * recommended), you'll have to add an additional <code>\"trickle\" : false</code>\n * attribute to the \"jsep\" object, to explicitly tell Janus you won't\n * send any \\c trickle candidate (by default Janus will always assume\n * support for trickle).\n *\n * If you're going to \\c trickle candidates, instead, there is an ad-hoc\n * message you can use to do so which is called, unsurprisingly, \\c trickle\n * and which you can use to send one or more trickle candidates to Janus.\n * Since such a message is related to a specific PeerConnection, it will\n * need to be addressed to the right Handle just as the \\c message introduced\n * previously. A \\c trickle message can contain three different kind of\n * information:\n *\n *  - a single trickle candidate;\n *  - an array of trickle candidates;\n *  - a null candidate or a \\c completed JSON object to notify the end of the\n * candidates.\n *\n * This is an example of a single candidate being trickled:\n *\n\\verbatim\n{\n\t\"janus\" : \"trickle\",\n\t\"transaction\" : \"hehe83hd8dw12e\",\n\t\"candidate\" : {\n\t\t\"sdpMid\" : \"video\",\n\t\t\"sdpMLineIndex\" : 1,\n\t\t\"candidate\" : \"...\"\n\t}\n}\n\\endverbatim\n *\n * This, instead, is an example of how to group more trickle candidates\n * in a single request (particularly useful if you're wrapping Janus in\n * your server and want to reduce the number of transactions):\n *\n\\verbatim\n{\n\t\"janus\" : \"trickle\",\n\t\"transaction\" : \"hehe83hd8dw12e\",\n\t\"candidates\" : [\n\t\t{\n\t\t\t\"sdpMid\" : \"video\",\n\t\t\t\"sdpMLineIndex\" : 1,\n\t\t\t\"candidate\" : \"...\"\n\t\t},\n\t\t{\n\t\t\t\"sdpMid\" : \"video\",\n\t\t\t\"sdpMLineIndex\" : 1,\n\t\t\t\"candidate\" : \"...\"\n\t\t},\n\t\t[..]\n\t]\n}\n\\endverbatim\n *\n * Finally, this is how you can tell Janus that you sent all the trickle\n * candidates that were gathered:\n *\n\\verbatim\n{\n\t\"janus\" : \"trickle\",\n\t\"transaction\" : \"hehe83hd8dw12e\",\n\t\"candidate\" : {\n\t\t\"completed\" : true\n\t}\n}\n\\endverbatim\n *\n * Plugins may handle this requests synchronously or asynchronously. In\n * the former, plugins would return a response to the request itself\n * immediately; in the latter, instead, the plugin would only notify a\n * successful reception of the request, which it would process later.\n * Considering the asynchronous nature of the Janus API, a successful\n * management of such messages within Janus would in such case result in\n * a \\c janus \"ack\" messages being sent back to the client. A logical response\n * to those messages, if needed, would be provided as an event in the\n * long-poll interface described previously, and clients would be able\n * to match it to the original request by means of the transaction\n * identifiers. It is worth noting, though, that should a WebRTC negotiation\n * be involved you don't have to expect an ANSWER to your OFFER to be\n * sent back in the same transaction. A plugin may decide, in its\n * application logic, to not provide you with an ANSWER right away, but\n * only after some internal state changes occur. It's up to your application\n * to handle the negotiation state accordingly.\n *\n * An example of an \"ack\" being sent back to the client, using the previous\n * sample request as a reference, is presented here:\n *\n\\verbatim\n{\n\t\"janus\" : \"ack\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\"\n}\n\\endverbatim\n *\n * If you receive this ack instead of a \"success\" response, you can be\n * sure the plugin has received the message, and is going to process it soon.\n *\n * In case of an error, instead, you'll receive an error message as the one\n * introduced before. This request, if issued with a POST to a valid plugin\n * handle endpoint, can only fail if you miss any of the required fields\n * in the request, if the plugin you tried to contact is not available in\n * the server anymore, if an error occurred in the plugin when trying to\n * receive the message or if the \\c jsep SDP you may have provided is\n * invalid.\n *\n * To destroy the plugin handle, instead, just send a \"detach\" \\c janus\n * request:\n *\n\\verbatim\n{\n\t\"janus\" : \"detach\",\n\t\"transaction\" : \"<random string>\"\n}\n\\endverbatim\n *\n * This will also destroy the endpoint created for this plugin handle.\n * If your plugin handle is also managing an ongoing WebRTC connection\n * with the plugin, make sure it is torn down as part of this process.\n * The plugin implementation and the Janus core should do this\n * automatically, but implementing the right behaviour in clients would\n * help avoid potential issues nonetheless. Notice that you may receive\n * \\c detached event after a handle has been detached, whether this was\n * done in response to a request or automatically, in response to an event.\n *\n * If you're interested in keeping the handle alive but want to hang up\n * the associated PeerConnection, if available, just send a \"hangup\" \\c janus\n * request:\n *\n\\verbatim\n{\n\t\"janus\" : \"hangup\",\n\t\"transaction\" : \"<random string>\"\n}\n\\endverbatim\n *\n * This is usually not required, as you can typically just hangup your\n * WebRTC PeerConnection normally and Janus will figure out it's gone\n * by itself. Anyway, there are cases where this might be useful (e.g.,\n * the connection was stuck in some weird ICE/DTLS state) as it can be\n * used to reset the connection state for the handle.\n *\n * \\section events WebRTC-related events\n *\n * As anticipated in the previous sections, Janus can send events and\n * notifications at any time through the long poll channel (or, as it\n * will be explained later, through the related push mechanisms made\n * available by other transport protocols ). While this channel is\n * mostly used to convey asynchronous notifications originated by\n * plugins as part of the messaging they may have with the application\n * using it, the same channel is actually used by Janus to trigger\n * events related to different aspects pertaining a specific handle.\n *\n * In particular, for each handle involving a PeerConnection Janus\n * provides notifications about its current state. To do so, the\n * following events may be received as well:\n *\n *  - \\c webrtcup: ICE and DTLS succeeded, and so Janus correctly\n * established a PeerConnection with the user/application;\n *  - \\c media: whether Janus is receiving (\\c receiving: \\c true/false)\n * audio/video (\\c type: \\c \"audio/video\") on this PeerConnection;\n *  - \\c slowlink: whether Janus is reporting trouble sending/receiving\n * (\\c uplink: \\c true/false) media on this PeerConnection;\n *  - \\c hangup: the PeerConnection was closed, either by Janus or by\n * the user/application, and as such cannot be used anymore.\n *\n * As such, to monitor the status of a PeerConnection as seen from\n * Janus you can make use of these events to track what's going on. A\n * correct flow for an active PeerConnection would be one that, after a\n * WebRTC negotiation and setup, results in a \\c webrtcup event followed\n * by two \\c media events (in case both audio and video have been\n * negotiated) specifying that the first audio/video packets have been\n * received. A \\c hangup event would inform the user/application that\n * no media is being exchanged with Janus anymore.\n *\n * Here are a few examples of how these events may look like.\n *\n * A PeerConnection becoming ready:\n *\n\\verbatim\n{\n\t\"janus\" : \"webrtcup\",\n\tsession_id: <the session identifier>,\n\tsender: <the handle identifier>\n}\n\\endverbatim\n *\n * First audio bytes being received by Janus:\n *\n\\verbatim\n{\n\t\"janus\" : \"media\",\n\tsession_id: <the session identifier>,\n\tsender: <the handle identifier>,\n\t\"type\" : \"audio\",\n\t\"receiving\" : true\n}\n\\endverbatim\n *\n * Audio not getting to Janus anymore for some reason:\n *\n\\verbatim\n{\n\t\"janus\" : \"media\",\n\t\"session_id\" : <the session identifier>,\n\t\"sender\" : <the handle identifier>\n\t\"type\" : \"audio\",\n\t\"receiving\" : false\n}\n\\endverbatim\n *\n * Audio getting to Janus again (same message as first audio):\n *\n\\verbatim\n{\n\t\"janus\" : \"media\",\n\t\"session_id\" : <the session identifier>,\n\t\"sender\" : <the handle identifier>\n\t\"type\" : \"audio\",\n\t\"receiving\" : true\n}\n\\endverbatim\n *\n * Janus reporting problems sending media to a user (user sent many NACKs\n * in the last second; uplink=true is from Janus' perspective):\n *\n\\verbatim\n{\n\t\"janus\" : \"slowlink\",\n\t\"session_id\" : <the session identifier>,\n\t\"sender\" : <the handle identifier>\n\t\"uplink\" : true,\n\t\"lost\" : <number of lost packets in the last second>\n}\n\\endverbatim\n *\n * PeerConnection closed for a DTLS alert (normal shutdown):\n *\n\\verbatim\n{\n\t\"janus\" : \"hangup\",\n\t\"session_id\" : <the session identifier>,\n\t\"sender\" : <the handle identifier>,\n\t\"reason\" : \"DTLS alert\"\n}\n\\endverbatim\n *\n * It is important to point out that the \\c media event notifications\n * only apply if your PeerConnection is going to actually send media to\n * Janus. A \\c recvonly PeerConnection, for instance (e.g., as the\n * Streaming plugin would create) would never trigger any \\c media\n * event, as Janus would never be receiving media, but only send it.\n *\n * \\section WS WebSockets Interface\n * WebSockets provide a more efficient means for implementing a bidirectional communication.\n * This is especially useful if you're wrapping the Janus API on your\n * servers, as it allows you to avoid all the noise and overhead introduced\n * by several concurrent HTTP transactions and long polls by relying on\n * what may be seen as a single \"control channel\".\n *\n * To interact with Janus using WebSockets you MUST specify a specific\n * subprotocol, named \\c janus-protocol, e.g.,\n *\n\\verbatim\nvar websocket = new WebSocket('ws://1.2.3.4:8188', 'janus-protocol');\n\\endverbatim\n *\n * The \\c janus.js library does this automatically.\n *\n * As anticipated at the beginning of this section, the actual messages\n * being exchanged are exactly the same. This means that all the concepts\n * introduced before still apply: you still create a session, attach to\n * a plugin and interact with it exactly the same way. What is different\n * is, of course, the REST path approach that becomes unavailable when\n * using a WebSocket as a control channel. To address the idenfitiers\n * that become missing using WebSockets, you'll need to add additional\n * fields to the requests when necessary.\n *\n * So, when you want to create a session using the REST API, you send a\n * POST to the server base path:\n *\n\\verbatim\n{\n\t\"janus\" : \"create\",\n\t\"transaction\" : \"<random alphanumeric string>\"\n}\n\\endverbatim\n *\n * The same applies if you're interested in getting generic info from the\n * Janus instance. Since there's no \\b GET you can use, a specific \\c janus\n * request type called \\c info is available:\n *\n\\verbatim\n{\n\t\"janus\" : \"info\",\n\t\"transaction\" : \"<random alphanumeric string>\"\n}\n\\endverbatim\n *\n * Since you'd contact the base path for both requests, you don't need to add any identifier\n * for this scenario. But if instead you want to attach to a plugin within\n * the context of a specific session, using the REST API you'd send a\n * post to the \\c /janus/<session-id> endpoint:\n *\n\\verbatim\n{\n\t\"janus\" : \"attach\",\n\t\"plugin\" : \"<the plugin's unique package name>\",\n\t\"transaction\" : \"<random string>\"\n}\n\\endverbatim\n *\n * To make this work with WebSockets as well, you need to add a further\n * field called \\c session_id in the request:\n *\n\\verbatim\n{\n\t\"janus\" : \"attach\",\n\t\"session_id\" : <the session identifier>,\t\t// NEW!\n\t\"plugin\" : \"<the plugin's unique package name>\",\n\t\"transaction\" : \"<random string>\"\n}\n\\endverbatim\n *\n * which will allow the WebSocket server to understand which session this\n * request pertains to. At the same time, when you need to address a\n * specific handle (e.g., to send a message to a plugin, or negotiate a\n * WebRTC PeerConnection) you'll need to add a \\c handle_id field to the\n * request as well, or the request will be rejected:\n *\n\\verbatim\n{\n\t\"janus\" : \"message\",\n\t\"session_id\" : <the session identifier>,\t\t// NEW!\n\t\"handle_id\" : <the handle identifier>,\t\t// NEW!\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"body\" : {\n\t\t\"audio\" : false\n\t}\n}\n\\endverbatim\n *\n * Considering the bidirectional nature of WebSockets and the fact that\n * the channel will be shared for different requests, you'll need to pay\n * extra attention to the \\c transaction identifier, which will allow you\n * to map incoming responses and events to the request you sent that\n * originated them.\n *\n * An \\b important aspect to point out is related to keep-alive messages\n * for WebSockets Janus channels. As explained above, a Janus session is\n * kept alive as long as there's no inactivity for \\c session_timeout seconds:\n * if no messages have been received in that time frame, the session is\n * marked as timed-out and, unless reclaimed, will be torn down by the server.\n * A normal activity on a session is usually enough to prevent that;\n * for a more prolonged inactivity with respect to messaging, on plain\n * HTTP the session is usually kept alive through the regular long poll\n * requests, which act as activity as long as the session is concerned.\n * This aid is obviously not possible when using WebSockets, where a single channel is\n * used both for sending requests and receiving events and responses. For\n * this reason, an ad-hoc message for keeping alive a Janus session should\n * to be triggered on a regular basis:\n *\n\\verbatim\n{\n\t\"janus\" : \"keepalive\",\n\t\"session_id\" : <the session identifier>,\n\t\"transaction\" : \"sBJNyUhH6Vc6\"\n}\n\\endverbatim\n *\n * This will make sure that the server detects activity on the session\n * even when no actual messages are being exchanged with handles.\n *\n * As a last point, another slight difference with WebSockets comes from\n * how push notifications are implemented. In the \\ref plainhttp this is\n * done via long polls: that is, you explicitly subscribe to notifications,\n * and have to do that again as soon as an event has been received. With\n * WebSockets, this is not needed: as soon as you create a session on a\n * WebSocket, that channel becomes automatically subscribed for events\n * related to that sessions, and you'll receive them on the same WebSocket.\n * For the same reason, as soon as the WebSocket is closed, all the sessions\n * created within its context are considered closed as well, and so their\n * resources (including all the handles and PeerConnections) will be\n * released as well.\n *\n * \\note The same \\c janus.js JavaScript library can be used both with the\n * REST and the WebSockets API: all you need to do is provide the right\n * Janus server address during the initialization and the library will\n * use one or the other according to the protocol prefix.\n *\n * \\section rabbit RabbitMQ interface\n * The semantics of how the requests have to be built, when compared to\n * the usage of plain HTTP, is exactly the same as for WebSockets, so\n * refer to the \\ref WS documentation for details about that.\n *\n * Of course, there are other aspects that differ when making use of\n * RabbitMQ messaging to talk to Janus, rather than using HTTP messages\n * or WebSockets. Specifically, RabbitMQ just basically forwards messages\n * on queues, and as such implementing a pseudo-bidirectional channel\n * as the Janus API requires some precaution.\n *\n * In particular, when configuring Janus to use RabbitMQ you'll have to\n * specify \\b two \\b queues:\n *\n * - a queue for \\b incoming messages (application -> Janus);\n * - a queue for \\b outgoing messages (Janus -> application).\n *\n * The proper usage of these queues will allow you to implement the kind\n * of bidirectional channel Janus needs.\n *\n * Another aspect to point out is that Janus requires all requests to\n * have a random \\c correlation_id identifier. In fact, as pointed out\n * in the previous sections, the Janus API is conceived as a request/response\n * protocol that can involve asynchronous notifications as well. In order\n * to make sure that an application can match a received response to one\n * of the requests made earlier, Janus copies the \\c correlation_id\n * identifier from the original request in the response to it: this is\n * compliant with the\n * <a href=\"https://www.rabbitmq.com/tutorials/tutorial-six-python.html\">RPC pattern</a>\n * as specified in the RabbitMQ documentation. Notifications originated by\n * Janus, instead, will not include a \\c correlation_id identifier, and as\n * such applications shouldn't expect any: applications will still be able\n * to match a notification to a request, if the involved plugin was\n * implemented to do so, by looking at the Janus-level \\c transaction\n * identifier.\n *\n * \\section apimqtt MQTT interface\n * The semantics of how the requests have to be built, when compared to\n * the usage of plain HTTP, is exactly the same as for WebSockets, so\n * refer to the \\ref WS documentation for details about that.\n *\n * Of course, there are other aspects that differ when making use of\n * MQTT messaging to talk to Janus, rather than using HTTP messages\n * or WebSockets. Similar to RabbitMQ, MQTT just basically forwards messages\n * on queues, and as such implementing a pseudo-bidirectional channel\n * as the Janus API requires some precaution.\n *\n * In particular, when configuring Janus to use MQTT you'll have to\n * specify \\b two \\b queues:\n *\n * - a queue for \\b incoming messages (application -> Janus);\n * - a queue for \\b outgoing messages (Janus -> application).\n *\n * The proper usage of these queues will allow you to implement the kind\n * of bidirectional channel Janus needs.\n *\n * \\section apinanomsg Nanomsg interface\n * The semantics of how the requests have to be built, when compared to\n * the usage of plain HTTP, is exactly the same as for WebSockets, RabbitMQ\n * and MQTT, so refer to the \\ref WS documentation for details about that.\n *\n * Apart from that, the only configuration needed is related to the Nanomsg\n * address to use, and whether it should be used to bind locally or to\n * connect to a remote endpoint. Notice that only the \\c NN_PAIR pattern\n * is supported by the plugin, so no Pub/Sub or other variations.\n *\n * \\section unix UnixSockets interface\n * The semantics of how the requests have to be built, when compared to\n * the usage of plain HTTP, is exactly the same as for WebSockets, RabbitMQ\n * MQTT and Nanomsg, so refer to the \\ref WS documentation for details about that.\n *\n * Apart from that, the only configuration needed is related to the path\n * the client and server will be sharing, and the socket type. Notice that only the\n * \\c SOCK_SEQPACKET and \\c SOCK_DGRAM types are supported in the plugin.\n *\n */\n\n/*! \\page auth Authenticating the Janus API\n * By default no authentication is involved when using the Janus API.\n * This means that the API is completely open, and that everybody can\n * talk to Janus and its plugins and set up media connections. There are\n * times, though, where limiting access to Janus may be desirable, e.g.,\n * when you want to prevent unauthorized users to join a service you\n * created, or when you wrap the Janus API in your server and you want\n * your application to be the only one to be able to interact with\n * Janus from a messaging perspective.\n *\n * There are a couple of ways to authenticate requests in Janus:\n *\n * - using a \\ref token (useful for web users);\n * - using a \\ref signed (useful for web users);\n * - using a \\ref secret (useful when wrapping the Janus API).\n *\n * \\section token Stored token based authentication mechanism\n * The token based authentication mechanism expects all users to provide,\n * in each request, a \\c token string attribute: if this token is\n * known to Janus, the request will be accepted, otherwise it will be\n * rejected as an \\c unauthorized response. Configuring the token based\n * authentication mechanism is easy enough: you can do that either via\n * the command line (\\c -A or \\c --token-auth ) or in the \\c janus.jcfg\n * configuration (\\c token_auth value in the \\c general section).\n *\n * These tokens are completely opaque to Janus, meaning they can be\n * pretty much anything that you want. Janus does not do any form of\n * authorization/authentication itself: it's up to you to provide it\n * with valid tokens users can use, e.g., as part of your server-side\n * application handling users. You can add and remove tokens\n * dynamically using the \\ref admin, which means you will need to enable\n * it if you want to use tokens, or otherwise all requests will fail\n * (Janus will never have a valid token, so all requests will be rejected).\n *\n * You add tokens using the \\c add_token admin request, while you\n * remove them using \\c remove_token. You can also limit the scope of tokens\n * to specific plugins, by passing a list of plugins to \\c add_token or\n * modifying the token properties via \\c allow_token and \\c disallow_token.\n * By default (\\c add_token without any plugin specified) Janus assumes\n * a new token is allowed to access all plugins. A list of all the\n * existing tokens can be retrieved with a \\c list_tokens request.\n *\n * Here are a couple of examples of how you can use the requests:\n *\n\\verbatim\n{\n\t\"janus\" : \"add_token\",\n\t\"token\": \"a1b2c3d4\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"admin_secret\": \"adminpassword\"\n}\n\\endverbatim\n *\n * This adds a new token (a1b2c3d4) that is allowed to access all the\n * plugins in Janus (no limitation provided in \\c add_token ). To create\n * a new token and limit the scope to a few selected plugins, you can\n * use this other syntax instead (notice the extra \\c plugins array):\n *\n\\verbatim\n{\n\t\"janus\" : \"add_token\",\n\t\"token\": \"a1b2c3d4\",\n\t\"plugins\": [\n\t\t\"janus.plugin.streaming\",\n\t\t\"janus.plugin.videoroom\"\n\t],\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"admin_secret\": \"adminpassword\"\n}\n\\endverbatim\n *\n * In this other example, we're creating a new token, and also telling\n * Janus that the only plugins a user with this token can access are\n * the Streaming and Videoroom plugins. An attempt to attach to a\n * different plugin (e.g., EchoTest) will result in an error.\n *\n * You can change the permissions a token has with respect to plugin\n * access at any time. In the following example, we add a new plugin\n * to the permissions for an existing token:\n *\n\\verbatim\n{\n\t\"janus\" : \"allow_token\",\n\t\"token\": \"a1b2c3d4\",\n\t\"plugins\": [\n\t\t\"janus.plugin.echotest\"\n\t],\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"admin_secret\": \"adminpassword\"\n}\n\\endverbatim\n *\n * This way, the provided token is now also allowed to access the EchoTest\n * plugin. To remove a permission, the syntax is this one instead:\n *\n\\verbatim\n{\n\t\"janus\" : \"disallow_token\",\n\t\"token\": \"a1b2c3d4\",\n\t\"plugins\": [\n\t\t\"janus.plugin.videoroom\"\n\t],\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"admin_secret\": \"adminpassword\"\n}\n\\endverbatim\n *\n * To retrieve a list of all the valid tokens Janus is aware of, together\n * with the plugins each of them is allowed to access, a \\c list_tokens\n * request can be used:\n *\n\\verbatim\n{\n\t\"janus\" : \"list_tokens\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"admin_secret\": \"adminpassword\"\n}\n\\endverbatim\n *\n * Finally, you can get rid of a token using a \\c remove_token request:\n *\n\\verbatim\n{\n\t\"janus\" : \"remove_token\",\n\t\"token\": \"a1b2c3d4\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"admin_secret\": \"adminpassword\"\n}\n\\endverbatim\n *\n * As anticipated, with the token based mechanism enabled, all users\n * will need to provide a valid token as part of their requests. This is\n * done by adding a \\c token attribute to the request root, e.g.:\n *\n\\verbatim\n{\n\t\"janus\" : \"create\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"token\": \"usertoken\"\n}\n\\endverbatim\n *\n * The same applies for the long poll GET messages as well, which will\n * need to contain the \\c token as a query string parameter.\n *\n * A valid token will mean the request will be accepted and processed\n * normally. A missing or invalid token, instead, will result in an\n * error being returned:\n *\n\\verbatim\n{\n\t\"janus\" : \"error\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"error\" : {\n\t\t\"code\" : 403,\n\t\t\"reason\" : \"Unauthorized request (wrong or missing secret/token)\"\n\t}\n}\n\\endverbatim\n *\n * An attempt to use a valid token to attach to a plugin it is not\n * allowed to access, instead, will result in a different error:\n *\n\\verbatim\n{\n\t\"janus\" : \"error\",\n\t\"transaction\" : \"sBJNyUhH6Vc6\",\n\t\"error\" : {\n\t\t\"code\" : 405,\n\t\t\"reason\" : \"Provided token can't access plugin 'janus.plugin.echotest'\"\n\t}\n}\n\\endverbatim\n *\n * \\section signed HMAC-Signed token authentication\n *\n * <div class=\"well\"><b>NOTE WELL:</b> At the time of writing, HMAC-Signed\n * tokens are ONLY available for the VideoRoom plugin. If you need authentication\n * for other plugins, you should use the \\ref token instead.</div>\n *\n * Simple token based authentication requires the application host to\n * continuously update the Janus instance on permission changes.\n * Since Janus stores the tokens in memory, it can be problematic to guarantee\n * the permissions of a dynamic application stay in sync with Janus.\n *\n * This problem can be solved by using a type of nonce / lease system to\n * let the application server generate automatically expiring tokens without\n * requiring direct communication with or any data storage in Janus.\n *\n * You can use the HMAC signed token mechanism by enabling token authentication\n * in general, as above (\\c -A or \\c --token-auth) and specifying an encryption\n * secret using \\c --token-auth-secret. The same can be accomplished using\n * \\c token_auth and \\c token_auth_secret in the \\c general section of\n * \\c janus.jcfg.\n *\n * With Signed token support enabled, dynamic token creation via the \\ref admin\n * is not supported. Instead, Janus will look for tokens with a format like:\n *\n\\verbatim\n<timestamp>,janus,<plugin1>[,plugin2...]:<signature>\n\\endverbatim\n *\n * Where \\c timestamp is a UNIX timestamp (seconds since 0:00 UTC, 1.1.1970)\n * that marks the point in time at which the token expires;\n * \\c plugin1 etc. are the \\c bundle names of plugins (such as \\c janus.plugin.videoroom);\n * and \\c signature is the base64-encoded HMAC-SHA1 signature of the expiry\n * timestamp in ASCII format, hashed using the \\c --token-auth-secret as a key.\n *\n * The following function can be used to sign tokens using the node.js crypto library:\n *\n\\verbatim\nconst crypto = require('crypto');\nfunction getJanusToken(realm, data = [], timeout = 24 * 60 * 60) {\n  const expiry = Math.floor(Date.now() / 1000) + timeout;\n\n  const strdata = [expiry.toString(), realm, ...data].join(',');\n  const hmac = crypto.createHmac('sha1', secret);\n  hmac.setEncoding('base64');\n  hmac.write(strdata);\n  hmac.end();\n\n  return [strdata, hmac.read()].join(':');\n};\n\nconst token = getJanusToken('janus', ['janus.plugin.videoroom']);\n\\endverbatim\n *\n * The \\c janus parameter here is the \\c realm of the token. For authenticating the\n * Janus API it should always be set to \\c janus.\n *\n * When Janus encounters a token, it will:\n *\n * - verify that the timestamp has not passed\n * - verify that the signature matches the timestamp\n * - if the request requires access to a plugin, verify that the signature allows access\n *\n * Since the auth secret should never leave the application side, a signature\n * like this can only be generated by the application server, which needs to\n * be configured using the same secret.\n *\n * Please note that tokens of this sort cannot be revoked after being signed\n * and passed to the client. Instead of signing tokens with late expirys,\n * it is recommended to use tokens with shorter durations and generate and\n * transition to a new token within the expiry time of every last token when\n * the lease time is unknown and security is critical.\n *\n * \\section secret Shared static secret\n * Several deployers showed an interest in wrapping the Janus API on\n * their server side: this allows them to keep the interaction with their\n * users the way it was before, while still benefiting from the features\n * Janus provides. This is an easy enough step, as it just needs developers\n * to relay the involved SDP, and implementing the Janus API messages to\n * handle the logic.\n *\n * That said, since in this case Janus would be contacted, through the API,\n * just by a limited number of applications (e.g., application servers\n * made in node.js, Ruby, Java Servlets or whatever) and not random\n * browsers, it is reasonable to involve a mechanism to control who is\n * allowed to contact and control it. The previous section described\n * how you can exploit a token based mechanism for authenticating\n * requests, but since in this case you only need a single application,\n * or a limited set of them,\n * to be able to talk to Janus, it's worthwhile to resort to something\n * simpler and more static. To allow for that, Janus also exposes a\n * shared API secret mechanism: that is, you configure Janus with a string\n * applications need to present when sending requests, and if they don't,\n * Janus rejects them with an \\c unauthorized message.\n *\n * Configuring the API secret mechanism is easy enough: you can do that\n * either via the command line (\\c -a or \\c --apisecret ) or in the\n * \\c janus.jcfg configuration (\\c api_secret value in the \\c general section).\n * When enabled, all requests addressed to that Janus instance \\b MUST\n * also contain an \\c apisecret field in the Janus message headers. For\n * instance, this message presented above would fail:\n *\n\\verbatim\n{\n\t\"janus\" : \"create\",\n\t\"transaction\" : \"<random alphanumeric string>\"\n}\n\\endverbatim\n *\n\\verbatim\n{\n\t\"janus\" : \"error\",\n\t\"transaction\" : \"<same as request>\"\n\t\"error\" : {\n\t\t\"code\" : 403,\n\t\t\"reason\" : \"Unauthorized request (wrong or missing secret/token)\"\n\t}\n}\n\\endverbatim\n *\n * For a successful transaction, the message would have to look like this:\n *\n\\verbatim\n{\n\t\"janus\" : \"create\",\n\t\"apisecret\" : \"<API secret configured in Janus>\",\n\t\"transaction\" : \"<random alphanumeric string>\"\n}\n\\endverbatim\n *\n * The same applies for the long poll GET messages as well, which will\n * need to contain the \\c apisecret as a query string parameter.\n *\n */\n\n/*! \\page admin Admin/Monitor API\n * Recent versions of Janus introduced a new feature: an Admin/Monitor\n * API that can be used to ask Janus for more specific information\n * related to sessions and handles. This is especially useful when you\n * want to debug issues at the media level.\n *\n * \\note Right now, this new API mostly allows you to retrieve information,\n * but only act on part of it: for more interaction (e.g., to force a\n * session removal), you can rely on the existing \\ref rest for the purpose.\n * Besides, notice that this is a pull-based API. If you're interested in\n * asynchronous notifications about the internal state of core and plugins,\n * check the recently added janus_eventhandler mechanism instead.\n *\n * The API, for security reasons, is typically not enabled by default in any of the\n * transport plugins: that's definitely the case for the stock transport\n * plugins, for instance, while additional, third party plugins may choose\n * to expose the functionality without requiring any tweaking. As to the\n * existing transport, you can enable the admin API by editing the \\c [ \\c admin \\c ]\n * section in the related transport configuration file (e.g., \\c janus.transport.http.jcfg\n * for the REST interface, to use the admin API over HTTP). The configuration\n * is pretty much the same as the one for the Janus API. In addition, you\n * can configure restrictions in the form of a password/secret that clients\n * need to provide or other transport-specific ones.\n *\n * For what concerns the syntax, it's very similar to the \\ref rest and\n * so this page will briefly discuss the differences. Notice that, when\n * using WebSockets, you'll have to use <code>janus-admin-protocol</code>\n * as the subprotocol, instead of the <code>janus-protocol</code> of the\n * regular Janus API.\n *\n * \\section adminreq Admin API requests\n * There are several different requests that this API implementents, so,\n * to make this documentation easier to read and the functionality easier\n * to identify, we can group requests depending on what they provide.\n *\n * \\subsection adminreqg Generic requests\n * - \\c info: get the generic on the Janus instance; this returns exactly\n * the same information that a Janus API \\c info request would return,\n * and doesn't require any secret;\n * - \\c ping: a simple ping/pong mechanism for the Admin API, that returns\n * a \\c pong back that the client can use as a healthcheck or to measure\n * the protocol round-trip time; together with the \\c info request introduced\n * above, it's the only one that doesn't require a secret;\n * - \\c loops_info: returns a summary of how many handles each static\n * event loop is currently responsible for, in case static event loops\n * are in use (returns an empty array otherwise).\n *\n * \\subsection adminreqc Configuration-related requests\n * - \\c get_status: returns the current value for the settings that can be\n * modified at runtime via the Admin API (see below);\n * - \\c set_session_timeout: change global session timeout value in Janus;\n * - \\c set_log_level: change the log level in Janus;\n * - \\c set_log_timestamps: selectively enable/disable adding a timestamp\n * to all log lines Janus writes on the console and/or to file;\n * - \\c set_log_colors: selectively enable/disable using colors in all\n * log lines Janus writes on the console and/or to file;\n * - \\c set_locking_debug: selectively enable/disable a live debugging of\n * the locks in Janus on the fly (useful if you're experiencing deadlocks\n * and want to investigate them);\n * - \\c set_refcount_debug: selectively enable/disable a live debugging of\n * the reference counters in Janus on the fly (useful if you're experiencing\n * memory leaks in the Janus structures and want to investigate them);\n * - \\c set_min_nack_queue: change the value of the min NACK queue window;\n * - \\c set_no_media_timer: change the value of the no-media timer property;\n * - \\c set_slowlink_threshold: change the value of the slowlink-threshold property.\n *\n * \\subsection adminreqk Token-related requests\n * - \\c add_token: add a valid token (only available if you enabled the \\ref token);\n * - \\c allow_token: give a token access to a plugin (only available if you enabled the \\ref token);\n * - \\c disallow_token: remove a token access from a plugin (only available if you enabled the \\ref token);\n * - \\c list_tokens: list the existing tokens (only available if you enabled the \\ref token);\n * - \\c remove_token: remove a token (only available if you enabled the \\ref token).\n *\n * \\subsection adminreqs Session-related requests\n * - \\c accept_new_sessions: configure whether Janus should accept new\n * incoming sessions or not; this can be particularly useful whenever, e.g.,\n * you want to stop accepting new sessions because you're draining this instance;\n * - \\c list_sessions: list all the sessions currently active in Janus\n * (returns an array of session identifiers);\n * - \\c set_session_timeout: change session timeout value in Janus;\n * - \\c destroy_session: destroy a specific session; this behaves exactly\n * as the \\c destroy request does in the Janus API.\n *\n * \\subsection adminreqh Handle- and WebRTC-related requests\n * - \\c list_handles: list all the ICE handles currently active in a Janus\n * session (returns an array of handle identifiers);\n * - \\c handle_info: list all the available info on a specific ICE handle;\n * if a \\c plugin_only property is set to \\c true then only the plugin-specific\n * information is returned, excluding the more verbose WebRTC info and stats;\n * - \\c start_pcap: start dumping incoming and outgoing RTP/RTCP packets\n * of a handle to a pcap file (e.g., for ex-post analysis via Wireshark);\n * - \\c stop_pcap: stop the pcap dump;\n * - \\c start_text2pcap: same as above, but saves to a text file instead,\n * to be fed to \\c text2pcap in order to generate a \\c .pcap or \\c .pcapng file;\n * - \\c stop_text2pcap: stop the text2pcap dump;\n * - \\c message_plugin: send a synchronous request to a plugin and return a\n * response; implemented by most plugins to facilitate and streamline the\n * management of plugin resources (e.g., creating rooms in a conference plugin);\n * - \\c hangup_webrtc: hangups the PeerConnection associated with a specific\n * handle; this behaves exactly as the \\c hangup request does in the Janus API.\n * - \\c detach_handle: detached a specific handle; this behaves exactly\n * as the \\c detach request does in the Janus API.\n *\n * \\subsection adminreqt Transport-related requests\n * - \\c query_transport: send a synchronous request to a transport plugin and\n * return a response; whether this is implemented, and what functionality is\n * provided, can vary from transport to transport, but in general this feature\n * is available to tweak some setting dynamically and/or query some internal\n * transport-specific information (e.g., the number of served connections).\n *\n * \\subsection adminreqe Event handlers-related requests\n * - \\c query_eventhandler: send a synchronous request to an event handler and\n * return a response; implemented by most event handlers to dynamically\n * configure some of their properties;\n * - \\c custom_event: push a custom \"external\" event to notify via event handlers;\n * this can be useful whenever info from a third-party application needs to be\n * easily correlated to events originated by Janus, or to push information\n * Janus doesn't have available (e.g., a script polling CPU usage regularly).\n *\n * \\subsection adminreql Custom logging-related requests\n * - \\c custom_logline: push a custom \"external\" string to print on the logs;\n * this can be useful whenever info from a third-party application needs to be\n * injected in the Janus logs for whatever reason. The log level can be chosen.\n *\n * \\subsection adminreqz Helper requests\n * - \\c resolve_address: helper request to evaluate whether this Janus instance\n * can resolve an address via DNS, and how long it takes;\n * - \\c test_stun: helper request to evaluate whether this Janus instance\n * can contact a STUN server, what is returned, and how long it takes.\n *\n * \\section adminsyntax Admin API syntax\n * Following the same spirit of the \\ref rest these methods need to be\n * invoked on the right path and/or providing the right \\c session_id and\n * \\c handle_id identifiers. Specifically, the following requests must be invoked\n * without any session/handle information, as they're global requests:\n *\n * - \\c info , \\c ping , \\c get_status , all the configuration setters, all\n * the token requests, all the event-handler related requests, all the\n * helper requests, \\c accept_new_sessions and \\c list_sessions\n *\n * Here's an example of how such a request and its related response might look like:\n *\n\\verbatim\nPOST /admin\n{\n\t\"janus\" : \"list_sessions\",\n\t\"transaction\" : \"<random alphanumeric string>\",\n\t\"admin_secret\" : \"<password specified in janus.jcfg, if any>\"\n}\n\\endverbatim\n *\n *\n\\verbatim\n{\n\t\"janus\" : \"success\",\n\t\"transaction\" : \"<same as the request>\",\n\t\"sessions\" : [\n\t\t<session ID #1>,\n\t\t<session ID #2>,\n\t\t[..]\n\t\t<session ID #n>\n\t]\n}\n\\endverbatim\n *\n * On the other hand, some requests may be targeting a specific session,\n * in which case a \\c session_id property must be provided. Specifically,\n * these are the requests that do need a valid session identifier:\n *\n * - \\c destroy_session , \\c list_handles\n *\n * Using the REST API, this can be done by appending the session identifier\n (e.g., one of the ID returned by a \\c list_sessions call) to the API root,\n * but a more generic approach that works for all transports is to just\n * specify a \\c session_id property in the request, e.g.:\n *\n\\verbatim\nPOST /admin/12345678\n{\n\t\"janus\" : \"list_handles\",\n\t\"session_id\" : 12345678,\n\t\"transaction\" : \"<random alphanumeric string>\",\n\t\"admin_secret\" : \"<password specified in janus.jcfg, if any>\"\n}\n\\endverbatim\n *\n *\n\\verbatim\n{\n\t\"janus\" : \"success\",\n\t\"transaction\" : \"<same as the request>\",\n\t\"session_id\" : 12345678,\n\t\"handles\" : [\n\t\t<handle ID #1>,\n\t\t<handle ID #2>,\n\t\t[..]\n\t\t<handle ID #n>\n\t]\n}\n\\endverbatim\n *\n * Finally, some requests need not only a valid session identifier, but\n * a handle identifier as well, as they may address a specific handle\n * that the Janus instance is serving. It is the case for all requests\n * that address a specific handle and/or the related PeerConnection,\n * namely:\n *\n * - \\c handle_info , all the pcap-related requests, \\c message_plugin ,\n * \\c hangup_webrtc and \\c detach_handle\n *\n * The following is an example of how a \\c handle_info call addressing\n * a specific handle might look like. Since this is a handle-specific\n * request, the correct handle identifier must be\n * referenced, e.g., by appending the ID to the session it belongs to\n * or adding a \\c handle_id attribute besides the \\c session_id parent:\n *\n\\verbatim\nPOST /admin/12345678/98765432\n{\n\t\"janus\" : \"handle_info\",\n\t\"session_id\" : 12345678,\n\t\"handle_id\" : 98765432,\n\t\"transaction\" : \"<random alphanumeric string>\",\n\t\"admin_secret\" : \"<password specified in janus.jcfg, if any>\"\n}\n\\endverbatim\n *\n *\n\\verbatim\n{\n\t\"janus\" : \"success\",\n\t\"transaction\" : \"<same as the request>\",\n\t\"session_id\" : 12345678,\n\t\"handle_id\" : 98765432,\n\t\"info\" : {\n\t\t\"session_id\" : 12345678,\n\t\t\"session_last_activity\": 7927759122,\n\t\t\"session_transport\": \"janus.transport.websockets\",\n\t\t\"handle_id\" : 98765432,\n\t\t\"opaque_id\": \"echotest-YZcsLRCI4uSV\",\n\t\t\"loop-running\": true,\n\t\t\"created\": 18695669309,\n\t\t\"current_time\": 18706199704,\n\t\t\"plugin\": \"janus.plugin.echotest\",\n\t\t\"plugin_specific\": {\n\t\t\t// plugin specific (e.g., EchoTest internals)\n\t\t},\n\t\t\"flags\": {\n\t\t\t// flags\n\t\t},\n\t\t\"agent-created\": 18696092523,\n\t\t\"ice-mode\": \"full\",\n\t\t\"ice-role\": \"controlled\",\n\t\t\"sdps\": {\n\t\t\t\"profile\": \"UDP/TLS/RTP/SAVPF\",\n\t\t\t\"local\": \"v=0[..]\",\n\t\t\t\"remote\": \"v=0[..]\"\n\t\t},\n\t\t\"queued-packets\": 0,\n\t\t\"streams\": [\n\t\t\t// WebRTC info, including SSRCs, codecs, ICE and DTLS states, RTCP stats, etc.\n\t\t]\n\t}\n}\n\\endverbatim\n *\n * With respect to the \\c handle_info request we used as an example, here,\n * the actual content of the last response is omitted for brevity, but\n * you're welcome to experiment with it in order to check whether more\n * information (of a different nature, maybe) may be useful to have. In\n * particular, you may want to play with the plugin-specific details, as\n * different plugins will return different information according to what\n * they provide: for instance, the VideoRoom plugin might clarify whether\n * a handle is being used for publishing media or for receiving it, and\n * what are the involved IDs, the current status of the delivery, and so on.\n * At the same time, the \\c streams object will contain invaluable details\n * related to the WebRTC PeerConnection associated with the handle, as\n * in input/output statistics statistics (bytes, bytes per seconds, NACKs,\n * etc.) or the SDP/ICE/DTLS states. Notice that the information as\n * returned by the Admin API here is just a snapshot: if you're more\n * interested in how this information evolves in a more dynamic way, you\n * may want to start using the Event Handlers instead, which return pretty\n * much the same information, but conveying it as dynamic events pushed\n * to an application you control.\n *\n * \\section adminpcap Capturing unencrypted WebRTC traffic\n * As anticipated, you can also enable/disable the dumping of the RTP/RTCP\n * packets a handle is sending and receiving to a pcap or text2pcap file. This is\n * especially useful for debugging reasons, e.g., to check whether or not\n * there are issues in a specific packet Janus is sending or receiving\n * with tools like Wireshark. Notice that this is not supposed to be used\n * for recording Janus streams: while it can be used for that, the\n * janus_recorder utility is much more suited for the task, and is what\n * all plugins make use of when they're interested in \\ref recordings .\n *\n * The syntax for the \\c start_pcap and \\c start_text2pcap commands is\n * trivial, and apart from the command name pretty much the same: all you\n * need to specify are information on the handle to dump, information\n * on the target file (target folder and filename), and whether to truncate\n * packets or not before dumping them:\n *\n\\verbatim\nPOST /admin/12345678/98765432\n{\n\t\"janus\" : \"start_pcap\",\t\t// Use start_text2pcap for a text file instead\n\t\"folder\" : \"<folder to save the dump to; optional, current folder if missing>\",\n\t\"filename\" : \"<filename of the dump; optional, random filename if missing>\",\n\t\"truncate\" : \"<number of bytes to truncate packet at; optional, truncate=0 (don't truncate) if missing>\",\n\t\"transaction\" : \"<random alphanumeric string>\",\n\t\"admin_secret\" : \"<password specified in janus.jcfg, if any>\"\n}\n\\endverbatim\n *\n * If successful, the full path of the dump file can be obtained by doing\n * a \\c handle_info request. A \\c stop_pcap or \\c start_text2pcap command\n * is even easier to generate, as it doesn't need any parameter:\n *\n\\verbatim\nPOST /admin/12345678/98765432\n{\n\t\"janus\" : \"stop_pcap\",\t\t// Use stop_text2pcap if you started a text-based capture\n\t\"transaction\" : \"<random alphanumeric string>\",\n\t\"admin_secret\" : \"<password specified in janus.jcfg, if any>\"\n}\n\\endverbatim\n *\n */\n\n/*! \\page deploy Deploying Janus\n *\n * When you're going to deploy Janus (e.g., to try the demos we made\n * available out-of-the-box), there's one thing that is important to point\n * out: while Janus does indeed provide an HTTP RESTful interface (documented\n * in \\ref rest), it does \\b NOT also act as a webserver for static files.\n * This means you'll need a different webserver to host static files, including\n * HTML/PHP/JSP/etc. pages, JavaScript files, images and whatever is part\n * of your web application.\n *\n * That said, deploying Janus is, in principle, quite simple: just start Janus on a\n * machine, put the HTML and JavaScript that will make use of it on a webserver\n * somewhere, make sure the JavaScript code is configured with the right\n * address for the server and you're done!\n *\n * Let's assume, for the sake of simplicity, that your webserver is serving\n * files on port \\c 80. By default, Janus binds on the \\c 8088 port for HTTP.\n * So, if Janus and the webserver hosting the are co-located, all you need to get your\n * application working is configure the web application to point to the right\n * address for the server. In the demos provided with these packages, this\n * is done by means of the \\c server variable:\n *\n \\verbatim\nvar server = \"http://\" + window.location.hostname + \":8088/janus\";\n \\endverbatim\n *\n * which basically tells the JavaScript application that the Janus API can be\n * contacted at the same host as the website but at a different port (8088) and path (/janus).\n * In case you configured the server differently, e.g., 7000 as the port\n * for HTTP and /my/custom/path as the API endpoint, the \\c server variable\n * could be built this way:\n *\n \\verbatim\nvar server = \"http://\" + window.location.hostname + \":7000/my/custom/path\";\n \\endverbatim\n *\n * In case the webserver and Janus are <b>NOT</b> colocated, instead, just\n * replace the \\c window.location.hostname part with the right address of\n * the server, e.g.:\n *\n \\verbatim\nvar server = \"http://www.example.com:8088/janus\";\n \\endverbatim\n *\n * It's important to point out, though, that this more \"static\" approach\n * only works if the webserver is serving files via HTTP. As soon as you\n * start involving \\b HTTPS, things start to get more complicated: in fact,\n * for security reasons you cannot contact an HTTP backend if the page is\n * made available via HTTPS. This means that if you're interested in serving\n * your web application via HTTPS, you'll need to enable the HTTPS embedded\n * webserver in Janus as well, and configure the JavaScript code to refer to\n * that itself, e.g.:\n *\n \\verbatim\nvar server = \"https://\" + window.location.hostname + \":8089/janus\";\n \\endverbatim\n *\n * assuming \\c 8089 is the port you configured Janus to use for HTTPS.\n * To make this more \"dynamic\", e.g., allow both HTTP and HTTPS instead of\n * just sticking to one, you might make use of something like this:\n *\n \\verbatim\nvar server = null;\nif(window.location.protocol === 'http:')\n\tserver = \"http://\" + window.location.hostname + \":8088/janus\";\nelse\n\tserver = \"https://\" + window.location.hostname + \":8089/janus\";\n \\endverbatim\n *\n * that is evaluate the right address to use at runtime.\n *\n * Anyway, there's a much easier way to address these scenarios, which\n * is explained in the next section.\n *\n * \\section apache Deploying Janus behind a web frontend\n *\n * To avoid most of the issues explained above, an easy approach can be\n * deploying Janus behind a frontend (e.g., Apache HTTPD, nginx, lighttpd\n * or others) that would act as a reverse proxy for incoming requests.\n * This would allow you to make the Janus API available as a relative path\n * of your web application, rather than a service reachable at a different\n * port and/or domain.\n *\n * Configuring the web application, as a consequence, would be even easier,\n * as all you'd need to do would be to provide a relative path for the API,\n * e.g.:\n *\n \\verbatim\nvar server = \"/janus\";\n \\endverbatim\n *\n * which would automatically work whether the page is served via HTTP or\n * HTTPS. In fact, all the HTTPS requests would be terminated at the webserver,\n * which would then always send simple HTTP messages to the server itself.\n *\n * An easy way to do so in Apache HTTPD is by means of the following directives:\n *\n \\verbatim\nProxyRequests Off\nProxyVia Off\nProxyPass /janus http://127.0.0.1:8088/janus retry=0\nProxyPassReverse /janus http://127.0.0.1:8088/janus\n \\endverbatim\n *\n * Different versions of HTTPD or different webservers may require a\n * different syntax, but the principle is usually always the same: you instruct\n * the webserver to act as a proxy for a local endpoint, in this case a\n * Janus instance colocated at the webserver and configured with the\n * default settings.\n *\n * A way to do the same with nginx, as explained by some Janus users\n * <a href=\"https://groups.google.com/forum/#!topic/meetecho-janus/dIv-4s0HOdw\">here</a>,\n * is the following directive:\n *\n \\verbatim\nlocation /janus {\n\tproxy_pass http://127.0.0.1:8088/janus;\n}\n \\endverbatim\n *\n * \\section webserver A quick and easy web server\n * While opening WebRTC-powered web applications by just opening the\n * application HTML files from file system works with some browsers, it\n * doesn't in others. Specifically, this works in Firefox but not in Chrome\n * (see <a href=\"https://github.com/meetecho/janus-gateway/issues/291\">issue #291</a>).\n * Anyway, considering that you will eventually want other people besides\n * you to use your Janus services, this means that to test and use Janus\n * you'll want/need to host your applications on a webserver.\n *\n * If you're not interested in configuring a full-fledged webserver, but\n * are only interested in a quick and easy way to test the demos, you can\n * make use of the embedded webservers some frameworks like PHP and Python\n * provide. To start a webserver for the demos, for instance, just open a\n * terminal in the \\c html folder of the project, and type:\n *\n *\\verbatim\nphp -S 0.0.0.0:8000\n \\endverbatim\n *\n * or:\n *\n *\\verbatim\npython -m SimpleHTTPServer 8000\n \\endverbatim\n *\n * This will setup a webserver on port \\c 8000 for you to use, meaning you'll\n * just need to have your browser open a local connection to that port to\n * try the demos:\n *\n *\\verbatim\nhttp://yourlocaliphere:8000\n \\endverbatim\n *\n * You can do the same on a different port to also access the HTML version of the Doxygen generated\n * documentation, starting the embedded webservers from the \\c docs/html\n * folder instead:\n *\n *\\verbatim\nphp -S 0.0.0.0:9000\n \\endverbatim\n *\n * or:\n *\n *\\verbatim\npython -m SimpleHTTPServer 9000\n \\endverbatim\n *\n * \\section deplyws Using Janus with WebSockets\n *\n * Configuring the use of WebSockets rather than the REST API in the JavaScript\n * library is quite trivial, as it's a matter of passing a \\c ws:// address\n * instead of an \\c http:// one to the constructor. That said, most of the same\n * considerations provided for the REST API apply here as well, e.g.,\n * to just use \\c window.location.hostname if the webserver and Janus are\n * colocated:\n *\n \\verbatim\nvar server = \"ws://\" + window.location.hostname + \":8188/\";\n \\endverbatim\n *\n * to specify the port if you change it:\n *\n \\verbatim\nvar server = \"ws://\" + window.location.hostname + \":7000/\";\n \\endverbatim\n *\n * and/or the right address of the server in case the webserver and Janus\n * are <b>NOT</b> colocated:\n *\n \\verbatim\nvar server = \"ws://www.example.com:8188/\";\n \\endverbatim\n *\n * Notice how the path (\\c /janus by default for HTTP) is not provided\n * for WebSockets, as it is ignored by the server.\n *\n * The considerations for deploying Janus behind a proxy/webserver, though,\n * differ if you use WebSockets, as most webservers don't provide an easy\n * way to proxy WebSocket requests, and usually require custom modifications\n * for the purpose. Recent versions of HTTPD (>= 2.4.5), with the right\n * module (proxy_wstunnel), do allow you to also proxy WebSockets requests the\n * same way you do with HTTP, which can be useful to do the same\n * WSS-to-WS proxying in a frontend. Here's a sample configuration:\n *\n \\verbatim\n<IfModule mod_proxy_wstunnel.c>\n\tProxyPass /janus-ws ws://127.0.0.1:8188 retry=0\n\tProxyPassReverse /janus-ws ws://127.0.0.1:8188\n</IfModule>\n \\endverbatim\n *\n * that will allow you to expose a <code>wss://myserver/janus-ws</code>\n * or <code>ws://myserver/janus-ws</code> address, and have all communication\n * forwarded to and from Janus at <code>ws://127.0.0.1:8188</code>.\n *\n * Similar configurations are probably available for other systems as well,\n * so in case this is something you're interested in, we recommend you\n * follow the best practices related to that made available by the web server developers.\n *\n * \\section both Using fallback addresses\n * As anticipated in the \\ref JS section, you can also pass an array of servers\n * to the Janus library initialization. This allows you, for instance, to\n * pass a link to both the WebSockets and REST interfaces, and have the\n * library try them both to see which one is reachable, e.g.:\n *\n \\verbatim\nvar ws_server = \"ws://\" + window.location.hostname + \":8188/\";\nvar http_server = \"http://\" + window.location.hostname + \":8088/janus\";\nvar servers = [ws_server, http_server];\n \\endverbatim\n *\n * which is especially useful if you're not sure whether or not WebSockets\n * will work in some specific networks. Please notice that, for the individual\n * servers listed in the array, the same considerations given above (e.g.,\n * in terms of relative vs. absolute linking) still apply.\n *\n * Such an approach can also be used when you've deployed several different\n * instances of Janus, and you want the library to try some and fallback\n * to others if any of them is not reachable for any reason.\n *\n */\n\n/*! \\page service Janus as a daemon/service\n *\n * By default, Janus starts in foreground, and as such works as a server\n * application that you start normally and displays output on the console.\n * That said, there are several reasons why you may not want to keep\n * Janus in the foreground, while still being interested in checking\n * the console to see what's happening.\n *\n * There are different ways to \"daemonize\" it and have it run as a service,\n * though. This page tries to summarize a few ways to do so, starting\n * from \"dumb\" approaches like sending to background and/or using screen/tmux,\n * to more sophisticated approaches involving \\c systemd, \\c upstart\n * and others.\n *\n * \\section daemon Running Janus as a daemon\n * Since version \\c 0.1.0, you can run Janus as a daemon application. To\n * do so, just pass either \\c -b or \\c --daemon as a command line\n * argument, and Janus will be daemonized. Just beware, though, that\n * since this results in stdout/stdin/stderr being closed, you MUST\n * specify a log file for Janus to use, either via command line (\\c -L\n * or \\c --log-file ) or in \\c janus.jcfg.\n *\n * \\section bg Running in background\n * Another simple way to run Janus in the background is to just append the\n * \\c & character to the command line. Anyway, this will still \"flood\" the console\n * with output from Janus. While there are ways to handle it (e.g., as\n * explained <a href=\"http://www.thegeekstuff.com/2010/05/unix-background-job/\">here</a>),\n * a nice and easy way to handle this is redirecting the output to a\n * separate file, e.g., a dedicated log:\n *\n \\verbatim\n/opt/janus/bin/janus -d 5 -6 >/path/to/mylogfile 2>&1 &\n \\endverbatim\n *\n * This is especially useful in case you want to keep a log of what\n * happened when Janus was running, and can also be used as a simple and\n * effective way to watch the console \"live\" using \\c tail:\n \\verbatim\ntail -f /path/to/mylogfile\n \\endverbatim\n *\n * \\section screen Terminal multiplexers\n * Another easy way to run Janus in the background is using terminal\n * multiplexers like \\c screen or \\c tmux. If you're not familiar with\n * such applications, you can find a quick overview\n * <a href=\"https://en.wikipedia.org/wiki/Terminal_multiplexer\">here</a>.\n *\n * The following is a simple example with \\c screen:\n *\n \\verbatim\nscreen -S janus -d -m\nscreen -r janus -X stuff $'/opt/janus/bin/janus -d 5 -6\\n'\n \\endverbatim\n *\n * This will create a session called \"janus\" and launch Janus in it with\n * a few command line options (in this case, just the option to enable\n * IPv6 support and set the debug to verbose). Janus will then be running\n * in the background: accessing the console is just a matter of attaching\n * to the \"janus\" screen:\n *\n \\verbatim\nscreen -r janus\n[CTRL+A+D to detach again]\n \\endverbatim\n *\n * Terminal multiplexers usually allow for logging the output to file\n * as well, if you want to keep an history of what happened during the\n * Janus lifetime.\n *\n * \\section systemd systemd\n * This section shows how you can add Janus as a service to\n * <a href=\"https://en.wikipedia.org/wiki/Systemd\">systemd</a>.\n *\n \\verbatim\n[Unit]\nDescription=Janus WebRTC Server\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=/opt/janus/bin/janus -o\nRestart=on-abnormal\nLimitNOFILE=65536\n\n[Install]\nWantedBy=multi-user.target\n \\endverbatim\n *\n * \\note Remember to adjust the path in ExecStart to match the Janus binary path.\n *\n * \\warning Please beware that, per the default \\c RateLimitInterval and\n * and \\c RateLimitBurst values in the default systemd configuration, logger\n * messages are dropped if they arrive faster than ~33 per second. You\n * may want to configure them accordingly, or otherwise Janus log messages\n * may be missing. To fix this, setting <code>RateLimitInterval=1s</code>\n * and <code>RateLimitBurst=2000</code> in\n * <code>/etc/systemd/journald.conf</code> is usually enough.\n *\n * \\note systemd example provided by\n * <a href=\"https://github.com/meetecho/janus-gateway/pull/306\">\\@saghul</a>\n *\n * \\section upstart upstart\n * This section shows how you can add Janus as a daemon to\n * <a href=\"http://upstart.ubuntu.com/\">upstart</a>, which is\n * typically available on Ubuntu systems.\n *\n \\verbatim\ndescription \"janus\"\n\nstart on filesystem or runlevel [2345]\nstop on runlevel [!2345]\nlimit nofile 50000 50000\nlimit core unlimited unlimited\n\nrespawn\nrespawn limit 10 5\n\nexec /opt/janus/bin/janus\n \\endverbatim\n *\n * \\note upstart example provided by\n * <a href=\"https://github.com/meetecho/janus-gateway/pull/306\">\\@ploxiln</a>\n *\n * \\warning In case starting Janus depends on some external conditions, you\n * may need to modify the \\c start and \\c stop lines accordingly. Here you can\n * find an <a href=\"https://github.com/meetecho/janus-gateway/pull/455\">example</a>,\n * provided by <a href=\"https://github.com/meetecho/janus-gateway/pull/455\">\\@stormbkk87</a>,\n * showing how you can wait, for instance, for RabbitMQ to start before starting Janus too.\n *\n * \\section sysvinit sysvinit\n * This section shows how you can add Janus as a daemon to\n * SysVinit based systems.\n *\n \\verbatim\n#!/bin/sh\n\n### BEGIN INIT INFO\n# Provides:          Janus\n# Required-Start:    $remote_fs $syslog\n# Required-Stop:     $remote_fs $syslog\n# Default-Start:     2 3 4 5\n# Default-Stop:      0 1 6\n# Short-Description: Janus WebRTC Server\n# Description:       Janus WebRTC Server\n### END INIT INFO\n\nDAEMON=/usr/bin/janus\nDAEMON_NAME=janus\n\n# Add any command line options for your daemon here\nDAEMON_OPTS=\"-D -o\"\n\n# This next line determines what user the script runs as.\n# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.\nDAEMON_USER=root\n\n# The process ID of the script when it runs is stored here:\nPIDFILE=/var/run/$DAEMON_NAME.pid\n\n. /lib/lsb/init-functions\n\ndo_start () {\n    log_daemon_msg \"Starting system $DAEMON_NAME daemon\"\n    start-stop-daemon --start --background --no-close --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS >> /var/log/$DAEMON_NAME.log 2>&1\n    log_end_msg $?\n}\ndo_stop () {\n    log_daemon_msg \"Stopping system $DAEMON_NAME daemon\"\n    start-stop-daemon --stop --pidfile $PIDFILE --retry 10\n    log_end_msg $?\n}\n\ncase \"$1\" in\n\n    start|stop)\n        do_${1}\n        ;;\n\n    restart|reload|force-reload)\n        do_stop\n        do_start\n        ;;\n\n    status)\n        status_of_proc \"$DAEMON_NAME\" \"$DAEMON\" && exit 0 || exit $?\n        ;;\n\n    *)\n        echo \"Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}\"\n        exit 1\n        ;;\n\nesac\nexit 0\n \\endverbatim\n *\n * \\note sysvinit example provided by\n * <a href=\"https://github.com/saghul\">\\@saghul</a>\n *\n * \\section supervisor supervisor\n * This section shows how you can add Janus to\n * <a href=\"http://supervisord.org/\">supervisor</a>, which is\n * typically available on Ubuntu systems.\n *\n \\verbatim\n[program:janus]\ncommand=/opt/janus/bin/janus\nuser=root\nautostart=true\nautorestart=true\nstderr_logfile=/var/log/janus.err.log\nstdout_logfile=/var/log/janus.out.log\n \\endverbatim\n *\n * \\note The above configuration file should be added to\n * <code>/etc/supervisor/conf.d/janus.conf</code>. Then the following commands\n * should be run:\n *\n \\verbatim\nsudo supervisorctl reread\nsudo supervisorctl update\n \\endverbatim\n *\n * \\section Others\n * TODO.\n *\n */\n\n/*! \\page debug Debugging Janus\n *\n * In the magical world of fairies and unicorns, the sun always shines\n * and everything always works smoothly and without issues. Unfortunately,\n * this is not the world we live in, and so you might still encounter\n * issues using Janus, e.g., unexpected crashes and the like. We always\n * try and tackle bugs as soon as we spot them, but some issues may be\n * always lingering in the background.\n *\n * Should you encounter a bug or a crash, open a new\n * <a href=\"https://github.com/meetecho/janus-gateway/issues/new\">issue</a>\n * on GitHub. Make sure you carefully read the\n * <a href=\"https://github.com/meetecho/janus-gateway/blob/master/.github/CONTRIBUTING.md\">guidelines</a>\n * for contributing, or otherwise we may decide to close the issue and\n * not even look at it.\n *\n * What's important for us to look into issues and bugs definitely is\n * having enough information to do so. As such, whenever possible try to\n * provide as many details and data as possible. Quite useful to us are\n * GDB stacktraces and/or AddressSanitizer output. The following sections\n * give a quick overview on how you can collect this information after\n * a crash, but for a more detailed description of the tools you should\n * refer to the related documentation pages and tutorials.\n *\n * \\section gdb GDB\n * GDB is the <a href=\"http://www.gnu.org/software/gdb/\">GNU Project Debugger</a>\n * and is an effective tool for looking at what has happened (or is\n * happening) inside an application. As such, it's quite useful to spot\n * bugs and the like, as it can provide information about the values of\n * variables when they were used and the application crashed.\n *\n * First of all make sure that debugging symbols are enabled by reconfiguring Janus like this:\n *\n \\verbatim\nCFLAGS=\"-Og -g3 -ggdb3 -fno-omit-frame-pointer\" ./configure --prefix=/opt/janus\n \\endverbatim\n *\n * Once done configuring, do a \\c make \\c clean (to make sure\n * everything is recompiled from scratch) and then a \\c make and \\c make \\c install.\n *\n * When Janus crashes, you should get a core dump file somewhere. This is\n * a recorded state of the application memory at the time of crashing, and\n * so a backtrace of what lead to an issue can help. You can open such\n * a core dump file via gdb this way:\n *\n \\verbatim\ngdb /path/to/bin/janus /path/to/coredump\ngdb bt\n \\endverbatim\n *\n * The \\c bt command retrieves the backtrace, and is what you should provide\n * as part of your new issue.\n *\n * \\note Please \\c DON'T paste this backtrace in the issue text. Use a\n * service like <a href=\"https://gist.github.com/\">Gist</a> or\n * <a href=\"http://pastebin.com/\">Pastebin</a> and pass the generated\n * link instead.\n *\n * \\section sanitizer Address Sanitizer\n * An even better tool for spotting issues is\n * <a href=\"https://code.google.com/p/address-sanitizer/\">Address Sanitizer</a>,\n * a fast memory error detector. Since it can spot memory errors, it's\n * very useful to find out about hidden race conditions and the like.\n *\n * Unlike GDB which can be used as is, though, to use Address Sanitizer\n * you'll first need to recompile Janus with some new settings, as it\n * requires a specific dependency on a library, libasan, which you'll need\n * to install through your repository manager if needed. Besides, you'll\n * need at least gcc 4.8 for this to work: older versions of gcc won't\n * work.\n *\n * Once you've installed libasan, reconfigure Janus like this:\n *\n \\verbatim\nCFLAGS=\"-O0 -g3 -ggdb3 -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address -fsanitize-address-use-after-scope -fno-sanitize-recover=all\" LDFLAGS=\"-fsanitize=address\" ./configure --prefix=/opt/janus\n \\endverbatim\n *\n * Of course you're free to add whatever additional configuration parameter\n * you were using before: the important parts are the environment variables\n * before that. Once done configuring, do a \\c make \\c clean (to make sure\n * everything is recompiled from scratch) and then a \\c make and \\c make \\c install\n * as usual.\n *\n * At this point, your Janus version should be Address Sanitizer compliant.\n * To make sure, try using \\c ldd to check whether libasan is indeed a\n * dependency or not:\n *\n \\verbatim\nldd janus | grep asan\n \\endverbatim\n *\n * If it is, you're done: whenever Janus crashes for any reason, you'll\n * get additional output from Address Sanitizer automatically with details\n * on what went wrong, and that's what you should provide as part of the\n * issue content. Just as a side note, please beware that using Address\n * Sanitizer Janus will run just a bit slower, even though not to the\n * point of being unusable (as when using, e.g., valgrind).\n *\n * \\note Please \\c DON'T paste Address Sanitizer output in the issue text.\n * Use a service like <a href=\"https://gist.github.com/\">Gist</a> or\n * <a href=\"http://pastebin.com/\">Pastebin</a> and pass the generated\n * link instead.\n *\n */\n\n/*! \\page pluginslist Plugins documentation\n * Out of the box, Janus comes with a set of different and heterogeneous\n * media manipulation plugins. These can be used individually or composed\n * together at an application level for building complex WebRTC-based\n * media applications.\n *\n * The list of plugins currently available is the following. Since new\n * plugins may become available available in the future, make sure you\n * come back to this page for more information.\n *\n * - \\ref echotest\n * - \\ref streaming\n * - \\ref videocall\n * - \\ref sip\n * - \\ref nosip\n * - \\ref audiobridge\n * - \\ref videoroom\n * - \\ref textroom\n * - \\ref recordplay\n * - \\ref lua\n * - \\ref duktape\n */\n\n/*! \\page eventhandlers Event handlers documentation\n * Controlling and monitoring a Janus instance can be done using the\n * \\ref admin which includes ways to query information related to the\n * state of ongoing PeerConnections (ICE/DTLS state, stats, etc.).\n * That said, while powerful and useful the Admin API is a poll-based\n * protocol: this means that you have to query for information yourself,\n * and if you want to keep up-to-date with what is happening, you have\n * to do that on a regular basis. As such, things can get problematic\n * when dealing with many sessions and handles running in your application,\n * as you may not be immediately aware of which session/handle corresponds\n * to what, or which of them belong to the same scenario (e.g., all\n * PeerConnections established in the context of the same VideoRoom).\n *\n * This is where Event Handlers can help. Just like media and transport\n * plugins, Event Handlers plugins in Janus themselves, meaning their\n * modular nature allows for extensibility. When enabling Event handlers,\n * Janus and other plugins generate real-time events related to several\n * different aspects that may be happening during its lifetime: these\n * events are then passed to all the available event handler plugins,\n * plugins that can then decide to do with these events whatever they want.\n * They might choose to store these events somehow, aggregate, dump or\n * process them, format and send them to an external application, and so\n * on. This really depends on what these events should be used for.\n *\n * Janus will generate events related to:\n * - session related events (e.g., session created/destroyed, etc.);\n * - handle related events (e.g., handle attached/detached, etc.);\n * - JSEP related events (e.g., got/sent offer/answer);\n * - WebRTC related events (e.g., PeerConnection up/down, ICE updates, DTLS updates, etc.);\n * - media related events (e.g., media started/stopped flowing, stats on packets/bytes, etc.);\n * - generic events originated by the Janus core (e.g., Janus started/stopped);\n * - events originated by plugins (content specific plugins themselves);\n * - events originated by transports (see above);\n * - events originated by external applications via the Admin API (content specific to source).\n *\n * \\section evhsyntax Events format\n * All events generated by Janus are JSON object, which have a shared\n * header and a custom body that will depend on the type (and in some\n * cases subtype) of the event itself.\n *\n * The header of the event will contain some information that is usually\n * common to all events: this includes \\c type and (when needed) \\c subtype\n * of the event, a timestamp in microseconds (so that you can know exactly\n * when the event was generated in the first place from the server's perspective,\n * no matter it was received), and various IDs. These IDs include the\n * session identifier (Janus session the event refers to), the handle\n * identifier (Janus handle the event refers to) and an opaque ID (set\n * by whoever controls the Janus API): as we'll see, all these IDs are\n * optional, as there are events that are not related to a session in\n * particular (e.g., a server shutdown event), but can be very important\n * for correlation purposes when they're used.\n *\n * The generic format of events is the following:\n\\verbatim\n    {\n        \"emitter\" : \"<string identifying the source of the event, if configured (optional)>\",\n        \"type\" : <numeric event type identifier>,\n        \"subtype\" : <numeric event subtype identifier (specific to the event type; optional)>,\n        \"timestamp\" : <time of when the event was generated>,\n        \"session_id\" : <unique session identifier, if provided/available (optional)>,\n        \"handle_id\" : <unique handle identifier, if provided/available (optional)>,\n        \"opaque_id\" : \"<user-provided opaque identifier, if provided/available (optional)>\",\n        \"event\" : {\n             <event body, custom depending on event type>\n        }\n    }\n\\endverbatim\n *\n * For instance, this is what an event related to a new session being created\n * would look like:\n *\n\\verbatim\n    {\n        \"emitter\": \"MyJanusInstance\",\n        \"type\": 1,\n        \"timestamp\": 1582211094846980,\n        \"session_id\": 3439056127855429,\n        \"event\": {\n            \"name\": \"created\",\n            \"transport\": {\n                \"transport\": \"janus.transport.http\",\n                \"id\": \"0x60400002c2d0\"\n            }\n        }\n    }\n\\endverbatim\n *\n * Since \\c type \\c 1 is related to session events, this event basically\n * tells us that a new session with ID \\c 3439056127855429 was created\n * (we know this from the \\c name property in the event body) on the Janus\n * instance called \\c MyJanusInstance at the timestamp \\c 1582211094846980 .\n * There are no \\c subtype , \\c handle_id or \\c opaque_id properties as\n * they were unneeded or not applicable here.\n *\n * \\section evhtypes Event types\n * As we've seen from the previous example, event types are numeric. In\n * order to figure out what you're receiving, you can refer to the\n * following table as a reference:\n *\n * <table class=\"table table-striped\">\n * <tr><th>Type</th><th>Category of event</th></tr>\n * <tr><td>1</td><td>Session related event</td></tr>\n * <tr><td>2</td><td>Handle related event</td></tr>\n * <tr><td>4</td><td>External event (injected via Admin API)</td></tr>\n * <tr><td>8</td><td>JSEP event (SDP offer/answer)</td></tr>\n * <tr><td>16</td><td>WebRTC state event (ICE/DTLS states, candidates, etc.)</td></tr>\n * <tr><td>32</td><td>Media event (media state, reports, etc.)</td></tr>\n * <tr><td>64</td><td>Plugin-originated event (e.g., event coming from VideoRoom)</td></tr>\n * <tr><td>128</td><td>Transport-originated event (e.g., WebSocket connection state)</td></tr>\n * <tr><td>256</td><td>Core event (server startup/shutdown)</td></tr>\n * </table>\n *\n * The types of events are not monotonically increasing as, internally,\n * they're represented as a mask: this allows event handler plugins to\n * only subscribe to a subset of them, rather than them all, when needed.\n *\n * The event \\c type property dictates the syntax of the \\c event body ,\n * meaning that the body for a JSEP event (type \\c 8 ) will for instance\n * be very different from a media event (type \\c 32 ). That said, the\n * format of some events can change even within the same type: for instance,\n * a WebRTC state event includes a lot of different notifications, including\n * ICE states, DTLS states, local and remote candidates, selected pair, etc.\n * In order to allow event handler recipients written in strongly typed\n * languages to be able to use different classes for the different events,\n * a different property called \\c subtype can help further discriminate\n * an event of a specified \\c type. At the time of writing, a \\c subtype\n * attribute will only be present if the event is of type \\c 256 (core),\n * \\c 16 (WebRTC) and \\c 32 (media): all other event types have a consistent\n * format, and so don't need this differentiation (e.g., type \\c 8 includes\n * both SDP offers and answers, but the nature of the SDP is indicated\n * in an attribute that has the same name in both cases).\n *\n * The available subtypes are the following:\n *\n * <table class=\"table table-striped\">\n * <tr><th colspan=2>Core subtype</th></tr>\n * <tr><td>1</td><td>Server startup</td></tr>\n * <tr><td>2</td><td>Server shutdown</td></tr>\n * <tr><th colspan=2>WebRTC subtype</th></tr>\n * <tr><td>1</td><td>ICE state</td></tr>\n * <tr><td>2</td><td>Local candidate</td></tr>\n * <tr><td>3</td><td>Remote candidate</td></tr>\n * <tr><td>4</td><td>Selected pair</td></tr>\n * <tr><td>5</td><td>DTLS state</td></tr>\n * <tr><td>6</td><td>PeerConnection state</td></tr>\n * <tr><th colspan=2>Media subtype</th></tr>\n * <tr><td>1</td><td>Medium state</td></tr>\n * <tr><td>2</td><td>Slow link</td></tr>\n * <tr><td>3</td><td>Report/stats</td></tr>\n * </table>\n *\n */\n\n/*! \\page recordings Recordings\n * Janus supports recordings of WebRTC sessions out of the box, assuming\n * plugins take advantage of the feature. Specifically, recording as a\n * functionality is implemented in the Janus core, specifically using the\n * janus_recorder structure and the related methods. When enabled, media\n * streams are recorded to a custom format (that we introduce below):\n * as such, it's then up to individual plugins to expose ways for users\n * to configure/start/stop recordings, and using the core functionality\n * to actually implementing the recording part. At the time of writing,\n * most plugins do implement it.\n *\n * \\note This section covers recordings for the purpose of storage,\n * archiving or further manipulation. If you're interested in recording\n * for the purpose of debugging media sessions, you may want to refer to\n * the \\ref adminpcap documentation instead, as that would allow you\n * to capture unencrypted RTP/RTCP packets sent/received to/from Janus\n * in a format tools like Wireshark and tcpdump support.\n *\n * \\section mjr Meetecho Janus Recordings\n * As anticipated, the janus_recorder core functionality saves media\n * streams to a custom format that we call the \"Meetecho Janus Recordings\"\n * format. When saved to file, the custom \\c mjr extension is used.\n *\n * Each \\c mjr file contains a single media stream. This means that if\n * if you want to record, for instance, an audio/video stream, you'll need\n * two separate \\c mjr files: one just for audio, and the other just for\n * video. Each \\c mjr file then basically just contains a structured\n * dump of the RTP packets exactly as they arrived: after a short header\n * that describes the data contained in the recording (i.e., type of\n * media, the codec, when it was created, when the first packet was written),\n * each RTP packet is saved together with info on its length. A partial\n * timestamp of when the packet was received is also stored, as this\n * allows for more realistic conversions to \\c pcap format when needed.\n *\n *\\verbatim\n+-----------------------------------------------+\n|               MJR00002 (8 bytes)              |\n+-----------------------------------------------+\n| LEN (2 bytes) | JSON header (variable length) |\n+-----------------------------------------------+\n|    MEET (4 bytes)     |  Recvd Time (4 bytes) |\n+-----------------------------------------------+\n| LEN (2 bytes) | RTP packet (variable length)  |\n+-----------------------------------------------+\n|    MEET (4 bytes)     |  Recvd Time (4 bytes) |\n+-----------------------------------------------+\n| LEN (2 bytes) | RTP packet (variable length)  |\n+-----------------------------------------------+\n|    MEET (4 bytes)     |  Recvd Time (4 bytes) |\n+-----------------------------------------------+\n| LEN (2 bytes) | RTP packet (variable length)  |\n+-----------------------------------------------+\n|                     ...                       |\n+-----------------------------------------------+\n \\endverbatim\n *\n * This makes the recording process very lightweight, as Janus doesn't\n * need to do anything more that just saving the packets to file: no\n * CPU intensive operation like transcoding or frame manipulation is\n * done by the janus_recorder instances. RTP packets don't even need to\n * be saved in order, as the \\c mjr files can be post-processed later\n * and, as we'll see, re-ordering of the stored packets is part of the\n * activities that our post-processor performs when doing so.\n *\n * \\subsection mjrdata Saving data channels\n * While we've so far only mentioned RTP packets, and so audio and video,\n * Janus actually also natively supports the recording of datachannels.\n * In that case, the recording will be a structured dump not of RTP\n * packets, but of the individual messages that were received. Just as\n * we said for audio and video, since \\c mjr files only cover individual\n * streams, data recordings will need their own instance as well.\n *\n * \\section mjrproc Post-processing the recordings\n * Once a recording is available in the \\c mjr format, it obviously needs\n * some transformation before it can be consumed by external tools, e.g.,\n * media players or third-party applications for further processing (like\n * muxing audio and video together, or mixing multiple video streams\n * together).\n\n * Out of the box, Janus comes with a simple post-processing tool whose\n * only job is re-ordering the available RTP packets, extracting the\n * media frames from the RTP packets (which for video might mean\n * extracting the same frame from multiple packets in sequence), and\n * saving the media frames to a well-known media format. To make this\n * simple, this tool can, for instance, convert an audio \\c mjr recording\n * to an \\c opus file, or a video \\c mjr recording containing VP8 packets\n * to a \\c webm file instead. Notice that no transcoding is done by this\n * postprocessor either: frames are extracted exactly as they were sent\n * originally, and just saved to a media container in a way that multimedia\n * applications can consume them. If further processing is needed (e.g.,\n * muxing, mixing, transcoding, or other), then this is up to third-party\n * applications that can work with existing media files.\n *\n * For more information on the Janus recordings post-processor, check\n * the documentation for the janus-pp-rec.c executable.\n *\n */\n\n/*! \\page resources Resources\n * This page contains pointers to some third-party resources, in particular\n * distro repos for an easier installation of Janus, where available,\n * applications, mobile/docker/Pi stuff, orchestration tools, client stacks,\n * libraries and bindings in other languages (that is tools that you find\n * helpful to interact with Janus outside of the context of the \\c janus.js\n * JavaScript library we provide) and so on. It is not a complete list,\n * but just a summary of the material that has been shared so far by the\n * <a href=\"https://janus.discourse.group/\">Community</a>.\n *\n * If you've developed anything related to Janus and you're willing to share\n * it with the community, or you know any such effort that is not listed\n * here, just let us know and we'll add it on this page.\n * <br/><br/>\n *\n * - <a href=\"#repos\">Distro repositories</a>\\n\n * - <a href=\"#stacks\">Client-side stacks</a>\\n\n * - <a href=\"#mobile\">Mobile resources</a>\\n\n * - <a href=\"#pi\">Raspberry Pi resources</a>\\n\n * - <a href=\"#docker\">Docker resources</a>\\n\n * - <a href=\"#config\">Configuration management</a>\\n\n * - <a href=\"#thirdplugins\">Third-party plugins</a>\\n\n * - <a href=\"#thirdtransports\">Third-party transports</a>\\n\n * - <a href=\"#evhandlers\">Event handlers (monitoring/troubleshooting)</a>\\n\n * - <a href=\"#orchestration\">Orchestration</a>\\n\n * - <a href=\"#applications\">Complete applications</a>\\n\n *\n * <hr/>\n *\n * \\section repos Distro repositories\n *\n * <table class=\"table table-striped\">\n * <tr><th>Distro</th><th>Author</th><th>Repo</th><th>Description</th></tr>\n * <tr>\n * \t\t<td>Debian</td>\n * \t\t<td>Debian VoIP Team</td>\n * \t\t<td><a href=\"https://packages.debian.org/source/sid/janus\">Janus packages (sid)</a></td>\n *\t\t<td>Debian Sid</td>\n * </tr>\n * <tr>\n * \t\t<td>Debian/Ubuntu</td>\n * \t\t<td><a href=\"https://github.com/saghul\">Saúl Ibarra Corretgé</a></td>\n * \t\t<td><a href=\"http://projects.ag-projects.com/projects/documentation/wiki/Repositories\">AG Projects Repo</a></td>\n *\t\t<td>Debian Jessie, Ubuntu Trusty and Ubuntu Xenial</td>\n * </tr>\n * <tr>\n * \t\t<td>Snap</td>\n * \t\t<td><a href=\"https://github.com/RSATom\">Sergey Radionov</a></td>\n * \t\t<td><a href=\"https://github.com/RSATom/janus-gateway-snap\">janus-gateway-snap</a></td>\n *\t\t<td>Helper repo for build Janus WebRTC Server on build.snapcraft.io</td>\n * </tr>\n * <tr>\n * \t\t<td>openSUSE/SUSE</td>\n * \t\t<td><a href=\"https://github.com/ancorgs\">Ancor Gonzalez Sosa</a></td>\n * \t\t<td><a href=\"https://build.opensuse.org/project/show/network:jangouts\">Janus packages</a></td>\n *\t\t<td>Repositories for several versions of SUSE and openSUSE</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section stacks Client-side stacks\n *\n * <table class=\"table table-striped\">\n * <tr><th>Language</th><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://github.com/meetecho\">Meetecho</a></td>\n * \t\t<td><a href=\"https://github.com/meetecho/janode\">janode</a></td>\n *\t\t<td>A Node.js adapter for the Janus WebRTC server</td>\n * </tr>\n  * <tr>\n * \t\t<td>TypeScript/node</td>\n * \t\t<td><a href=\"https://github.com/smyrgeorge\">smyrgeorge</a></td>\n * \t\t<td><a href=\"https://github.com/smyrgeorge/janus-gateway-tsdx\">/janus-gateway-tsdx</a></td>\n *\t\t<td>Modern typescript client for janus gateway. Based on websockets</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://github.com/ndarilek\">Nolan Darilek</a></td>\n * \t\t<td><a href=\"https://github.com/ndarilek/node-janus\">node-janus</a></td>\n *\t\t<td>node and browserify compatible integration layer for the Janus WebRTC server</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://github.com/DamonOehlman\">Damon Oehlman</a></td>\n * \t\t<td><a href=\"https://github.com/rtc-io/rtc-janus\">rtc-janus</a></td>\n *\t\t<td>node and browserify compatible integration layer for the Janus WebRTC server:\n * experimental and incomplete as per the author words, but a good starting point</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://github.com/sjkummer\">Sebastian Schmid</a></td>\n * \t\t<td><a href=\"https://github.com/sjkummer/janus-gateway-js\">janus-gateway-js</a></td>\n * \t\t<td>Simple JavaScript client for janus-gateway that runs in the browser as well as in Node.js</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://www.sipwise.com/\">sip:wise</a></td>\n * \t\t<td><a href=\"https://www.npmjs.com/~sipwise\">admin/videoroom client</a></td>\n * \t\t<td>Node.js clients that implement the Admin API and VideoRoom functionality</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://github.com/mquander\">Marshall Quander</a></td>\n * \t\t<td><a href=\"https://github.com/networked-aframe/minijanus.js\">minijanus.js</a></td>\n * \t\t<td>A super-simplistic and -minimal wrapper for talking to the Janus signalling API</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://github.com/TechTeamer\">TechTeamer</a></td>\n * \t\t<td><a href=\"https://github.com/TechTeamer/janus-api\">janus-api</a></td>\n * \t\t<td>Javascript (node and browser side) API for Janus WebRTC server</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/node</td>\n * \t\t<td><a href=\"https://github.com/uwejan\">Saddam Uwejan</a></td>\n * \t\t<td><a href=\"https://github.com/uwejan/janus-api-mqtt\">janus-api-mqtt</a></td>\n * \t\t<td>Javascript (node and browser side) API for Janus WebRTC Gateway, Using MQTT</td>\n * </tr>\n * <tr>\n * \t\t<td>JavaScript/Angular</td>\n * \t\t<td><a href=\"https://github.com/kevin29a\">Kevin Thompson</a></td>\n * \t\t<td><a href=\"https://kevin29a.github.io/angular-janus\">janus-angular</a></td>\n * \t\t<td>Angular Component for implementing a videoroom</td>\n * </tr>\n * <tr>\n * \t\t<td>ruby</td>\n * \t\t<td><a href=\"https://github.com/cargomedia\">Cargo Media</a></td>\n * \t\t<td><a href=\"https://github.com/cargomedia/janus-gateway-ruby\">janus-gateway-ruby</a></td>\n * \t\t<td>ruby client side API wrapper for the Janus API (websocket only at the moment)</td>\n * </tr>\n * <tr>\n * \t\t<td>C#/.NET</td>\n * \t\t<td><a href=\"https://github.com/Computician\">Benjamin Trent</a></td>\n * \t\t<td><a href=\"https://github.com/Computician/JanusSharp\">JanusSharp</a></td>\n * \t\t<td>C#/.Net client side API wrapper for the Janus API</td>\n * </tr>\n * <tr>\n * \t\t<td>PHP</td>\n * \t\t<td><a href=\"https://github.com/BenJaziaSadok\">Mohamed Sadok Ben Jazia</a></td>\n * \t\t<td><a href=\"https://github.com/BenJaziaSadok/janus-gateway-php\">janus-gateway-php</a></td>\n * \t\t<td>Client-side PHP/javascript implementation of the Janus and Admin APIs</td>\n * </tr>\n * <tr>\n * \t\t<td>PHP</td>\n * \t\t<td><a href=\"https://github.com/mvnrsa\">Marnus van Niekerk</a></td>\n * \t\t<td><a href=\"https://github.com/mvnrsa/JanusPHPclass\">Janus PHP Class</a></td>\n * \t\t<td>Client-side PHP Class implementing the Janus Admin API</td>\n * </tr>\n * <tr>\n * \t\t<td>Golang</td>\n * \t\t<td><a href=\"https://github.com/nowylie\">Nicholas Wylie</a></td>\n * \t\t<td><a href=\"https://github.com/nowylie/go-janus\">go-janus</a></td>\n * \t\t<td>Golang library to the Janus API (Unix Sockets/SOCK_DGRAM only at the moment)</td>\n * </tr>\n * <tr>\n * \t\t<td>Elixir</td>\n * \t\t<td><a href=\"https://github.com/ndarilek\">Nolan Darilek</a></td>\n * \t\t<td><a href=\"https://github.com/ndarilek/elixir-janus\">elixir-janus</a></td>\n *\t\t<td>Elixir client side API wrapper for the Janus WebRTC server</td>\n * </tr>\n * <tr>\n * \t\t<td>OBS</td>\n * \t\t<td><a href=\"https://github.com/CoSMoSoftware\">CoSMo Software</a></td>\n * \t\t<td><a href=\"https://github.com/CoSMoSoftware/OBS-studio-webrtc\">OBS-studio-webrtc</a></td>\n *\t\t<td>This is a fork of obs-studio with support for webrtc in general, and Janus Video Room plugin in particular</td>\n * </tr>\n * <tr>\n * \t\t<td>OCaml</td>\n * \t\t<td><a href=\"https://github.com/monstasat\">Alexander Yanin</a></td>\n * \t\t<td><a href=\"https://github.com/monstasat/janus-ocaml\">Janus-ocaml</a></td>\n *\t\t<td>Library for Janus WebRTC server handling written in OCaml (HTTP only)</td>\n * </tr>\n * <tr>\n * \t\t<td>Haskell</td>\n * \t\t<td><a href=\"https://github.com/oofp\">oofp</a></td>\n * \t\t<td><a href=\"https://github.com/oofp/janus-connector\">janus-connector</a></td>\n *\t\t<td>Haskell binding of Janus client protocol using WebSocket transport with examples</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section mobile Mobile resources\n *\n * <table class=\"table table-striped\">\n * <tr><th>OS</th><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td>Android/iOS</td>\n * \t\t<td><a href=\"https://github.com/meetecho\">Meetecho</a></td>\n * \t\t<td><a href=\"https://github.com/meetecho/janus-mobile-sdk\">janus-mobile-sdk</a></td>\n *\t\t<td>Janus Client SDK [ABANDONED PROJECT]</td>\n * </tr>\n * <tr>\n * \t\t<td>Android</td>\n * \t\t<td><a href=\"https://github.com/Computician\">Benjamin Trent</a></td>\n * \t\t<td><a href=\"https://github.com/Computician/janus-gateway-android\">janus-gateway-android</a></td>\n *\t\t<td>API wrapper that utilizes the native WebRTC build and is made to ease communication with Janus</td>\n * </tr>\n * <tr>\n * \t\t<td>iOS</td>\n * \t\t<td><a href=\"https://github.com/davibe\">Davide Bertola</a></td>\n * \t\t<td><a href=\"https://github.com/davibe/cordova-webrtc-janus-gateway\">cordova-webrtc-janus-gateway</a></td>\n * \t\t<td>cordova application that interfaces with Janus and is based on the PhoneRTC cordova plugin</td>\n * </tr>\n * <tr>\n * \t\t<td>Android/iOS</td>\n * \t\t<td><a href=\"https://github.com/atyenoria\">Akinori Nakajima</a></td>\n * \t\t<td><a href=\"https://github.com/atyenoria/react-native-webrtc-janus-gateway\">react-native-webrtc-janus-gateway</a></td>\n * \t\t<td>Video conference system for mobile application on react-native-webrtc + Janus WebRTC server</td>\n * </tr>\n * <tr>\n * \t\t<td>Android/iOS</td>\n * \t\t<td><a href=\"https://github.com/WorldViews\">WorldViews</a></td>\n * \t\t<td><a href=\"https://github.com/WorldViews/JanusMobile\">JanusMobile</a></td>\n * \t\t<td>react-native based Janus mobile client</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section pi Raspberry Pi resources\n *\n * <table class=\"table table-striped\">\n * <tr><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td><a href=\"http://linux-projects.org\">Linux Projects</a></td>\n * \t\t<td><a href=\"http://www.linux-projects.org/rpi-videoconference-demo-os/\">Rpi VideoConference OS</a></td>\n *\t\t<td>Ready-to-use OS for video conferences over the web from a Raspberry Pi</td>\n * </tr>\n * <tr>\n * \t\t<td><a href=\"http://linux-projects.org\">Linux Projects</a></td>\n * \t\t<td><a href=\"http://www.linux-projects.org/uv4l/tutorials/janus-gateway/\">UV4L, User Space Video Collection</a></td>\n * \t\t<td>Software modules providing solutions for encrypted live data, audio and video streaming, mirroring, conferencing</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section docker Docker resources\n *\n * <table class=\"table table-striped\">\n * <tr><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/krull\">Brendan Jocson</a></td>\n * \t\t<td><a href=\"https://github.com/krull/docker-janus\">docker-janus</a></td>\n *\t\t<td>Debian 8 based docker image for Meetecho's Janus Gateway</td>\n * </tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/atyenoria\">Akinori Nakajima</a></td>\n * \t\t<td><a href=\"https://github.com/atyenoria/janus-gateway-docker\">janus-gateway-docker</a></td>\n *\t\t<td>Janus WebRTC server Docker Image for Media Streaming Expert User</td>\n * </tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/canyanio\">Canyan.io</a></td>\n * \t\t<td><a href=\"https://github.com/canyanio/janus-gateway-docker\">janus-gateway-docker</a></td>\n *\t\t<td>Docker image for the Janus WebRTC Server</td>\n * </tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/bartbalaz\">Bartosz Balazinski</a></td>\n * \t\t<td><a href=\"https://github.com/bartbalaz/janus-container\">janus-container</a></td>\n *\t\t<td>Janus container</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section config Configuration management\n *\n * <table class=\"table table-striped\">\n * <tr><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/bitsy-ai\">Leigh Johnson</a></td>\n * \t\t<td><a href=\"https://galaxy.ansible.com/bitsyai/janus_gateway\">ansible-role-janus-gateway</a></td>\n *\t\t<td>Ansible role to build and deploy Janus</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section thirdplugins Third-party plugins\n *\n * <table class=\"table table-striped\">\n * <tr><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/cargomedia\">Cargo Media</a></td>\n * \t\t<td><a href=\"https://github.com/cargomedia/janus-gateway-rtpbroadcast\">janus-gateway-rtpbroadcast</a></td>\n * \t\t<td>Janus-gateway plugin to broadcast RTP video</td>\n * </tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/mquander\">Marshall Quander</a></td>\n * \t\t<td><a href=\"https://github.com/networked-aframe/janus-plugin-rs\">janus-plugin-rs</a></td>\n * \t\t<td>Rust bindings and wrappers for creating Janus plugins in Rust</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section thirdtransports Third-party transports\n *\n * <table class=\"table table-striped\">\n * <tr><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/nowylie\">Nicholas Wylie</a></td>\n * \t\t<td><a href=\"https://github.com/nowylie/janus-ud-transport\">janus-ud-transport</a></td>\n * \t\t<td>Janus transport plugin that adds support for Datagram messages over Unix Domain Sockets</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section evhandlers Event handlers (monitoring/troubleshooting)\n *\n * <table class=\"table table-striped\">\n * <tr><th>Language</th><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td>node-js</td>\n * \t\t<td><a href=\"https://github.com/thehunmonkgroup\">Chad Phillips</a></td>\n * \t\t<td><a href=\"https://github.com/thehunmonkgroup/janus-event-server\">janus-event-server</a></td>\n * \t\t<td>Simple plugin-based server to receive/process events from Janus</td>\n * </tr>\n * <tr>\n * \t\t<td>Rust</td>\n * \t\t<td><a href=\"https://github.com/mozilla\">Mozilla</a></td>\n * \t\t<td><a href=\"https://github.com/mozilla/janus-eventhandler-sqlite\">janus-eventhandler-sqlite</a></td>\n * \t\t<td>A Janus event handler plugin that writes events to a SQLite database</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section orchestration Orchestration\n *\n * <table class=\"table table-striped\">\n * <tr><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/OpenSight\">OpenSight</a></td>\n * \t\t<td><a href=\"https://github.com/OpenSight/janus-cloud\">janus-cloud</a></td>\n * \t\t<td>An API proxy for Janus WebRTC server cluster</td>\n * </tr>\n * </table>\n * <br/>\n *\n * \\section applications Complete applications\n *\n * <table class=\"table table-striped\">\n * <tr><th>Author</th><th>Project</th><th>Description</th></tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/jangouts\">jangouts</a></td>\n * \t\t<td><a href=\"https://github.com/jangouts/jangouts\">jangouts</a></td>\n * \t\t<td>Videoconferencing based on WebRTC and Janus Gateway with an UI inspired by Google Hangouts</td>\n * </tr>\n * <tr>\n * \t\t<td><a href=\"https://github.com/gjovanov\">Goran Jovanov</a></td>\n * \t\t<td><a href=\"https://github.com/gjovanov/roomler\">roomler</a></td>\n * \t\t<td>Roomler - Video collaboration tool using WebRTC (Janus Gateway)</td>\n * </tr>\n * </table>\n * <br/>\n *\n */\n\n/*! \\page ide Writing code for Janus\n *\n * Janus is open source, which means the whole source code is available for\n * both its core internals and all plugins. As such, it's easy to write code\n * to fix issues, add new features, implement new modules and so on.\n *\n * You don't really need any integrated development environment (IDE) to\n * write code for Janus, but in case you do want to make use of one, you\n * may need to tweak your development environment accordingly. The Janus\n * codebase makes use of many macros defined at <code>configure</code> time\n * to figure out what's available and what should be compiled, for instance,\n * and the IDE you're using may need to be aware of those to do its job.\n *\n * The following snippet is an example of how you can configure, for instance,\n * Visual Studio Code on Ubuntu 20.04:\n *\n\\verbatim\n{\n    \"configurations\": [\n        {\n            \"name\": \"Linux\",\n            \"compilerPath\": \"/usr/bin/gcc\",\n            \"cStandard\": \"gnu17\",\n            \"includePath\": [\n                \"${workspaceFolder}/**\",\n                \"/usr/lib/x86_64-linux-gnu/glib-2.0/include\",\n                \"/usr/include/**\"\n            ],\n            \"intelliSenseMode\": \"linux-gcc-x64\",\n            \"defines\": [\n                \"CONFDIR=\\\"\\\"\",\n                \"EVENTDIR=\\\"\\\"\",\n                \"HAVE_CLOSE_ASYNC=1\",\n                \"HAS_DTLS_WINDOW_SIZE=1\",\n                \"HAVE_ICE_NOMINATION=1\",\n                \"HAVE_LIBCURL=1\",\n                \"HAVE_LIBNICE_TCP=1\",\n                \"HAVE_LIBOGG=1\",\n                \"HAVE_LIBOPUS=1\",\n                \"HAVE_LIBWEBSOCKETS_PEER_SIMPLE=1\",\n                \"HAVE_PORTRANGE=1\",\n                \"HAVE_SCTP=1\",\n                \"HAVE_SRTP_2=1\",\n                \"LOGGERDIR=\\\"\\\"\",\n                \"PLUGINDIR=\\\"\\\"\",\n                \"TRANSPORTDIR=\\\"\\\"\"\n            ]\n        }\n    ],\n    \"version\": 4\n}\n\\endverbatim\n *\n */\n\n/*! \\page README README\n *  \\verbinclude README.md\n */\n\n/*! \\page CREDITS Credits\n *\n * Janus WebRTC Server © 2014-2026 <a href=\"https://www.meetecho.com/\">Meetecho</a> (https://www.meetecho.com/)\n *\n * \\b Author:\n *         Lorenzo Miniero <lorenzo@meetecho.com>\n *\n * Several open source components have been used to implement this software:\n *\n * - \\b GLib: http://library.gnome.org/devel/glib/\n * - \\b pkg-config: http://www.freedesktop.org/wiki/Software/pkg-config/\n * - \\b Jansson: http://www.digip.org/jansson/ (JSON)\n * - \\b libconfig: https://hyperrealm.github.io/libconfig/ (configuration files)\n * - \\b libnice: https://libnice.freedesktop.org/ (ICE/STUN/TURN, at least v0.1.16 suggested, v0.1.18 recommended)\n * - \\b OpenSSL: http://www.openssl.org/ (DTLS, at least v1.0.1e)\n * - \\b libsrtp: https://github.com/cisco/libsrtp (SRTP, at least v2.x suggested)\n * - \\b usrsctp: https://github.com/sctplab/usrsctp (\\c optional, Data Channels)\n * - \\b libmicrohttpd: http://www.gnu.org/software/libmicrohttpd/ (\\c optional, Web server, at least v0.9.59)\n * - \\b libwebsockets: https://libwebsockets.org/ (\\c optional, at least v4.x suggested, WebSockets)\n * - \\b rabbitmq-c: https://github.com/alanxz/rabbitmq-c (\\c optional, v1.0.4, RabbitMQ)\n * - \\b paho.mqtt.c: https://eclipse.org/paho/clients/c (\\c optional, v1.1.0 for MQTT v3.1 & v3.1.1 or v1.3.0 for MQTT v5)\n * - \\b nanomsg: https://nanomsg.org/ (\\c optional, Nanomsg)\n * - \\b Sofia-SIP: https://github.com/freeswitch/sofia-sip (\\c optional, only needed for the SIP plugin)\n * - \\b libopus: http://opus-codec.org/ (\\c optional, only needed for the AudioBridge plugin)\n * - \\b libogg: http://xiph.org/ogg/ (\\c optional, only needed for the AudioBridge and Streaming plugins)\n * - \\b libcurl: https://curl.haxx.se/libcurl/ (\\c optional, only needed for the TURN REST API,\n * RTSP support in the Streaming plugin and the sample Event Handler plugin)\n * - \\b zlib: https://zlib.net/ (gzip compression utility)\n * - \\b Lua: https://www.lua.org/download.html (\\c optional, only needed for the Lua plugin)\n * - \\b Duktape: https://duktape.org/ (\\c optional, only needed for the Duktape plugin)\n * - \\b npm: https://docs.npmjs.com/ (\\c optional, used during build for generating JavaScript modules)\n *\n */\n\n/*! \\page COPYING License\n * This program is free software, distributed under the terms of the GNU\n * General Public License Version 3.\n *\n * If you're interested in a commercial license (e.g., because GPLv3 is\n * not suited for what you need, you're interested in technical support\n * or want to sponsor the development of new features), feel free to\n * <a href=\"https://janus.conf.meetecho.com/support\">contact us</a>.\n *\n *  \\verbinclude COPYING\n */\n\n/*! \\page CHANGELOG Tagged versions and Changelog\n * There are different tagged versions on the Janus repository. We usually\n * tag a new version any time a breaking change and/or a set of comprehensive\n * changes and fixes is going to be merged/applied to Janus, and so the\n * Changelog below can act as a simple and quick summary of which changes\n * are available in each version.\n *\n * It's very important to point out, though, that tagged version are \\b NOT\n * to be considered \\b stable versions. This is a common misunderstanding.\n * The only version we consider \\b stable is \\b master as it's the only\n * branch where we continuously provide fixes and enhancements: this is\n * particularly important in the WebRTC world, where it's not uncommon to\n * see features breaking overnight due to changes in how browsers and other\n * WebRTC devices implement things. As such, again, a tagged version is\n * \\b only a way to take a snapshot of where Janus was at a specific point\n * in time, and before a more or less major change occurred. While you're\n * free to stick to tagged versions for your deployments (e.g., because\n * that's how provisioning is usually done in your company), please notice\n * we will ignore issues and reports addressing any other branch that is\n * not master: due to lack of time and resources, we simply cannot go and\n * investigate issues we may have fixed already, so if you're experiencing\n * issues, make sure you can replicate them on master as well first.\n *\n *  \\verbinclude CHANGELOG.md\n */\n\n/*! \\page FAQ Frequently Asked Questions\n * This page contains a list of FAQ as gathered on the\n * <a href=\"https://janus.discourse.group/\">Community</a> and the\n * <a href=\"https://github.com/meetecho/janus-gateway/issues\">Issues</a>\n * page on GitHub. It obviously also includes things we're being asked all the\n * time in general! If your question is not listed here or not available\n * anywhere in this documentation, feel free to refer to the group for\n * generic help, or to the Issues page for bugs in the implementation.\\n\\n\n *\n * <hr>\n *\n * -# <a href=\"#janus\">What is Janus?</a>\\n\n * -# <a href=\"#meetecho\">What is Meetecho?</a>\\n\n * -# <a href=\"#pronounce\">Now that we're at it, how is Meetecho pronounced??</a>\\n\n * -# <a href=\"#videos\">I just started with Janus and am overwhelmed by the amount of info, documentation, etc... any easy way to dive in?</a>\\n\n * -# <a href=\"#origin\">Why is Janus called like that?</a>\\n\n * -# <a href=\"#license\">Is the license AGPLv3 or GPLv3? Do you provide alternative license mechanisms as well?</a>\\n\n * -# <a href=\"#OS\">On what OS can I install Janus?</a>\\n\n * -# <a href=\"#datachans\">Are Data Channels supported?</a>\\n\n * -# <a href=\"#usrsctp\">I don't care about Data Channels, do I have to compile usrsctp anyway?</a>\\n\n * -# <a href=\"#usrsctperr\">I can't install usrsctp, I'm getting errors about dereferencing pointers?</a>\\n\n * -# <a href=\"#gateway\">Can I use Janus as a gateway to my Freeswitch/Kamailio/Asterisk/other SIP infrastructure?</a>\\n\n * -# <a href=\"#jssip\">Can I use existing SIP stacks (e.g., JsSIP) with Janus?</a>\\n\n * -# <a href=\"#transcoding\">Does Janus support transcoding?</a>\\n\n * -# <a href=\"#recording\">Does Janus support recording?</a>\\n\n * -# <a href=\"#websockets\">Can I use WebSockets instead of plain HTTP to interact with Janus?</a>\\n\n * -# <a href=\"#rabbitmq\">Can I use RabbitMQ instead of HTTP/WebSockets to interact with Janus?</a>\\n\n * -# <a href=\"#unixsockets\">Can I use Unix Sockets instead of HTTP/WebSockets/RabbitMQ to interact with Janus?</a>\\n\n * -# <a href=\"#mqtt\">Can I use MQTT instead of HTTP/WebSockets/RabbitMQ/Unix Sockets to interact with Janus?</a>\\n\n * -# <a href=\"#nanomsg\">Can I use Nanomsg instead of HTTP/WebSockets/RabbitMQ/MQTT/Unix Sockets to interact with Janus?</a>\\n\n * -# <a href=\"#transports\">What about \\<my favourite control protocol\\> instead?</a>\\n\n * -# <a href=\"#demos\">I've launched Janus, how do I try the demos?</a>\\n\n * -# <a href=\"#certificates\">I'm trying the demos, but I get \"Janus down\" or certificate errors!</a>\\n\n * -# <a href=\"#nodejs\">Can I use Janus with node.js or other frameworks?</a>\\n\n * -# <a href=\"#monitoring\">How do I monitor/manage my Janus instance?</a>\\n\n * -# <a href=\"#writeplugin\">I want to write my own plugin, where do I start?</a>\\n\n * -# <a href=\"#ulimit\">I'm using the VideoRoom plugin and, when several users are handled, I get\n *    a \"Too many open files\" errors and Janus crashes</a>\\n\n * -# <a href=\"#aesgcm\">I get an undefined reference to <code>srtp_crypto_policy_set_aes_gcm_256_16_auth</code>\n *    when building Janus</a>\\n\n * -# <a href=\"#https\">When I enable the HTTPS web server in Janus, the CPU goes crazy</a>\\n\n * -# <a href=\"#turn\">I enabled TURN in the Janus configuration, but my clients behind a firewall can't connect</a>\\n\n * -# <a href=\"#benchmark\">Is there any benchmark on Janus performances?</a>\\n\n * -# <a href=\"#scaling\">How do I scale Janus?</a>\\n\n * -# <a href=\"#requests\">Can you implement a feature/plugin/application I want?</a>\\n\n * -# <a href=\"#learning\">I want to learn more on Janus!</a>\\n\n *\n * <hr>\n *\n * \\anchor janus\n * -# <b>What is Janus?</b>\\n\n *    .\n *    Janus is an open source WebRTC server written by <a href=\"https://www.meetecho.com\">Meetecho</a>,\n *    conceived as modular and, as much as possible,\n *    general purpose. It acts as a WebRTC endpoint browsers can interact\n *    with, and different modules can determine what should be done with\n *    the media. This means that you can do SIP, videoconferencing, streaming\n *    and tons of other stuff using the same box! And if what you need is\n *    not there, you can always write your own module and expand Janus.\\n\\n\n *    .\n * \\anchor meetecho\n * -# <b>What is Meetecho?</b>\\n\n *    .\n *    Meetecho is a company founded by a few researchers, post-docs and\n *    Ph.Ds coming from the Universty of Napoli Federico II. We've been\n *    working on real-time multimedia applications over the Internet for\n *    years, and at one point we decided to try and make products out\n *    of our research efforts. Our web conferencing platform and Janus\n *    are part of those efforts.\\n\\n\n *    .\n * \\anchor pronounce\n * -# <b>Now that we're at it, how is Meetecho pronounced??</b>\\n\n *    .\n *    We're being asked that a lot! We've heard so many variants and different\n *    pronounciations of the name that we could make a book out of it!\n *    When we chose the name, we wanted it to sound like\n *    <a href=\"https://www.youtube.com/watch?v=TkgDOMSv9PE\">this</a>,\n *    which means \\a awesome! in Italian. The extra H was a mistake on our\n *    side, as obviously \\a echo is pronounced differently than \\a eco! Long\n *    story short, it doesn't really matter how you pronounce it: just\n *    do it and help us be famous! :-)\\n\\n\n *    .\n * \\anchor videos\n * -# <b>I just started with Janus and am overwhelmed by the amount of info, documentation, etc... any easy way to dive in?</b>\\n\n *    .\n *    Fair point! Janus and its concepts/APIs can be a lot to digest in a short time...\n *    An easy way to start is watching one of the several presentations we've done\n *    on Janus these last months. Most if not all of them try to provide a\n *    complete introduction to the context Janus was built in, how it was\n *    born, the architecture we chose for it and so on. As such, it should\n *    be a useful starting point for whoever is interested in learning\n *    what Janus can do and how to use it.\\n\\n\n *    There are several videos available that you can find online (most of the slides\n *    available on <a href=\"https://www.slideshare.net/LorenzoMiniero/\">SlideShare</a>),\n *    here are some of them as quick pointers:\n *    - Presentations at <a href=\"https://www.youtube.com/watch?v=u0PyXgAC8m4\">vuc485</a>,\n *    <a href=\"https://www.youtube.com/watch?v=VnflQF7oCLA\">vuc584</a>\n *    and <a href=\"https://www.youtube.com/watch?v=XnQ7ECOpRIU\">vuc640</a> (last one more focused on IETF remote participation);\n *    - Presentation at <a href=\"https://www.youtube.com/watch?v=SFeWYewoL7Q\">OpenSIPS summit 2016</a>;\n *    - Presentation at <a href=\"https://www.youtube.com/watch?v=yvt-vMHW83c\">Kamailio World 2016</a>;\n *    - Presentation at <a href=\"https://www.youtube.com/watch?v=WfNciKbsP80\">WebRTC Meetup Tokyo #12</a>;\n *    - Presentation at <a href=\"https://www.youtube.com/watch?v=gArqopeNQY0\">DevDay Napoli</a> (in Italian);\n *    - Presentation on Event Handlers at <a href=\"https://www.youtube.com/watch?v=UT9CDhoHL_Q\">FOSDEM 2017</a>;\n *    - Presentation on different SIP options in Janus at <a href=\"https://www.youtube.com/watch?v=anmyMC6Ovl8&t=25112s\">OpenSIPS 2017</a>;\n *    - Presentation on WebRTC load testing at <a href=\"https://www.youtube.com/watch?v=SnvTAsYtZ5s\">Kamailio World 2017</a>;\n *    - Presentation on SIP, WebRTC and Asterisk at <a href=\"https://www.youtube.com/watch?v=IaPDufOzVek\">Astricon 2017</a>;\n *    - Presentation at <a href=\"https://www.youtube.com/watch?v=cSV0X1JgcKY\">WebRTC Rockstars Tour (Singapore)</a>;\n *    - Presentation at <a href=\"https://youtu.be/dsVDzh9sS1A?t=51m00s\">WebRTC Meetup Tokyo #17</a> (in Japanese);\n *    - Interview on the origins of Janus, at <a href=\"http://www.allthingsrtc.org/2017/10/30/lorenzo-miniero-creator-janus/\">AllThingRTC.org</a>;\n *    - Presentation on the Janus Lua plugin, at <a href=\"https://www.youtube.com/watch?v=NVLdbbnmUAA\">FOSDEM 2018</a>;\n *    - Presentation on how to integrate Janus and HOMER/HEPIC, at <a href=\"https://www.youtube.com/watch?v=YUZqhu2-tbM\">OpenSIPS 2018</a>;\n *    - Presentation on Security, Authentication and Privacy in WebRTC, at <a href=\"https://www.youtube.com/watch?v=ewNJMci62rs\">Kamailio World 2018</a>;\n *    - Presentation on Janus and Scalability, at <a href=\"https://www.youtube.com/watch?v=zxRwELmyWU0\">CommCon 2018</a>;\n *    - Interview on Janus, at <a href=\"https://www.youtube.com/watch?v=dzQhyw1jiaA\">ClueCon Weekly</a>;\n *    - Presentation at <a href=\"https://vimeo.com/303014581\">Voip2day 2018</a> (here for the <a href=\"https://vimeo.com/302814764\">Spanish version</a>);\n *    - Presentation on Janus and SIP at <a href=\"https://www.youtube.com/watch?v=beHHL0Ew5xY\">OpenSIPS 2019</a>;\n *    - Presentation on RTC fuzzing in Janus at <a href=\"https://www.youtube.com/watch?v=YTN88fUiGoI\">Kamailio World 2019</a>;\n *    - Presentation on Multistream support in Janus (Unified Plan) at <a href=\"https://www.youtube.com/watch?v=xbsHYvBjsdY\">CommCon 2019</a>;\n *    - Presentation on Janus at <a href=\"https://www.youtube.com/watch?v=2LHto3iufzU\">ClueCon 2019</a>;\n *    - Presentation on Simulcast and SVC at <a href=\"https://www.youtube.com/watch?v=6COV44AORlo\">IIT RTC 2019</a>;\n *    - Presentation on RTP forwarders at <a href=\"https://fosdem.org/2020/schedule/event/janus/\">FOSDEM 2020</a>;\n *    - Presentation on using Janus for Virtual Events at <a href=\"https://2020.commcon.xyz/session/turning-live-events-to-virtual-with-janus/\">CommCon 2020</a>;\n *    - Workshop on Janus (lesson/tutorial) at <a href=\"https://www.youtube.com/watch?v=Ga_7lukslxw\">ClueCon 2020</a>;\n *    - Presentation on E2EE (end-to-end encryption) and Insertable Streams at <a href=\"https://youtu.be/SmN8-6ZFuOo?t=2004\">ClueCon 2020</a>;\n *    - Workshop on Janus and SIP (lesson/tutorial) at <a href=\"https://youtu.be/fv9KwrguR-4?t=3544\">OpenSIPS 2020</a>;\n *    - Presentation on SFUs and MCUs at <a href=\"https://www.youtube.com/watch?v=0ldgX_0w5QU\">IIT RTC 2020</a>;\n *    - Presentation on using Janus for IETF Virtual Participation at the <a href=\"https://youtu.be/WeoKtWfmxnU?t=286\">MOPS WG session (IETF109)</a>;\n *    - Presentation on WebRTC (and Janus) as a way to help musicians at <a href=\"https://fosdem.org/2021/schedule/event/webrtc_musicians/\">FOSDEM 2021</a>;\n *    - Workshop on Janus RTP forwarders (lesson/tutorial) at <a href=\"https://www.youtube.com/watch?v=uW8ztFCkxVk\">ClueCon TGI2021</a>;\n *    - Presentation on Janus and NDI at <a href=\"https://youtu.be/RZ58vapWfUw?t=1755\">ClueCon TGI2021</a>;\n *    - Presentation on Janus and multicast at the <a href=\"https://youtu.be/cT_74CRBsQs?t=5910\">MBONED WG session (IETF 110)</a>;\n *    - Presentation on how to use WHIP and Janus for broadcasting at <a href=\"https://www.youtube.com/watch?v=b_QBd3WnGgY\">IIT RTC 2021</a>;\n *    - Presentation on the efforts to integrate audio redundancy via RED in Janus at <a href=\"https://www.youtube.com/watch?v=NWEYv6OILZI\">ClueCon 2021</a>;\n *    - Presentation on how to use WHIP, NDI and Janus together at <a href=\"https://2021.commcon.xyz/talks/whip-ndi-and-janus-genesis-of-a-broadcasting-demo\">CommCon 2021</a>;\n *    - Presentation on Janode, the official Janus nodejs SDK, at <a href=\"https://2021.commcon.xyz/talks/janode-the-janus-node-js-sdk\">CommCon 2021</a>;\n *    - Presentation on WebRTC broadcasting with WHIP and how to scale at <a href=\"https://fosdem.org/2022/schedule/event/rtc_whip/\">FOSDEM 2022</a>;\n *    - Presentation on SIP call transfers and multiple calls at <a href=\"https://youtu.be/vSFZe8SsTbM?t=6379\">OpenSIPS 2022</a>;\n *    - Presentation on SFU/VideoRoom cascading at <a href=\"https://www.youtube.com/watch?v=z7SvEQcDEgM\">IIT RTC 2022</a>;\n *    - Presentation on Social audio applications with Janus at <a href=\"https://fosdem.org/2023/schedule/event/janus/\">FOSDEM 2023</a>;\n *    - Presentation on Real-Time Text in SIP and WebRTC at <a href=\"https://www.youtube.com/watch?v=yehYE34d3mI&t=19937s\">Kamailio World 2023</a>;\n *    - Presentation on Hybrid meetings using Janus at <a href=\"https://www.youtube.com/watch?v=ZqRvUxgf00s\">CommCon 2023</a>;\n *    - Presentation on Bandwidth Estimation (BWE) in Janus at <a href=\"https://www.youtube.com/watch?v=7L35Q_5aqCc\">RTC.ON 2023</a>;\n *    - Presentation on WHIP, WHEP and WebRTC broadcasting at <a href=\"https://www.youtube.com/watch?v=L0JkNphlEmw\">TADSummit Paris 2023</a>;\n *    - Presentation on AV1/SVC support in Janus at <a href=\"https://fosdem.org/2024/schedule/event/fosdem-2024-3000-getting-av1-svc-to-work-in-the-janus-webrtc-server/\">FOSDEM 2024</a>;\n *    - Presentation on experimental application protocols support in the SIP plugin at <a href=\"https://www.youtube.com/watch?v=Cho556qHRaE\">OpenSIPS Summit 2024</a>;\n *    - Presentation on experimental SIP trunking support in the SIP plugin at <a href=\"https://www.youtube.com/watch?v=_e0fWnWWOeY&t=3855s\">Kamailio World 2024</a>;\n *    - Presentation on prototyping an interaction between WebRTC and QUIC (RTP Over QUIC, Media Over QUIC) at <a href=\"https://www.youtube.com/watch?v=bq9LRCfxz_E\">RTC.ON 2024</a>;\n *    - Presentation on RTP Over QUIC at <a href=\"https://www.youtube.com/watch?v=J0_4nO5a4ZI\">Kamailio World 2025</a>;\n *    - Presentation on multistream support in the SIP and NoSIP plugins at <a href=\"https://www.youtube.com/live/lN96vOjhhhI?si=wiwS0KbyFBYRF9ez&t=1751\">OpenSIPS Summit 2025</a>. \\n\\n\n *    Apart from these presentations, make sure you also check the slides\n *    and presentations from <a href=\"https://januscon.it\">JanusCon</a> (including the\n *    <a href=\"https://januscon.it/2019\">2019 edition</a>),\n *    the Janus conference we hosted here in Napoli.\n *    \\n\\n\n *    .\n * \\anchor origin\n * -# <b>Why is Janus called like that?</b>\\n\n *    .\n *    Quoting <a href=\"http://en.wikipedia.org/wiki/Janus\">Wikipedia</a>:\n *    \"<i>In ancient Roman religion and myth, Janus (Latin: Ianus, pronounced\n *    [ˈiaː.nus]) is the god of beginnings and transitions, and thereby of\n *    gates, doors, passages, endings and time. He is usually depicted as\n *    having two faces, since he looks to the future and to the past.</i>\"\n *    Considering the general purpose nature of our server, where one face always looks\n *    at the future (WebRTC) and the other at the past (whatever the modules\n *    allows you to do, be it legacy stuff or not), Janus looked like the\n *    perfect name for it! And besides, we're Italian, Ancient Rome was\n *    awesome and mythology rules... ;-)\n *    \\n\\n\n *    .\n * \\anchor januslicense\n * -# <b>Is the license AGPLv3 or GPLv3? Do you provide alternative license mechanisms as well?</b>\\n\n *    .\n *    Janus is licensend under the GPLv3 licence. At the very beginning\n *    we chose AGPLv3 for a simple reason: we wanted our work to be open source,\n *    and we wanted interested people to play with it and contribute back\n *    whatever improvement they could provide to the core. This is not always\n *    the case with open source software, which is sometimes just seen as\n *    free stuff you can exploit without helping back, and AGPLv3 looked like\n *    the easiest way to do that. That said, we were almost immediately\n *    made aware that this license mechanism is\n *    <a href=\"https://groups.google.com/forum/?pli=1#!searchin/discuss-webrtc/janus/discuss-webrtc/LJHXkIsAaEU/fHgJ2z0sxfoJ\">not very appreciated</a>\n *    around, especially because of some\n *    interpretations about it that could affect the way proprietary stuff\n *    is deployed. We obviously cared about these concerns, and that's what\n *    eventually lead us to pick GPLv3 as a license, which should make it\n *    easier for Janus to be integrated in heterogeneous scenarios.\\n\\n\n *    Anyway, if for some reason you're not comfortable with GPLv3 for your\n *    needs, we have a <a href=\"https://janus.conf.meetecho.com/support\">commercial license</a>\n *    offering as well.\\n\\n\n *    .\n * \\anchor OS\n * -# <b>On what OS can I install Janus?</b>\\n\n *    .\n *    At the moment only Linux is officially supported. Anyway, Janus does\n *    compile on Mac OS as well, thanks to contributions from the community,\n *    and so should work fine there as well. Windows support is also provided\n *    as a <a href=\"https://github.com/meetecho/janus-gateway/pull/597\">pull request</a>\n *    by a Janus contributor: anyway, please beware that Windows support is behind the current\n *    version of development.\\n\\n\n *    .\n * \\anchor datachans\n * -# <b>Are Data Channels supported?</b>\\n\n *    .\n *    Yes they are! Starting from version \\c 0.0.3 of Janus, we added a first\n *    experimental support to Data Channels, that can as such be used in\n *    plugins that choose to support them. Right now, they are handled in\n *    several plugins like the Echo Test, VideoCall, VideoRoom and TextRoom plugins.\\n\\n\n *    Please notice that right now we only support text data: hopefully support\n *    for binary streams will be available soon as well. Until that happens,\n *    you'll have to resort to text-based encodings like Base64 to share\n *    binary data through Janus.\\n\\n\n *    .\n * \\anchor usrsctp\n * -# <b>I don't care about Data Channels, do I have to compile usrsctp anyway?</b>\\n\n *    .\n *    Support for Data Channels is optional, so if you didn't install the library\n *    the configure script will go around them and compile Janus without Data Channels\n *    support. If you did install the library and don't care about Data Channels,\n *    but only about audio and/or video, pass the \\c --disable-data-channels\n *    option to the configure script to explicitly disable them. If you're updating\n *    an existing install, issue a \\c make \\c clean before compiling again, or\n *    you might encounter issues with pre-existing symbols.\\n\\n\n *    .\n * \\anchor usrsctperr\n * -# <b>I can't install usrsctp, I'm getting errors about dereferencing pointers?</b>\\n\n *    .\n *    Apparently recent compilers are stricter with respect to some code\n *    syntaxes, and this seems to be affecting the way \\c usrsctp is written\n *    as of now. Some users managed to fix this issue by passing an export\n *    before the \\c bootstrap and \\c configure steps in the \\c usrsctp\n *    compilation:\\n\\n\n *\\verbatim\n   export CFLAGS=\"-fno-strict-aliasing\" ./bootstrap\n   export CFLAGS=\"-fno-strict-aliasing\" ./configure --prefix=/usr\n   make && make install\n \\endverbatim\n *    Another solution seems to be removing all the \\c -O2 occurrences\n *    in the generated \\c configure script.\\n\\n\n *    .\n * \\anchor gateway\n * -# <b>Can I use Janus as a gateway to my Freeswitch/Kamailio/Asterisk/other SIP infrastructure?</b>\\n\n *    .\n *    Of course! One of the modules we provide out of the box is a SIP\n *    gateway plugin based on the <a href=\"https://github.com/freeswitch/sofia-sip\">Sofia-SIP</a>\n *    library stack. These plugin allows a web user to register at a SIP\n *    proxy/server either as an authenticated user or as a guest, and\n *    start or receive calls from other SIP users, including other web users\n *    exploiting the same plugin. Janus will take care of all the WebRTC-related\n *    stuff (ICE, DTLS, SRTP), which means that on the SIP side it will\n *    be plain RTP only, much easier and lighter to handle for legacy\n *    implementations. SDES-SRTP is also supported on the SIP side in case\n *    it is required.\\n\\n\n *    .\n * \\anchor jssip\n * -# <b>Can I use existing SIP stacks (e.g., JsSIP) with Janus?</b>\\n\n *    .\n *    Janus uses a custom JSON-based protocol for all the communication\n *    between web users and plugins in the server, so no, that's not\n *    possible right now. While there are some controls that give you access\n *    to some of the SIP details (e.g., requesting SRTP negotiation or injecting\n *    custom headers), the SIP plugin in Janus only exposes some very\n *    high level information to web users (e.g., registration failed/succeeded,\n *    incoming call, decline, hangup, etc.), without delving in any SIP-related\n *    detail, which is instead completely demanded to the server-side plugin\n *    itself. This ensures that web users can take advantage of SIP functionality\n *    in an easy way, without having to worry about the details and complexity\n *    of SIP within JavaScript.\\n\\n\n *    .\n * \\anchor transcoding\n * -# <b>Does Janus support transcoding?</b>\\n\n *    .\n *    Janus is a simple intermediary between WebRTC users and server-side\n *    plugins providing application logic, so no, it doesn't provide any\n *    transcoding functionality per-se. If transcoding is needed, this is\n *    supposed to be implemented within plugins themselves. That said,\n *    apart from the AudioBridge plugin which acts as an audio MCU,\n *    none of the plugins we provide out-of the box does transcoding,\n *    since we wanted the implementation to be lightweight and besides\n *    there are several existing tools and third-party implementations that\n *    could be leveraged for the purpose.\\n\\n\n *    .\n * \\anchor recording\n * -# <b>Does Janus support recording?</b>\\n\n *    .\n *    Yep! The Janus core provides an integrated way of recording all\n *    the media streams that go through it, whether it's audio, video or\n *    data, and almost all stock plugins do exploit it one way or another.\n *    Recording is implemented as a structured dump of all RTP and data\n *    files exactly as they were transmitted or sent: as such, no manipulation\n *    is done while recording, and all post-processing is demanded to\n *    an external tool. For more information on this, check the\n *    <a href=\"https://janus.conf.meetecho.com/docs/janus-pp-rec_8c.html#details\">documentation</a>.\\n\\n\n *    .\n * \\anchor websockets\n * -# <b>Can I use WebSockets instead of plain HTTP to interact with Janus?</b>\\n\n *    .\n *    Since version \\c 0.0.4, you can! At first we chose a REST-based plain HTTP communication\n *    for a simple reason: we noticed that there were some scenarios (e.g.,\n *    client firewalls) where websockets wouldn't work, even though WebRTC\n *    did. To improve the chances of success in the communication, we\n *    then chose this simpler approach with respect to signalling. Besides,\n *    plain HTTP is much easier to proxy and/or place behind frontends\n *    like Apache HTTPD or nginx than WebSockets, another aspect that\n *    played a decisive role in our decision, as we were also very interested\n *    in making the integration of Janus in heterogeneous environments\n *    as easy as possible. That said, WebSockets also provide substantial\n *    benefits when compared to plain HTTP, and definitely make life easier to\n *    server side integrators as well, e.g., in terms of overhead and use of\n *    resources. For more information, check the \\ref WS page. \\n\\n\n *    .\n * \\anchor rabbitmq\n * -# <b>Can I use RabbitMQ instead of HTTP/WebSockets to interact with Janus?</b>\\n\n *    .\n *    Since version \\c 0.0.6, you can! This is a feature that several\n *    developers asked for, especially those that are interested in wrapping\n *    the Janus API on the server side, and implement the communication\n *    on the client side their own way. Specifically, Janus now supports\n *    RabbitMQ based messaging as an alternative \"transport\" for API\n *    requests, responses and notifications, meaning it can be used with\n *    or without HTTP and WebSockets, depending on your requirements.\n *    For more information, check the \\ref rabbit page. \\n\\n\n *    .\n * \\anchor unixsockets\n * -# <b>Can I use Unix Sockets instead of HTTP/WebSockets/RabbitMQ to interact with Janus?</b>\\n\n *    .\n *    Since version \\c 0.2.1, you can! More specifically, you can use a\n *    \\c SOCK_SEQPACKET or a \\c SOCK_DGRAM socket to control a Janus instance.\n *    Notice that this feature is only available when installing Janus on a Linux machine.\n *    For more information, check the \\ref unix page. \\n\\n\n *    .\n * \\anchor mqtt\n * -# <b>Can I use MQTT instead of HTTP/WebSockets/RabbitMQ/Unix Sockets to interact with Janus?</b>\\n\n *    .\n *    Since version \\c 0.2.1, you can! This was a very welcome contribution by\n *    a Janus user, as it makes it even easier to have Janus interact with IoT deployments.\n *    For more information, check the \\ref apimqtt page. \\n\\n\n *    .\n * \\anchor nanomsg\n * -# <b>Can I use Nanomsg instead of HTTP/WebSockets/RabbitMQ/MQTT/Unix Sockets to interact with Janus?</b>\\n\n *    .\n *    Since version \\c 0.4.2, you can! For more information, check the \\ref apinanomsg page. \\n\\n\n *    .\n * \\anchor transports\n * -# <b>What about \\<my favourite control protocol\\> instead?</b>\\n\n *    .\n *    Since version \\c 0.1.0, the transports for the Janus API have been made\n *    modular. This means that, assuming a transport plugin implementing\n *    support for your favourite protocol has been made available, you\n *    can just add it to the Janus transport modules folder and it should\n *    work. If no plugin has been implemented, you may even want to do so\n *    yourself, or ask for ours or others help.\\n\\n\n *    .\n * \\anchor demos\n * -# <b>I've launched Janus, how do I try the demos?</b>\\n\n *    .\n *    The demos are available in the \\c html folder in the project. That\n *    said, the integrated web server in Janus does not serve static files\n *    as well, so you'll have to make them available using a different\n *    webserver. Details about how to deploy Janus are provided in\n *    \\ref deploy.\\n\\n\n *    .\n * \\anchor certificates\n * -# <b>I'm trying the demos, but I get \"Janus down\" or certificate errors!</b>\\n\n *    .\n *    Again, make sure you've read and are following the instructions on how to\n *    effectively deploy Janus in \\ref deploy. Most likely you're either trying\n *    to open the WebRTC demos using HTTP instead of HTTPS (something that\n *    browsers will not allow, if not on localhost for testing), trying\n *    to contact Janus via an insecure protocol through a secure page, or\n *    using an insecure certificate for Janus itself. All of these problems\n *    are explained and solved easily in the \\ref deploy documentation.\\n\\n\n *    .\n * \\anchor nodejs\n * -# <b>Can I use Janus with node.js or other frameworks?</b>\\n\n *    .\n *    Not natively, but since interaction with Janus is demanded to\n *    a JSON based communication transported on a network protocol (HTTP,\n *    WebSockets, RabbitMQ and others), this can be abstracted quite easily\n *    in several different ways. A common approach is implementing the Janus\n *    API in a server-side application, so that you handle users your own way\n *    and with your own API, while controlling Janus as a media server yourself\n *    by wrapping its API. Check the \\ref resources documentation page\n *    for a list of Janus API wrappers in different programming languages\n *    that ou can use within your application\n *    to control a Janus instance from there instead of the browser. Should\n *    you decide to write your own Janus API wrapper, don't hesitate to let\n *    us know so that we can advertise yours there too!\\n\\n\n *    .\n * \\anchor monitoring\n * -# <b>How do I monitor/manage my Janus instance?</b>\\n\n *    .\n *    Since version \\c 0.2.2 there are a couple of different ways you can do\n *    that. The first and most immediate one is making use of the Admin API,\n *    which you can find documented in the \\ref admin page. It is a request/response\n *    protocol that can be used to poll information on existing sessions and\n *    handles, and which also provides a lot of useful WebRTC-related information\n *    on PeerConnections Janus is managing. You can also find a very useful\n *    <a href=\"https://www.meetecho.com/blog/understanding-the-janus-admin-api\">blog post</a>\n *    where we describe in detail how the Admin API can be used to investigate issues. \\n\\n\n *    A more recent, and potentially much more powerful, addition to Janus\n *    are the Event Handlers. Unlike the Admin API, which is query-based,\n *    Event Handlers provide a mechanism to dynamically feed you with real-time\n *    and live data on whatever is happening within Janus, whether it's the\n *    core or any of the plugins. You can find more information, along with\n *    an example of how to receive such events, in the\n *    <a href=\"https://github.com/meetecho/janus-gateway/pull/536\">pull request</a>\n *    page from where this effort was merged. More guidelines and implementations\n *    will definitely follow. \\n\\n\n *    .\n * \\anchor writeplugin\n * -# <b>I want to write my own plugin, where do I start?</b>\\n\n *    .\n *    Great! Depending on the kind of plugin you want to implement (application plugin,\n *    transport plugin, event handler plugin), some of the details may change,\n *    but the concept is typically the same. There are APIs you can refer to,\n *    documented in \\ref pluginapi , \\ref transportapi and \\ref eventhandlerapi.\n *    The existing plugins are also an excellent way to start to get\n *    comfortable with the API: a good starting point may be the Echo Test\n *    plugin, which is a very simple and barebone implementation that\n *    simply bounces back whatever it is sent, and also involves some\n *    rough application logic to determine its behaviour (e.g., messages\n *    coming from web users that selectively enable or disable video).\n *    You may want to check third-party plugins as well in the \\ref resources\n *    as well, as they'll give you an idea of how other users did the same.\n *    If you need any help with this, feel free to ask for help on the Google group:\n *    we're always excited whenever new Janus plugins are realized!\\n\\n\n *    .\n * \\anchor ulimit\n * -# <b>I'm using the VideoRoom plugin and, when several users are handled, I get\n *    a \"Too many open files\" errors and Janus crashes</b>\\n\n *    .\n *    As all applications on Linux environments, Janus is bound to the\n *    constraints imposed by the OS and/or the user. One of these constraints\n *    is related to how many file descriptors the application, or all the\n *    applications, can open. On several distributions this number is, by\n *    default, quite low, which can cause the issue you experienced. This\n *    value, together with others, can be modified, per-user or for all users,\n *    using the \\c ulimit application. For a simple and quick way to handle\n *    this refer to the guide provided by the MongoDB developers:\n *    http://docs.mongodb.org/manual/reference/ulimit/ \\n\\n\n *    .\n * \\anchor aesgcm\n * -# <b>I get an undefined reference to <code>srtp_crypto_policy_set_aes_gcm_256_16_auth</code>\n *    when building Janus</b>\\n\n *    .\n *    As explained in the README, you have to build libsrtp or libsrtp2 with\n *    the <code>--enable-openssl</code> or <code>--enable-nss</code> flags,\n *    since they're needed to support AES-GCM in our SRTP streams. If you\n *    don't want AES-GCM support, pass the <code>--disable-aes-gcm</code>\n *    option when configuring Janus instead: remember to do a\n *    <code>make clean</code> before recompiling Janus after the change. \\n\\n\n *    .\n * \\anchor https\n * -# <b>When I enable the HTTPS web server in Janus, the CPU goes crazy</b>\\n\n *    .\n *    As discussed in a recent <a href=\"https://groups.google.com/forum/?pli=1#!topic/meetecho-janus/lD8A0VqXsNs\">post</a>\n *    on our Google group, this is caused by an occasional problem within\n *    libmicrohttpd, the library we use to implement an embedded web server\n *    in Janus. This is not deterministic, as the high CPU usage does happen\n *    on some distributions (e.g., Ubuntu 12.04), while it doesn't on\n *    others (e.g., Ubuntu 14.04). Anyway, this only can happen if you enable\n *    HTTPS support within Janus itself: you can still have a safe HTTPS\n *    usage with Janus if you deploy it behind a frontend (e.g., Apache HTTPD)\n *    that takes care of this on its behalf. Refer to the \\ref deploy section\n *    for more details about this.\\n\\n\n *    .\n * \\anchor turn\n * -# <b>I enabled TURN in the Janus configuration, but my clients behind a firewall can't connect</b>\\n\n *    .\n *    As explained in the \\c janus.jcfg documentation, the TURN settings you\n *    configure there refer to Janus \\b itself, \\b not the clients that make use\n *    of its services. This means that, whether you configure a static TURN\n *    server and the related credentials, or the REST API client stack to\n *    retrieve them dynamically, you're asking Janus to gather relay candidates\n *    for ITSELF. This is only useful if you know that your Janus instance\n *    will not be deployed on a public address (as it usually will) but will\n *    instead potentially sit behind a restrictive component like a firewall,\n *    or if you simply want to limit the number of open ports and so force\n *    all media traffic to go through the TURN server port alone.\\n\\n\n *    If you want your clients to gather relay candidates, instead, this\n *    needs to be done on the client side (e.g., in JavaScript, if your\n *    clients are web applications). If you're using our \\ref JS check\n *    the \\c echotest.js code, which has a commented portion that explains\n *    how to provide one or more TURN servers to make use of. If you're\n *    handling the WebRTC-related stuff on your own and are contacting\n *    Janus some other way, please refer to the related way of specifying\n *    STUN and TURN servers instead.\\n\\n\n *    .\n * \\anchor benchmark\n * -# <b>Is there any benchmark on Janus performances?</b>\\n\n *    .\n *    Benchmarking Janus is not an easy task, especially if we consider\n *    its general purpose nature. In fact, Janus by itself does not much\n *    more than negotiating WebRTC PeerConnections and relaying frames\n *    around, while most of the application login is in the modules. As\n *    you can imagine, different modules may have very different requirements\n *    and impact on the performances. For instance, the Echo Test is probably\n *    much lighter than the Video SFU, even if they're handling the same\n *    number of users. This means that such a benchmarking does not have\n *    much sense unless you contextualise the scenarios.\\n\\n\n *    You can find some results on tests we made ourselves in a couple of\n *    publications we wrote and available\n *    <a href=\"https://janus.conf.meetecho.com/citeus\">here</a>. One of those\n *    introduces a WebRTC stress testing tool we implemented called Jattack,\n *    for which a <a href=\"https://www.youtube.com/watch?v=UwNq8p0m1js\">video of the presentation</a>\n *    is available. You can find some more details and considerations in an older\n *    <a href=\"https://groups.google.com/forum/?pli=1#!topic/meetecho-janus/ydGcbMt7IX0\">post</a>\n *    on our Google group too.\\n\\n\n *    .\n * \\anchor scaling\n * -# <b>How do I scale Janus?</b>\\n\n *    .\n *    This question is way too generic to answer here. As explained in the previous\n *    point, Janus is not an easily measurable entity, as it very much depends\n *    on which of the plugins you're involving in the scenarios that interest you\n *    and how. The same considerations also apply to scaling the scenario you\n *    implemented, as different plugins may require different approaches to\n *    do that. \\n\\n\n *    In general, Janus does not have any built-in multi-tenant support. Each\n *    Janus instance is conceived to be standalone, and works by itself. That\n *    said, there are different ways in which you can implement scalability in\n *    your service. One of them is implementing a wrapper/controller application\n *    that is in charge of multiple Janus instances, so that it can distribute\n *    the load depending on the requests. If the scenario allows for it,\n *    you can also mix concepts from different plugins on different Janus\n *    instances (e.g., use the RTP forwardiing feature of the VideoRoom plugin\n *    to make the same VideoRoom publisher available as a Streaming mountpoint\n *    on other Janus machines). Anyway, as explained each application may have\n *    very different requirements when it comes to scalability. \\n\\n\n *    Should you be interested in implementing scalability in your application and\n *    need help, <a href=\"https://janus.conf.meetecho.com/support\">contact us</a>.\\n\\n\n *    .\n * \\anchor requests\n * -# <b>Can you implement a feature/plugin/application I want?</b>\\n\n *    .\n *    We're constantly working on new features and on improving what's\n *    already there, and we do love feedback from users. That said, we're\n *    a small team and we do have to pay our bills, so we always have to\n *    reserve our resources wisely. If there's a feature you'd like to\n *    see implemented, tell us on our Google group, and discuss it with\n *    other users: it may be on our schedule, or someone else may be\n *    already working on it to contribute it back to the project. You\n *    may even want to try and build it yourself and help us make Janus\n *    even better! \\n\\n\n *    If you really need something that isn't there, you may also want to consider\n *    <a href=\"https://janus.conf.meetecho.com/support\">contacting us</a>\n *    for a sponsored development or consulting.\\n\\n\n *    .\n * \\anchor learning\n * -# <b>I want to learn more on Janus!</b>\\n\n *    .\n *    That's great! The first obvious suggestion is of course to carefully\n *    read the documentation here: we worked hard on it and are constantly\n *    expanding it, so it should always be the first source of information\n *    whenever in doubt. We have a great Discourse as well where the\n *    <a href=\"https://janus.discourse.group/\">community</a>\n *    shares questions, data and more everyday, so make sure you join too! \\n\\n\n *    We also provide training on Janus and WebRTC in general, so if interested\n *    just <a href=\"https://janus.conf.meetecho.com/support\">contact us</a>.\\n\\n\n *    .\n */\n\n/*! \\defgroup core Core\n * \\brief Core implementation of the server\n * \\details The Janus WebRTC Server is founded on a core that glues the\n * involved parts together. The main code is janus.c that implements\n * the logic behind the server itself: it implements the web server that\n * interacts with browsers, and handles sessions with them. This includes\n * taking care of media signalling and negotiation, and bridging peers\n * with available plugins.\n */\n\n/*! \\defgroup protocols Protocols\n * \\brief Implementations of the WebRTC protocols\n * \\details The WebRTC specification (WEBRTC/RTCWEB) currently mandates\n * the usage of a few protocols and techniques. The code in this group\n * takes care of them all (SDP, ICE, DTLS-SRTP, RTP/RTCP).\n * \\ingroup core\n */\n\n/*! \\defgroup plugins Plugins\n * \\brief Janus plugins available out of the box\n * \\details In order to showcase how different plugins can implement\n * completely different applications on top of the Janus core, a few\n * plugin implementations are provided out of the box. The API for\n * writing a new plugin is specified in the \\ref pluginapi section.\n */\n\n/*! \\defgroup pluginapi Plugin API\n * \\brief Plugin API (aka, how to write your own plugin)\n * \\details The plugin.h header specifies the API a plugin needs to\n * implement and make available in order to be used by the server and\n * exposed to browsers.\n * \\ingroup plugins\n */\n\n/*! \\defgroup luapapi Lua plugin API\n * \\brief Lua plugin (aka, how to write your own plugin in Lua)\n * \\details The Janus Lua plugin allows you to write your application\n * logic in Lua, instead of creating a new plugin in C: check the\n * documentation in janus_lua.c for info on the APIs. Some additional\n * hooks in C may be required, which is what the janus_lua_data.h and\n * janus_lua_extra.c code is for.\n * \\ingroup plugins\n */\n\n/*! \\defgroup jspapi Duktape plugin API\n * \\brief Duktape plugin (aka, how to write your own plugin in JavaScript)\n * \\details The Janus Duktape plugin allows you to write your application\n * logic in JavaScript, instead of creating a new plugin in C: check the\n * documentation in janus_duktape.c for info on the APIs. Some additional\n * hooks in C may be required, which is what the janus_duktape_data.h and\n * janus_duktape_extra.c code is for.\n * \\ingroup plugins\n */\n\n/*! \\defgroup transports Transports\n * \\brief Transport plugins available out of the box\n * \\details In order to showcase how different transport plugins can implement\n * support for the Janus API over different transport protocols, a few\n * plugin implementations are provided out of the box. The API for\n * writing a new plugin is specified in the \\ref transportapi section.\n */\n\n/*! \\defgroup transportapi Transport API\n * \\brief Transport API (aka, how to write your own transport plugin)\n * \\details The transport.h header specifies the API a plugin needs to\n * implement and make available in order to expose a new transport protocol\n * that can be used by the server for talking the Janus API.\n * \\ingroup transports\n */\n\n/*! \\defgroup eventhandlers Event Handlers\n * \\brief Event handler plugins available out of the box\n * \\details In order to showcase how different plugins can handle events\n * originated by the Janus core or any of its plugins, a sample plugin\n * implementation is provided out of the box. The API for writing a new\n * event handler plugin is specified in the \\ref eventhandlerapi section.\n */\n\n/*! \\defgroup eventhandlerapi Event Handler API\n * \\brief Event Handler API (aka, how to write your own event handler plugin)\n * \\details The eventhandler.h header specifies the API a plugin needs to\n * implement and make available in order to receive events from Janus.\n * \\ingroup eventhandlers\n */\n\n/*! \\defgroup loggers Loggers\n * \\brief Logger plugins available out of the box\n * \\details In order to showcase how different plugins can handle log lines\n * originated by the Janus core or any of its plugins, a sample plugin\n * implementation is provided out of the box. The API for writing a new\n * logger plugin is specified in the \\ref loggerapi section.\n */\n\n/*! \\defgroup loggerapi Logger API\n * \\brief Logger API (aka, how to write your own logger plugin)\n * \\details The logger.h header specifies the API a plugin needs to\n * implement and make available in order to receive log lines from Janus.\n * \\ingroup loggers\n */\n\n/*! \\defgroup tools Tools and utilities\n * \\brief Tools and utilities\n * \\details Set of simple tools and utilities that may be of help when\n * used in conjunction with Janus.\n */\n\n/*! \\defgroup postprocessing Recordings post-processing utility\n * \\brief \\ref recordings post-processing utility\n * \\details This simple utility (janus-pp-rec.c) allows you to\n * post-process \\ref recordings generated by the janus_recorder helper (e.g.,\n * in the Video SFU plugin).\n * \\ingroup tools\n */\n"
  },
  {
    "path": "src/mutex.h",
    "content": "/*! \\file    mutex.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\brief    Semaphores, Mutexes and Conditions\n * \\details  Implementation (based on GMutex or pthread_mutex) of a locking mechanism based on mutexes and conditions.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_MUTEX_H\n#define JANUS_MUTEX_H\n\n#include <pthread.h>\n#include <errno.h>\n\n#include \"debug.h\"\n\nextern int lock_debug;\n\n#ifdef USE_PTHREAD_MUTEX\n\n/*! \\brief Janus mutex implementation */\ntypedef pthread_mutex_t janus_mutex;\n/*! \\brief Janus mutex initialization */\n#define janus_mutex_init(a) pthread_mutex_init(a,NULL)\n/*! \\brief Janus static mutex initializer */\n#define JANUS_MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER\n/*! \\brief Janus mutex destruction */\n#define janus_mutex_destroy(a) pthread_mutex_destroy(a)\n/*! \\brief Janus mutex lock without debug */\n#define janus_mutex_lock_nodebug(a) pthread_mutex_lock(a)\n/*! \\brief Janus mutex lock with debug (prints the line that locked a mutex) */\n#define janus_mutex_lock_debug(a) { JANUS_PRINT(\"[%s:%s:%d:lock] %p\\n\", __FILE__, __FUNCTION__, __LINE__, a); pthread_mutex_lock(a); }\n/*! \\brief Janus mutex lock wrapper (selective locking debug) */\n#define janus_mutex_lock(a) { if(!lock_debug) { janus_mutex_lock_nodebug(a); } else { janus_mutex_lock_debug(a); } }\n/*! \\brief Janus mutex try lock without debug */\n#define janus_mutex_trylock_nodebug(a) { ret = !pthread_mutex_trylock(a); }\n/*! \\brief Janus mutex try lock with debug (prints the line that tried to lock a mutex) */\n#define janus_mutex_trylock_debug(a) { JANUS_PRINT(\"[%s:%s:%d:trylock] %p\\n\", __FILE__, __FUNCTION__, __LINE__, a); ret = !pthread_mutex_trylock(a); }\n/*! \\brief Janus mutex try lock wrapper (selective locking debug) */\n#define janus_mutex_trylock(a) ({ int ret; if(!lock_debug) { janus_mutex_trylock_nodebug(a); } else { janus_mutex_trylock_debug(a); } ret; })\n/*! \\brief Janus mutex unlock without debug */\n#define janus_mutex_unlock_nodebug(a) pthread_mutex_unlock(a)\n/*! \\brief Janus mutex unlock with debug (prints the line that unlocked a mutex) */\n#define janus_mutex_unlock_debug(a) { JANUS_PRINT(\"[%s:%s:%d:unlock] %p\\n\", __FILE__, __FUNCTION__, __LINE__, a); pthread_mutex_unlock(a); }\n/*! \\brief Janus mutex unlock wrapper (selective locking debug) */\n#define janus_mutex_unlock(a) { if(!lock_debug) { janus_mutex_unlock_nodebug(a); } else { janus_mutex_unlock_debug(a); } }\n\n/*! \\brief Janus condition implementation */\ntypedef pthread_cond_t janus_condition;\n/*! \\brief Janus condition initialization */\n#define janus_condition_init(a) pthread_cond_init(a,NULL)\n/*! \\brief Janus condition destruction */\n#define janus_condition_destroy(a) pthread_cond_destroy(a)\n/*! \\brief Janus condition wait */\n#define janus_condition_wait(a, b) pthread_cond_wait(a, b);\n/*! \\brief Janus condition timed wait */\n#define janus_condition_wait_until(a, b, c) { \\\n\tconst struct timespec jct = { \\\n\t\t.tv_sec = c / G_USEC_PER_SEC, \\\n\t\t.tv_nsec = (c % G_USEC_PER_SEC)*1000 \\\n\t}; \\\n\tpthread_cond_timedwait(a, b, &jct); \\\n}\n/*! \\brief Janus condition signal */\n#define janus_condition_signal(a) pthread_cond_signal(a);\n/*! \\brief Janus condition broadcast */\n#define janus_condition_broadcast(a) pthread_cond_broadcast(a);\n\n#else\n\n/*! \\brief Janus mutex implementation */\ntypedef GMutex janus_mutex;\n/*! \\brief Janus mutex initialization */\n#define janus_mutex_init(a) g_mutex_init(a)\n/*! \\brief Janus static mutex initializer */\n#define JANUS_MUTEX_INITIALIZER {0}\n/*! \\brief Janus mutex destruction */\n#define janus_mutex_destroy(a) g_mutex_clear(a)\n/*! \\brief Janus mutex lock without debug */\n#define janus_mutex_lock_nodebug(a) g_mutex_lock(a)\n/*! \\brief Janus mutex lock with debug (prints the line that locked a mutex) */\n#define janus_mutex_lock_debug(a) { JANUS_PRINT(\"[%s:%s:%d:lock] %p\\n\", __FILE__, __FUNCTION__, __LINE__, a); g_mutex_lock(a); }\n/*! \\brief Janus mutex lock wrapper (selective locking debug) */\n#define janus_mutex_lock(a) { if(!lock_debug) { janus_mutex_lock_nodebug(a); } else { janus_mutex_lock_debug(a); } }\n/*! \\brief Janus mutex try lock without debug */\n#define janus_mutex_trylock_nodebug(a) { ret = g_mutex_trylock(a); }\n/*! \\brief Janus mutex try lock with debug (prints the line that tried to lock a mutex) */\n#define janus_mutex_trylock_debug(a) { JANUS_PRINT(\"[%s:%s:%d:trylock] %p\\n\", __FILE__, __FUNCTION__, __LINE__, a); ret = g_mutex_trylock(a); }\n/*! \\brief Janus mutex try lock wrapper (selective locking debug) */\n#define janus_mutex_trylock(a) ({ gboolean ret; if(!lock_debug) { janus_mutex_trylock_nodebug(a); } else { janus_mutex_trylock_debug(a); } ret; })\n/*! \\brief Janus mutex unlock without debug */\n#define janus_mutex_unlock_nodebug(a) g_mutex_unlock(a)\n/*! \\brief Janus mutex unlock with debug (prints the line that unlocked a mutex) */\n#define janus_mutex_unlock_debug(a) { JANUS_PRINT(\"[%s:%s:%d:unlock] %p\\n\", __FILE__, __FUNCTION__, __LINE__, a); g_mutex_unlock(a); }\n/*! \\brief Janus mutex unlock wrapper (selective locking debug) */\n#define janus_mutex_unlock(a) { if(!lock_debug) { janus_mutex_unlock_nodebug(a); } else { janus_mutex_unlock_debug(a); } }\n\n/*! \\brief Janus condition implementation */\ntypedef GCond janus_condition;\n/*! \\brief Janus condition initialization */\n#define janus_condition_init(a) g_cond_init(a)\n/*! \\brief Janus condition destruction */\n#define janus_condition_destroy(a) g_cond_clear(a)\n/*! \\brief Janus condition wait */\n#define janus_condition_wait(a, b) g_cond_wait(a, b);\n/*! \\brief Janus condition wait until */\n#define janus_condition_wait_until(a, b, c) g_cond_wait_until(a, b, c);\n/*! \\brief Janus condition signal */\n#define janus_condition_signal(a) g_cond_signal(a);\n/*! \\brief Janus condition broadcast */\n#define janus_condition_broadcast(a) g_cond_broadcast(a);\n\n#endif\n\n#endif\n"
  },
  {
    "path": "src/options.c",
    "content": "/*! \\file    options.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Command line options parser for Janus\n * \\details  Helper code to parse the Janus command line options using GOptionEntry.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include \"options.h\"\n#include \"debug.h\"\n\nstatic GOptionContext *opts = NULL;\n\ngboolean janus_options_parse(janus_options *options, int argc, char *argv[]) {\n\t/* Supported command-line arguments */\n\tGOptionEntry opt_entries[] = {\n\t\t{ \"daemon\", 'b', 0, G_OPTION_ARG_NONE, &options->daemon, \"Launch Janus in background as a daemon\", NULL },\n\t\t{ \"pid-file\", 'p', 0, G_OPTION_ARG_STRING, &options->pid_file, \"Open the specified PID file when starting Janus (default=none)\", \"path\" },\n\t\t{ \"disable-stdout\", 'N', 0, G_OPTION_ARG_NONE, &options->disable_stdout, \"Disable stdout based logging\", NULL },\n\t\t{ \"log-stdout\", 0, 0, G_OPTION_ARG_NONE, &options->log_stdout, \"Log to stdout, even when the process is daemonized\", NULL },\n\t\t{ \"log-file\", 'L', 0, G_OPTION_ARG_STRING, &options->log_file, \"Log to the specified file (default=stdout only)\", \"path\" },\n\t\t{ \"log-rotate-sig\", 'R', 0, G_OPTION_ARG_STRING, &options->log_rotate_sig, \"Signal to trigger log reloading (e.g. SIGUSR1) (default=none)\", \"signal\" },\n\t\t{ \"cwd-path\", 'H', 0, G_OPTION_ARG_STRING, &options->cwd_path, \"Working directory for Janus daemon process (default=/)\", \"path\" },\n\t\t{ \"interface\", 'i', 0, G_OPTION_ARG_STRING, &options->interface, \"Interface to use (will be the public IP)\", \"ipaddress\" },\n\t\t{ \"plugins-folder\", 'P', 0, G_OPTION_ARG_STRING, &options->plugins_folder, \"Plugins folder (default=./plugins)\", \"path\" },\n\t\t{ \"config\", 'C', 0, G_OPTION_ARG_STRING, &options->config_file, \"Configuration file to use\", \"filename\" },\n\t\t{ \"configs-folder\", 'F', 0, G_OPTION_ARG_STRING, &options->configs_folder, \"Configuration files folder (default=./conf)\", \"path\" },\n\t\t{ \"cert-pem\", 'c', 0, G_OPTION_ARG_STRING, &options->cert_pem, \"DTLS certificate\", \"filename\" },\n\t\t{ \"cert-key\", 'k', 0, G_OPTION_ARG_STRING, &options->cert_key, \"DTLS certificate key\", \"filename\" },\n\t\t{ \"cert-pwd\", 'K', 0, G_OPTION_ARG_STRING, &options->cert_pwd, \"DTLS certificate key passphrase (if needed)\", \"text\" },\n\t\t{ \"stun-server\", 'S', 0, G_OPTION_ARG_STRING, &options->stun_server, \"STUN server(:port) to use, if needed (e.g., Janus behind NAT, default=none)\", \"address:port\" },\n\t\t{ \"nat-1-1\", '1', 0, G_OPTION_ARG_STRING, &options->nat_1_1, \"Comma-separated list of public IPs to put in all host candidates, assuming a 1:1 NAT is in place (e.g., Amazon EC2 instances, default=none)\", \"ips\" },\n\t\t{ \"keep-private-host\", '2', 0, G_OPTION_ARG_NONE, &options->keep_private_host, \"When nat-1-1 is used (e.g., Amazon EC2 instances), don't remove the private host, but keep both to simulate STUN\", NULL },\n\t\t{ \"ice-enforce-list\", 'E', 0, G_OPTION_ARG_STRING, &options->ice_enforce_list, \"Comma-separated list of the only interfaces to use for ICE gathering; partial strings are supported (e.g., eth0 or eno1,wlan0, default=none)\", \"list\" },\n\t\t{ \"ice-ignore-list\", 'X', 0, G_OPTION_ARG_STRING, &options->ice_ignore_list, \"Comma-separated list of interfaces or IP addresses to ignore for ICE gathering; partial strings are supported (e.g., vmnet8,192.168.0.1,10.0.0.1 or vmnet,192.168., default=vmnet)\", \"list\" },\n\t\t{ \"ipv6-candidates\", '6', 0, G_OPTION_ARG_NONE, &options->ipv6_candidates, \"Whether to enable IPv6 candidates or not\", NULL },\n\t\t{ \"ipv6-link-local\", 'O', 0, G_OPTION_ARG_NONE, &options->ipv6_link_local, \"Whether IPv6 link-local candidates should be gathered as well\", NULL },\n\t\t{ \"full-trickle\", 'f', 0, G_OPTION_ARG_NONE, &options->full_trickle, \"Do full-trickle instead of half-trickle\", NULL },\n\t\t{ \"ice-lite\", 'I', 0, G_OPTION_ARG_NONE, &options->ice_lite, \"Whether to enable the ICE Lite mode or not\", NULL },\n\t\t{ \"ice-tcp\", 'T', 0, G_OPTION_ARG_NONE, &options->ice_tcp, \"Whether to enable ICE-TCP or not (warning: only works with ICE Lite)\", NULL },\n\t\t{ \"min-nack-queue\", 'Q', 0, G_OPTION_ARG_INT, &options->min_nack_queue, \"Minimum size of the NACK queue (in ms) per user for retransmissions, no matter the RTT\", \"number\" },\n\t\t{ \"no-media-timer\", 't', 0, G_OPTION_ARG_INT, &options->no_media_timer, \"Time (in s) that should pass with no media (audio or video) being received before Janus notifies you about this\", \"number\" },\n\t\t{ \"slowlink-threshold\", 'W', 0, G_OPTION_ARG_INT, &options->slowlink_threshold, \"Number of lost packets (per s) that should trigger a 'slowlink' Janus API event to users (default=0, feature disabled)\", \"number\" },\n\t\t{ \"rtp-port-range\", 'r', 0, G_OPTION_ARG_STRING, &options->rtp_port_range, \"Port range to use for RTP/RTCP\", \"min-max\" },\n\t\t{ \"twcc-period\", 'B', 0, G_OPTION_ARG_INT, &options->twcc_period, \"How often (in ms) to send TWCC feedback back to senders, if negotiated (default=200ms)\", \"number\" },\n\t\t{ \"server-name\", 'n', 0, G_OPTION_ARG_STRING, &options->server_name, \"Public name of this Janus instance (default=MyJanusInstance)\", \"name\" },\n\t\t{ \"session-timeout\", 's', 0, G_OPTION_ARG_INT, &options->session_timeout, \"Session timeout value, in seconds (default=60)\", \"number\" },\n\t\t{ \"reclaim-session-timeout\", 'm', 0, G_OPTION_ARG_INT, &options->reclaim_session_timeout, \"Reclaim session timeout value, in seconds (default=0)\", \"number\" },\n\t\t{ \"debug-level\", 'd', 0, G_OPTION_ARG_INT, &options->debug_level, \"Debug/logging level (0=disable debugging, 7=maximum debug level; default=4)\", \"1-7\" },\n\t\t{ \"debug-timestamps\", 'D', 0, G_OPTION_ARG_NONE, &options->debug_timestamps, \"Enable debug/logging timestamps\", NULL },\n\t\t{ \"disable-colors\", 'o', 0, G_OPTION_ARG_NONE, &options->disable_colors, \"Disable color in the logging\", NULL },\n\t\t{ \"debug-locks\", 'M', 0, G_OPTION_ARG_NONE, &options->debug_locks, \"Enable debugging of locks/mutexes (very verbose!)\", NULL },\n\t\t{ \"apisecret\", 'a', 0, G_OPTION_ARG_STRING, &options->apisecret, \"API secret all requests need to pass in order to be accepted by Janus (useful when wrapping Janus API requests in a server, none by default)\", \"randomstring\" },\n\t\t{ \"token-auth\", 'A', 0, G_OPTION_ARG_NONE, &options->token_auth, \"Enable token-based authentication for all requests\", NULL },\n\t\t{ \"token-auth-secret\", 0, 0, G_OPTION_ARG_STRING, &options->token_auth_secret, \"Secret to verify HMAC-signed tokens with, to be used with -A\", \"randomstring\" },\n\t\t{ \"event-handlers\", 'e', 0, G_OPTION_ARG_NONE, &options->event_handlers, \"Enable event handlers\", NULL },\n\t\t{ \"no-webrtc-encryption\", 'w', 0, G_OPTION_ARG_NONE, &options->no_webrtc_encryption, \"Disable WebRTC encryption, so no DTLS or SRTP (only for debugging!)\", NULL },\n\t\t{ \"version\", 'V', 0, G_OPTION_ARG_NONE, &options->print_version, \"Print version and exit\", NULL },\n\t\t{ NULL, 0, 0, 0, NULL, NULL, NULL },\n\t};\n\n\t/* Parse the command-line arguments */\n\tGError *error = NULL;\n\topts = g_option_context_new(\"\");\n\tg_option_context_set_help_enabled(opts, TRUE);\n\tg_option_context_add_main_entries(opts, opt_entries, NULL);\n\tif(!g_option_context_parse(opts, &argc, &argv, &error)) {\n\t\tJANUS_PRINT(\"%s\\n\", error->message);\n\t\tg_error_free(error);\n\t\tjanus_options_destroy();\n\t\treturn FALSE;\n\t}\n\n\t/* Done */\n\treturn TRUE;\n}\n\nvoid janus_options_destroy(void) {\n\tg_option_context_free(opts);\n\topts = NULL;\n}\n"
  },
  {
    "path": "src/options.h",
    "content": "/*! \\file    options.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Command line options parser for Janus (headers)\n * \\details  Helper code to parse the Janus command line options using GOptionEntry.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_OPTIONS\n#define JANUS_OPTIONS\n\n#include <glib.h>\n\n/*! \\brief Struct containing the parsed command line options for Janus */\ntypedef struct janus_options {\n\tgboolean daemon;\n\tconst char *pid_file;\n\tgboolean disable_stdout;\n\tgboolean log_stdout;\n\tconst char *log_file;\n\tconst char *log_rotate_sig;\n\tconst char *cwd_path;\n\tconst char *interface;\n\tconst char *plugins_folder;\n\tconst char *config_file;\n\tconst char *configs_folder;\n\tconst char *cert_pem;\n\tconst char *cert_key;\n\tconst char *cert_pwd;\n\tconst char *stun_server;\n\tconst char *nat_1_1;\n\tgboolean keep_private_host;\n\tconst char *ice_enforce_list;\n\tconst char *ice_ignore_list;\n\tgboolean ipv6_candidates;\n\tgboolean ipv6_link_local;\n\tgboolean full_trickle;\n\tgboolean ice_lite;\n\tgboolean ice_tcp;\n\tint min_nack_queue;\n\tint no_media_timer;\n\tint slowlink_threshold;\n\tconst char *rtp_port_range;\n\tint twcc_period;\n\tconst char *server_name;\n\tint session_timeout;\n\tint reclaim_session_timeout;\n\tint debug_level;\n\tgboolean debug_timestamps;\n\tgboolean disable_colors;\n\tgboolean debug_locks;\n\tconst char *apisecret;\n\tgboolean token_auth;\n\tconst char *token_auth_secret;\n\tgboolean event_handlers;\n\tgboolean no_webrtc_encryption;\n\tgboolean print_version;\n} janus_options;\n\n/*! \\brief Helper method to parse the command line options\n * @param opts A pointer to the janus_options instance to save the options to\n * @param argc The number of arguments\n * @param argv The command line arguments\n * @returns TRUE if successful, FALSE otherwise */\ngboolean janus_options_parse(janus_options *opts, int argc, char *argv[]);\n\n/*! \\brief Helper method to get rid of the options parser resources */\nvoid janus_options_destroy(void);\n\n#endif\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/COPYING",
    "content": "Copyright 2002-2008 \tXiph.org Foundation\nCopyright 2002-2008 \tJean-Marc Valin\nCopyright 2005-2007\tAnalog Devices Inc.\nCopyright 2005-2008\tCommonwealth Scientific and Industrial Research\n                        Organisation (CSIRO)\nCopyright 1993, 2002, 2006 David Rowe\nCopyright 2003 \t\tEpicGames\nCopyright 1992-1994\tJutta Degener, Carsten Bormann\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n- Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\n- Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\n- Neither the name of the Xiph.org Foundation nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/arch.h",
    "content": "/* Copyright (C) 2003 Jean-Marc Valin */\n/**\n   @file arch.h\n   @brief Various architecture definitions Speex\n*/\n/*\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions\n   are met:\n\n   - Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n   - Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n   - Neither the name of the Xiph.org Foundation nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR\n   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n\n#ifndef ARCH_H\n#define ARCH_H\n\n/* A couple test to catch stupid option combinations */\n#ifdef FIXED_POINT\n\n#ifdef FLOATING_POINT\n#error You cannot compile as floating point and fixed point at the same time\n#endif\n#ifdef USE_SSE\n#error SSE is only for floating-point\n#endif\n#if defined(ARM4_ASM) + defined(ARM5E_ASM) + defined(BFIN_ASM) > 1\n#error Make up your mind. What CPU do you have?\n#endif\n#ifdef VORBIS_PSYCHO\n#error Vorbis-psy model currently not implemented in fixed-point\n#endif\n\n#else\n\n#ifndef FLOATING_POINT\n#error You now need to define either FIXED_POINT or FLOATING_POINT\n#endif\n#if defined(ARM4_ASM) || defined(ARM5E_ASM) || defined(BFIN_ASM)\n#error I suppose you can have a [ARM4/ARM5E/Blackfin] that has float instructions?\n#endif\n#ifdef FIXED_DEBUG\n#error \"Don't you think enabling fixed-point is a good thing to do if you want to debug that?\"\n#endif\n\n\n#endif\n\n#ifndef OUTSIDE_SPEEX\n#include \"speex/speexdsp_types.h\"\n#endif\n\n#define ABS(x) ((x) < 0 ? (-(x)) : (x))      /**< Absolute integer value. */\n#define ABS16(x) ((x) < 0 ? (-(x)) : (x))    /**< Absolute 16-bit value.  */\n#define MIN16(a,b) ((a) < (b) ? (a) : (b))   /**< Maximum 16-bit value.   */\n#define MAX16(a,b) ((a) > (b) ? (a) : (b))   /**< Maximum 16-bit value.   */\n#define ABS32(x) ((x) < 0 ? (-(x)) : (x))    /**< Absolute 32-bit value.  */\n#define MIN32(a,b) ((a) < (b) ? (a) : (b))   /**< Maximum 32-bit value.   */\n#define MAX32(a,b) ((a) > (b) ? (a) : (b))   /**< Maximum 32-bit value.   */\n\n#ifdef FIXED_POINT\n\ntypedef spx_int16_t spx_word16_t;\ntypedef spx_int32_t spx_word32_t;\ntypedef spx_word32_t spx_mem_t;\ntypedef spx_word16_t spx_coef_t;\ntypedef spx_word16_t spx_lsp_t;\ntypedef spx_word32_t spx_sig_t;\n\n#define Q15ONE 32767\n\n#define LPC_SCALING  8192\n#define SIG_SCALING  16384\n#define LSP_SCALING  8192.\n#define GAMMA_SCALING 32768.\n#define GAIN_SCALING 64\n#define GAIN_SCALING_1 0.015625\n\n#define LPC_SHIFT    13\n#define LSP_SHIFT    13\n#define SIG_SHIFT    14\n#define GAIN_SHIFT   6\n\n#define WORD2INT(x) ((x) < -32767 ? -32768 : ((x) > 32766 ? 32767 : (x)))\n\n#define VERY_SMALL 0\n#define VERY_LARGE32 ((spx_word32_t)2147483647)\n#define VERY_LARGE16 ((spx_word16_t)32767)\n#define Q15_ONE ((spx_word16_t)32767)\n\n\n#ifdef FIXED_DEBUG\n#include \"fixed_debug.h\"\n#else\n\n#include \"fixed_generic.h\"\n\n#ifdef ARM5E_ASM\n#include \"fixed_arm5e.h\"\n#elif defined(ARM4_ASM)\n#include \"fixed_arm4.h\"\n#elif defined(BFIN_ASM)\n#include \"fixed_bfin.h\"\n#endif\n\n#endif\n\n\n#else\n\ntypedef float spx_mem_t;\ntypedef float spx_coef_t;\ntypedef float spx_lsp_t;\ntypedef float spx_sig_t;\ntypedef float spx_word16_t;\ntypedef float spx_word32_t;\n\n#define Q15ONE 1.0f\n#define LPC_SCALING  1.f\n#define SIG_SCALING  1.f\n#define LSP_SCALING  1.f\n#define GAMMA_SCALING 1.f\n#define GAIN_SCALING 1.f\n#define GAIN_SCALING_1 1.f\n\n\n#define VERY_SMALL 1e-15f\n#define VERY_LARGE32 1e15f\n#define VERY_LARGE16 1e15f\n#define Q15_ONE ((spx_word16_t)1.f)\n\n#define QCONST16(x,bits) (x)\n#define QCONST32(x,bits) (x)\n\n#define NEG16(x) (-(x))\n#define NEG32(x) (-(x))\n#define EXTRACT16(x) (x)\n#define EXTEND32(x) (x)\n#define SHR16(a,shift) (a)\n#define SHL16(a,shift) (a)\n#define SHR32(a,shift) (a)\n#define SHL32(a,shift) (a)\n#define PSHR16(a,shift) (a)\n#define PSHR32(a,shift) (a)\n#define VSHR32(a,shift) (a)\n#define SATURATE16(x,a) (x)\n#define SATURATE32(x,a) (x)\n#define SATURATE32PSHR(x,shift,a) (x)\n\n#define PSHR(a,shift)       (a)\n#define SHR(a,shift)       (a)\n#define SHL(a,shift)       (a)\n#define SATURATE(x,a) (x)\n\n#define ADD16(a,b) ((a)+(b))\n#define SUB16(a,b) ((a)-(b))\n#define ADD32(a,b) ((a)+(b))\n#define SUB32(a,b) ((a)-(b))\n#define MULT16_16_16(a,b)     ((a)*(b))\n#define MULT16_32_32(a,b)     ((a)*(b))\n#define MULT16_16(a,b)     ((spx_word32_t)(a)*(spx_word32_t)(b))\n#define MAC16_16(c,a,b)     ((c)+(spx_word32_t)(a)*(spx_word32_t)(b))\n\n#define MULT16_32_Q15(a,b)     ((a)*(b))\n#define MULT16_32_P15(a,b)     ((a)*(b))\n\n#define MAC16_32_Q15(c,a,b)     ((c)+(a)*(b))\n\n#define MAC16_16_Q11(c,a,b)     ((c)+(a)*(b))\n#define MAC16_16_Q13(c,a,b)     ((c)+(a)*(b))\n#define MAC16_16_P13(c,a,b)     ((c)+(a)*(b))\n#define MULT16_16_Q11_32(a,b)     ((a)*(b))\n#define MULT16_16_Q13(a,b)     ((a)*(b))\n#define MULT16_16_Q14(a,b)     ((a)*(b))\n#define MULT16_16_Q15(a,b)     ((a)*(b))\n#define MULT16_16_P15(a,b)     ((a)*(b))\n#define MULT16_16_P13(a,b)     ((a)*(b))\n#define MULT16_16_P14(a,b)     ((a)*(b))\n\n#define DIV32_16(a,b)     (((spx_word32_t)(a))/(spx_word16_t)(b))\n#define PDIV32_16(a,b)     (((spx_word32_t)(a))/(spx_word16_t)(b))\n#define DIV32(a,b)     (((spx_word32_t)(a))/(spx_word32_t)(b))\n#define PDIV32(a,b)     (((spx_word32_t)(a))/(spx_word32_t)(b))\n\n#define WORD2INT(x) ((x) < -32767.5f ? -32768 : \\\n                    ((x) > 32766.5f ? 32767 : (spx_int16_t)floor(.5 + (x))))\n#endif\n\n\n#if defined(CONFIG_TI_C54X) || defined(CONFIG_TI_C55X)\n\n/* 2 on TI C5x DSP */\n#define BYTES_PER_CHAR 2\n#define BITS_PER_CHAR 16\n#define LOG2_BITS_PER_CHAR 4\n\n#else\n\n#define BYTES_PER_CHAR 1\n#define BITS_PER_CHAR 8\n#define LOG2_BITS_PER_CHAR 3\n\n#endif\n\n\n\n#ifdef FIXED_DEBUG\nextern long long spx_mips;\n#endif\n\n\n#endif\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/jitter.c",
    "content": "/* Copyright (C) 2002 Jean-Marc Valin\n   File: speex_jitter.h\n\n   Adaptive jitter buffer for Speex\n\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions\n   are met:\n\n   - Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n   - Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n   - Neither the name of the Xiph.org Foundation nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR\n   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n*/\n\n/*\nTODO:\n- Add short-term estimate\n- Defensive programming\n  + warn when last returned < last desired (begative buffering)\n  + warn if update_delay not called between get() and tick() or is called twice in a row\n- Linked list structure for holding the packets instead of the current fixed-size array\n  + return memory to a pool\n  + allow pre-allocation of the pool\n  + optional max number of elements\n- Statistics\n  + drift\n  + loss\n  + late\n  + jitter\n  + buffering delay\n*/\n#ifdef HAVE_CONFIG_H\n#include \"config.h\"\n#endif\n\n\n#include \"arch.h\"\n#include \"speex/speex_jitter.h\"\n#include \"os_support.h\"\n\n#ifndef NULL\n#define NULL 0\n#endif\n\n#define SPEEX_JITTER_MAX_BUFFER_SIZE 200   /**< Maximum number of packets in jitter buffer */\n\n#define TSUB(a,b) ((spx_int32_t)((a)-(b)))\n\n#define GT32(a,b) (((spx_int32_t)((a)-(b)))>0)\n#define GE32(a,b) (((spx_int32_t)((a)-(b)))>=0)\n#define LT32(a,b) (((spx_int32_t)((a)-(b)))<0)\n#define LE32(a,b) (((spx_int32_t)((a)-(b)))<=0)\n\n#define ROUND_DOWN(x, step) ((x)<0 ? ((x)-(step)+1)/(step)*(step) : (x)/(step)*(step))\n\n#define MAX_TIMINGS 40\n#define MAX_BUFFERS 3\n#define TOP_DELAY 40\n\n/** Buffer that keeps the time of arrival of the latest packets */\nstruct TimingBuffer {\n   int filled;                         /**< Number of entries occupied in \"timing\" and \"counts\"*/\n   int curr_count;                     /**< Number of packet timings we got (including those we discarded) */\n   spx_int32_t timing[MAX_TIMINGS];    /**< Sorted list of all timings (\"latest\" packets first) */\n   spx_int16_t counts[MAX_TIMINGS];    /**< Order the packets were put in (will be used for short-term estimate) */\n};\n\nstatic void tb_init(struct TimingBuffer *tb)\n{\n   tb->filled = 0;\n   tb->curr_count = 0;\n}\n\n/* Add the timing of a new packet to the TimingBuffer */\nstatic void tb_add(struct TimingBuffer *tb, spx_int16_t timing)\n{\n   int pos;\n   /* Discard packet that won't make it into the list because they're too early */\n   if (tb->filled >= MAX_TIMINGS && timing >= tb->timing[tb->filled-1])\n   {\n      tb->curr_count++;\n      return;\n   }\n\n   /* Find where the timing info goes in the sorted list */\n   pos = 0;\n   /* FIXME: Do bisection instead of linear search */\n   while (pos<tb->filled && timing >= tb->timing[pos])\n   {\n      pos++;\n   }\n\n   speex_assert(pos <= tb->filled && pos < MAX_TIMINGS);\n\n   /* Shift everything so we can perform the insertion */\n   if (pos < tb->filled)\n   {\n      int move_size = tb->filled-pos;\n      if (tb->filled == MAX_TIMINGS)\n         move_size -= 1;\n      SPEEX_MOVE(&tb->timing[pos+1], &tb->timing[pos], move_size);\n      SPEEX_MOVE(&tb->counts[pos+1], &tb->counts[pos], move_size);\n   }\n   /* Insert */\n   tb->timing[pos] = timing;\n   tb->counts[pos] = tb->curr_count;\n\n   tb->curr_count++;\n   if (tb->filled<MAX_TIMINGS)\n      tb->filled++;\n}\n\n\n\n/** Jitter buffer structure */\nstruct JitterBuffer_ {\n   spx_uint32_t pointer_timestamp;                             /**< Timestamp of what we will *get* next */\n   spx_uint32_t last_returned_timestamp;                       /**< Useful for getting the next packet with the same timestamp (for fragmented media) */\n   spx_uint32_t next_stop;                                     /**< Estimated time the next get() will be called */\n\n   spx_int32_t buffered;                                       /**< Amount of data we think is still buffered by the application (timestamp units)*/\n\n   spx_uint32_t buffer_size;\n   JitterBufferPacket packets[SPEEX_JITTER_MAX_BUFFER_SIZE];   /**< Packets stored in the buffer */\n   spx_uint32_t arrival[SPEEX_JITTER_MAX_BUFFER_SIZE];         /**< Packet arrival time (0 means it was late, even though it's a valid timestamp) */\n\n   void (*destroy) (void *);                                   /**< Callback for destroying a packet */\n\n   spx_int32_t delay_step;                                     /**< Size of the steps when adjusting buffering (timestamp units) */\n   spx_int32_t concealment_size;                               /**< Size of the packet loss concealment \"units\" */\n   int reset_state;                                            /**< True if state was just reset        */\n   int buffer_margin;                                          /**< How many frames we want to keep in the buffer (lower bound) */\n   int late_cutoff;                                            /**< How late must a packet be for it not to be considered at all */\n   int interp_requested;                                       /**< An interpolation is requested by speex_jitter_update_delay() */\n   int auto_adjust;                                            /**< Whether to automatically adjust the delay at any time */\n\n   struct TimingBuffer _tb[MAX_BUFFERS];                       /**< Don't use those directly */\n   struct TimingBuffer *timeBuffers[MAX_BUFFERS];              /**< Storing arrival time of latest frames so we can compute some stats */\n   int window_size;                                            /**< Total window over which the late frames are counted */\n   int subwindow_size;                                         /**< Sub-window size for faster computation  */\n   int max_late_rate;                                          /**< Absolute maximum amount of late packets tolerable (in percent) */\n   int latency_tradeoff;                                       /**< Latency equivalent of losing one percent of packets */\n   int auto_tradeoff;                                          /**< Latency equivalent of losing one percent of packets (automatic default) */\n\n   int lost_count;                                             /**< Number of consecutive lost packets  */\n};\n\n/** Based on available data, this computes the optimal delay for the jitter buffer.\n   The optimised function is in timestamp units and is:\n   cost = delay + late_factor*[number of frames that would be late if we used that delay]\n   @param tb Array of buffers\n   @param late_factor Equivalent cost of a late frame (in timestamp units)\n */\nstatic spx_int16_t compute_opt_delay(JitterBuffer *jitter)\n{\n   int i;\n   spx_int16_t opt=0;\n   spx_int32_t best_cost=0x7fffffff;\n   int late = 0;\n   int pos[MAX_BUFFERS];\n   int tot_count;\n   float late_factor;\n   int penalty_taken = 0;\n   int best = 0;\n   int worst = 0;\n   spx_int32_t deltaT;\n   struct TimingBuffer *tb;\n\n   tb = jitter->_tb;\n\n   /* Number of packet timings we have received (including those we didn't keep) */\n   tot_count = 0;\n   for (i=0;i<MAX_BUFFERS;i++)\n      tot_count += tb[i].curr_count;\n   if (tot_count==0)\n      return 0;\n\n   /* Compute cost for one lost packet */\n   if (jitter->latency_tradeoff != 0)\n      late_factor = jitter->latency_tradeoff * 100.0f / tot_count;\n   else\n      late_factor = jitter->auto_tradeoff * jitter->window_size/tot_count;\n\n   /*fprintf(stderr, \"late_factor = %f\\n\", late_factor);*/\n   for (i=0;i<MAX_BUFFERS;i++)\n      pos[i] = 0;\n\n   /* Pick the TOP_DELAY \"latest\" packets (doesn't need to actually be late\n      for the current settings) */\n   for (i=0;i<TOP_DELAY;i++)\n   {\n      int j;\n      int next=-1;\n      int latest = 32767;\n      /* Pick latest among all sub-windows */\n      for (j=0;j<MAX_BUFFERS;j++)\n      {\n         if (pos[j] < tb[j].filled && tb[j].timing[pos[j]] < latest)\n         {\n            next = j;\n            latest = tb[j].timing[pos[j]];\n         }\n      }\n      if (next != -1)\n      {\n         spx_int32_t cost;\n\n         if (i==0)\n            worst = latest;\n         best = latest;\n         latest = ROUND_DOWN(latest, jitter->delay_step);\n         pos[next]++;\n\n         /* Actual cost function that tells us how bad using this delay would be */\n         cost = -latest + late_factor*late;\n         /*fprintf(stderr, \"cost %d = %d + %f * %d\\n\", cost, -latest, late_factor, late);*/\n         if (cost < best_cost)\n         {\n            best_cost = cost;\n            opt = latest;\n         }\n      } else {\n         break;\n      }\n\n      /* For the next timing we will consider, there will be one more late packet to count */\n      late++;\n      /* Two-frame penalty if we're going to increase the amount of late frames (hysteresis) */\n      if (latest >= 0 && !penalty_taken)\n      {\n         penalty_taken = 1;\n         late+=4;\n      }\n   }\n\n   deltaT = best-worst;\n   /* This is a default \"automatic latency tradeoff\" when none is provided */\n   jitter->auto_tradeoff = 1 + deltaT/TOP_DELAY;\n   /*fprintf(stderr, \"auto_tradeoff = %d (%d %d %d)\\n\", jitter->auto_tradeoff, best, worst, i);*/\n\n   /* FIXME: Compute a short-term estimate too and combine with the long-term one */\n\n   /* Prevents reducing the buffer size when we haven't really had much data */\n   if (tot_count < TOP_DELAY && opt > 0)\n      return 0;\n   return opt;\n}\n\n\n/** Initialise jitter buffer */\nEXPORT JitterBuffer *jitter_buffer_init(int step_size)\n{\n   JitterBuffer *jitter = (JitterBuffer*)speex_alloc(sizeof(JitterBuffer));\n   if (jitter)\n   {\n      int i;\n      spx_int32_t tmp;\n      for (i=0;i<SPEEX_JITTER_MAX_BUFFER_SIZE;i++)\n         jitter->packets[i].data=NULL;\n      jitter->delay_step = step_size;\n      jitter->concealment_size = step_size;\n      /*FIXME: Should this be 0 or 1?*/\n      jitter->buffer_margin = 0;\n      jitter->late_cutoff = 50;\n      jitter->destroy = NULL;\n      jitter->latency_tradeoff = 0;\n      jitter->auto_adjust = 1;\n      jitter->buffer_size = SPEEX_JITTER_MAX_BUFFER_SIZE;\n      tmp = 4;\n      jitter_buffer_ctl(jitter, JITTER_BUFFER_SET_MAX_LATE_RATE, &tmp);\n      jitter_buffer_reset(jitter);\n   }\n   return jitter;\n}\n\n/** Reset jitter buffer */\nEXPORT void jitter_buffer_reset(JitterBuffer *jitter)\n{\n   int i;\n   for (i=0;i<SPEEX_JITTER_MAX_BUFFER_SIZE;i++)\n   {\n      if (jitter->packets[i].data)\n      {\n         if (jitter->destroy)\n            jitter->destroy(jitter->packets[i].data);\n         else\n            speex_free(jitter->packets[i].data);\n         jitter->packets[i].data = NULL;\n      }\n   }\n   /* Timestamp is actually undefined at this point */\n   jitter->pointer_timestamp = 0;\n   jitter->next_stop = 0;\n   jitter->reset_state = 1;\n   jitter->lost_count = 0;\n   jitter->buffered = 0;\n   jitter->auto_tradeoff = 32000;\n\n   for (i=0;i<MAX_BUFFERS;i++)\n   {\n      tb_init(&jitter->_tb[i]);\n      jitter->timeBuffers[i] = &jitter->_tb[i];\n   }\n   /*fprintf (stderr, \"reset\\n\");*/\n}\n\n/** Destroy jitter buffer */\nEXPORT void jitter_buffer_destroy(JitterBuffer *jitter)\n{\n   jitter_buffer_reset(jitter);\n   speex_free(jitter);\n}\n\n/** Take the following timing into consideration for future calculations */\nstatic void update_timings(JitterBuffer *jitter, spx_int32_t timing)\n{\n   if (timing < -32767)\n      timing = -32767;\n   if (timing > 32767)\n      timing = 32767;\n   /* If the current sub-window is full, perform a rotation and discard oldest sub-widow */\n   if (jitter->timeBuffers[0]->curr_count >= jitter->subwindow_size)\n   {\n      int i;\n      /*fprintf(stderr, \"Rotate buffer\\n\");*/\n      struct TimingBuffer *tmp = jitter->timeBuffers[MAX_BUFFERS-1];\n      for (i=MAX_BUFFERS-1;i>=1;i--)\n         jitter->timeBuffers[i] = jitter->timeBuffers[i-1];\n      jitter->timeBuffers[0] = tmp;\n      tb_init(jitter->timeBuffers[0]);\n   }\n   tb_add(jitter->timeBuffers[0], timing);\n}\n\n/** Compensate all timings when we do an adjustment of the buffering */\nstatic void shift_timings(JitterBuffer *jitter, spx_int16_t amount)\n{\n   int i, j;\n   for (i=0;i<MAX_BUFFERS;i++)\n   {\n      for (j=0;j<jitter->timeBuffers[i]->filled;j++)\n         jitter->timeBuffers[i]->timing[j] += amount;\n   }\n}\n\n\n/** Put one packet into the jitter buffer */\nEXPORT void jitter_buffer_put(JitterBuffer *jitter, const JitterBufferPacket *packet)\n{\n   spx_uint32_t i,j;\n   int late;\n   /*fprintf (stderr, \"put packet %d %d\\n\", timestamp, span);*/\n\n   /* Cleanup buffer (remove old packets that weren't played) */\n   if (!jitter->reset_state)\n   {\n      for (i=0;i<jitter->buffer_size;i++)\n      {\n         /* Make sure we don't discard a \"just-late\" packet in case we want to play it next (if we interpolate). */\n         if (jitter->packets[i].data && LE32(jitter->packets[i].timestamp + jitter->packets[i].span, jitter->pointer_timestamp))\n         {\n            /*fprintf (stderr, \"cleaned (not played)\\n\");*/\n            if (jitter->destroy)\n               jitter->destroy(jitter->packets[i].data);\n            else\n               speex_free(jitter->packets[i].data);\n            jitter->packets[i].data = NULL;\n         }\n      }\n   }\n\n   /*fprintf(stderr, \"arrival: %d %d %d\\n\", packet->timestamp, jitter->next_stop, jitter->pointer_timestamp);*/\n   /* Check if packet is late (could still be useful though) */\n   if (!jitter->reset_state && LT32(packet->timestamp, jitter->next_stop))\n   {\n      update_timings(jitter, ((spx_int32_t)packet->timestamp) - ((spx_int32_t)jitter->next_stop) - jitter->buffer_margin);\n      late = 1;\n   } else {\n      late = 0;\n   }\n\n   /* For some reason, the consumer has failed the last 20 fetches. Make sure this packet is\n    * used to resync. */\n   if (jitter->lost_count>20)\n   {\n      jitter_buffer_reset(jitter);\n   }\n\n   /* Only insert the packet if it's not hopelessly late (i.e. totally useless) */\n   if (jitter->reset_state || GE32(packet->timestamp+packet->span+jitter->delay_step, jitter->pointer_timestamp))\n   {\n\n      /*Find an empty slot in the buffer*/\n      for (i=0;i<jitter->buffer_size;i++)\n      {\n         if (jitter->packets[i].data==NULL)\n            break;\n      }\n\n      /*No place left in the buffer, need to make room for it by discarding the oldest packet */\n      if (i==jitter->buffer_size)\n      {\n         int earliest=jitter->packets[0].timestamp;\n         i=0;\n         for (j=1;j<jitter->buffer_size;j++)\n         {\n            if (!jitter->packets[i].data || LT32(jitter->packets[j].timestamp,earliest))\n            {\n               earliest = jitter->packets[j].timestamp;\n               i=j;\n            }\n         }\n         if (jitter->destroy)\n            jitter->destroy(jitter->packets[i].data);\n         else\n            speex_free(jitter->packets[i].data);\n         jitter->packets[i].data=NULL;\n         /*fprintf (stderr, \"Buffer is full, discarding earliest frame %d (currently at %d)\\n\", timestamp, jitter->pointer_timestamp);*/\n      }\n\n      /* Copy packet in buffer */\n      if (jitter->destroy)\n      {\n         jitter->packets[i].data = packet->data;\n      } else {\n         jitter->packets[i].data=(char*)speex_alloc(packet->len);\n         for (j=0;j<packet->len;j++)\n            jitter->packets[i].data[j]=packet->data[j];\n      }\n      jitter->packets[i].timestamp=packet->timestamp;\n      jitter->packets[i].span=packet->span;\n      jitter->packets[i].len=packet->len;\n      jitter->packets[i].sequence=packet->sequence;\n      jitter->packets[i].user_data=packet->user_data;\n      if (jitter->reset_state || late)\n         jitter->arrival[i] = 0;\n      else\n         jitter->arrival[i] = jitter->next_stop;\n   } else {\n\t  /* The original version of libspeex-dsp leaks packets when we\n\t   * get here, since the application has no way of knowing whether\n\t   * a packet was actually queued or not: as such, when this\n\t   * happens, we destroy the packet that was passed ourselves */\n      if (jitter->destroy)\n         jitter->destroy(packet->data);\n   }\n\n\n}\n\n/** Get one packet from the jitter buffer */\nEXPORT int jitter_buffer_get(JitterBuffer *jitter, JitterBufferPacket *packet, spx_int32_t desired_span, spx_int32_t *start_offset)\n{\n   spx_uint32_t i;\n   unsigned int j;\n   spx_int16_t opt;\n\n   if (start_offset != NULL)\n      *start_offset = 0;\n\n   /* Syncing on the first call */\n   if (jitter->reset_state)\n   {\n      int found = 0;\n      /* Find the oldest packet */\n      spx_uint32_t oldest=0;\n      for (i=0;i<jitter->buffer_size;i++)\n      {\n         if (jitter->packets[i].data && (!found || LT32(jitter->packets[i].timestamp,oldest)))\n         {\n            oldest = jitter->packets[i].timestamp;\n            found = 1;\n         }\n      }\n      if (found)\n      {\n         jitter->reset_state=0;\n         jitter->pointer_timestamp = oldest;\n         jitter->next_stop = oldest;\n      } else {\n         packet->timestamp = 0;\n         packet->span = jitter->interp_requested;\n         return JITTER_BUFFER_MISSING;\n      }\n   }\n\n\n   jitter->last_returned_timestamp = jitter->pointer_timestamp;\n\n   if (jitter->interp_requested != 0)\n   {\n      packet->timestamp = jitter->pointer_timestamp;\n      packet->span = jitter->interp_requested;\n\n      /* Increment the pointer because it got decremented in the delay update */\n      jitter->pointer_timestamp += jitter->interp_requested;\n      packet->len = 0;\n      /*fprintf (stderr, \"Deferred interpolate\\n\");*/\n\n      jitter->interp_requested = 0;\n\n      jitter->buffered = packet->span - desired_span;\n      return JITTER_BUFFER_INSERTION;\n   }\n\n   /* Searching for the packet that fits best */\n\n   /* Search the buffer for a packet with the right timestamp and spanning the whole current chunk */\n   for (i=0;i<jitter->buffer_size;i++)\n   {\n      if (jitter->packets[i].data && jitter->packets[i].timestamp==jitter->pointer_timestamp && GE32(jitter->packets[i].timestamp+jitter->packets[i].span,jitter->pointer_timestamp+desired_span))\n         break;\n   }\n\n   /* If no match, try for an \"older\" packet that still spans (fully) the current chunk */\n   if (i==jitter->buffer_size)\n   {\n      for (i=0;i<jitter->buffer_size;i++)\n      {\n         if (jitter->packets[i].data && LE32(jitter->packets[i].timestamp, jitter->pointer_timestamp) && GE32(jitter->packets[i].timestamp+jitter->packets[i].span,jitter->pointer_timestamp+desired_span))\n            break;\n      }\n   }\n\n   /* If still no match, try for an \"older\" packet that spans part of the current chunk */\n   if (i==jitter->buffer_size)\n   {\n      for (i=0;i<jitter->buffer_size;i++)\n      {\n         if (jitter->packets[i].data && LE32(jitter->packets[i].timestamp, jitter->pointer_timestamp) && GT32(jitter->packets[i].timestamp+jitter->packets[i].span,jitter->pointer_timestamp))\n            break;\n      }\n   }\n\n   /* If still no match, try for earliest packet possible */\n   if (i==jitter->buffer_size)\n   {\n      int found = 0;\n      spx_uint32_t best_time=0;\n      int best_span=0;\n      int besti=0;\n      for (i=0;i<jitter->buffer_size;i++)\n      {\n         /* check if packet starts within current chunk */\n         if (jitter->packets[i].data && LT32(jitter->packets[i].timestamp,jitter->pointer_timestamp+desired_span) && GE32(jitter->packets[i].timestamp,jitter->pointer_timestamp))\n         {\n            if (!found || LT32(jitter->packets[i].timestamp,best_time) || (jitter->packets[i].timestamp==best_time && GT32(jitter->packets[i].span,best_span)))\n            {\n               best_time = jitter->packets[i].timestamp;\n               best_span = jitter->packets[i].span;\n               besti = i;\n               found = 1;\n            }\n         }\n      }\n      if (found)\n      {\n         i=besti;\n         /*fprintf (stderr, \"incomplete: %d %d %d %d\\n\", jitter->packets[i].timestamp, jitter->pointer_timestamp, chunk_size, jitter->packets[i].span);*/\n      }\n   }\n\n   /* If we find something */\n   if (i!=jitter->buffer_size)\n   {\n      spx_int32_t offset;\n\n      /* We (obviously) haven't lost this packet */\n      jitter->lost_count = 0;\n\n      /* In this case, 0 isn't as a valid timestamp */\n      if (jitter->arrival[i] != 0)\n      {\n         update_timings(jitter, ((spx_int32_t)jitter->packets[i].timestamp) - ((spx_int32_t)jitter->arrival[i]) - jitter->buffer_margin);\n      }\n\n\n      /* Copy packet */\n      if (jitter->destroy)\n      {\n         packet->data = jitter->packets[i].data;\n         packet->len = jitter->packets[i].len;\n      } else {\n         if (jitter->packets[i].len > packet->len)\n         {\n            speex_warning_int(\"jitter_buffer_get(): packet too large to fit. Size is\", jitter->packets[i].len);\n         } else {\n            packet->len = jitter->packets[i].len;\n         }\n         for (j=0;j<packet->len;j++)\n            packet->data[j] = jitter->packets[i].data[j];\n         /* Remove packet */\n         speex_free(jitter->packets[i].data);\n      }\n      jitter->packets[i].data = NULL;\n      /* Set timestamp and span (if requested) */\n      offset = (spx_int32_t)jitter->packets[i].timestamp-(spx_int32_t)jitter->pointer_timestamp;\n      if (start_offset != NULL)\n         *start_offset = offset;\n      else if (offset != 0)\n         speex_warning_int(\"jitter_buffer_get() discarding non-zero start_offset\", offset);\n\n      packet->timestamp = jitter->packets[i].timestamp;\n      jitter->last_returned_timestamp = packet->timestamp;\n\n      packet->span = jitter->packets[i].span;\n      packet->sequence = jitter->packets[i].sequence;\n      packet->user_data = jitter->packets[i].user_data;\n      /* Point to the end of the current packet */\n      jitter->pointer_timestamp = jitter->packets[i].timestamp+jitter->packets[i].span;\n\n      jitter->buffered = packet->span - desired_span;\n\n      if (start_offset != NULL)\n         jitter->buffered += *start_offset;\n\n      return JITTER_BUFFER_OK;\n   }\n\n\n   /* If we haven't found anything worth returning */\n\n   /*fprintf (stderr, \"not found\\n\");*/\n   jitter->lost_count++;\n   /*fprintf (stderr, \"m\");*/\n   /*fprintf (stderr, \"lost_count = %d\\n\", jitter->lost_count);*/\n\n   opt = compute_opt_delay(jitter);\n\n   /* Should we force an increase in the buffer or just do normal interpolation? */\n   if (opt < 0)\n   {\n      /* Need to increase buffering */\n\n      /* Shift histogram to compensate */\n      shift_timings(jitter, -opt);\n\n      packet->timestamp = jitter->pointer_timestamp;\n      packet->span = -opt;\n      /* Don't move the pointer_timestamp forward */\n      packet->len = 0;\n\n      jitter->buffered = packet->span - desired_span;\n      return JITTER_BUFFER_INSERTION;\n      /*jitter->pointer_timestamp -= jitter->delay_step;*/\n      /*fprintf (stderr, \"Forced to interpolate\\n\");*/\n   } else {\n      /* Normal packet loss */\n      packet->timestamp = jitter->pointer_timestamp;\n\n      desired_span = ROUND_DOWN(desired_span, jitter->concealment_size);\n      packet->span = desired_span;\n      jitter->pointer_timestamp += desired_span;\n      packet->len = 0;\n\n      jitter->buffered = packet->span - desired_span;\n      return JITTER_BUFFER_MISSING;\n      /*fprintf (stderr, \"Normal loss\\n\");*/\n   }\n\n\n}\n\nEXPORT int jitter_buffer_get_another(JitterBuffer *jitter, JitterBufferPacket *packet)\n{\n   spx_uint32_t i, j;\n   for (i=0;i<jitter->buffer_size;i++)\n   {\n      if (jitter->packets[i].data && jitter->packets[i].timestamp==jitter->last_returned_timestamp)\n         break;\n   }\n   if (i!=jitter->buffer_size)\n   {\n      /* Copy packet */\n      packet->len = jitter->packets[i].len;\n      if (jitter->destroy)\n      {\n         packet->data = jitter->packets[i].data;\n      } else {\n         for (j=0;j<packet->len;j++)\n            packet->data[j] = jitter->packets[i].data[j];\n         /* Remove packet */\n         speex_free(jitter->packets[i].data);\n      }\n      jitter->packets[i].data = NULL;\n      packet->timestamp = jitter->packets[i].timestamp;\n      packet->span = jitter->packets[i].span;\n      packet->sequence = jitter->packets[i].sequence;\n      packet->user_data = jitter->packets[i].user_data;\n      return JITTER_BUFFER_OK;\n   } else {\n      packet->data = NULL;\n      packet->len = 0;\n      packet->span = 0;\n      return JITTER_BUFFER_MISSING;\n   }\n}\n\n/* Let the jitter buffer know it's the right time to adjust the buffering delay to the network conditions */\nstatic int _jitter_buffer_update_delay(JitterBuffer *jitter, JitterBufferPacket *packet, spx_int32_t *start_offset)\n{\n   spx_int16_t opt = compute_opt_delay(jitter);\n   /*fprintf(stderr, \"opt adjustment is %d \", opt);*/\n\n   if (opt < 0)\n   {\n      shift_timings(jitter, -opt);\n\n      jitter->pointer_timestamp += opt;\n      jitter->interp_requested = -opt;\n      /*fprintf (stderr, \"Decision to interpolate %d samples\\n\", -opt);*/\n   } else if (opt > 0)\n   {\n      shift_timings(jitter, -opt);\n      jitter->pointer_timestamp += opt;\n      /*fprintf (stderr, \"Decision to drop %d samples\\n\", opt);*/\n   }\n\n   return opt;\n}\n\n/* Let the jitter buffer know it's the right time to adjust the buffering delay to the network conditions */\nEXPORT int jitter_buffer_update_delay(JitterBuffer *jitter, JitterBufferPacket *packet, spx_int32_t *start_offset)\n{\n   /* If the programmer calls jitter_buffer_update_delay() directly,\n      automatically disable auto-adjustment */\n   jitter->auto_adjust = 0;\n\n   return _jitter_buffer_update_delay(jitter, packet, start_offset);\n}\n\n/** Get pointer timestamp of jitter buffer */\nEXPORT int jitter_buffer_get_pointer_timestamp(JitterBuffer *jitter)\n{\n   return jitter->pointer_timestamp;\n}\n\nEXPORT void jitter_buffer_tick(JitterBuffer *jitter)\n{\n   /* Automatically-adjust the buffering delay if requested */\n   if (jitter->auto_adjust)\n      _jitter_buffer_update_delay(jitter, NULL, NULL);\n\n   if (jitter->buffered >= 0)\n   {\n      jitter->next_stop = jitter->pointer_timestamp - jitter->buffered;\n   } else {\n      jitter->next_stop = jitter->pointer_timestamp;\n      speex_warning_int(\"jitter buffer sees negative buffering, your code might be broken. Value is \", jitter->buffered);\n   }\n   jitter->buffered = 0;\n}\n\nEXPORT void jitter_buffer_remaining_span(JitterBuffer *jitter, spx_uint32_t rem)\n{\n   /* Automatically-adjust the buffering delay if requested */\n   if (jitter->auto_adjust)\n      _jitter_buffer_update_delay(jitter, NULL, NULL);\n\n   if (jitter->buffered < 0)\n      speex_warning_int(\"jitter buffer sees negative buffering, your code might be broken. Value is \", jitter->buffered);\n   jitter->next_stop = jitter->pointer_timestamp - rem;\n}\n\n\n/* Used like the ioctl function to control the jitter buffer parameters */\nEXPORT int jitter_buffer_ctl(JitterBuffer *jitter, int request, void *ptr)\n{\n   int count;\n   spx_uint32_t i;\n   spx_int32_t buffer_size;\n   switch(request)\n   {\n      case JITTER_BUFFER_SET_MARGIN:\n         jitter->buffer_margin = *(spx_int32_t*)ptr;\n         break;\n      case JITTER_BUFFER_GET_MARGIN:\n         *(spx_int32_t*)ptr = jitter->buffer_margin;\n         break;\n      case JITTER_BUFFER_GET_AVALIABLE_COUNT:\n         count = 0;\n         for (i=0;i<jitter->buffer_size;i++)\n         {\n            if (jitter->packets[i].data && LE32(jitter->pointer_timestamp, jitter->packets[i].timestamp))\n            {\n               count++;\n            }\n         }\n         *(spx_int32_t*)ptr = count;\n         break;\n      case JITTER_BUFFER_SET_DESTROY_CALLBACK:\n         jitter->destroy = (void (*) (void *))ptr;\n         break;\n      case JITTER_BUFFER_GET_DESTROY_CALLBACK:\n         *(void (**) (void *))ptr = jitter->destroy;\n         break;\n      case JITTER_BUFFER_SET_DELAY_STEP:\n         jitter->delay_step = *(spx_int32_t*)ptr;\n         break;\n      case JITTER_BUFFER_GET_DELAY_STEP:\n         *(spx_int32_t*)ptr = jitter->delay_step;\n         break;\n      case JITTER_BUFFER_SET_CONCEALMENT_SIZE:\n         jitter->concealment_size = *(spx_int32_t*)ptr;\n         break;\n      case JITTER_BUFFER_GET_CONCEALMENT_SIZE:\n         *(spx_int32_t*)ptr = jitter->concealment_size;\n         break;\n      case JITTER_BUFFER_SET_MAX_LATE_RATE:\n         jitter->max_late_rate = *(spx_int32_t*)ptr;\n         jitter->window_size = 100*TOP_DELAY/jitter->max_late_rate;\n         jitter->subwindow_size = jitter->window_size/MAX_BUFFERS;\n         break;\n      case JITTER_BUFFER_GET_MAX_LATE_RATE:\n         *(spx_int32_t*)ptr = jitter->max_late_rate;\n         break;\n      case JITTER_BUFFER_SET_LATE_COST:\n         jitter->latency_tradeoff = *(spx_int32_t*)ptr;\n         break;\n      case JITTER_BUFFER_GET_LATE_COST:\n         *(spx_int32_t*)ptr = jitter->latency_tradeoff;\n         break;\n      case JITTER_BUFFER_SET_LIMIT:\n         buffer_size = *(spx_int32_t*)ptr;\n         jitter->buffer_size = (buffer_size > 1 && buffer_size <= SPEEX_JITTER_MAX_BUFFER_SIZE) ? buffer_size : SPEEX_JITTER_MAX_BUFFER_SIZE;\n         jitter_buffer_reset(jitter);\n         break;\n      default:\n         speex_warning_int(\"Unknown jitter_buffer_ctl request: \", request);\n         return -1;\n   }\n   return 0;\n}\n\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/os_support.h",
    "content": "/* Copyright (C) 2007 Jean-Marc Valin\n\n   File: os_support.h\n   This is the (tiny) OS abstraction layer. Aside from math.h, this is the\n   only place where system headers are allowed.\n\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions are\n   met:\n\n   1. Redistributions of source code must retain the above copyright notice,\n   this list of conditions and the following disclaimer.\n\n   2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n   3. The name of the author may not be used to endorse or promote products\n   derived from this software without specific prior written permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n   IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n   DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,\n   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n   ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n   POSSIBILITY OF SUCH DAMAGE.\n*/\n\n#ifndef OS_SUPPORT_H\n#define OS_SUPPORT_H\n\n#include <string.h>\n#include <stdio.h>\n#include <stdlib.h>\n\n#ifdef HAVE_CONFIG_H\n#include \"config.h\"\n#endif\n#ifdef OS_SUPPORT_CUSTOM\n#include \"os_support_custom.h\"\n#endif\n\n/** Speex wrapper for calloc. To do your own dynamic allocation, all you need to do is replace this function, speex_realloc and speex_free\n    NOTE: speex_alloc needs to CLEAR THE MEMORY */\n#ifndef OVERRIDE_SPEEX_ALLOC\nstatic inline void *speex_alloc (int size)\n{\n   /* WARNING: this is not equivalent to malloc(). If you want to use malloc()\n      or your own allocator, YOU NEED TO CLEAR THE MEMORY ALLOCATED. Otherwise\n      you will experience strange bugs */\n   return calloc(size,1);\n}\n#endif\n\n/** Same as speex_alloc, except that the area is only needed inside a Speex call (might cause problem with wideband though) */\n#ifndef OVERRIDE_SPEEX_ALLOC_SCRATCH\nstatic inline void *speex_alloc_scratch (int size)\n{\n   /* Scratch space doesn't need to be cleared */\n   return calloc(size,1);\n}\n#endif\n\n/** Speex wrapper for realloc. To do your own dynamic allocation, all you need to do is replace this function, speex_alloc and speex_free */\n#ifndef OVERRIDE_SPEEX_REALLOC\nstatic inline void *speex_realloc (void *ptr, int size)\n{\n   return realloc(ptr, size);\n}\n#endif\n\n/** Speex wrapper for calloc. To do your own dynamic allocation, all you need to do is replace this function, speex_realloc and speex_alloc */\n#ifndef OVERRIDE_SPEEX_FREE\nstatic inline void speex_free (void *ptr)\n{\n   free(ptr);\n}\n#endif\n\n/** Same as speex_free, except that the area is only needed inside a Speex call (might cause problem with wideband though) */\n#ifndef OVERRIDE_SPEEX_FREE_SCRATCH\nstatic inline void speex_free_scratch (void *ptr)\n{\n   free(ptr);\n}\n#endif\n\n/** Copy n elements from src to dst. The 0* term provides compile-time type checking  */\n#ifndef OVERRIDE_SPEEX_COPY\n#define SPEEX_COPY(dst, src, n) (memcpy((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) ))\n#endif\n\n/** Copy n elements from src to dst, allowing overlapping regions. The 0* term\n    provides compile-time type checking */\n#ifndef OVERRIDE_SPEEX_MOVE\n#define SPEEX_MOVE(dst, src, n) (memmove((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) ))\n#endif\n\n/** For n elements worth of memory, set every byte to the value of c, starting at address dst */\n#ifndef OVERRIDE_SPEEX_MEMSET\n#define SPEEX_MEMSET(dst, c, n) (memset((dst), (c), (n)*sizeof(*(dst))))\n#endif\n\n\n#ifndef OVERRIDE_SPEEX_FATAL\nstatic inline void _speex_fatal(const char *str, const char *file, int line)\n{\n   fprintf (stderr, \"Fatal (internal) error in %s, line %d: %s\\n\", file, line, str);\n   exit(1);\n}\n#endif\n\n#ifndef OVERRIDE_SPEEX_WARNING\nstatic inline void speex_warning(const char *str)\n{\n#ifndef DISABLE_WARNINGS\n   fprintf (stderr, \"warning: %s\\n\", str);\n#endif\n}\n#endif\n\n#ifndef OVERRIDE_SPEEX_WARNING_INT\nstatic inline void speex_warning_int(const char *str, int val)\n{\n#ifndef DISABLE_WARNINGS\n   fprintf (stderr, \"warning: %s %d\\n\", str, val);\n#endif\n}\n#endif\n\n#ifndef OVERRIDE_SPEEX_NOTIFY\nstatic inline void speex_notify(const char *str)\n{\n#ifndef DISABLE_NOTIFICATIONS\n   fprintf (stderr, \"notification: %s\\n\", str);\n#endif\n}\n#endif\n\n#ifndef OVERRIDE_SPEEX_PUTC\n/** Speex wrapper for putc */\nstatic inline void _speex_putc(int ch, void *file)\n{\n   FILE *f = (FILE *)file;\n   fprintf(f, \"%c\", ch);\n}\n#endif\n\n#define speex_fatal(str) _speex_fatal(str, __FILE__, __LINE__);\n#define speex_assert(cond) {if (!(cond)) {speex_fatal(\"assertion failed: \" #cond);}}\n\n#ifndef RELEASE\nstatic inline void print_vec(float *vec, int len, char *name)\n{\n   int i;\n   printf (\"%s \", name);\n   for (i=0;i<len;i++)\n      printf (\" %f\", vec[i]);\n   printf (\"\\n\");\n}\n#endif\n\n#endif\n\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/resample.c",
    "content": "/* Copyright (C) 2007-2008 Jean-Marc Valin\n   Copyright (C) 2008      Thorvald Natvig\n\n   File: resample.c\n   Arbitrary resampling code\n\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions are\n   met:\n\n   1. Redistributions of source code must retain the above copyright notice,\n   this list of conditions and the following disclaimer.\n\n   2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n   3. The name of the author may not be used to endorse or promote products\n   derived from this software without specific prior written permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n   IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n   DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,\n   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n   ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n   POSSIBILITY OF SUCH DAMAGE.\n*/\n\n/*\n   The design goals of this code are:\n      - Very fast algorithm\n      - SIMD-friendly algorithm\n      - Low memory requirement\n      - Good *perceptual* quality (and not best SNR)\n\n   Warning: This resampler is relatively new. Although I think I got rid of\n   all the major bugs and I don't expect the API to change anymore, there\n   may be something I've missed. So use with caution.\n\n   This algorithm is based on this original resampling algorithm:\n   Smith, Julius O. Digital Audio Resampling Home Page\n   Center for Computer Research in Music and Acoustics (CCRMA),\n   Stanford University, 2007.\n   Web published at https://ccrma.stanford.edu/~jos/resample/.\n\n   There is one main difference, though. This resampler uses cubic\n   interpolation instead of linear interpolation in the above paper. This\n   makes the table much smaller and makes it possible to compute that table\n   on a per-stream basis. In turn, being able to tweak the table for each\n   stream makes it possible to both reduce complexity on simple ratios\n   (e.g. 2/3), and get rid of the rounding operations in the inner loop.\n   The latter both reduces CPU time and makes the algorithm more SIMD-friendly.\n*/\n\n#ifdef HAVE_CONFIG_H\n#include \"config.h\"\n#endif\n\n#ifdef OUTSIDE_SPEEX\n#include <stdlib.h>\nstatic void *speex_alloc(int size) {return calloc(size,1);}\nstatic void *speex_realloc(void *ptr, int size) {return realloc(ptr, size);}\nstatic void speex_free(void *ptr) {free(ptr);}\n#ifndef EXPORT\n#define EXPORT\n#endif\n#include \"speex_resampler.h\"\n#include \"arch.h\"\n#else /* OUTSIDE_SPEEX */\n\n#include \"speex/speex_resampler.h\"\n#include \"arch.h\"\n#include \"os_support.h\"\n#endif /* OUTSIDE_SPEEX */\n\n#include <math.h>\n#include <limits.h>\n\n#ifndef M_PI\n#define M_PI 3.14159265358979323846\n#endif\n\n#define IMAX(a,b) ((a) > (b) ? (a) : (b))\n#define IMIN(a,b) ((a) < (b) ? (a) : (b))\n\n#ifndef NULL\n#define NULL 0\n#endif\n\n#ifndef UINT32_MAX\n#define UINT32_MAX 4294967295U\n#endif\n\n#ifdef USE_SSE\n#include \"resample_sse.h\"\n#endif\n\n#ifdef USE_NEON\n#include \"resample_neon.h\"\n#endif\n\n/* Number of elements to allocate on the stack */\n#ifdef VAR_ARRAYS\n#define FIXED_STACK_ALLOC 8192\n#else\n#define FIXED_STACK_ALLOC 1024\n#endif\n\ntypedef int (*resampler_basic_func)(SpeexResamplerState *, spx_uint32_t , const spx_word16_t *, spx_uint32_t *, spx_word16_t *, spx_uint32_t *);\n\nstruct SpeexResamplerState_ {\n   spx_uint32_t in_rate;\n   spx_uint32_t out_rate;\n   spx_uint32_t num_rate;\n   spx_uint32_t den_rate;\n\n   int    quality;\n   spx_uint32_t nb_channels;\n   spx_uint32_t filt_len;\n   spx_uint32_t mem_alloc_size;\n   spx_uint32_t buffer_size;\n   int          int_advance;\n   int          frac_advance;\n   float  cutoff;\n   spx_uint32_t oversample;\n   int          initialised;\n   int          started;\n\n   /* These are per-channel */\n   spx_int32_t  *last_sample;\n   spx_uint32_t *samp_frac_num;\n   spx_uint32_t *magic_samples;\n\n   spx_word16_t *mem;\n   spx_word16_t *sinc_table;\n   spx_uint32_t sinc_table_length;\n   resampler_basic_func resampler_ptr;\n\n   int    in_stride;\n   int    out_stride;\n} ;\n\nstatic const double kaiser12_table[68] = {\n   0.99859849, 1.00000000, 0.99859849, 0.99440475, 0.98745105, 0.97779076,\n   0.96549770, 0.95066529, 0.93340547, 0.91384741, 0.89213598, 0.86843014,\n   0.84290116, 0.81573067, 0.78710866, 0.75723148, 0.72629970, 0.69451601,\n   0.66208321, 0.62920216, 0.59606986, 0.56287762, 0.52980938, 0.49704014,\n   0.46473455, 0.43304576, 0.40211431, 0.37206735, 0.34301800, 0.31506490,\n   0.28829195, 0.26276832, 0.23854851, 0.21567274, 0.19416736, 0.17404546,\n   0.15530766, 0.13794294, 0.12192957, 0.10723616, 0.09382272, 0.08164178,\n   0.07063950, 0.06075685, 0.05193064, 0.04409466, 0.03718069, 0.03111947,\n   0.02584161, 0.02127838, 0.01736250, 0.01402878, 0.01121463, 0.00886058,\n   0.00691064, 0.00531256, 0.00401805, 0.00298291, 0.00216702, 0.00153438,\n   0.00105297, 0.00069463, 0.00043489, 0.00025272, 0.00013031, 0.0000527734,\n   0.00001000, 0.00000000};\n/*\nstatic const double kaiser12_table[36] = {\n   0.99440475, 1.00000000, 0.99440475, 0.97779076, 0.95066529, 0.91384741,\n   0.86843014, 0.81573067, 0.75723148, 0.69451601, 0.62920216, 0.56287762,\n   0.49704014, 0.43304576, 0.37206735, 0.31506490, 0.26276832, 0.21567274,\n   0.17404546, 0.13794294, 0.10723616, 0.08164178, 0.06075685, 0.04409466,\n   0.03111947, 0.02127838, 0.01402878, 0.00886058, 0.00531256, 0.00298291,\n   0.00153438, 0.00069463, 0.00025272, 0.0000527734, 0.00000500, 0.00000000};\n*/\nstatic const double kaiser10_table[36] = {\n   0.99537781, 1.00000000, 0.99537781, 0.98162644, 0.95908712, 0.92831446,\n   0.89005583, 0.84522401, 0.79486424, 0.74011713, 0.68217934, 0.62226347,\n   0.56155915, 0.50119680, 0.44221549, 0.38553619, 0.33194107, 0.28205962,\n   0.23636152, 0.19515633, 0.15859932, 0.12670280, 0.09935205, 0.07632451,\n   0.05731132, 0.04193980, 0.02979584, 0.02044510, 0.01345224, 0.00839739,\n   0.00488951, 0.00257636, 0.00115101, 0.00035515, 0.00000000, 0.00000000};\n\nstatic const double kaiser8_table[36] = {\n   0.99635258, 1.00000000, 0.99635258, 0.98548012, 0.96759014, 0.94302200,\n   0.91223751, 0.87580811, 0.83439927, 0.78875245, 0.73966538, 0.68797126,\n   0.63451750, 0.58014482, 0.52566725, 0.47185369, 0.41941150, 0.36897272,\n   0.32108304, 0.27619388, 0.23465776, 0.19672670, 0.16255380, 0.13219758,\n   0.10562887, 0.08273982, 0.06335451, 0.04724088, 0.03412321, 0.02369490,\n   0.01563093, 0.00959968, 0.00527363, 0.00233883, 0.00050000, 0.00000000};\n\nstatic const double kaiser6_table[36] = {\n   0.99733006, 1.00000000, 0.99733006, 0.98935595, 0.97618418, 0.95799003,\n   0.93501423, 0.90755855, 0.87598009, 0.84068475, 0.80211977, 0.76076565,\n   0.71712752, 0.67172623, 0.62508937, 0.57774224, 0.53019925, 0.48295561,\n   0.43647969, 0.39120616, 0.34752997, 0.30580127, 0.26632152, 0.22934058,\n   0.19505503, 0.16360756, 0.13508755, 0.10953262, 0.08693120, 0.06722600,\n   0.05031820, 0.03607231, 0.02432151, 0.01487334, 0.00752000, 0.00000000};\n\nstruct FuncDef {\n   const double *table;\n   int oversample;\n};\n\nstatic const struct FuncDef kaiser12_funcdef = {kaiser12_table, 64};\n#define KAISER12 (&kaiser12_funcdef)\nstatic const struct FuncDef kaiser10_funcdef = {kaiser10_table, 32};\n#define KAISER10 (&kaiser10_funcdef)\nstatic const struct FuncDef kaiser8_funcdef = {kaiser8_table, 32};\n#define KAISER8 (&kaiser8_funcdef)\nstatic const struct FuncDef kaiser6_funcdef = {kaiser6_table, 32};\n#define KAISER6 (&kaiser6_funcdef)\n\nstruct QualityMapping {\n   int base_length;\n   int oversample;\n   float downsample_bandwidth;\n   float upsample_bandwidth;\n   const struct FuncDef *window_func;\n};\n\n\n/* This table maps conversion quality to internal parameters. There are two\n   reasons that explain why the up-sampling bandwidth is larger than the\n   down-sampling bandwidth:\n   1) When up-sampling, we can assume that the spectrum is already attenuated\n      close to the Nyquist rate (from an A/D or a previous resampling filter)\n   2) Any aliasing that occurs very close to the Nyquist rate will be masked\n      by the sinusoids/noise just below the Nyquist rate (guaranteed only for\n      up-sampling).\n*/\nstatic const struct QualityMapping quality_map[11] = {\n   {  8,  4, 0.830f, 0.860f, KAISER6 }, /* Q0 */\n   { 16,  4, 0.850f, 0.880f, KAISER6 }, /* Q1 */\n   { 32,  4, 0.882f, 0.910f, KAISER6 }, /* Q2 */  /* 82.3% cutoff ( ~60 dB stop) 6  */\n   { 48,  8, 0.895f, 0.917f, KAISER8 }, /* Q3 */  /* 84.9% cutoff ( ~80 dB stop) 8  */\n   { 64,  8, 0.921f, 0.940f, KAISER8 }, /* Q4 */  /* 88.7% cutoff ( ~80 dB stop) 8  */\n   { 80, 16, 0.922f, 0.940f, KAISER10}, /* Q5 */  /* 89.1% cutoff (~100 dB stop) 10 */\n   { 96, 16, 0.940f, 0.945f, KAISER10}, /* Q6 */  /* 91.5% cutoff (~100 dB stop) 10 */\n   {128, 16, 0.950f, 0.950f, KAISER10}, /* Q7 */  /* 93.1% cutoff (~100 dB stop) 10 */\n   {160, 16, 0.960f, 0.960f, KAISER10}, /* Q8 */  /* 94.5% cutoff (~100 dB stop) 10 */\n   {192, 32, 0.968f, 0.968f, KAISER12}, /* Q9 */  /* 95.5% cutoff (~100 dB stop) 10 */\n   {256, 32, 0.975f, 0.975f, KAISER12}, /* Q10 */ /* 96.6% cutoff (~100 dB stop) 10 */\n};\n/*8,24,40,56,80,104,128,160,200,256,320*/\nstatic double compute_func(float x, const struct FuncDef *func)\n{\n   float y, frac;\n   double interp[4];\n   int ind;\n   y = x*func->oversample;\n   ind = (int)floor(y);\n   frac = (y-ind);\n   /* CSE with handle the repeated powers */\n   interp[3] =  -0.1666666667*frac + 0.1666666667*(frac*frac*frac);\n   interp[2] = frac + 0.5*(frac*frac) - 0.5*(frac*frac*frac);\n   /*interp[2] = 1.f - 0.5f*frac - frac*frac + 0.5f*frac*frac*frac;*/\n   interp[0] = -0.3333333333*frac + 0.5*(frac*frac) - 0.1666666667*(frac*frac*frac);\n   /* Just to make sure we don't have rounding problems */\n   interp[1] = 1.f-interp[3]-interp[2]-interp[0];\n\n   /*sum = frac*accum[1] + (1-frac)*accum[2];*/\n   return interp[0]*func->table[ind] + interp[1]*func->table[ind+1] + interp[2]*func->table[ind+2] + interp[3]*func->table[ind+3];\n}\n\n#if 0\n#include <stdio.h>\nint main(int argc, char **argv)\n{\n   int i;\n   for (i=0;i<256;i++)\n   {\n      printf (\"%f\\n\", compute_func(i/256., KAISER12));\n   }\n   return 0;\n}\n#endif\n\n#ifdef FIXED_POINT\n/* The slow way of computing a sinc for the table. Should improve that some day */\nstatic spx_word16_t sinc(float cutoff, float x, int N, const struct FuncDef *window_func)\n{\n   /*fprintf (stderr, \"%f \", x);*/\n   float xx = x * cutoff;\n   if (fabs(x)<1e-6f)\n      return WORD2INT(32768.*cutoff);\n   else if (fabs(x) > .5f*N)\n      return 0;\n   /*FIXME: Can it really be any slower than this? */\n   return WORD2INT(32768.*cutoff*sin(M_PI*xx)/(M_PI*xx) * compute_func(fabs(2.*x/N), window_func));\n}\n#else\n/* The slow way of computing a sinc for the table. Should improve that some day */\nstatic spx_word16_t sinc(float cutoff, float x, int N, const struct FuncDef *window_func)\n{\n   /*fprintf (stderr, \"%f \", x);*/\n   float xx = x * cutoff;\n   if (fabs(x)<1e-6)\n      return cutoff;\n   else if (fabs(x) > .5*N)\n      return 0;\n   /*FIXME: Can it really be any slower than this? */\n   return cutoff*sin(M_PI*xx)/(M_PI*xx) * compute_func(fabs(2.*x/N), window_func);\n}\n#endif\n\n#ifdef FIXED_POINT\nstatic void cubic_coef(spx_word16_t x, spx_word16_t interp[4])\n{\n   /* Compute interpolation coefficients. I'm not sure whether this corresponds to cubic interpolation\n   but I know it's MMSE-optimal on a sinc */\n   spx_word16_t x2, x3;\n   x2 = MULT16_16_P15(x, x);\n   x3 = MULT16_16_P15(x, x2);\n   interp[0] = PSHR32(MULT16_16(QCONST16(-0.16667f, 15),x) + MULT16_16(QCONST16(0.16667f, 15),x3),15);\n   interp[1] = EXTRACT16(EXTEND32(x) + SHR32(SUB32(EXTEND32(x2),EXTEND32(x3)),1));\n   interp[3] = PSHR32(MULT16_16(QCONST16(-0.33333f, 15),x) + MULT16_16(QCONST16(.5f,15),x2) - MULT16_16(QCONST16(0.16667f, 15),x3),15);\n   /* Just to make sure we don't have rounding problems */\n   interp[2] = Q15_ONE-interp[0]-interp[1]-interp[3];\n   if (interp[2]<32767)\n      interp[2]+=1;\n}\n#else\nstatic void cubic_coef(spx_word16_t frac, spx_word16_t interp[4])\n{\n   /* Compute interpolation coefficients. I'm not sure whether this corresponds to cubic interpolation\n   but I know it's MMSE-optimal on a sinc */\n   interp[0] =  -0.16667f*frac + 0.16667f*frac*frac*frac;\n   interp[1] = frac + 0.5f*frac*frac - 0.5f*frac*frac*frac;\n   /*interp[2] = 1.f - 0.5f*frac - frac*frac + 0.5f*frac*frac*frac;*/\n   interp[3] = -0.33333f*frac + 0.5f*frac*frac - 0.16667f*frac*frac*frac;\n   /* Just to make sure we don't have rounding problems */\n   interp[2] = 1.-interp[0]-interp[1]-interp[3];\n}\n#endif\n\nstatic int resampler_basic_direct_single(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_word16_t *in, spx_uint32_t *in_len, spx_word16_t *out, spx_uint32_t *out_len)\n{\n   const int N = st->filt_len;\n   int out_sample = 0;\n   int last_sample = st->last_sample[channel_index];\n   spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index];\n   const spx_word16_t *sinc_table = st->sinc_table;\n   const int out_stride = st->out_stride;\n   const int int_advance = st->int_advance;\n   const int frac_advance = st->frac_advance;\n   const spx_uint32_t den_rate = st->den_rate;\n   spx_word32_t sum;\n\n   while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len))\n   {\n      const spx_word16_t *sinct = & sinc_table[samp_frac_num*N];\n      const spx_word16_t *iptr = & in[last_sample];\n\n#ifndef OVERRIDE_INNER_PRODUCT_SINGLE\n      int j;\n      sum = 0;\n      for(j=0;j<N;j++) sum += MULT16_16(sinct[j], iptr[j]);\n\n/*    This code is slower on most DSPs which have only 2 accumulators.\n      Plus this this forces truncation to 32 bits and you lose the HW guard bits.\n      I think we can trust the compiler and let it vectorize and/or unroll itself.\n      spx_word32_t accum[4] = {0,0,0,0};\n      for(j=0;j<N;j+=4) {\n        accum[0] += MULT16_16(sinct[j], iptr[j]);\n        accum[1] += MULT16_16(sinct[j+1], iptr[j+1]);\n        accum[2] += MULT16_16(sinct[j+2], iptr[j+2]);\n        accum[3] += MULT16_16(sinct[j+3], iptr[j+3]);\n      }\n      sum = accum[0] + accum[1] + accum[2] + accum[3];\n*/\n      sum = SATURATE32PSHR(sum, 15, 32767);\n#else\n      sum = inner_product_single(sinct, iptr, N);\n#endif\n\n      out[out_stride * out_sample++] = sum;\n      last_sample += int_advance;\n      samp_frac_num += frac_advance;\n      if (samp_frac_num >= den_rate)\n      {\n         samp_frac_num -= den_rate;\n         last_sample++;\n      }\n   }\n\n   st->last_sample[channel_index] = last_sample;\n   st->samp_frac_num[channel_index] = samp_frac_num;\n   return out_sample;\n}\n\n#ifdef FIXED_POINT\n#else\n/* This is the same as the previous function, except with a double-precision accumulator */\nstatic int resampler_basic_direct_double(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_word16_t *in, spx_uint32_t *in_len, spx_word16_t *out, spx_uint32_t *out_len)\n{\n   const int N = st->filt_len;\n   int out_sample = 0;\n   int last_sample = st->last_sample[channel_index];\n   spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index];\n   const spx_word16_t *sinc_table = st->sinc_table;\n   const int out_stride = st->out_stride;\n   const int int_advance = st->int_advance;\n   const int frac_advance = st->frac_advance;\n   const spx_uint32_t den_rate = st->den_rate;\n   double sum;\n\n   while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len))\n   {\n      const spx_word16_t *sinct = & sinc_table[samp_frac_num*N];\n      const spx_word16_t *iptr = & in[last_sample];\n\n#ifndef OVERRIDE_INNER_PRODUCT_DOUBLE\n      int j;\n      double accum[4] = {0,0,0,0};\n\n      for(j=0;j<N;j+=4) {\n        accum[0] += sinct[j]*iptr[j];\n        accum[1] += sinct[j+1]*iptr[j+1];\n        accum[2] += sinct[j+2]*iptr[j+2];\n        accum[3] += sinct[j+3]*iptr[j+3];\n      }\n      sum = accum[0] + accum[1] + accum[2] + accum[3];\n#else\n      sum = inner_product_double(sinct, iptr, N);\n#endif\n\n      out[out_stride * out_sample++] = PSHR32(sum, 15);\n      last_sample += int_advance;\n      samp_frac_num += frac_advance;\n      if (samp_frac_num >= den_rate)\n      {\n         samp_frac_num -= den_rate;\n         last_sample++;\n      }\n   }\n\n   st->last_sample[channel_index] = last_sample;\n   st->samp_frac_num[channel_index] = samp_frac_num;\n   return out_sample;\n}\n#endif\n\nstatic int resampler_basic_interpolate_single(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_word16_t *in, spx_uint32_t *in_len, spx_word16_t *out, spx_uint32_t *out_len)\n{\n   const int N = st->filt_len;\n   int out_sample = 0;\n   int last_sample = st->last_sample[channel_index];\n   spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index];\n   const int out_stride = st->out_stride;\n   const int int_advance = st->int_advance;\n   const int frac_advance = st->frac_advance;\n   const spx_uint32_t den_rate = st->den_rate;\n   spx_word32_t sum;\n\n   while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len))\n   {\n      const spx_word16_t *iptr = & in[last_sample];\n\n      const int offset = samp_frac_num*st->oversample/st->den_rate;\n#ifdef FIXED_POINT\n      const spx_word16_t frac = PDIV32(SHL32((samp_frac_num*st->oversample) % st->den_rate,15),st->den_rate);\n#else\n      const spx_word16_t frac = ((float)((samp_frac_num*st->oversample) % st->den_rate))/st->den_rate;\n#endif\n      spx_word16_t interp[4];\n\n\n#ifndef OVERRIDE_INTERPOLATE_PRODUCT_SINGLE\n      int j;\n      spx_word32_t accum[4] = {0,0,0,0};\n\n      for(j=0;j<N;j++) {\n        const spx_word16_t curr_in=iptr[j];\n        accum[0] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset-2]);\n        accum[1] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset-1]);\n        accum[2] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset]);\n        accum[3] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset+1]);\n      }\n\n      cubic_coef(frac, interp);\n      sum = MULT16_32_Q15(interp[0],accum[0]) + MULT16_32_Q15(interp[1],accum[1]) + MULT16_32_Q15(interp[2],accum[2]) + MULT16_32_Q15(interp[3],accum[3]);\n      sum = SATURATE32PSHR(sum, 15, 32767);\n#else\n      cubic_coef(frac, interp);\n      sum = interpolate_product_single(iptr, st->sinc_table + st->oversample + 4 - offset - 2, N, st->oversample, interp);\n#endif\n\n      out[out_stride * out_sample++] = sum;\n      last_sample += int_advance;\n      samp_frac_num += frac_advance;\n      if (samp_frac_num >= den_rate)\n      {\n         samp_frac_num -= den_rate;\n         last_sample++;\n      }\n   }\n\n   st->last_sample[channel_index] = last_sample;\n   st->samp_frac_num[channel_index] = samp_frac_num;\n   return out_sample;\n}\n\n#ifdef FIXED_POINT\n#else\n/* This is the same as the previous function, except with a double-precision accumulator */\nstatic int resampler_basic_interpolate_double(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_word16_t *in, spx_uint32_t *in_len, spx_word16_t *out, spx_uint32_t *out_len)\n{\n   const int N = st->filt_len;\n   int out_sample = 0;\n   int last_sample = st->last_sample[channel_index];\n   spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index];\n   const int out_stride = st->out_stride;\n   const int int_advance = st->int_advance;\n   const int frac_advance = st->frac_advance;\n   const spx_uint32_t den_rate = st->den_rate;\n   spx_word32_t sum;\n\n   while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len))\n   {\n      const spx_word16_t *iptr = & in[last_sample];\n\n      const int offset = samp_frac_num*st->oversample/st->den_rate;\n#ifdef FIXED_POINT\n      const spx_word16_t frac = PDIV32(SHL32((samp_frac_num*st->oversample) % st->den_rate,15),st->den_rate);\n#else\n      const spx_word16_t frac = ((float)((samp_frac_num*st->oversample) % st->den_rate))/st->den_rate;\n#endif\n      spx_word16_t interp[4];\n\n\n#ifndef OVERRIDE_INTERPOLATE_PRODUCT_DOUBLE\n      int j;\n      double accum[4] = {0,0,0,0};\n\n      for(j=0;j<N;j++) {\n        const double curr_in=iptr[j];\n        accum[0] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset-2]);\n        accum[1] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset-1]);\n        accum[2] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset]);\n        accum[3] += MULT16_16(curr_in,st->sinc_table[4+(j+1)*st->oversample-offset+1]);\n      }\n\n      cubic_coef(frac, interp);\n      sum = MULT16_32_Q15(interp[0],accum[0]) + MULT16_32_Q15(interp[1],accum[1]) + MULT16_32_Q15(interp[2],accum[2]) + MULT16_32_Q15(interp[3],accum[3]);\n#else\n      cubic_coef(frac, interp);\n      sum = interpolate_product_double(iptr, st->sinc_table + st->oversample + 4 - offset - 2, N, st->oversample, interp);\n#endif\n\n      out[out_stride * out_sample++] = PSHR32(sum,15);\n      last_sample += int_advance;\n      samp_frac_num += frac_advance;\n      if (samp_frac_num >= den_rate)\n      {\n         samp_frac_num -= den_rate;\n         last_sample++;\n      }\n   }\n\n   st->last_sample[channel_index] = last_sample;\n   st->samp_frac_num[channel_index] = samp_frac_num;\n   return out_sample;\n}\n#endif\n\n/* This resampler is used to produce zero output in situations where memory\n   for the filter could not be allocated.  The expected numbers of input and\n   output samples are still processed so that callers failing to check error\n   codes are not surprised, possibly getting into infinite loops. */\nstatic int resampler_basic_zero(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_word16_t *in, spx_uint32_t *in_len, spx_word16_t *out, spx_uint32_t *out_len)\n{\n   int out_sample = 0;\n   int last_sample = st->last_sample[channel_index];\n   spx_uint32_t samp_frac_num = st->samp_frac_num[channel_index];\n   const int out_stride = st->out_stride;\n   const int int_advance = st->int_advance;\n   const int frac_advance = st->frac_advance;\n   const spx_uint32_t den_rate = st->den_rate;\n\n   (void)in;\n   while (!(last_sample >= (spx_int32_t)*in_len || out_sample >= (spx_int32_t)*out_len))\n   {\n      out[out_stride * out_sample++] = 0;\n      last_sample += int_advance;\n      samp_frac_num += frac_advance;\n      if (samp_frac_num >= den_rate)\n      {\n         samp_frac_num -= den_rate;\n         last_sample++;\n      }\n   }\n\n   st->last_sample[channel_index] = last_sample;\n   st->samp_frac_num[channel_index] = samp_frac_num;\n   return out_sample;\n}\n\nstatic int multiply_frac(spx_uint32_t *result, spx_uint32_t value, spx_uint32_t num, spx_uint32_t den)\n{\n   spx_uint32_t major = value / den;\n   spx_uint32_t remain = value % den;\n   /* TODO: Could use 64 bits operation to check for overflow. But only guaranteed in C99+ */\n   if (remain > UINT32_MAX / num || major > UINT32_MAX / num\n       || major * num > UINT32_MAX - remain * num / den)\n      return RESAMPLER_ERR_OVERFLOW;\n   *result = remain * num / den + major * num;\n   return RESAMPLER_ERR_SUCCESS;\n}\n\nstatic int update_filter(SpeexResamplerState *st)\n{\n   spx_uint32_t old_length = st->filt_len;\n   spx_uint32_t old_alloc_size = st->mem_alloc_size;\n   int use_direct;\n   spx_uint32_t min_sinc_table_length;\n   spx_uint32_t min_alloc_size;\n\n   st->int_advance = st->num_rate/st->den_rate;\n   st->frac_advance = st->num_rate%st->den_rate;\n   st->oversample = quality_map[st->quality].oversample;\n   st->filt_len = quality_map[st->quality].base_length;\n\n   if (st->num_rate > st->den_rate)\n   {\n      /* down-sampling */\n      st->cutoff = quality_map[st->quality].downsample_bandwidth * st->den_rate / st->num_rate;\n      if (multiply_frac(&st->filt_len,st->filt_len,st->num_rate,st->den_rate) != RESAMPLER_ERR_SUCCESS)\n         goto fail;\n      /* Round up to make sure we have a multiple of 8 for SSE */\n      st->filt_len = ((st->filt_len-1)&(~0x7))+8;\n      if (2*st->den_rate < st->num_rate)\n         st->oversample >>= 1;\n      if (4*st->den_rate < st->num_rate)\n         st->oversample >>= 1;\n      if (8*st->den_rate < st->num_rate)\n         st->oversample >>= 1;\n      if (16*st->den_rate < st->num_rate)\n         st->oversample >>= 1;\n      if (st->oversample < 1)\n         st->oversample = 1;\n   } else {\n      /* up-sampling */\n      st->cutoff = quality_map[st->quality].upsample_bandwidth;\n   }\n\n#ifdef RESAMPLE_FULL_SINC_TABLE\n   use_direct = 1;\n   if (INT_MAX/sizeof(spx_word16_t)/st->den_rate < st->filt_len)\n      goto fail;\n#else\n   /* Choose the resampling type that requires the least amount of memory */\n   use_direct = st->filt_len*st->den_rate <= st->filt_len*st->oversample+8\n                && INT_MAX/sizeof(spx_word16_t)/st->den_rate >= st->filt_len;\n#endif\n   if (use_direct)\n   {\n      min_sinc_table_length = st->filt_len*st->den_rate;\n   } else {\n      if ((INT_MAX/sizeof(spx_word16_t)-8)/st->oversample < st->filt_len)\n         goto fail;\n\n      min_sinc_table_length = st->filt_len*st->oversample+8;\n   }\n   if (st->sinc_table_length < min_sinc_table_length)\n   {\n      spx_word16_t *sinc_table = (spx_word16_t *)speex_realloc(st->sinc_table,min_sinc_table_length*sizeof(spx_word16_t));\n      if (!sinc_table)\n         goto fail;\n\n      st->sinc_table = sinc_table;\n      st->sinc_table_length = min_sinc_table_length;\n   }\n   if (use_direct)\n   {\n      spx_uint32_t i;\n      for (i=0;i<st->den_rate;i++)\n      {\n         spx_uint32_t j;\n         for (j=0;j<st->filt_len;j++)\n         {\n            st->sinc_table[i*st->filt_len+j] = sinc(st->cutoff,((j-(spx_int32_t)st->filt_len/2+1)-((float)i)/st->den_rate), st->filt_len, quality_map[st->quality].window_func);\n         }\n      }\n#ifdef FIXED_POINT\n      st->resampler_ptr = resampler_basic_direct_single;\n#else\n      if (st->quality>8)\n         st->resampler_ptr = resampler_basic_direct_double;\n      else\n         st->resampler_ptr = resampler_basic_direct_single;\n#endif\n      /*fprintf (stderr, \"resampler uses direct sinc table and normalised cutoff %f\\n\", cutoff);*/\n   } else {\n      spx_int32_t i;\n      for (i=-4;i<(spx_int32_t)(st->oversample*st->filt_len+4);i++)\n         st->sinc_table[i+4] = sinc(st->cutoff,(i/(float)st->oversample - st->filt_len/2), st->filt_len, quality_map[st->quality].window_func);\n#ifdef FIXED_POINT\n      st->resampler_ptr = resampler_basic_interpolate_single;\n#else\n      if (st->quality>8)\n         st->resampler_ptr = resampler_basic_interpolate_double;\n      else\n         st->resampler_ptr = resampler_basic_interpolate_single;\n#endif\n      /*fprintf (stderr, \"resampler uses interpolated sinc table and normalised cutoff %f\\n\", cutoff);*/\n   }\n\n   /* Here's the place where we update the filter memory to take into account\n      the change in filter length. It's probably the messiest part of the code\n      due to handling of lots of corner cases. */\n\n   /* Adding buffer_size to filt_len won't overflow here because filt_len\n      could be multiplied by sizeof(spx_word16_t) above. */\n   min_alloc_size = st->filt_len-1 + st->buffer_size;\n   if (min_alloc_size > st->mem_alloc_size)\n   {\n      spx_word16_t *mem;\n      if (INT_MAX/sizeof(spx_word16_t)/st->nb_channels < min_alloc_size)\n          goto fail;\n      else if (!(mem = (spx_word16_t*)speex_realloc(st->mem, st->nb_channels*min_alloc_size * sizeof(*mem))))\n          goto fail;\n\n      st->mem = mem;\n      st->mem_alloc_size = min_alloc_size;\n   }\n   if (!st->started)\n   {\n      spx_uint32_t i;\n      for (i=0;i<st->nb_channels*st->mem_alloc_size;i++)\n         st->mem[i] = 0;\n      /*speex_warning(\"reinit filter\");*/\n   } else if (st->filt_len > old_length)\n   {\n      spx_uint32_t i;\n      /* Increase the filter length */\n      /*speex_warning(\"increase filter size\");*/\n      for (i=st->nb_channels;i--;)\n      {\n         spx_uint32_t j;\n         spx_uint32_t olen = old_length;\n         /*if (st->magic_samples[i])*/\n         {\n            /* Try and remove the magic samples as if nothing had happened */\n\n            /* FIXME: This is wrong but for now we need it to avoid going over the array bounds */\n            olen = old_length + 2*st->magic_samples[i];\n            for (j=old_length-1+st->magic_samples[i];j--;)\n               st->mem[i*st->mem_alloc_size+j+st->magic_samples[i]] = st->mem[i*old_alloc_size+j];\n            for (j=0;j<st->magic_samples[i];j++)\n               st->mem[i*st->mem_alloc_size+j] = 0;\n            st->magic_samples[i] = 0;\n         }\n         if (st->filt_len > olen)\n         {\n            /* If the new filter length is still bigger than the \"augmented\" length */\n            /* Copy data going backward */\n            for (j=0;j<olen-1;j++)\n               st->mem[i*st->mem_alloc_size+(st->filt_len-2-j)] = st->mem[i*st->mem_alloc_size+(olen-2-j)];\n            /* Then put zeros for lack of anything better */\n            for (;j<st->filt_len-1;j++)\n               st->mem[i*st->mem_alloc_size+(st->filt_len-2-j)] = 0;\n            /* Adjust last_sample */\n            st->last_sample[i] += (st->filt_len - olen)/2;\n         } else {\n            /* Put back some of the magic! */\n            st->magic_samples[i] = (olen - st->filt_len)/2;\n            for (j=0;j<st->filt_len-1+st->magic_samples[i];j++)\n               st->mem[i*st->mem_alloc_size+j] = st->mem[i*st->mem_alloc_size+j+st->magic_samples[i]];\n         }\n      }\n   } else if (st->filt_len < old_length)\n   {\n      spx_uint32_t i;\n      /* Reduce filter length, this a bit tricky. We need to store some of the memory as \"magic\"\n         samples so they can be used directly as input the next time(s) */\n      for (i=0;i<st->nb_channels;i++)\n      {\n         spx_uint32_t j;\n         spx_uint32_t old_magic = st->magic_samples[i];\n         st->magic_samples[i] = (old_length - st->filt_len)/2;\n         /* We must copy some of the memory that's no longer used */\n         /* Copy data going backward */\n         for (j=0;j<st->filt_len-1+st->magic_samples[i]+old_magic;j++)\n            st->mem[i*st->mem_alloc_size+j] = st->mem[i*st->mem_alloc_size+j+st->magic_samples[i]];\n         st->magic_samples[i] += old_magic;\n      }\n   }\n   return RESAMPLER_ERR_SUCCESS;\n\nfail:\n   st->resampler_ptr = resampler_basic_zero;\n   /* st->mem may still contain consumed input samples for the filter.\n      Restore filt_len so that filt_len - 1 still points to the position after\n      the last of these samples. */\n   st->filt_len = old_length;\n   return RESAMPLER_ERR_ALLOC_FAILED;\n}\n\nEXPORT SpeexResamplerState *speex_resampler_init(spx_uint32_t nb_channels, spx_uint32_t in_rate, spx_uint32_t out_rate, int quality, int *err)\n{\n   return speex_resampler_init_frac(nb_channels, in_rate, out_rate, in_rate, out_rate, quality, err);\n}\n\nEXPORT SpeexResamplerState *speex_resampler_init_frac(spx_uint32_t nb_channels, spx_uint32_t ratio_num, spx_uint32_t ratio_den, spx_uint32_t in_rate, spx_uint32_t out_rate, int quality, int *err)\n{\n   SpeexResamplerState *st;\n   int filter_err;\n\n   if (nb_channels == 0 || ratio_num == 0 || ratio_den == 0 || quality > 10 || quality < 0)\n   {\n      if (err)\n         *err = RESAMPLER_ERR_INVALID_ARG;\n      return NULL;\n   }\n   st = (SpeexResamplerState *)speex_alloc(sizeof(SpeexResamplerState));\n   if (!st)\n   {\n      if (err)\n         *err = RESAMPLER_ERR_ALLOC_FAILED;\n      return NULL;\n   }\n   st->initialised = 0;\n   st->started = 0;\n   st->in_rate = 0;\n   st->out_rate = 0;\n   st->num_rate = 0;\n   st->den_rate = 0;\n   st->quality = -1;\n   st->sinc_table_length = 0;\n   st->mem_alloc_size = 0;\n   st->filt_len = 0;\n   st->mem = 0;\n   st->resampler_ptr = 0;\n\n   st->cutoff = 1.f;\n   st->nb_channels = nb_channels;\n   st->in_stride = 1;\n   st->out_stride = 1;\n\n   st->buffer_size = 160;\n\n   /* Per channel data */\n   if (!(st->last_sample = (spx_int32_t*)speex_alloc(nb_channels*sizeof(spx_int32_t))))\n      goto fail;\n   if (!(st->magic_samples = (spx_uint32_t*)speex_alloc(nb_channels*sizeof(spx_uint32_t))))\n      goto fail;\n   if (!(st->samp_frac_num = (spx_uint32_t*)speex_alloc(nb_channels*sizeof(spx_uint32_t))))\n      goto fail;\n\n   speex_resampler_set_quality(st, quality);\n   speex_resampler_set_rate_frac(st, ratio_num, ratio_den, in_rate, out_rate);\n\n   filter_err = update_filter(st);\n   if (filter_err == RESAMPLER_ERR_SUCCESS)\n   {\n      st->initialised = 1;\n   } else {\n      speex_resampler_destroy(st);\n      st = NULL;\n   }\n   if (err)\n      *err = filter_err;\n\n   return st;\n\nfail:\n   if (err)\n      *err = RESAMPLER_ERR_ALLOC_FAILED;\n   speex_resampler_destroy(st);\n   return NULL;\n}\n\nEXPORT void speex_resampler_destroy(SpeexResamplerState *st)\n{\n   speex_free(st->mem);\n   speex_free(st->sinc_table);\n   speex_free(st->last_sample);\n   speex_free(st->magic_samples);\n   speex_free(st->samp_frac_num);\n   speex_free(st);\n}\n\nstatic int speex_resampler_process_native(SpeexResamplerState *st, spx_uint32_t channel_index, spx_uint32_t *in_len, spx_word16_t *out, spx_uint32_t *out_len)\n{\n   int j=0;\n   const int N = st->filt_len;\n   int out_sample = 0;\n   spx_word16_t *mem = st->mem + channel_index * st->mem_alloc_size;\n   spx_uint32_t ilen;\n\n   st->started = 1;\n\n   /* Call the right resampler through the function ptr */\n   out_sample = st->resampler_ptr(st, channel_index, mem, in_len, out, out_len);\n\n   if (st->last_sample[channel_index] < (spx_int32_t)*in_len)\n      *in_len = st->last_sample[channel_index];\n   *out_len = out_sample;\n   st->last_sample[channel_index] -= *in_len;\n\n   ilen = *in_len;\n\n   for(j=0;j<N-1;++j)\n     mem[j] = mem[j+ilen];\n\n   return RESAMPLER_ERR_SUCCESS;\n}\n\nstatic int speex_resampler_magic(SpeexResamplerState *st, spx_uint32_t channel_index, spx_word16_t **out, spx_uint32_t out_len) {\n   spx_uint32_t tmp_in_len = st->magic_samples[channel_index];\n   spx_word16_t *mem = st->mem + channel_index * st->mem_alloc_size;\n   const int N = st->filt_len;\n\n   speex_resampler_process_native(st, channel_index, &tmp_in_len, *out, &out_len);\n\n   st->magic_samples[channel_index] -= tmp_in_len;\n\n   /* If we couldn't process all \"magic\" input samples, save the rest for next time */\n   if (st->magic_samples[channel_index])\n   {\n      spx_uint32_t i;\n      for (i=0;i<st->magic_samples[channel_index];i++)\n         mem[N-1+i]=mem[N-1+i+tmp_in_len];\n   }\n   *out += out_len*st->out_stride;\n   return out_len;\n}\n\n#ifdef FIXED_POINT\nEXPORT int speex_resampler_process_int(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_int16_t *in, spx_uint32_t *in_len, spx_int16_t *out, spx_uint32_t *out_len)\n#else\nEXPORT int speex_resampler_process_float(SpeexResamplerState *st, spx_uint32_t channel_index, const float *in, spx_uint32_t *in_len, float *out, spx_uint32_t *out_len)\n#endif\n{\n   spx_uint32_t j;\n   spx_uint32_t ilen = *in_len;\n   spx_uint32_t olen = *out_len;\n   spx_word16_t *x = st->mem + channel_index * st->mem_alloc_size;\n   const int filt_offs = st->filt_len - 1;\n   const spx_uint32_t xlen = st->mem_alloc_size - filt_offs;\n   const int istride = st->in_stride;\n\n   if (st->magic_samples[channel_index])\n      olen -= speex_resampler_magic(st, channel_index, &out, olen);\n   if (! st->magic_samples[channel_index]) {\n      while (ilen && olen) {\n        spx_uint32_t ichunk = (ilen > xlen) ? xlen : ilen;\n        spx_uint32_t ochunk = olen;\n\n        if (in) {\n           for(j=0;j<ichunk;++j)\n              x[j+filt_offs]=in[j*istride];\n        } else {\n          for(j=0;j<ichunk;++j)\n            x[j+filt_offs]=0;\n        }\n        speex_resampler_process_native(st, channel_index, &ichunk, out, &ochunk);\n        ilen -= ichunk;\n        olen -= ochunk;\n        out += ochunk * st->out_stride;\n        if (in)\n           in += ichunk * istride;\n      }\n   }\n   *in_len -= ilen;\n   *out_len -= olen;\n   return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS;\n}\n\n#ifdef FIXED_POINT\nEXPORT int speex_resampler_process_float(SpeexResamplerState *st, spx_uint32_t channel_index, const float *in, spx_uint32_t *in_len, float *out, spx_uint32_t *out_len)\n#else\nEXPORT int speex_resampler_process_int(SpeexResamplerState *st, spx_uint32_t channel_index, const spx_int16_t *in, spx_uint32_t *in_len, spx_int16_t *out, spx_uint32_t *out_len)\n#endif\n{\n   spx_uint32_t j;\n   const int istride_save = st->in_stride;\n   const int ostride_save = st->out_stride;\n   spx_uint32_t ilen = *in_len;\n   spx_uint32_t olen = *out_len;\n   spx_word16_t *x = st->mem + channel_index * st->mem_alloc_size;\n   const spx_uint32_t xlen = st->mem_alloc_size - (st->filt_len - 1);\n#ifdef VAR_ARRAYS\n   const unsigned int ylen = (olen < FIXED_STACK_ALLOC) ? olen : FIXED_STACK_ALLOC;\n   spx_word16_t ystack[ylen];\n#else\n   const unsigned int ylen = FIXED_STACK_ALLOC;\n   spx_word16_t ystack[FIXED_STACK_ALLOC];\n#endif\n\n   st->out_stride = 1;\n\n   while (ilen && olen) {\n     spx_word16_t *y = ystack;\n     spx_uint32_t ichunk = (ilen > xlen) ? xlen : ilen;\n     spx_uint32_t ochunk = (olen > ylen) ? ylen : olen;\n     spx_uint32_t omagic = 0;\n\n     if (st->magic_samples[channel_index]) {\n       omagic = speex_resampler_magic(st, channel_index, &y, ochunk);\n       ochunk -= omagic;\n       olen -= omagic;\n     }\n     if (! st->magic_samples[channel_index]) {\n       if (in) {\n         for(j=0;j<ichunk;++j)\n#ifdef FIXED_POINT\n           x[j+st->filt_len-1]=WORD2INT(in[j*istride_save]);\n#else\n           x[j+st->filt_len-1]=in[j*istride_save];\n#endif\n       } else {\n         for(j=0;j<ichunk;++j)\n           x[j+st->filt_len-1]=0;\n       }\n\n       speex_resampler_process_native(st, channel_index, &ichunk, y, &ochunk);\n     } else {\n       ichunk = 0;\n       ochunk = 0;\n     }\n\n     for (j=0;j<ochunk+omagic;++j)\n#ifdef FIXED_POINT\n        out[j*ostride_save] = ystack[j];\n#else\n        out[j*ostride_save] = WORD2INT(ystack[j]);\n#endif\n\n     ilen -= ichunk;\n     olen -= ochunk;\n     out += (ochunk+omagic) * ostride_save;\n     if (in)\n       in += ichunk * istride_save;\n   }\n   st->out_stride = ostride_save;\n   *in_len -= ilen;\n   *out_len -= olen;\n\n   return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS;\n}\n\nEXPORT int speex_resampler_process_interleaved_float(SpeexResamplerState *st, const float *in, spx_uint32_t *in_len, float *out, spx_uint32_t *out_len)\n{\n   spx_uint32_t i;\n   int istride_save, ostride_save;\n   spx_uint32_t bak_out_len = *out_len;\n   spx_uint32_t bak_in_len = *in_len;\n   istride_save = st->in_stride;\n   ostride_save = st->out_stride;\n   st->in_stride = st->out_stride = st->nb_channels;\n   for (i=0;i<st->nb_channels;i++)\n   {\n      *out_len = bak_out_len;\n      *in_len = bak_in_len;\n      if (in != NULL)\n         speex_resampler_process_float(st, i, in+i, in_len, out+i, out_len);\n      else\n         speex_resampler_process_float(st, i, NULL, in_len, out+i, out_len);\n   }\n   st->in_stride = istride_save;\n   st->out_stride = ostride_save;\n   return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS;\n}\n\nEXPORT int speex_resampler_process_interleaved_int(SpeexResamplerState *st, const spx_int16_t *in, spx_uint32_t *in_len, spx_int16_t *out, spx_uint32_t *out_len)\n{\n   spx_uint32_t i;\n   int istride_save, ostride_save;\n   spx_uint32_t bak_out_len = *out_len;\n   spx_uint32_t bak_in_len = *in_len;\n   istride_save = st->in_stride;\n   ostride_save = st->out_stride;\n   st->in_stride = st->out_stride = st->nb_channels;\n   for (i=0;i<st->nb_channels;i++)\n   {\n      *out_len = bak_out_len;\n      *in_len = bak_in_len;\n      if (in != NULL)\n         speex_resampler_process_int(st, i, in+i, in_len, out+i, out_len);\n      else\n         speex_resampler_process_int(st, i, NULL, in_len, out+i, out_len);\n   }\n   st->in_stride = istride_save;\n   st->out_stride = ostride_save;\n   return st->resampler_ptr == resampler_basic_zero ? RESAMPLER_ERR_ALLOC_FAILED : RESAMPLER_ERR_SUCCESS;\n}\n\nEXPORT int speex_resampler_set_rate(SpeexResamplerState *st, spx_uint32_t in_rate, spx_uint32_t out_rate)\n{\n   return speex_resampler_set_rate_frac(st, in_rate, out_rate, in_rate, out_rate);\n}\n\nEXPORT void speex_resampler_get_rate(SpeexResamplerState *st, spx_uint32_t *in_rate, spx_uint32_t *out_rate)\n{\n   *in_rate = st->in_rate;\n   *out_rate = st->out_rate;\n}\n\nstatic inline spx_uint32_t compute_gcd(spx_uint32_t a, spx_uint32_t b)\n{\n   while (b != 0)\n   {\n      spx_uint32_t temp = a;\n\n      a = b;\n      b = temp % b;\n   }\n   return a;\n}\n\nEXPORT int speex_resampler_set_rate_frac(SpeexResamplerState *st, spx_uint32_t ratio_num, spx_uint32_t ratio_den, spx_uint32_t in_rate, spx_uint32_t out_rate)\n{\n   spx_uint32_t fact;\n   spx_uint32_t old_den;\n   spx_uint32_t i;\n\n   if (ratio_num == 0 || ratio_den == 0)\n      return RESAMPLER_ERR_INVALID_ARG;\n\n   if (st->in_rate == in_rate && st->out_rate == out_rate && st->num_rate == ratio_num && st->den_rate == ratio_den)\n      return RESAMPLER_ERR_SUCCESS;\n\n   old_den = st->den_rate;\n   st->in_rate = in_rate;\n   st->out_rate = out_rate;\n   st->num_rate = ratio_num;\n   st->den_rate = ratio_den;\n\n   fact = compute_gcd(st->num_rate, st->den_rate);\n\n   st->num_rate /= fact;\n   st->den_rate /= fact;\n\n   if (old_den > 0)\n   {\n      for (i=0;i<st->nb_channels;i++)\n      {\n         if (multiply_frac(&st->samp_frac_num[i],st->samp_frac_num[i],st->den_rate,old_den) != RESAMPLER_ERR_SUCCESS)\n            return RESAMPLER_ERR_OVERFLOW;\n         /* Safety net */\n         if (st->samp_frac_num[i] >= st->den_rate)\n            st->samp_frac_num[i] = st->den_rate-1;\n      }\n   }\n\n   if (st->initialised)\n      return update_filter(st);\n   return RESAMPLER_ERR_SUCCESS;\n}\n\nEXPORT void speex_resampler_get_ratio(SpeexResamplerState *st, spx_uint32_t *ratio_num, spx_uint32_t *ratio_den)\n{\n   *ratio_num = st->num_rate;\n   *ratio_den = st->den_rate;\n}\n\nEXPORT int speex_resampler_set_quality(SpeexResamplerState *st, int quality)\n{\n   if (quality > 10 || quality < 0)\n      return RESAMPLER_ERR_INVALID_ARG;\n   if (st->quality == quality)\n      return RESAMPLER_ERR_SUCCESS;\n   st->quality = quality;\n   if (st->initialised)\n      return update_filter(st);\n   return RESAMPLER_ERR_SUCCESS;\n}\n\nEXPORT void speex_resampler_get_quality(SpeexResamplerState *st, int *quality)\n{\n   *quality = st->quality;\n}\n\nEXPORT void speex_resampler_set_input_stride(SpeexResamplerState *st, spx_uint32_t stride)\n{\n   st->in_stride = stride;\n}\n\nEXPORT void speex_resampler_get_input_stride(SpeexResamplerState *st, spx_uint32_t *stride)\n{\n   *stride = st->in_stride;\n}\n\nEXPORT void speex_resampler_set_output_stride(SpeexResamplerState *st, spx_uint32_t stride)\n{\n   st->out_stride = stride;\n}\n\nEXPORT void speex_resampler_get_output_stride(SpeexResamplerState *st, spx_uint32_t *stride)\n{\n   *stride = st->out_stride;\n}\n\nEXPORT int speex_resampler_get_input_latency(SpeexResamplerState *st)\n{\n  return st->filt_len / 2;\n}\n\nEXPORT int speex_resampler_get_output_latency(SpeexResamplerState *st)\n{\n  return ((st->filt_len / 2) * st->den_rate + (st->num_rate >> 1)) / st->num_rate;\n}\n\nEXPORT int speex_resampler_skip_zeros(SpeexResamplerState *st)\n{\n   spx_uint32_t i;\n   for (i=0;i<st->nb_channels;i++)\n      st->last_sample[i] = st->filt_len/2;\n   return RESAMPLER_ERR_SUCCESS;\n}\n\nEXPORT int speex_resampler_reset_mem(SpeexResamplerState *st)\n{\n   spx_uint32_t i;\n   for (i=0;i<st->nb_channels;i++)\n   {\n      st->last_sample[i] = 0;\n      st->magic_samples[i] = 0;\n      st->samp_frac_num[i] = 0;\n   }\n   for (i=0;i<st->nb_channels*(st->filt_len-1);i++)\n      st->mem[i] = 0;\n   return RESAMPLER_ERR_SUCCESS;\n}\n\nEXPORT const char *speex_resampler_strerror(int err)\n{\n   switch (err)\n   {\n      case RESAMPLER_ERR_SUCCESS:\n         return \"Success.\";\n      case RESAMPLER_ERR_ALLOC_FAILED:\n         return \"Memory allocation failed.\";\n      case RESAMPLER_ERR_BAD_STATE:\n         return \"Bad resampler state.\";\n      case RESAMPLER_ERR_INVALID_ARG:\n         return \"Invalid argument.\";\n      case RESAMPLER_ERR_PTR_OVERLAP:\n         return \"Input and output buffers overlap.\";\n      default:\n         return \"Unknown error. Bad error code or strange version mismatch.\";\n   }\n}\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/speex/speex_jitter.h",
    "content": "/* Copyright (C) 2002 Jean-Marc Valin */\n/**\n   @file speex_jitter.h\n   @brief Adaptive jitter buffer for Speex\n*/\n/*\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions\n   are met:\n\n   - Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n   - Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n   - Neither the name of the Xiph.org Foundation nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n   A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR\n   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n*/\n\n#ifndef SPEEX_JITTER_H\n#define SPEEX_JITTER_H\n/** @defgroup JitterBuffer JitterBuffer: Adaptive jitter buffer\n *  This is the jitter buffer that reorders UDP/RTP packets and adjusts the buffer size\n * to maintain good quality and low latency.\n *  @{\n */\n\n#include \"speexdsp_types.h\"\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n/** Generic adaptive jitter buffer state */\nstruct JitterBuffer_;\n\n/** Generic adaptive jitter buffer state */\ntypedef struct JitterBuffer_ JitterBuffer;\n\n/** Definition of an incoming packet */\ntypedef struct _JitterBufferPacket JitterBufferPacket;\n\n/** Definition of an incoming packet */\nstruct _JitterBufferPacket {\n   char        *data;       /**< Data bytes contained in the packet */\n   spx_uint32_t len;        /**< Length of the packet in bytes */\n   spx_uint32_t timestamp;  /**< Timestamp for the packet */\n   spx_uint32_t span;       /**< Time covered by the packet (same units as timestamp) */\n   spx_uint16_t sequence;   /**< RTP Sequence number if available (0 otherwise) */\n   spx_uint32_t user_data;  /**< Put whatever data you like here (it's ignored by the jitter buffer) */\n};\n\n/** Packet has been retrieved */\n#define JITTER_BUFFER_OK 0\n/** Packet is lost or is late */\n#define JITTER_BUFFER_MISSING 1\n/** A \"fake\" packet is meant to be inserted here to increase buffering */\n#define JITTER_BUFFER_INSERTION 2\n/** There was an error in the jitter buffer */\n#define JITTER_BUFFER_INTERNAL_ERROR -1\n/** Invalid argument */\n#define JITTER_BUFFER_BAD_ARGUMENT -2\n\n\n/** Set minimum amount of extra buffering required (margin) */\n#define JITTER_BUFFER_SET_MARGIN 0\n/** Get minimum amount of extra buffering required (margin) */\n#define JITTER_BUFFER_GET_MARGIN 1\n/* JITTER_BUFFER_SET_AVAILABLE_COUNT wouldn't make sense */\n\n/** Get the amount of available packets currently buffered */\n#define JITTER_BUFFER_GET_AVAILABLE_COUNT 3\n/** Included because of an early misspelling (will remove in next release) */\n#define JITTER_BUFFER_GET_AVALIABLE_COUNT 3\n\n/** Assign a function to destroy unused packet. When setting that, the jitter\n    buffer no longer copies packet data. */\n#define JITTER_BUFFER_SET_DESTROY_CALLBACK 4\n/**  */\n#define JITTER_BUFFER_GET_DESTROY_CALLBACK 5\n\n/** Tell the jitter buffer to only adjust the delay in multiples of the step parameter provided */\n#define JITTER_BUFFER_SET_DELAY_STEP 6\n/**  */\n#define JITTER_BUFFER_GET_DELAY_STEP 7\n\n/** Tell the jitter buffer to only do concealment in multiples of the size parameter provided */\n#define JITTER_BUFFER_SET_CONCEALMENT_SIZE 8\n#define JITTER_BUFFER_GET_CONCEALMENT_SIZE 9\n\n/** Absolute max amount of loss that can be tolerated regardless of the delay. Typical loss\n    should be half of that or less. */\n#define JITTER_BUFFER_SET_MAX_LATE_RATE 10\n#define JITTER_BUFFER_GET_MAX_LATE_RATE 11\n\n/** Equivalent cost of one percent late packet in timestamp units */\n#define JITTER_BUFFER_SET_LATE_COST 12\n#define JITTER_BUFFER_GET_LATE_COST 13\n\n#define JITTER_BUFFER_SET_LIMIT 101\n\n\n/** Initialises jitter buffer\n *\n * @param step_size Starting value for the size of concleanment packets and delay\n       adjustment steps. Can be changed at any time using JITTER_BUFFER_SET_DELAY_STEP\n       and JITTER_BUFFER_GET_CONCEALMENT_SIZE.\n * @return Newly created jitter buffer state\n */\nJitterBuffer *jitter_buffer_init(int step_size);\n\n/** Restores jitter buffer to its original state\n *\n * @param jitter Jitter buffer state\n */\nvoid jitter_buffer_reset(JitterBuffer *jitter);\n\n/** Destroys jitter buffer\n *\n * @param jitter Jitter buffer state\n */\nvoid jitter_buffer_destroy(JitterBuffer *jitter);\n\n/** Put one packet into the jitter buffer\n *\n * @param jitter Jitter buffer state\n * @param packet Incoming packet\n*/\nvoid jitter_buffer_put(JitterBuffer *jitter, const JitterBufferPacket *packet);\n\n/** Get one packet from the jitter buffer\n *\n * @param jitter Jitter buffer state\n * @param packet Returned packet\n * @param desired_span Number of samples (or units) we wish to get from the buffer (no guarantee)\n * @param current_timestamp Timestamp for the returned packet\n*/\nint jitter_buffer_get(JitterBuffer *jitter, JitterBufferPacket *packet, spx_int32_t desired_span, spx_int32_t *start_offset);\n\n/** Used right after jitter_buffer_get() to obtain another packet that would have the same timestamp.\n * This is mainly useful for media where a single \"frame\" can be split into several packets.\n *\n * @param jitter Jitter buffer state\n * @param packet Returned packet\n */\nint jitter_buffer_get_another(JitterBuffer *jitter, JitterBufferPacket *packet);\n\n/** Get pointer timestamp of jitter buffer\n *\n * @param jitter Jitter buffer state\n*/\nint jitter_buffer_get_pointer_timestamp(JitterBuffer *jitter);\n\n/** Advance by one tick\n *\n * @param jitter Jitter buffer state\n*/\nvoid jitter_buffer_tick(JitterBuffer *jitter);\n\n/** Telling the jitter buffer about the remaining data in the application buffer\n * @param jitter Jitter buffer state\n * @param rem Amount of data buffered by the application (timestamp units)\n */\nvoid jitter_buffer_remaining_span(JitterBuffer *jitter, spx_uint32_t rem);\n\n/** Used like the ioctl function to control the jitter buffer parameters\n *\n * @param jitter Jitter buffer state\n * @param request ioctl-type request (one of the JITTER_BUFFER_* macros)\n * @param ptr Data exchanged to-from function\n * @return 0 if no error, -1 if request in unknown\n*/\nint jitter_buffer_ctl(JitterBuffer *jitter, int request, void *ptr);\n\nint jitter_buffer_update_delay(JitterBuffer *jitter, JitterBufferPacket *packet, spx_int32_t *start_offset);\n\n/* @} */\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/speex/speex_resampler.h",
    "content": "/* Copyright (C) 2007 Jean-Marc Valin\n\n   File: speex_resampler.h\n   Resampling code\n\n   The design goals of this code are:\n      - Very fast algorithm\n      - Low memory requirement\n      - Good *perceptual* quality (and not best SNR)\n\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions are\n   met:\n\n   1. Redistributions of source code must retain the above copyright notice,\n   this list of conditions and the following disclaimer.\n\n   2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n   3. The name of the author may not be used to endorse or promote products\n   derived from this software without specific prior written permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n   IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n   DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,\n   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n   ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n   POSSIBILITY OF SUCH DAMAGE.\n*/\n\n\n#ifndef SPEEX_RESAMPLER_H\n#define SPEEX_RESAMPLER_H\n\n#ifdef OUTSIDE_SPEEX\n\n/********* WARNING: MENTAL SANITY ENDS HERE *************/\n\n/* If the resampler is defined outside of Speex, we change the symbol names so that\n   there won't be any clash if linking with Speex later on. */\n\n/* #define RANDOM_PREFIX your software name here */\n#ifndef RANDOM_PREFIX\n#error \"Please define RANDOM_PREFIX (above) to something specific to your project to prevent symbol name clashes\"\n#endif\n\n#define CAT_PREFIX2(a,b) a ## b\n#define CAT_PREFIX(a,b) CAT_PREFIX2(a, b)\n\n#define speex_resampler_init CAT_PREFIX(RANDOM_PREFIX,_resampler_init)\n#define speex_resampler_init_frac CAT_PREFIX(RANDOM_PREFIX,_resampler_init_frac)\n#define speex_resampler_destroy CAT_PREFIX(RANDOM_PREFIX,_resampler_destroy)\n#define speex_resampler_process_float CAT_PREFIX(RANDOM_PREFIX,_resampler_process_float)\n#define speex_resampler_process_int CAT_PREFIX(RANDOM_PREFIX,_resampler_process_int)\n#define speex_resampler_process_interleaved_float CAT_PREFIX(RANDOM_PREFIX,_resampler_process_interleaved_float)\n#define speex_resampler_process_interleaved_int CAT_PREFIX(RANDOM_PREFIX,_resampler_process_interleaved_int)\n#define speex_resampler_set_rate CAT_PREFIX(RANDOM_PREFIX,_resampler_set_rate)\n#define speex_resampler_get_rate CAT_PREFIX(RANDOM_PREFIX,_resampler_get_rate)\n#define speex_resampler_set_rate_frac CAT_PREFIX(RANDOM_PREFIX,_resampler_set_rate_frac)\n#define speex_resampler_get_ratio CAT_PREFIX(RANDOM_PREFIX,_resampler_get_ratio)\n#define speex_resampler_set_quality CAT_PREFIX(RANDOM_PREFIX,_resampler_set_quality)\n#define speex_resampler_get_quality CAT_PREFIX(RANDOM_PREFIX,_resampler_get_quality)\n#define speex_resampler_set_input_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_set_input_stride)\n#define speex_resampler_get_input_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_get_input_stride)\n#define speex_resampler_set_output_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_set_output_stride)\n#define speex_resampler_get_output_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_get_output_stride)\n#define speex_resampler_get_input_latency CAT_PREFIX(RANDOM_PREFIX,_resampler_get_input_latency)\n#define speex_resampler_get_output_latency CAT_PREFIX(RANDOM_PREFIX,_resampler_get_output_latency)\n#define speex_resampler_skip_zeros CAT_PREFIX(RANDOM_PREFIX,_resampler_skip_zeros)\n#define speex_resampler_reset_mem CAT_PREFIX(RANDOM_PREFIX,_resampler_reset_mem)\n#define speex_resampler_strerror CAT_PREFIX(RANDOM_PREFIX,_resampler_strerror)\n\n#define spx_int16_t short\n#define spx_int32_t int\n#define spx_uint16_t unsigned short\n#define spx_uint32_t unsigned int\n\n#define speex_assert(cond)\n\n#else /* OUTSIDE_SPEEX */\n\n#include \"speexdsp_types.h\"\n\n#endif /* OUTSIDE_SPEEX */\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#define SPEEX_RESAMPLER_QUALITY_MAX 10\n#define SPEEX_RESAMPLER_QUALITY_MIN 0\n#define SPEEX_RESAMPLER_QUALITY_DEFAULT 4\n#define SPEEX_RESAMPLER_QUALITY_VOIP 3\n#define SPEEX_RESAMPLER_QUALITY_DESKTOP 5\n\nenum {\n   RESAMPLER_ERR_SUCCESS         = 0,\n   RESAMPLER_ERR_ALLOC_FAILED    = 1,\n   RESAMPLER_ERR_BAD_STATE       = 2,\n   RESAMPLER_ERR_INVALID_ARG     = 3,\n   RESAMPLER_ERR_PTR_OVERLAP     = 4,\n   RESAMPLER_ERR_OVERFLOW        = 5,\n\n   RESAMPLER_ERR_MAX_ERROR\n};\n\nstruct SpeexResamplerState_;\ntypedef struct SpeexResamplerState_ SpeexResamplerState;\n\n/** Create a new resampler with integer input and output rates.\n * @param nb_channels Number of channels to be processed\n * @param in_rate Input sampling rate (integer number of Hz).\n * @param out_rate Output sampling rate (integer number of Hz).\n * @param quality Resampling quality between 0 and 10, where 0 has poor quality\n * and 10 has very high quality.\n * @return Newly created resampler state\n * @retval NULL Error: not enough memory\n */\nSpeexResamplerState *speex_resampler_init(spx_uint32_t nb_channels,\n                                          spx_uint32_t in_rate,\n                                          spx_uint32_t out_rate,\n                                          int quality,\n                                          int *err);\n\n/** Create a new resampler with fractional input/output rates. The sampling\n * rate ratio is an arbitrary rational number with both the numerator and\n * denominator being 32-bit integers.\n * @param nb_channels Number of channels to be processed\n * @param ratio_num Numerator of the sampling rate ratio\n * @param ratio_den Denominator of the sampling rate ratio\n * @param in_rate Input sampling rate rounded to the nearest integer (in Hz).\n * @param out_rate Output sampling rate rounded to the nearest integer (in Hz).\n * @param quality Resampling quality between 0 and 10, where 0 has poor quality\n * and 10 has very high quality.\n * @return Newly created resampler state\n * @retval NULL Error: not enough memory\n */\nSpeexResamplerState *speex_resampler_init_frac(spx_uint32_t nb_channels,\n                                               spx_uint32_t ratio_num,\n                                               spx_uint32_t ratio_den,\n                                               spx_uint32_t in_rate,\n                                               spx_uint32_t out_rate,\n                                               int quality,\n                                               int *err);\n\n/** Destroy a resampler state.\n * @param st Resampler state\n */\nvoid speex_resampler_destroy(SpeexResamplerState *st);\n\n/** Resample a float array. The input and output buffers must *not* overlap.\n * @param st Resampler state\n * @param channel_index Index of the channel to process for the multi-channel\n * base (0 otherwise)\n * @param in Input buffer\n * @param in_len Number of input samples in the input buffer. Returns the\n * number of samples processed\n * @param out Output buffer\n * @param out_len Size of the output buffer. Returns the number of samples written\n */\nint speex_resampler_process_float(SpeexResamplerState *st,\n                                   spx_uint32_t channel_index,\n                                   const float *in,\n                                   spx_uint32_t *in_len,\n                                   float *out,\n                                   spx_uint32_t *out_len);\n\n/** Resample an int array. The input and output buffers must *not* overlap.\n * @param st Resampler state\n * @param channel_index Index of the channel to process for the multi-channel\n * base (0 otherwise)\n * @param in Input buffer\n * @param in_len Number of input samples in the input buffer. Returns the number\n * of samples processed\n * @param out Output buffer\n * @param out_len Size of the output buffer. Returns the number of samples written\n */\nint speex_resampler_process_int(SpeexResamplerState *st,\n                                 spx_uint32_t channel_index,\n                                 const spx_int16_t *in,\n                                 spx_uint32_t *in_len,\n                                 spx_int16_t *out,\n                                 spx_uint32_t *out_len);\n\n/** Resample an interleaved float array. The input and output buffers must *not* overlap.\n * @param st Resampler state\n * @param in Input buffer\n * @param in_len Number of input samples in the input buffer. Returns the number\n * of samples processed. This is all per-channel.\n * @param out Output buffer\n * @param out_len Size of the output buffer. Returns the number of samples written.\n * This is all per-channel.\n */\nint speex_resampler_process_interleaved_float(SpeexResamplerState *st,\n                                               const float *in,\n                                               spx_uint32_t *in_len,\n                                               float *out,\n                                               spx_uint32_t *out_len);\n\n/** Resample an interleaved int array. The input and output buffers must *not* overlap.\n * @param st Resampler state\n * @param in Input buffer\n * @param in_len Number of input samples in the input buffer. Returns the number\n * of samples processed. This is all per-channel.\n * @param out Output buffer\n * @param out_len Size of the output buffer. Returns the number of samples written.\n * This is all per-channel.\n */\nint speex_resampler_process_interleaved_int(SpeexResamplerState *st,\n                                             const spx_int16_t *in,\n                                             spx_uint32_t *in_len,\n                                             spx_int16_t *out,\n                                             spx_uint32_t *out_len);\n\n/** Set (change) the input/output sampling rates (integer value).\n * @param st Resampler state\n * @param in_rate Input sampling rate (integer number of Hz).\n * @param out_rate Output sampling rate (integer number of Hz).\n */\nint speex_resampler_set_rate(SpeexResamplerState *st,\n                              spx_uint32_t in_rate,\n                              spx_uint32_t out_rate);\n\n/** Get the current input/output sampling rates (integer value).\n * @param st Resampler state\n * @param in_rate Input sampling rate (integer number of Hz) copied.\n * @param out_rate Output sampling rate (integer number of Hz) copied.\n */\nvoid speex_resampler_get_rate(SpeexResamplerState *st,\n                              spx_uint32_t *in_rate,\n                              spx_uint32_t *out_rate);\n\n/** Set (change) the input/output sampling rates and resampling ratio\n * (fractional values in Hz supported).\n * @param st Resampler state\n * @param ratio_num Numerator of the sampling rate ratio\n * @param ratio_den Denominator of the sampling rate ratio\n * @param in_rate Input sampling rate rounded to the nearest integer (in Hz).\n * @param out_rate Output sampling rate rounded to the nearest integer (in Hz).\n */\nint speex_resampler_set_rate_frac(SpeexResamplerState *st,\n                                   spx_uint32_t ratio_num,\n                                   spx_uint32_t ratio_den,\n                                   spx_uint32_t in_rate,\n                                   spx_uint32_t out_rate);\n\n/** Get the current resampling ratio. This will be reduced to the least\n * common denominator.\n * @param st Resampler state\n * @param ratio_num Numerator of the sampling rate ratio copied\n * @param ratio_den Denominator of the sampling rate ratio copied\n */\nvoid speex_resampler_get_ratio(SpeexResamplerState *st,\n                               spx_uint32_t *ratio_num,\n                               spx_uint32_t *ratio_den);\n\n/** Set (change) the conversion quality.\n * @param st Resampler state\n * @param quality Resampling quality between 0 and 10, where 0 has poor\n * quality and 10 has very high quality.\n */\nint speex_resampler_set_quality(SpeexResamplerState *st,\n                                 int quality);\n\n/** Get the conversion quality.\n * @param st Resampler state\n * @param quality Resampling quality between 0 and 10, where 0 has poor\n * quality and 10 has very high quality.\n */\nvoid speex_resampler_get_quality(SpeexResamplerState *st,\n                                 int *quality);\n\n/** Set (change) the input stride.\n * @param st Resampler state\n * @param stride Input stride\n */\nvoid speex_resampler_set_input_stride(SpeexResamplerState *st,\n                                      spx_uint32_t stride);\n\n/** Get the input stride.\n * @param st Resampler state\n * @param stride Input stride copied\n */\nvoid speex_resampler_get_input_stride(SpeexResamplerState *st,\n                                      spx_uint32_t *stride);\n\n/** Set (change) the output stride.\n * @param st Resampler state\n * @param stride Output stride\n */\nvoid speex_resampler_set_output_stride(SpeexResamplerState *st,\n                                      spx_uint32_t stride);\n\n/** Get the output stride.\n * @param st Resampler state copied\n * @param stride Output stride\n */\nvoid speex_resampler_get_output_stride(SpeexResamplerState *st,\n                                      spx_uint32_t *stride);\n\n/** Get the latency introduced by the resampler measured in input samples.\n * @param st Resampler state\n */\nint speex_resampler_get_input_latency(SpeexResamplerState *st);\n\n/** Get the latency introduced by the resampler measured in output samples.\n * @param st Resampler state\n */\nint speex_resampler_get_output_latency(SpeexResamplerState *st);\n\n/** Make sure that the first samples to go out of the resamplers don't have\n * leading zeros. This is only useful before starting to use a newly created\n * resampler. It is recommended to use that when resampling an audio file, as\n * it will generate a file with the same length. For real-time processing,\n * it is probably easier not to use this call (so that the output duration\n * is the same for the first frame).\n * @param st Resampler state\n */\nint speex_resampler_skip_zeros(SpeexResamplerState *st);\n\n/** Reset a resampler so a new (unrelated) stream can be processed.\n * @param st Resampler state\n */\nint speex_resampler_reset_mem(SpeexResamplerState *st);\n\n/** Returns the English meaning for an error code\n * @param err Error code\n * @return English string\n */\nconst char *speex_resampler_strerror(int err);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/speex/speexdsp_config_types.h",
    "content": "#ifndef __SPEEX_TYPES_H__\n#define __SPEEX_TYPES_H__\n\n#include <stdint.h>\n\ntypedef int16_t spx_int16_t;\ntypedef uint16_t spx_uint16_t;\ntypedef int32_t spx_int32_t;\ntypedef uint32_t spx_uint32_t;\n\n#endif\n\n"
  },
  {
    "path": "src/plugins/audiobridge-deps/speex/speexdsp_types.h",
    "content": "/* speexdsp_types.h taken from libogg */\n/********************************************************************\n *                                                                  *\n * THIS FILE IS PART OF THE OggVorbis SOFTWARE CODEC SOURCE CODE.   *\n * USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS     *\n * GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE *\n * IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING.       *\n *                                                                  *\n * THE OggVorbis SOURCE CODE IS (C) COPYRIGHT 1994-2002             *\n * by the Xiph.Org Foundation http://www.xiph.org/                  *\n *                                                                  *\n ********************************************************************\n\n function: #ifdef jail to whip a few platforms into the UNIX ideal.\n last mod: $Id: os_types.h 7524 2004-08-11 04:20:36Z conrad $\n\n ********************************************************************/\n/**\n   @file speexdsp_types.h\n   @brief Speex types\n*/\n#ifndef _SPEEX_TYPES_H\n#define _SPEEX_TYPES_H\n\n#if defined(_WIN32)\n\n#  if defined(__CYGWIN__)\n#    include <_G_config.h>\n     typedef _G_int32_t spx_int32_t;\n     typedef _G_uint32_t spx_uint32_t;\n     typedef _G_int16_t spx_int16_t;\n     typedef _G_uint16_t spx_uint16_t;\n#  elif defined(__MINGW32__)\n     typedef short spx_int16_t;\n     typedef unsigned short spx_uint16_t;\n     typedef int spx_int32_t;\n     typedef unsigned int spx_uint32_t;\n#  elif defined(__MWERKS__)\n     typedef int spx_int32_t;\n     typedef unsigned int spx_uint32_t;\n     typedef short spx_int16_t;\n     typedef unsigned short spx_uint16_t;\n#  else\n     /* MSVC/Borland */\n     typedef __int32 spx_int32_t;\n     typedef unsigned __int32 spx_uint32_t;\n     typedef __int16 spx_int16_t;\n     typedef unsigned __int16 spx_uint16_t;\n#  endif\n\n#elif defined(__MACOS__)\n\n#  include <sys/types.h>\n   typedef SInt16 spx_int16_t;\n   typedef UInt16 spx_uint16_t;\n   typedef SInt32 spx_int32_t;\n   typedef UInt32 spx_uint32_t;\n\n#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */\n\n#  include <sys/types.h>\n   typedef int16_t spx_int16_t;\n   typedef u_int16_t spx_uint16_t;\n   typedef int32_t spx_int32_t;\n   typedef u_int32_t spx_uint32_t;\n\n#elif defined(__BEOS__)\n\n   /* Be */\n#  include <inttypes.h>\n   typedef int16_t spx_int16_t;\n   typedef u_int16_t spx_uint16_t;\n   typedef int32_t spx_int32_t;\n   typedef u_int32_t spx_uint32_t;\n\n#elif defined (__EMX__)\n\n   /* OS/2 GCC */\n   typedef short spx_int16_t;\n   typedef unsigned short spx_uint16_t;\n   typedef int spx_int32_t;\n   typedef unsigned int spx_uint32_t;\n\n#elif defined (DJGPP)\n\n   /* DJGPP */\n   typedef short spx_int16_t;\n   typedef int spx_int32_t;\n   typedef unsigned int spx_uint32_t;\n\n#elif defined(R5900)\n\n   /* PS2 EE */\n   typedef int spx_int32_t;\n   typedef unsigned spx_uint32_t;\n   typedef short spx_int16_t;\n\n#elif defined(__SYMBIAN32__)\n\n   /* Symbian GCC */\n   typedef signed short spx_int16_t;\n   typedef unsigned short spx_uint16_t;\n   typedef signed int spx_int32_t;\n   typedef unsigned int spx_uint32_t;\n\n#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X)\n\n   typedef short spx_int16_t;\n   typedef unsigned short spx_uint16_t;\n   typedef long spx_int32_t;\n   typedef unsigned long spx_uint32_t;\n\n#elif defined(CONFIG_TI_C6X)\n\n   typedef short spx_int16_t;\n   typedef unsigned short spx_uint16_t;\n   typedef int spx_int32_t;\n   typedef unsigned int spx_uint32_t;\n\n#else\n\n#include \"speexdsp_config_types.h\"\n\n#endif\n\n#endif  /* _SPEEX_TYPES_H */\n"
  },
  {
    "path": "src/plugins/duktape/echotest.js",
    "content": "// This is a simple example of an echo test application built in JavaScript,\n// and conceived to be used in conjunction with the janus_duktape.c plugin.\n// Obviously, it must NOT be confused with the echotest.js in the html\n// folder, which contains the JavaScript code for the web demo instead...\n\n// Example details\nname = \"echotest.js\";\n\n// Let's add more info to errors\nError.prototype.toString = function () {\n\treturn this.name + ': ' + this.message + ' (at line ' + this.lineNumber + ')';\n};\n// Let's add a prefix to all console.log lines\nvar originalConsoleLog = console.log;\nconsole.log = function() {\n\targs = [];\n\targs.push('[\\x1b[36m' + name + '\\x1b[0m] ');\n\tfor(var i=0; i<arguments.length; i++) {\n\t\targs.push(arguments[i]);\n\t}\n\toriginalConsoleLog.apply(console, args);\n};\nconsole.log(\"Loading script...\");\n\n// We'll import our own hacky SDP parser, so we'll need the folder from the core\nvar folder = getModulesFolder();\nconsole.log('Modules folder:', folder);\n\n// To require external modules with Duktape, we need a modSearch function:\n// https://github.com/svaarala/duktape-wiki/blob/master/HowtoModules.md\nDuktape.modSearch = function(id) {\n\tconsole.log('Loading module:', id);\n\t// We read the file from the folder the core returned\n\tvar res = readFile(folder + '/' + id + '.js');\n\tif(typeof res === 'string') {\n\t\tconsole.log('Module loaded');\n\t\treturn res;\n\t}\n\tthrow new Error('Module not found: ' + id);\n}\n\n// Let's import our ugly SDP parser now\nvar sdpUtils = require(\"janus-sdp\");\n\n// State and properties\nvar sessions = {};\nvar tasks = [];\n\n// Just for fun, let's override the plugin info with our own\nfunction getVersion() {\n\treturn 12;\n}\nfunction getVersionString() {\n\treturn \"0.0.12\";\n}\nfunction getDescription() {\n\treturn \"This is echotest.js, a JavaScript/Duktape based clone of janus.plugin.echotest\";\n}\nfunction getName() {\n\treturn \"JavaScript based EchoTest\";\n}\nfunction getAuthor() {\n\treturn \"Lorenzo Miniero\";\n}\nfunction getPackage() {\n\treturn \"janus.plugin.echojs\";\n}\n\n// Methods\nfunction init(config) {\n\t// This is where we initialize the plugin, for static properties\n\tconsole.log(\"Initializing...\")\n\tif(config) {\n\t\tconsole.log(\"Configuration file provided (\" + config + \"), but we don't need it\");\n\t}\n\tconsole.log(\"Initialized\");\n\t// Just for fun (and to showcase the feature), let's send an event to handlers:\n\t// notice how the first argument is 0, meaning this event is not tied to any session\n\tvar event = { event: \"loaded\", script: name };\n\tnotifyEvent(0, JSON.stringify(event));\n}\n\nfunction destroy() {\n\t// This is where we deinitialize the plugin, when Janus shuts down\n\tconsole.log(\"Deinitialized\");\n}\n\nfunction createSession(id) {\n\t// Keep track of a new session\n\tconsole.log(\"Created new session:\", id);\n\tsessions[id] = { id: id, lua: name };\n\t// By default, we accept and relay all streams\n\tconfigureMedium(id, \"audio\", \"in\", true);\n\tconfigureMedium(id, \"audio\", \"out\", true);\n\tconfigureMedium(id, \"video\", \"in\", true);\n\tconfigureMedium(id, \"video\", \"out\", true);\n\tconfigureMedium(id, \"data\", \"in\", true);\n\tconfigureMedium(id, \"data\", \"out\", true);\n}\n\nfunction destroySession(id) {\n\t// A Janus plugin session has gone\n\tconsole.log(\"Destroyed session:\", id)\n\thangupMedia(id);\n\tdelete sessions[id];\n}\n\nfunction querySession(id) {\n\t// Return info on a session\n\tconsole.log(\"Queried session:\", id)\n\tvar s = sessions[id];\n\tif(!s)\n\t\treturn null;\n\tvar info = { script: s[\"lua\"], id: s[\"id\"] };\n\treturn JSON.stringify(info);\n}\n\nfunction handleMessage(id, tr, msg, jsep) {\n\t// Handle a message, synchronously or asynchronously, and return\n\t// something accordingly: if it's the latter, we'll do a coroutine\n\tconsole.log(\"Handling message for session:\", id)\n\tvar s = sessions[id];\n\tif(!s) {\n\t\t// Session not found: return value is a negative integer\n\t\treturn -1;\n\t}\n\t// Decode the message JSON string\n\tvar msgT = JSON.parse(msg);\n\t// Let's return a synchronous response if there's no jsep, asynchronous otherwise\n\tif(!jsep) {\n\t\tvar res = processRequest(id, msgT);\n\t\tvar response = { echotest: \"response\", result: \"ok\" };\n\t\tif(res < 0)\n\t\t\tresponse[\"result\"] = \"error\";\n\t\t// Synchronous response: return value is a JSON string\n\t\treturn JSON.stringify(response);\n\t} else {\n\t\t// Decode the JSEP JSON string too\n\t\tvar jsepT = JSON.parse(jsep);\n\t\t// We'll need a coroutine here: the scheduler will resume it later\n\t\ttasks.push({ id: id, tr: tr, msg: msgT, jsep: jsepT });\n\t\t// Return explaining that this is will be handled asynchronously\n\t\tpokeScheduler();\n\t\t// Asynchronous response: return value is a positive integer\n\t\treturn 1;\n\t}\n}\n\nfunction handleAdminMessage(message) {\n\t// This is just to showcase how you can handle incoming messages\n\t// coming from the Admin API: we return the same message as a test\n\tconsole.log(\"Got admin message:\", message);\n\treturn message;\n}\n\nfunction setupMedia(id) {\n\t// WebRTC is now available\n\tconsole.log(\"WebRTC PeerConnection is up for session:\", id);\n\t// Attach the session's stream to itself (echo test)\n\taddRecipient(id, id);\n}\n\nfunction hangupMedia(id) {\n\t// WebRTC not available anymore\n\tconsole.log(\"WebRTC PeerConnection is down for session:\", id);\n\t// Detach the stream\n\tremoveRecipient(id, id);\n\t// Clear some flags\n\tvar s = sessions[id];\n\tif(s) {\n\t\ts.audioCodec = null;\n\t\ts.videoCodec = null;\n\t}\n}\n\nfunction incomingTextData(id, buf, len, label, protocol) {\n\t// Relaying RTP/RTCP in JavaScript makes no sense, but just for fun\n\t// we handle data channel messages ourselves to manipulate them\n\tvar edit = \"[\" + name + \"] --> \" + buf;\n\trelayTextData(id, edit, edit.length, label, protocol);\n}\n\nfunction incomingBinaryData(id, buf, len, label, protocol) {\n\t// If the data we're getting is binary, send it back as it is\n\trelayBinaryData(id, buf, len, label, protocol);\n}\n\nfunction dataReady(id) {\n\t// This callback is invoked when the datachannel first becomes\n\t// available (meaning you should never send data before it has been\n\t// invoked at least once), but also when the datachannel is ready to\n\t// receive more data (buffers are empty), which means it can be used\n\t// to throttle outgoing data and not send too much at a time.\n}\n\nfunction substreamChanged(id, substream) {\n\t// If simulcast is used, this callback is invoked when the substream\n\t// we're sending to this session changes: 0=low, 1=medium, 2=high\n\tconsole.log(\"Substream changed for session \" + id + \": \" + substream);\n\t// Let's send an event so that the user is aware\n\tvar event = { echotest: \"event\", videocodec: \"vp8\", substream: substream };\n\tvar jsonevent = JSON.stringify(event);\n\tpushEvent(id, null, jsonevent, null);\n}\n\nfunction temporalLayerChanged(id, temporal) {\n\t// If simulcast is used, this callback is invoked when the temporal\n\t// layer we're sending to this session changes: 0=lowfps, 1=maxfps\n\tconsole.log(\"Temporal layer changed for session \" + id + \": \" + temporal);\n\t// Let's send an event so that the user is aware\n\tvar event = { echotest: \"event\", videocodec: \"vp8\", temporal: temporal };\n\tvar jsonevent = JSON.stringify(event);\n\tpushEvent(id, null, jsonevent, null);\n}\n\nfunction resumeScheduler() {\n\t// This is the function responsible for resuming coroutines associated\n\t// with whatever is relevant to the JS script, e.g., for this script,\n\t// with asynchronous requests: if you're handling async stuff yourself,\n\t// you're free not to use this and just return, but the C Duktape plugin\n\t// expects this method to exist so it MUST be present, even if empty\n\tconsole.log(\"Resuming coroutines\");\n\tfor(var index in tasks) {\n\t\tvar task = tasks[index];\n\t\tprocessAsync(task);\n\t}\n\tconsole.log(\"Coroutines resumed\");\n\ttasks = [];\n}\n\n// We use this internal method to process an API request\nfunction processRequest(id, msg) {\n\tif(!msg) {\n\t\tconsole.log(\"Invalid request\");\n\t\treturn -1;\n\t}\n\t// We implement most of the existing EchoTest API messages, here\n\tif(msg[\"audio\"] === true) {\n\t\tconfigureMedium(id, \"audio\", \"in\", true);\n\t\tconfigureMedium(id, \"audio\", \"out\", true);\n\t} else if(msg[\"audio\"] === false) {\n\t\tconfigureMedium(id, \"audio\", \"in\", false);\n\t\tconfigureMedium(id, \"audio\", \"out\", false);\n\t}\n\tif(msg[\"video\"] === true) {\n\t\tconfigureMedium(id, \"video\", \"in\", true);\n\t\tconfigureMedium(id, \"video\", \"out\", true);\n\t\tsendPli(id);\n\t} else if(msg[\"video\"] === false) {\n\t\tconfigureMedium(id, \"video\", \"in\", false);\n\t\tconfigureMedium(id, \"video\", \"out\", false);\n\t}\n\tif(msg[\"data\"] === true) {\n\t\tconfigureMedium(id, \"data\", \"in\", true);\n\t\tconfigureMedium(id, \"data\", \"out\", true);\n\t} else if(msg[\"data\"] === false) {\n\t\tconfigureMedium(id, \"data\", \"in\", false);\n\t\tconfigureMedium(id, \"data\", \"out\", false);\n\t}\n\tif(msg[\"bitrate\"] !== null && msg[\"bitrate\"] !== undefined) {\n\t\tsetBitrate(id, msg[\"bitrate\"]);\n\t}\n\tif(msg[\"substream\"] !== null && msg[\"substream\"] !== undefined) {\n\t\tsetSubstream(id, msg[\"substream\"]);\n\t\tsendPli(id);\n\t}\n\tif(msg[\"temporal\"] !== null && msg[\"temporal\"] !== undefined) {\n\t\tsetTemporalLayer(id, msg[\"temporal\"]);\n\t\tsendPli(id);\n\t}\n\tif(msg[\"keyframe\"] !== null && msg[\"keyframe\"] !== undefined) {\n\t\tsendPli(id);\n\t}\n\tif(msg[\"record\"] === true) {\n\t\tvar fnbase = msg[\"filename\"];\n\t\tif(!fnbase) {\n\t\t\tfnbase = \"duktape-echotest-\" + id + \"-\" + new Date().getTime();\n\t\t}\n\t\t// For the sake of simplicity, we're assuming Opus/VP8 here; in\n\t\t// practice, you'll need to check what was negotiated. If you\n\t\t// want the codec-specific info to be saved to the .mjr file as\n\t\t// well, you'll need to add the '/fmtp=<info>' to the codec name,\n\t\t// e.g.:    \"vp9/fmtp=profile-id=2\"\n\t\tstartRecording(id,\n\t\t\t\"audio\", \"opus\", \"/tmp\", fnbase + \"-audio\",\n\t\t\t\"video\", \"vp8\", \"/tmp\", fnbase + \"-video\",\n\t\t\t\"data\", \"text\", \"/tmp\", fnbase + \"-data\"\n\t\t);\n\t} else if(msg[\"record\"] === false) {\n\t\tstopRecording(id, \"audio\", \"video\", \"data\");\n\t}\n\treturn 0;\n}\n\n// We use this other function to process asynchronous requests\nfunction processAsync(task) {\n\t// We'll only execute this when the scheduler resumes a task\n\tvar id = task.id;\n\tvar tr = task.tr;\n\tvar msg = task.msg;\n\tvar jsep = task.jsep;\n\tconsole.log(\"Handling async message for session:\", id);\n\tvar s = sessions[id];\n\tif(!s) {\n\t\tconsole.log(\"Can't handle async message: no such session\");\n\t\treturn;\n\t}\n\tvar offer = sdpUtils.parse(jsep.sdp)\n\tconsole.log(\"Got offer:\", offer);\n\tvar answer = sdpUtils.generateAnswer(offer, { audio: true, video: true, data: true,\n\t\taudioCodec: msg[\"audiocodec\"], videoCodec: msg[\"videocodec\"],\n\t\tvp9Profile: msg[\"videoprofile\"], h264Profile: msg[\"videoprofile\"] });\n\tconsole.log(\"Generated answer:\", answer);\n\tconsole.log(\"Processing request:\", msg);\n\tprocessRequest(id, msg);\n\tconsole.log(\"Pushing event:\");\n\tvar event = { echotest: \"event\", result: \"ok\" };\n\tconsole.log(\"  --\", event);\n\tvar jsepanswer = { type: \"answer\", sdp: sdpUtils.render(answer) };\n\tconsole.log(\"  --\", jsepanswer);\n\tpushEvent(id, tr, JSON.stringify(event), JSON.stringify(jsepanswer));\n\t// Just for fun (and to showcase the feature), let's send an event to handlers;\n\t// notice how we pass the id now, meaning this event is tied to a specific session\n\tevent = { event: \"processed\", request: msg };\n\tnotifyEvent(id, JSON.stringify(event));\n}\n\n// Done\nconsole.log(\"Script loaded\");\n"
  },
  {
    "path": "src/plugins/duktape/janus-sdp.js",
    "content": "// Set of utilities for parsing, processing && managing Janus SDPs in JS,\n// as the C Janus SDP utils that Janus provides are unavailable otherwise\n\nvar JANUSSDP = {};\n\nJANUSSDP.parse = function(text) {\n\tif(!text)\n\t\treturn null;\n\tvar lines = text.split(\"\\r\\n\");\n\tvar sdp = [];\n\tfor(var index in lines) {\n\t\tvar line = lines[index];\n\t\tvar t = line.substring(0, 1);\n\t\tvar ll = line.substring(2);\n\t\tvar sc = ll.indexOf(\":\");\n\t\tvar n, v;\n\t\tif(sc < 0) {\n\t\t\tn = ll;\n\t\t} else {\n\t\t\tn = ll.substring(0, sc);\n\t\t\tv = ll.substring(sc+1);\n\t\t}\n\t\tsdp.push({ type: t, name: n, value: v });\n\t}\n\treturn sdp;\n}\n\nJANUSSDP.render = function(sdp) {\n\tif(!sdp)\n\t\treturn null;\n\tvar sdpString = \"\";\n\tfor(var index in sdp) {\n\t\tvar a = sdp[index];\n\t\tif(!a.value) {\n\t\t\tsdpString += a.type + \"=\" + a.name + \"\\r\\n\";\n\t\t} else {\n\t\t\tsdpString += a.type + \"=\" + a.name + \":\" + a.value + \"\\r\\n\";\n\t\t}\n\t}\n\treturn sdpString;\n}\n\nJANUSSDP.findPayloadType = function(sdp, codec, profile) {\n\tif(!sdp || !codec)\n\t\treturn -1\n\tvar pt = -1;\n\tvar codecUpper = codec.toUpperCase();\n\tvar codecLower = codec.toLowerCase();\n\tvar checkProfile = false;\n\tfor(var index in sdp) {\n\t\tvar a = sdp[index];\n\t\tif(checkProfile && a.name === \"fmtp\" && a.value) {\n\t\t\tcheckProfile = false;\n\t\t\tif(codec === \"vp9\") {\n\t\t\t\tif(a.value.indexOf(\"profile-level=\"+profile) != -1) {\n\t\t\t\t\t// Found\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else if(codec === \"h264\") {\n\t\t\t\tif(a.value.indexOf(\"profile-level-id=\"+profile.toLowerCase()) != -1 ||\n\t\t\t\t\t\ta.value.indexOf(\"profile-level-id=\"+profile.toUpperCase()) != -1) {\n\t\t\t\t\t// Found\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(a.name === \"rtpmap\" && a.value) {\n\t\t\tif(a.value.indexOf(codecLower) != -1 || a.value.indexOf(codecUpper) !== -1) {\n\t\t\t\tpt = parseInt(a.value);\n\t\t\t\tif(!profile) {\n\t\t\t\t\t// We're done\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\t// We need to make sure the profile matches\n\t\t\t\t\tcheckProfile = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn pt;\n}\n\nJANUSSDP.findCodec = function(sdp, pt) {\n\tif(!sdp || pt === null || pt === undefined)\n\t\treturn -1;\n\tif(pt === 0) {\n\t\treturn \"pcmu\";\n\t} else if(pt === 8) {\n\t\treturn \"pcma\";\n\t} else if(pt === 9) {\n\t\treturn \"g722\";\n\t}\n\tvar codec = null;\n\tfor(var index in sdp) {\n\t\tvar a = sdp[index];\n\t\tif(a.name === \"rtpmap\" && a.value) {\n\t\t\tvar n = parseInt(a.value);\n\t\t\tif(n === pt) {\n\t\t\t\tif(a.value.indexOf(\"vp8\") !== -1 || a.value.indexOf(\"VP8\") !== -1) {\n\t\t\t\t\tcodec = \"vp8\";\n\t\t\t\t} else if(a.value.indexOf(\"vp9\") !== -1 || a.value.indexOf(\"VP9\") !== -1) {\n\t\t\t\t\tcodec = \"vp9\";\n\t\t\t\t} else if(a.value.indexOf(\"h264\") !== -1 || a.value.indexOf(\"H264\") !== -1) {\n\t\t\t\t\tcodec = \"h264\";\n\t\t\t\t} else if(a.value.indexOf(\"opus\") !== -1 || a.value.indexOf(\"OPUS\") !== -1) {\n\t\t\t\t\tcodec = \"opus\";\n\t\t\t\t} else if(a.value.indexOf(\"multiopus\") !== -1 || a.value.indexOf(\"MULTIOPUS\") !== -1) {\n\t\t\t\t\tcodec = \"multiopus\";\n\t\t\t\t} else if(a.value.indexOf(\"pcmu\") !== -1 || a.value.indexOf(\"PCMU\") !== -1) {\n\t\t\t\t\tcodec = \"pcmu\";\n\t\t\t\t} else if(a.value.indexOf(\"pcma\") !== -1 || a.value.indexOf(\"PCMA\") !== -1) {\n\t\t\t\t\tcodec = \"pcma\";\n\t\t\t\t} else if(a.value.indexOf(\"isac16\") !== -1 || a.value.indexOf(\"ISAC16\") !== -1) {\n\t\t\t\t\tcodec = \"isac16\";\n\t\t\t\t} else if(a.value.indexOf(\"isac32\") !== -1 || a.value.indexOf(\"ISAC32\") !== -1) {\n\t\t\t\t\tcodec = \"isac32\";\n\t\t\t\t} else if(a.value.indexOf(\"telephone-event\") !== -1 || a.value.indexOf(\"TELEPHONE-EVENT\") !== -1) {\n\t\t\t\t\tcodec = \"isac32\";\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn codec;\n}\n\nJANUSSDP.removePayloadType = function(sdp, pt) {\n\tif(!sdp || pt === null || pt === undefined)\n\t\treturn;\n\tfor(var index=0; index<sdp.length; index++) {\n\t\tvar a = sdp[index];\n\t\tif(a.type === \"m\") {\n\t\t\tvar m = a.name.replace(\" \" + pt + \" \", \" \");\n\t\t\tif(m)\n\t\t\t\ta.name = m;\n\t\t\ta.name += \"\\r\\n\";\n\t\t\tm = a.name.replace(\" \" + pt + \"\\r\\n\", \"\\r\\n\");\n\t\t\tif(m)\n\t\t\t\ta.name = m;\n\t\t\ta.name = a.name.replace(\"\\r\\n\", \"\");\n\t\t} else if(a.type === \"a\" && a.value) {\n\t\t\tvar n = parseInt(a.value);\n\t\t\tif(n === pt) {\n\t\t\t\tsdp.splice(index, 1);\n\t\t\t\tindex--;\n\t\t\t}\n\t\t}\n\t}\n}\n\nJANUSSDP.generateOffer = function(options) {\n\t// Let's set some defaults for the options, in case none were given\n\toptions = options || {};\n\tif(options.audio === null || options.audio === undefined)\n\t\toptions.audio = true;\n\tif(options.audio === true && (options.audioPt === null || options.audioPt === undefined))\n\t\toptions.audioPt = 111;\n\tif(options.audio === true) {\n\t\tif(!options.audioCodec)\n\t\t\toptions.audioCodec = \"opus\";\n\t\tif(options.audioCodec === \"opus\") {\n\t\t\toptions.audioRtpmap = \"opus/48000/2\";\n\t\t} else if(options.audioCodec === \"multiopus\") {\n\t\t\toptions.audioRtpmap = \"multiopus/48000/6\";\n\t\t} else if(options.audioCodec === \"pcmu\") {\n\t\t\toptions.audioRtpmap = \"PCMU/8000\";\n\t\t\toptions.audioPt = 0;\n\t\t} else if(options.audioCodec === \"pcma\") {\n\t\t\toptions.audioRtpmap = \"PCMA/8000\";\n\t\t\toptions.audioPt = 8;\n\t\t} else if(options.audioCodec === \"g722\") {\n\t\t\toptions.audioRtpmap = \"G722/8000\";\n\t\t\toptions.audioPt = 9;\n\t\t} else if(options.audioCodec === \"isac16\") {\n\t\t\toptions.audioRtpmap = \"ISAC/16000\";\n\t\t} else if(options.audioCodec === \"isac32\") {\n\t\t\toptions.audioRtpmap = \"ISAC/32000\";\n\t\t} else {\n\t\t\t// Unsupported codec\n\t\t\toptions.audio = false;\n\t\t}\n\t}\n\tif(!options.audioDir)\n\t\toptions.audioDir = \"sendrecv\";\n\tif(options.video === null || options.video === undefined)\n\t\toptions.video = true;\n\tif(options.video === true && (options.videoPt === null || options.videoPt === undefined))\n\t\toptions.videoPt = 96;\n\tif(options.video === true) {\n\t\tif(!options.videoCodec)\n\t\t\toptions.videoCodec = \"vp8\";\n\t\tif(options.videoCodec === \"vp8\") {\n\t\t\toptions.videoRtpmap = \"VP8/90000\";\n\t\t} else if(options.videoCodec === \"vp9\") {\n\t\t\toptions.videoRtpmap = \"VP9/90000\";\n\t\t} else if(options.videoCodec === \"h264\") {\n\t\t\toptions.videoRtpmap = \"H264/90000\";\n\t\t} else {\n\t\t\t// Unsupported codec\n\t\t\toptions.video = false;\n\t\t}\n\t}\n\tif(!options.videoDir)\n\t\toptions.videoDir = \"sendrecv\";\n\tif(!options.videoRtcpfb)\n\t\toptions.videoRtcpfb = true;\n\tif(options.data === null || options.data === undefined)\n\t\toptions.data = false;\n\tif(options.data)\n\t\toptions.dataDir = \"sendrecv\";\n\tif(!options.address)\n\t\toptions.address = \"127.0.0.1\";\n\tif(options.ipv6 !== true && options.ipv6 !== false)\n\t\toptions.ipv6 = false;\n\tif(!options.sessionName)\n\t\toptions.sessionName = \"Janus Duktape session\";\n\t// Do we have enough for an offer?\n\tif(!options.audio && !options.video && !options.data)\n\t\treturn null;\n\t// Let's prepare the offer\n\tvar offer = [];\n\t// Let's start from the session-level attributes\n\toffer.push({ type: \"v\", name: \"0\" });\n\toffer.push({ type: \"o\", name: \"- \" + Math.floor(Math.random(4294967296)) + \" 1 IN \" +\n\t\t(options.ipv6 ? \"IP6 \" : \"IP4 \") + options.address });\n\toffer.push({ type: \"s\", name: options.sessionName });\n\toffer.push({ type: \"t\", name: \"0 0\" });\n\toffer.push({ type: \"c\", name: \"IN \" + (options.ipv6 ? \"IP6 \" : \"IP4 \") + options.address });\n\t// Now let's add the media lines\n\tif(options.audio) {\n\t\toffer.push({ type: \"m\", name: \"audio 9 UDP/TLS/RTP/SAVPF \" + options.audioPt });\n\t\toffer.push({ type: \"c\", name: \"IN \" + (options.ipv6 ? \"IP6 \" : \"IP4 \") + options.address });\n\t\toffer.push({ type: \"a\", name: options.audioDir });\n\t\toffer.push({ type: \"a\", name: \"rtpmap\", value: options.audioPt + \" \" + options.audioRtpmap });\n\t\tif(options.audioFmtp) {\n\t\t\toffer.push({ type: \"a\", name: \"fmtp\", value: options.audioPt + \" \" + options.audioFmtp });\n\t\t}\n\t}\n\tif(options.video) {\n\t\toffer.push({ type: \"m\", name: \"video 9 UDP/TLS/RTP/SAVPF \" + options.videoPt });\n\t\toffer.push({ type: \"c\", name: \"IN \" + (options.ipv6 ? \"IP6 \" : \"IP4 \") + options.address });\n\t\toffer.push({ type: \"a\", name: options.videoDir });\n\t\toffer.push({ type: \"a\", name: \"rtpmap\", value: options.videoPt + \" \" + options.videoRtpmap });\n\t\tif(options.videoRtcpfb) {\n\t\t\toffer.push({ type: \"a\", name: \"rtcp-fb\", value: options.videoPt + \" ccm fir\" });\n\t\t\toffer.push({ type: \"a\", name: \"rtcp-fb\", value: options.videoPt + \" nack\" });\n\t\t\toffer.push({ type: \"a\", name: \"rtcp-fb\", value: options.videoPt + \" nack pli\" });\n\t\t\toffer.push({ type: \"a\", name: \"rtcp-fb\", value: options.videoPt + \" goog-remb\" });\n\t\t}\n\t\tif(options.videoCodec === \"vp9\" && options.vp9Profile) {\n\t\t\toffer.push({ type: \"a\", name: \"fmtp\", value: options.videoPt + \" profile-id=\" + options.vp9Profile });\n\t\t} else if(options.videoCodec === \"h264\" && options.h264Profile) {\n\t\t\toffer.push({ type: \"a\", name: \"fmtp\", value: options.videoPt + \" profile-level-id=\" + options.h264Profile + \";packetization-mode=1\" });\n\t\t} else if(options.videoFmtp) {\n\t\t\toffer.push({ type: \"a\", name: \"fmtp\", value: options.videoPt + \" \" + options.videoFmtp });\n\t\t} else if(options.videoCodec === \"h264\") {\n\t\t\toffer.push({ type: \"a\", name: \"fmtp\", value: options.videoPt + \" profile-level-id=42e01f;packetization-mode=1\" });\n\t\t}\n\t}\n\tif(options.data) {\n\t\toffer.push({ type: \"m\", name: \"application 9 DTLS/SCTP 5000\" });\n\t\toffer.push({ type: \"c\", name: \"IN \" + (options.ipv6 ? \"IP6 \" : \"IP4 \") + options.address });\n\t\toffer.push({ type: \"a\", name: \"sendrecv\" });\n\t\toffer.push({ type: \"a\", name: \"sctmap\", value: \"5000 webrtc-datachannel 16\" });\n\t}\n\t// Done\n\treturn offer;\n}\n\nJANUSSDP.generateAnswer = function(offer, options) {\n\tif(!offer)\n\t\treturn null;\n\t// Let's set some defaults for the options, in case none were given\n\toptions = options || {};\n\tif(options.audio === null || options.audio === undefined)\n\t\toptions.audio = true;\n\tif(options.audio && !options.audioCodec) {\n\t\tif(JANUSSDP.findPayloadType(offer, \"opus\") !== -1) {\n\t\t\toptions.audioCodec = \"opus\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"multiopus\") !== -1) {\n\t\t\toptions.audioCodec = \"multiopus\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"pcmu\") !== -1) {\n\t\t\toptions.audioCodec = \"pcmu\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"pcma\") !== -1) {\n\t\t\toptions.audioCodec = \"pcma\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"g722\") !== -1) {\n\t\t\toptions.audioCodec = \"g722\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"isac16\") !== -1) {\n\t\t\toptions.audioCodec = \"isac16\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"isac32\") !== -1) {\n\t\t\toptions.audioCodec = \"isac32\";\n\t\t}\n\t}\n\tif(options.video === null || options.video === undefined)\n\t\toptions.video = true;\n\tif(options.video && !options.videoCodec) {\n\t\tif(JANUSSDP.findPayloadType(offer, \"vp8\") !== -1) {\n\t\t\toptions.videoCodec = \"vp8\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"vp9\", options.vp9Profile) !== -1) {\n\t\t\toptions.videoCodec = \"vp9\";\n\t\t} else if(JANUSSDP.findPayloadType(offer, \"h264\", options.h264Profile) !== -1) {\n\t\t\toptions.videoCodec = \"h264\";\n\t\t}\n\t}\n\tif(options.data === null || options.data === undefined)\n\t\toptions.data = true;\n\tif(options.disableTwcc === null || options.disableTwcc === undefined)\n\t\toptions.disableTwcc = false;\n\t// Let's prepare the answer\n\tvar answer = [];\n\t// Iterate on all lines\n\tvar audio = 0, video = 0, data = 0;\n\tvar audioPt = -1, videoPt = -1;\n\tvar medium = null;\n\tvar reject = false;\n\tfor(var index in offer) {\n\t\tvar a = offer[index];\n\t\tif(!medium && a.type !== \"m\") {\n\t\t\t// We just copy all the session-level attributes\n\t\t\tif(!a.value)\n\t\t\t\tanswer.push(a);\n\t\t}\n\t\tif(a.type === \"m\") {\n\t\t\t// New m-line\n\t\t\treject = false;\n\t\t\tif(a.name.indexOf(\"audio\") !== -1) {\n\t\t\t\tmedium = \"audio\";\n\t\t\t\taudio++;\n\t\t\t\tif(audioPt < 0)\n\t\t\t\t\taudioPt = JANUSSDP.findPayloadType(offer, options.audioCodec);\n\t\t\t\tif(audioPt < 0)\n\t\t\t\t\taudio++;\n\t\t\t\tif(audio > 1) {\n\t\t\t\t\treject = true;\n\t\t\t\t\tanswer.push({ type: \"m\", name: \"audio 0 UDP/TLS/RTP/SAVPF 0\" });\n\t\t\t\t} else {\n\t\t\t\t\tanswer.push({ type: \"m\", name: \"audio 9 UDP/TLS/RTP/SAVPF \" + audioPt });\n\t\t\t\t}\n\t\t\t} else if(a.name.indexOf(\"video\") !== -1) {\n\t\t\t\tmedium = \"video\";\n\t\t\t\tvideo++;\n\t\t\t\tif(videoPt < 0) {\n\t\t\t\t\tif(options.videoCodec === \"vp9\") {\n\t\t\t\t\t\tvideoPt = JANUSSDP.findPayloadType(offer, options.videoCodec, options.vp9Profile);\n\t\t\t\t\t} else if(options.videoCodec == \"h264\") {\n\t\t\t\t\t\tvideoPt = JANUSSDP.findPayloadType(offer, options.videoCodec, options.h264Profile);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvideoPt = JANUSSDP.findPayloadType(offer, options.videoCodec);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif(videoPt < 0)\n\t\t\t\t\tvideo++;\n\t\t\t\tif(video > 1) {\n\t\t\t\t\treject = true;\n\t\t\t\t\tanswer.push({ type: \"m\", name: \"video 0 UDP/TLS/RTP/SAVPF 0\" });\n\t\t\t\t} else {\n\t\t\t\t\tanswer.push({ type: \"m\", name: \"video 9 UDP/TLS/RTP/SAVPF \" + videoPt });\n\t\t\t\t}\n\t\t\t} else if(a.name.indexOf(\"application\") !== -1) {\n\t\t\t\tmedium = \"application\";\n\t\t\t\tdata = data+1\n\t\t\t\tif(data > 1) {\n\t\t\t\t\treject = true\n\t\t\t\t\tanswer.push({ type: \"m\", name: \"application 0 DTLS/SCTP 5000\" });\n\t\t\t\t} else {\n\t\t\t\t\tanswer.push({ type: \"m\", name: a.name });\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(a.type === \"a\") {\n\t\t\tif(a.name === \"sendonly\") {\n\t\t\t\tanswer.push({ type: \"a\", name: \"recvonly\" });\n\t\t\t} else if(a.name === \"recvonly\") {\n\t\t\t\tanswer.push({ type: \"a\", name: \"sendonly\" });\n\t\t\t} else if(a.value) {\n\t\t\t\tif(a.name === \"rtpmap\" || a.name === \"fmtp\" || a.name === \"rtcp-fb\") {\n\t\t\t\t\t// Drop attributes associated to payload types we're getting rid of\n\t\t\t\t\tvar n = parseInt(a.value);\n\t\t\t\t\tif(medium === \"audio\" && n === audioPt) {\n\t\t\t\t\t\tanswer.push(a);\n\t\t\t\t\t} else if(medium === \"video\" && n === videoPt) {\n\t\t\t\t\t\tanswer.push(a);\n\t\t\t\t\t}\n\t\t\t\t} else if (a.name === \"extmap\") {\n\t\t\t\t\t// We do negotiate some RTP extensions: check if there's a direction first\n\t\t\t\t\tvar value = a.value;\n\t\t\t\t\tif(a.value.indexOf(\"/sendonly\") > 0) {\n\t\t\t\t\t\tvalue = a.value.replace(\"/sendonly\", \"/recvonly\");\n\t\t\t\t\t} else if(a.value.indexOf(\"/recvonly\") > 0) {\n\t\t\t\t\t\tvalue = a.value.replace(\"/recvonly\", \"/sendonly\");\n\t\t\t\t\t}\n\t\t\t\t\t// Now check if we have to negotiate the extension\n\t\t\t\t\tif(a.value.indexOf(\"urn:ietf:params:rtp-hdrext:sdes:mid\") !== -1) {\n\t\t\t\t\t\tanswer.push({ type: \"a\", name: a.name, value: value });\n\t\t\t\t\t} else if(a.value.indexOf(\"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\") !== -1) {\n\t\t\t\t\t\tanswer.push({ type: \"a\", name: a.name, value: value });\n\t\t\t\t\t} else if(a.value.indexOf(\"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\") !== -1) {\n\t\t\t\t\t\tanswer.push({ type: \"a\", name: a.name, value: value });\n\t\t\t\t\t} else if(options.disableTwcc !== true && a.value.indexOf(\"draft-holmer-rmcat-transport-wide-cc-extensions-01\") !== -1) {\n\t\t\t\t\t\tanswer.push({ type: \"a\", name: a.name, value: value });\n\t\t\t\t\t} else if(options.enableAudioLevel !== false && a.value.indexOf(\"urn:ietf:params:rtp-hdrext:ssrc-audio-level\") !== -1) {\n\t\t\t\t\t\tanswer.push({ type: \"a\", name: a.name, value: value });\n\t\t\t\t\t} else if(options.enableAudioLevel !== false && a.value.indexOf(\"dependency-descriptor-rtp-header-extension\") !== -1) {\n\t\t\t\t\t\tanswer.push({ type: \"a\", name: a.name, value: value });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tanswer.push(a);\n\t\t\t}\n\t\t\t// TODO Handle/filter other attributes\n\t\t}\n\t}\n\t// Done\n\treturn answer;\n}\n\nmodule.exports = JANUSSDP;\n"
  },
  {
    "path": "src/plugins/duktape-deps/AUTHORS.rst",
    "content": "===============\nDuktape authors\n===============\n\nCopyright\n=========\n\nDuktape copyrights are held by its authors.  Each author has a copyright\nto their contribution, and agrees to irrevocably license the contribution\nunder the Duktape ``LICENSE.txt``.\n\nAuthors\n=======\n\nPlease include an e-mail address, a link to your GitHub profile, or something\nsimilar to allow your contribution to be identified accurately.\n\nThe following people have contributed code, website contents, or Wiki contents,\nand agreed to irrevocably license their contributions under the Duktape\n``LICENSE.txt`` (in order of appearance):\n\n* Sami Vaarala <sami.vaarala@iki.fi>\n* Niki Dobrev\n* Andreas Öman <andreas@lonelycoder.com>\n* László Langó <llango.u-szeged@partner.samsung.com>\n* Legimet <legimet.calc@gmail.com>\n* Karl Skomski <karl@skomski.com>\n* Bruce Pascoe <fatcerberus1@gmail.com>\n* René Hollander <rene@rene8888.at>\n* Julien Hamaide (https://github.com/crazyjul)\n* Sebastian Götte (https://github.com/jaseg)\n* Tomasz Magulski (https://github.com/magul)\n* \\D. Bohdan (https://github.com/dbohdan)\n* Ondřej Jirman (https://github.com/megous)\n* Saúl Ibarra Corretgé <saghul@gmail.com>\n* Jeremy HU <huxingyi@msn.com>\n* Ole André Vadla Ravnås (https://github.com/oleavr)\n* Harold Brenes (https://github.com/harold-b)\n* Oliver Crow (https://github.com/ocrow)\n* Jakub Chłapiński (https://github.com/jchlapinski)\n* Brett Vickers (https://github.com/beevik)\n* Dominik Okwieka (https://github.com/okitec)\n* Remko Tronçon (https://el-tramo.be)\n* Romero Malaquias (rbsm@ic.ufal.br)\n* Michael Drake <michael.drake@codethink.co.uk>\n* Steven Don (https://github.com/shdon)\n* Simon Stone (https://github.com/sstone1)\n* \\J. McC. (https://github.com/jmhmccr)\n* Jakub Nowakowski (https://github.com/jimvonmoon)\n* Tommy Nguyen (https://github.com/tn0502)\n* Fabrice Fontaine (https://github.com/ffontaine)\n* Christopher Hiller (https://github.com/boneskull)\n* Gonzalo Diethelm (https://github.com/gonzus)\n* Michal Kasperek (https://github.com/michalkas)\n* Andrew Janke (https://github.com/apjanke)\n* Steve Fan (https://github.com/stevefan1999)\n* Edward Betts (https://github.com/edwardbetts)\n* Ozhan Duz (https://github.com/webfolderio)\n* Akos Kiss (https://github.com/akosthekiss)\n* TheBrokenRail (https://github.com/TheBrokenRail)\n* Jesse Doyle (https://github.com/jessedoyle)\n* Gero Kuehn (https://github.com/dc6jgk)\n* James Swift (https://github.com/phraemer)\n* Luis de Bethencourt (https://github.com/luisbg)\n* Ian Whyman (https://github.com/v00d00)\n* Rick Sayre (https://github.com/whorfin)\n\nOther contributions\n===================\n\nThe following people have contributed something other than code (e.g. reported\nbugs, provided ideas, etc; roughly in order of appearance):\n\n* Greg Burns\n* Anthony Rabine\n* Carlos Costa\n* Aurélien Bouilland\n* Preet Desai (Pris Matic)\n* judofyr (http://www.reddit.com/user/judofyr)\n* Jason Woofenden\n* Michał Przybyś\n* Anthony Howe\n* Conrad Pankoff\n* Jim Schimpf\n* Rajaran Gaunker (https://github.com/zimbabao)\n* Andreas Öman\n* Doug Sanden\n* Josh Engebretson (https://github.com/JoshEngebretson)\n* Remo Eichenberger (https://github.com/remoe)\n* Mamod Mehyar (https://github.com/mamod)\n* David Demelier (https://github.com/markand)\n* Tim Caswell (https://github.com/creationix)\n* Mitchell Blank Jr (https://github.com/mitchblank)\n* https://github.com/yushli\n* Seo Sanghyeon (https://github.com/sanxiyn)\n* Han ChoongWoo (https://github.com/tunz)\n* Joshua Peek (https://github.com/josh)\n* Bruce E. Pascoe (https://github.com/fatcerberus)\n* https://github.com/Kelledin\n* https://github.com/sstruchtrup\n* Michael Drake (https://github.com/tlsa)\n* https://github.com/chris-y\n* Laurent Zubiaur (https://github.com/lzubiaur)\n* Neil Kolban (https://github.com/nkolban)\n* Wilhelm Wanecek (https://github.com/wanecek)\n* Andrew Janke (https://github.com/apjanke)\n* Unamer (https://github.com/unamer)\n* Karl Dahlke (eklhad@gmail.com)\n\nIf you are accidentally missing from this list, send me an e-mail\n(``sami.vaarala@iki.fi``) and I'll fix the omission.\n"
  },
  {
    "path": "src/plugins/duktape-deps/LICENSE.txt",
    "content": "===============\nDuktape license\n===============\n\n(http://opensource.org/licenses/MIT)\n\nCopyright (c) 2013-2019 by Duktape authors (see AUTHORS.rst)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "src/plugins/duktape-deps/duk_console.c",
    "content": "/*\n *  Minimal 'console' binding.\n *\n *  https://github.com/DeveloperToolsWG/console-object/blob/master/api.md\n *  https://developers.google.com/web/tools/chrome-devtools/debug/console/console-reference\n *  https://developer.mozilla.org/en/docs/Web/API/console\n */\n\n#include <stdio.h>\n#include <stdarg.h>\n#include \"duktape.h\"\n#include \"duk_console.h\"\n#include \"../../debug.h\"\n\n/* XXX: Add some form of log level filtering. */\n\n/* XXX: Should all output be written via e.g. console.write(formattedMsg)?\n * This would make it easier for user code to redirect all console output\n * to a custom backend.\n */\n\n/* XXX: Init console object using duk_def_prop() when that call is available. */\n\nstatic duk_ret_t duk__console_log_helper(duk_context *ctx, int level, const char *error_name) {\n\t\n\tduk_idx_t n = duk_get_top(ctx);\n\tduk_idx_t i;\n\n\tduk_get_global_string(ctx, \"console\");\n\tduk_get_prop_string(ctx, -1, \"format\");\n\n\tfor (i = 0; i < n; i++) {\n\t\tif (duk_check_type_mask(ctx, i, DUK_TYPE_MASK_OBJECT)) {\n\t\t\t/* Slow path formatting. */\n\t\t\tduk_dup(ctx, -1);  /* console.format */\n\t\t\tduk_dup(ctx, i);\n\t\t\tduk_call(ctx, 1);\n\t\t\tduk_replace(ctx, i);  /* arg[i] = console.format(arg[i]); */\n\t\t}\n\t}\n\n\tduk_pop_2(ctx);\n\n\tduk_push_string(ctx, \" \");\n\tduk_insert(ctx, 0);\n\tduk_join(ctx, n);\n\n\tif (error_name) {\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"%s\", duk_require_string(ctx, -1));\n\t\tduk_push_string(ctx, \"name\");\n\t\tduk_push_string(ctx, error_name);\n\t\tduk_def_prop(ctx, -3, DUK_DEFPROP_FORCE | DUK_DEFPROP_HAVE_VALUE);  /* to get e.g. 'Trace: 1 2 3' */\n\t\tduk_get_prop_string(ctx, -1, \"stack\");\n\t}\n\n\tJANUS_LOG(level, \"%s\\n\", duk_to_string(ctx, -1));\n\t\n\treturn 0;\n}\n\nstatic duk_ret_t duk__console_assert(duk_context *ctx) {\n\tif (duk_to_boolean(ctx, 0)) {\n\t\treturn 0;\n\t}\n\tduk_remove(ctx, 0);\n\n\treturn duk__console_log_helper(ctx, LOG_ERR, \"AssertionError\");\n}\n\nstatic duk_ret_t duk__console_log(duk_context *ctx) {\n\treturn duk__console_log_helper(ctx, LOG_INFO, NULL);\n}\n\nstatic duk_ret_t duk__console_trace(duk_context *ctx) {\n\treturn duk__console_log_helper(ctx, LOG_INFO, \"Trace\");\n}\n\nstatic duk_ret_t duk__console_info(duk_context *ctx) {\n\treturn duk__console_log_helper(ctx, LOG_INFO, NULL);\n}\n\nstatic duk_ret_t duk__console_warn(duk_context *ctx) {\n\treturn duk__console_log_helper(ctx, LOG_WARN, NULL);\n}\n\nstatic duk_ret_t duk__console_error(duk_context *ctx) {\n\treturn duk__console_log_helper(ctx, LOG_ERR, \"Error\");\n}\n\nstatic duk_ret_t duk__console_dir(duk_context *ctx) {\n\t/* For now, just share the formatting of .log() */\n\treturn duk__console_log_helper(ctx, LOG_INFO, 0);\n}\n\nstatic void duk__console_reg_vararg_func(duk_context *ctx, duk_c_function func, const char *name, duk_uint_t flags) {\n\tduk_push_c_function(ctx, func, DUK_VARARGS);\n\tduk_push_string(ctx, \"name\");\n\tduk_push_string(ctx, name);\n\tduk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_FORCE);  /* Improve stacktraces by displaying function name */\n\tduk_set_magic(ctx, -1, (duk_int_t) flags);\n\tduk_put_prop_string(ctx, -2, name);\n}\n\nvoid duk_console_init(duk_context *ctx, duk_uint_t flags) {\n\tduk_uint_t flags_orig;\n\n\t/* If both DUK_CONSOLE_STDOUT_ONLY and DUK_CONSOLE_STDERR_ONLY where specified,\n\t * just turn off DUK_CONSOLE_STDOUT_ONLY and keep DUK_CONSOLE_STDERR_ONLY.\n\t */\n\tif ((flags & DUK_CONSOLE_STDOUT_ONLY) && (flags & DUK_CONSOLE_STDERR_ONLY)) {\n\t    flags &= ~DUK_CONSOLE_STDOUT_ONLY;\n\t}\n\t/* Remember the (possibly corrected) flags we received. */\n\tflags_orig = flags;\n\n\tduk_push_object(ctx);\n\n\t/* Custom function to format objects; user can replace.\n\t * For now, try JX-formatting and if that fails, fall back\n\t * to ToString(v).\n\t */\n\tduk_eval_string(ctx,\n\t\t\"(function (E) {\"\n\t\t    \"return function format(v){\"\n\t\t        \"try{\"\n\t\t            \"return E('jx',v);\"\n\t\t        \"}catch(e){\"\n\t\t            \"return String(v);\"  /* String() allows symbols, ToString() internal algorithm doesn't. */\n\t\t        \"}\"\n\t\t    \"};\"\n\t\t\"})(Duktape.enc)\");\n\tduk_put_prop_string(ctx, -2, \"format\");\n\n\tflags = flags_orig;\n\tif (!(flags & DUK_CONSOLE_STDOUT_ONLY) && !(flags & DUK_CONSOLE_STDERR_ONLY)) {\n\t    /* No output indicators were specified; these levels go to stdout. */\n\t    flags |= DUK_CONSOLE_STDOUT_ONLY;\n\t}\n\tduk__console_reg_vararg_func(ctx, duk__console_assert, \"assert\", flags);\n\tduk__console_reg_vararg_func(ctx, duk__console_log, \"log\", flags);\n\tduk__console_reg_vararg_func(ctx, duk__console_log, \"debug\", flags);  /* alias to console.log */\n\tduk__console_reg_vararg_func(ctx, duk__console_trace, \"trace\", flags);\n\tduk__console_reg_vararg_func(ctx, duk__console_info, \"info\", flags);\n\n\tflags = flags_orig;\n\tif (!(flags & DUK_CONSOLE_STDOUT_ONLY) && !(flags & DUK_CONSOLE_STDERR_ONLY)) {\n\t    /* No output indicators were specified; these levels go to stderr. */\n\t    flags |= DUK_CONSOLE_STDERR_ONLY;\n\t}\n\tduk__console_reg_vararg_func(ctx, duk__console_warn, \"warn\", flags);\n\tduk__console_reg_vararg_func(ctx, duk__console_error, \"error\", flags);\n\tduk__console_reg_vararg_func(ctx, duk__console_error, \"exception\", flags);  /* alias to console.error */\n\tduk__console_reg_vararg_func(ctx, duk__console_dir, \"dir\", flags);\n\n\tduk_put_global_string(ctx, \"console\");\n\n\t/* Proxy wrapping: ensures any undefined console method calls are\n\t * ignored silently.  This was required specifically by the\n\t * DeveloperToolsWG proposal (and was implemented also by Firefox:\n\t * https://bugzilla.mozilla.org/show_bug.cgi?id=629607).  This is\n\t * apparently no longer the preferred way of implementing console.\n\t * When Proxy is enabled, whitelist at least .toJSON() to avoid\n\t * confusing JX serialization of the console object.\n\t */\n\n\tif (flags & DUK_CONSOLE_PROXY_WRAPPER) {\n\t\t/* Tolerate failure to initialize Proxy wrapper in case\n\t\t * Proxy support is disabled.\n\t\t */\n\t\t(void) duk_peval_string_noresult(ctx,\n\t\t\t\"(function(){\"\n\t\t\t    \"var D=function(){};\"\n\t\t\t    \"var W={toJSON:true};\"  /* whitelisted */\n\t\t\t    \"console=new Proxy(console,{\"\n\t\t\t        \"get:function(t,k){\"\n\t\t\t            \"var v=t[k];\"\n\t\t\t            \"return typeof v==='function'||W[k]?v:D;\"\n\t\t\t        \"}\"\n\t\t\t    \"});\"\n\t\t\t\"})();\"\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/plugins/duktape-deps/duk_console.h",
    "content": "#if !defined(DUK_CONSOLE_H_INCLUDED)\n#define DUK_CONSOLE_H_INCLUDED\n\n#include \"duktape.h\"\n\n#if defined(__cplusplus)\nextern \"C\" {\n#endif\n\n/* Use a proxy wrapper to make undefined methods (console.foo()) no-ops. */\n#define DUK_CONSOLE_PROXY_WRAPPER  (1U << 0)\n\n/* Flush output after every call. */\n#define DUK_CONSOLE_FLUSH          (1U << 1)\n\n/* Send output to stdout only (default is mixed stdout/stderr). */\n#define DUK_CONSOLE_STDOUT_ONLY    (1U << 2)\n\n/* Send output to stderr only (default is mixed stdout/stderr). */\n#define DUK_CONSOLE_STDERR_ONLY    (1U << 3)\n\n/* Initialize the console system */\nextern void duk_console_init(duk_context *ctx, duk_uint_t flags);\n\n#if defined(__cplusplus)\n}\n#endif  /* end 'extern \"C\"' wrapper */\n\n#endif  /* DUK_CONSOLE_H_INCLUDED */\n"
  },
  {
    "path": "src/plugins/duktape-deps/duk_module_duktape.c",
    "content": "/*\n *  Duktape 1.x compatible module loading framework\n */\n\n#include \"duktape.h\"\n#include \"duk_module_duktape.h\"\n\n/* (v)snprintf() is missing before MSVC 2015.  Note that _(v)snprintf() does\n * NOT NUL terminate on truncation, but that's OK here.\n * http://stackoverflow.com/questions/2915672/snprintf-and-visual-studio-2010\n */\n#if defined(_MSC_VER) && (_MSC_VER < 1900)\n#define snprintf _snprintf\n#endif\n\n#if 0  /* Enable manually */\n#define DUK__ASSERT(x) do { \\\n\t\tif (!(x)) { \\\n\t\t\tfprintf(stderr, \"ASSERTION FAILED at %s:%d: \" #x \"\\n\", __FILE__, __LINE__); \\\n\t\t\tfflush(stderr);  \\\n\t\t} \\\n\t} while (0)\n#define DUK__ASSERT_TOP(ctx,val) do { \\\n\t\tDUK__ASSERT(duk_get_top((ctx)) == (val)); \\\n\t} while (0)\n#else\n#define DUK__ASSERT(x) do { (void) (x); } while (0)\n#define DUK__ASSERT_TOP(ctx,val) do { (void) ctx; (void) (val); } while (0)\n#endif\n\nstatic void duk__resolve_module_id(duk_context *ctx, const char *req_id, const char *mod_id) {\n\tduk_uint8_t buf[DUK_COMMONJS_MODULE_ID_LIMIT];\n\tduk_uint8_t *p;\n\tduk_uint8_t *q;\n\tduk_uint8_t *q_last;  /* last component */\n\tduk_int_t int_rc;\n\n\tDUK__ASSERT(req_id != NULL);\n\t/* mod_id may be NULL */\n\n\t/*\n\t *  A few notes on the algorithm:\n\t *\n\t *    - Terms are not allowed to begin with a period unless the term\n\t *      is either '.' or '..'.  This simplifies implementation (and\n\t *      is within CommonJS modules specification).\n\t *\n\t *    - There are few output bound checks here.  This is on purpose:\n\t *      the resolution input is length checked and the output is never\n\t *      longer than the input.  The resolved output is written directly\n\t *      over the input because it's never longer than the input at any\n\t *      point in the algorithm.\n\t *\n\t *    - Non-ASCII characters are processed as individual bytes and\n\t *      need no special treatment.  However, U+0000 terminates the\n\t *      algorithm; this is not an issue because U+0000 is not a\n\t *      desirable term character anyway.\n\t */\n\n\t/*\n\t *  Set up the resolution input which is the requested ID directly\n\t *  (if absolute or no current module path) or with current module\n\t *  ID prepended (if relative and current module path exists).\n\t *\n\t *  Suppose current module is 'foo/bar' and relative path is './quux'.\n\t *  The 'bar' component must be replaced so the initial input here is\n\t *  'foo/bar/.././quux'.\n\t */\n\n\tif (mod_id != NULL && req_id[0] == '.') {\n\t\tint_rc = snprintf((char *) buf, sizeof(buf), \"%s/../%s\", mod_id, req_id);\n\t} else {\n\t\tint_rc = snprintf((char *) buf, sizeof(buf), \"%s\", req_id);\n\t}\n\tif (int_rc >= (duk_int_t) sizeof(buf) || int_rc < 0) {\n\t\t/* Potentially truncated, NUL not guaranteed in any case.\n\t\t * The (int_rc < 0) case should not occur in practice.\n\t\t */\n\t\tgoto resolve_error;\n\t}\n\tDUK__ASSERT(strlen((const char *) buf) < sizeof(buf));  /* at most sizeof(buf) - 1 */\n\n\t/*\n\t *  Resolution loop.  At the top of the loop we're expecting a valid\n\t *  term: '.', '..', or a non-empty identifier not starting with a period.\n\t */\n\n\tp = buf;\n\tq = buf;\n\tfor (;;) {\n\t\tduk_uint_fast8_t c;\n\n\t\t/* Here 'p' always points to the start of a term.\n\t\t *\n\t\t * We can also unconditionally reset q_last here: if this is\n\t\t * the last (non-empty) term q_last will have the right value\n\t\t * on loop exit.\n\t\t */\n\n\t\tDUK__ASSERT(p >= q);  /* output is never longer than input during resolution */\n\n\t\tq_last = q;\n\n\t\tc = *p++;\n\t\tif (c == 0) {\n\t\t\tgoto resolve_error;\n\t\t} else if (c == '.') {\n\t\t\tc = *p++;\n\t\t\tif (c == '/') {\n\t\t\t\t/* Term was '.' and is eaten entirely (including dup slashes). */\n\t\t\t\tgoto eat_dup_slashes;\n\t\t\t}\n\t\t\tif (c == '.' && *p == '/') {\n\t\t\t\t/* Term was '..', backtrack resolved name by one component.\n\t\t\t\t *  q[-1] = previous slash (or beyond start of buffer)\n\t\t\t\t *  q[-2] = last char of previous component (or beyond start of buffer)\n\t\t\t\t */\n\t\t\t\tp++;  /* eat (first) input slash */\n\t\t\t\tDUK__ASSERT(q >= buf);\n\t\t\t\tif (q == buf) {\n\t\t\t\t\tgoto resolve_error;\n\t\t\t\t}\n\t\t\t\tDUK__ASSERT(*(q - 1) == '/');\n\t\t\t\tq--;  /* Backtrack to last output slash (dups already eliminated). */\n\t\t\t\tfor (;;) {\n\t\t\t\t\t/* Backtrack to previous slash or start of buffer. */\n\t\t\t\t\tDUK__ASSERT(q >= buf);\n\t\t\t\t\tif (q == buf) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif (*(q - 1) == '/') {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tq--;\n\t\t\t\t}\n\t\t\t\tgoto eat_dup_slashes;\n\t\t\t}\n\t\t\tgoto resolve_error;\n\t\t} else if (c == '/') {\n\t\t\t/* e.g. require('/foo'), empty terms not allowed */\n\t\t\tgoto resolve_error;\n\t\t} else {\n\t\t\tfor (;;) {\n\t\t\t\t/* Copy term name until end or '/'. */\n\t\t\t\t*q++ = c;\n\t\t\t\tc = *p++;\n\t\t\t\tif (c == 0) {\n\t\t\t\t\t/* This was the last term, and q_last was\n\t\t\t\t\t * updated to match this term at loop top.\n\t\t\t\t\t */\n\t\t\t\t\tgoto loop_done;\n\t\t\t\t} else if (c == '/') {\n\t\t\t\t\t*q++ = '/';\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\t/* write on next loop */\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t eat_dup_slashes:\n\t\tfor (;;) {\n\t\t\t/* eat dup slashes */\n\t\t\tc = *p;\n\t\t\tif (c != '/') {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tp++;\n\t\t}\n\t}\n loop_done:\n\t/* Output #1: resolved absolute name. */\n\tDUK__ASSERT(q >= buf);\n\tduk_push_lstring(ctx, (const char *) buf, (size_t) (q - buf));\n\n\t/* Output #2: last component name. */\n\tDUK__ASSERT(q >= q_last);\n\tDUK__ASSERT(q_last >= buf);\n\tduk_push_lstring(ctx, (const char *) q_last, (size_t) (q - q_last));\n\treturn;\n\n resolve_error:\n\t(void) duk_type_error(ctx, \"cannot resolve module id: %s\", (const char *) req_id);\n}\n\n/* Stack indices for better readability. */\n#define DUK__IDX_REQUESTED_ID   0   /* module id requested */\n#define DUK__IDX_REQUIRE        1   /* current require() function */\n#define DUK__IDX_REQUIRE_ID     2   /* the base ID of the current require() function, resolution base */\n#define DUK__IDX_RESOLVED_ID    3   /* resolved, normalized absolute module ID */\n#define DUK__IDX_LASTCOMP       4   /* last component name in resolved path */\n#define DUK__IDX_DUKTAPE        5   /* Duktape object */\n#define DUK__IDX_MODLOADED      6   /* Duktape.modLoaded[] module cache */\n#define DUK__IDX_UNDEFINED      7   /* 'undefined', artifact of lookup */\n#define DUK__IDX_FRESH_REQUIRE  8   /* new require() function for module, updated resolution base */\n#define DUK__IDX_EXPORTS        9   /* default exports table */\n#define DUK__IDX_MODULE         10  /* module object containing module.exports, etc */\n\nstatic duk_ret_t duk__require(duk_context *ctx) {\n\tconst char *str_req_id;  /* requested identifier */\n\tconst char *str_mod_id;  /* require.id of current module */\n\tduk_int_t pcall_rc;\n\n\t/* NOTE: we try to minimize code size by avoiding unnecessary pops,\n\t * so the stack looks a bit cluttered in this function.  DUK__ASSERT_TOP()\n\t * assertions are used to ensure stack configuration is correct at each\n\t * step.\n\t */\n\n\t/*\n\t *  Resolve module identifier into canonical absolute form.\n\t */\n\n\tstr_req_id = duk_require_string(ctx, DUK__IDX_REQUESTED_ID);\n\tduk_push_current_function(ctx);\n\tduk_get_prop_string(ctx, -1, \"id\");\n\tstr_mod_id = duk_get_string(ctx, DUK__IDX_REQUIRE_ID);  /* ignore non-strings */\n\tduk__resolve_module_id(ctx, str_req_id, str_mod_id);\n\tstr_req_id = NULL;\n\tstr_mod_id = NULL;\n\n\t/* [ requested_id require require.id resolved_id last_comp ] */\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_LASTCOMP + 1);\n\n\t/*\n\t *  Cached module check.\n\t *\n\t *  If module has been loaded or its loading has already begun without\n\t *  finishing, return the same cached value (module.exports).  The\n\t *  value is registered when module load starts so that circular\n\t *  references can be supported to some extent.\n\t */\n\n\tduk_push_global_stash(ctx);\n\tduk_get_prop_string(ctx, -1, \"\\xff\" \"module:Duktape\");\n\tduk_remove(ctx, -2);  /* Lookup stashed, original 'Duktape' object. */\n\tduk_get_prop_string(ctx, DUK__IDX_DUKTAPE, \"modLoaded\");  /* Duktape.modLoaded */\n\tduk_require_type_mask(ctx, DUK__IDX_MODLOADED, DUK_TYPE_MASK_OBJECT);\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_MODLOADED + 1);\n\n\tduk_dup(ctx, DUK__IDX_RESOLVED_ID);\n\tif (duk_get_prop(ctx, DUK__IDX_MODLOADED)) {\n\t\t/* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded Duktape.modLoaded[id] ] */\n\t\tduk_get_prop_string(ctx, -1, \"exports\");  /* return module.exports */\n\t\treturn 1;\n\t}\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_UNDEFINED + 1);\n\n\t/* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined ] */\n\n\t/*\n\t *  Module not loaded (and loading not started previously).\n\t *\n\t *  Create a new require() function with 'id' set to resolved ID\n\t *  of module being loaded.  Also create 'exports' and 'module'\n\t *  tables but don't register exports to the loaded table yet.\n\t *  We don't want to do that unless the user module search callbacks\n\t *  succeeds in finding the module.\n\t */\n\n\t/* Fresh require: require.id is left configurable (but not writable)\n\t * so that is not easy to accidentally tweak it, but it can still be\n\t * done with Object.defineProperty().\n\t *\n\t * XXX: require.id could also be just made non-configurable, as there\n\t * is no practical reason to touch it (at least from ECMAScript code).\n\t */\n\tduk_push_c_function(ctx, duk__require, 1 /*nargs*/);\n\tduk_push_string(ctx, \"name\");\n\tduk_push_string(ctx, \"require\");\n\tduk_def_prop(ctx, DUK__IDX_FRESH_REQUIRE, DUK_DEFPROP_HAVE_VALUE);  /* not writable, not enumerable, not configurable */\n\tduk_push_string(ctx, \"id\");\n\tduk_dup(ctx, DUK__IDX_RESOLVED_ID);\n\tduk_def_prop(ctx, DUK__IDX_FRESH_REQUIRE, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_SET_CONFIGURABLE);  /* a fresh require() with require.id = resolved target module id */\n\n\t/* Module table:\n\t * - module.exports: initial exports table (may be replaced by user)\n\t * - module.id is non-writable and non-configurable, as the CommonJS\n\t *   spec suggests this if possible\n\t * - module.filename: not set, defaults to resolved ID if not explicitly\n\t *   set by modSearch() (note capitalization, not .fileName, matches Node.js)\n\t * - module.name: not set, defaults to last component of resolved ID if\n\t *   not explicitly set by modSearch()\n\t */\n\tduk_push_object(ctx);  /* exports */\n\tduk_push_object(ctx);  /* module */\n\tduk_push_string(ctx, \"exports\");\n\tduk_dup(ctx, DUK__IDX_EXPORTS);\n\tduk_def_prop(ctx, DUK__IDX_MODULE, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_SET_WRITABLE | DUK_DEFPROP_SET_CONFIGURABLE);  /* module.exports = exports */\n\tduk_push_string(ctx, \"id\");\n\tduk_dup(ctx, DUK__IDX_RESOLVED_ID);  /* resolved id: require(id) must return this same module */\n\tduk_def_prop(ctx, DUK__IDX_MODULE, DUK_DEFPROP_HAVE_VALUE);  /* module.id = resolved_id; not writable, not enumerable, not configurable */\n\tduk_compact(ctx, DUK__IDX_MODULE);  /* module table remains registered to modLoaded, minimize its size */\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 1);\n\n\t/* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module ] */\n\n\t/* Register the module table early to modLoaded[] so that we can\n\t * support circular references even in modSearch().  If an error\n\t * is thrown, we'll delete the reference.\n\t */\n\tduk_dup(ctx, DUK__IDX_RESOLVED_ID);\n\tduk_dup(ctx, DUK__IDX_MODULE);\n\tduk_put_prop(ctx, DUK__IDX_MODLOADED);  /* Duktape.modLoaded[resolved_id] = module */\n\n\t/*\n\t *  Call user provided module search function and build the wrapped\n\t *  module source code (if necessary).  The module search function\n\t *  can be used to implement pure Ecmacsript, pure C, and mixed\n\t *  ECMAScript/C modules.\n\t *\n\t *  The module search function can operate on the exports table directly\n\t *  (e.g. DLL code can register values to it).  It can also return a\n\t *  string which is interpreted as module source code (if a non-string\n\t *  is returned the module is assumed to be a pure C one).  If a module\n\t *  cannot be found, an error must be thrown by the user callback.\n\t *\n\t *  Because Duktape.modLoaded[] already contains the module being\n\t *  loaded, circular references for C modules should also work\n\t *  (although expected to be quite rare).\n\t */\n\n\tduk_push_string(ctx, \"(function(require,exports,module){\");\n\n\t/* Duktape.modSearch(resolved_id, fresh_require, exports, module). */\n\tduk_get_prop_string(ctx, DUK__IDX_DUKTAPE, \"modSearch\");  /* Duktape.modSearch */\n\tduk_dup(ctx, DUK__IDX_RESOLVED_ID);\n\tduk_dup(ctx, DUK__IDX_FRESH_REQUIRE);\n\tduk_dup(ctx, DUK__IDX_EXPORTS);\n\tduk_dup(ctx, DUK__IDX_MODULE);  /* [ ... Duktape.modSearch resolved_id last_comp fresh_require exports module ] */\n\tpcall_rc = duk_pcall(ctx, 4 /*nargs*/);  /* -> [ ... source ] */\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 3);\n\n\tif (pcall_rc != DUK_EXEC_SUCCESS) {\n\t\t/* Delete entry in Duktape.modLoaded[] and rethrow. */\n\t\tgoto delete_rethrow;\n\t}\n\n\t/* If user callback did not return source code, module loading\n\t * is finished (user callback initialized exports table directly).\n\t */\n\tif (!duk_is_string(ctx, -1)) {\n\t\t/* User callback did not return source code, so module loading\n\t\t * is finished: just update modLoaded with final module.exports\n\t\t * and we're done.\n\t\t */\n\t\tgoto return_exports;\n\t}\n\n\t/* Finish the wrapped module source.  Force module.filename as the\n\t * function .fileName so it gets set for functions defined within a\n\t * module.  This also ensures loggers created within the module get\n\t * the module ID (or overridden filename) as their default logger name.\n\t * (Note capitalization: .filename matches Node.js while .fileName is\n\t * used elsewhere in Duktape.)\n\t */\n\tduk_push_string(ctx, \"\\n})\");  /* Newline allows module last line to contain a // comment. */\n\tduk_concat(ctx, 3);\n\tif (!duk_get_prop_string(ctx, DUK__IDX_MODULE, \"filename\")) {\n\t\t/* module.filename for .fileName, default to resolved ID if\n\t\t * not present.\n\t\t */\n\t\tduk_pop(ctx);\n\t\tduk_dup(ctx, DUK__IDX_RESOLVED_ID);\n\t}\n\tpcall_rc = duk_pcompile(ctx, DUK_COMPILE_EVAL);\n\tif (pcall_rc != DUK_EXEC_SUCCESS) {\n\t\tgoto delete_rethrow;\n\t}\n\tpcall_rc = duk_pcall(ctx, 0);  /* -> eval'd function wrapper (not called yet) */\n\tif (pcall_rc != DUK_EXEC_SUCCESS) {\n\t\tgoto delete_rethrow;\n\t}\n\n\t/* Module has now evaluated to a wrapped module function.  Force its\n\t * .name to match module.name (defaults to last component of resolved\n\t * ID) so that it is shown in stack traces too.  Note that we must not\n\t * introduce an actual name binding into the function scope (which is\n\t * usually the case with a named function) because it would affect the\n\t * scope seen by the module and shadow accesses to globals of the same name.\n\t * This is now done by compiling the function as anonymous and then forcing\n\t * its .name without setting a \"has name binding\" flag.\n\t */\n\n\tduk_push_string(ctx, \"name\");\n\tif (!duk_get_prop_string(ctx, DUK__IDX_MODULE, \"name\")) {\n\t\t/* module.name for .name, default to last component if\n\t\t * not present.\n\t\t */\n\t\tduk_pop(ctx);\n\t\tduk_dup(ctx, DUK__IDX_LASTCOMP);\n\t}\n\tduk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_FORCE);\n\n\t/*\n\t *  Call the wrapped module function.\n\t *\n\t *  Use a protected call so that we can update Duktape.modLoaded[resolved_id]\n\t *  even if the module throws an error.\n\t */\n\n\t/* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module mod_func ] */\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 2);\n\n\tduk_dup(ctx, DUK__IDX_EXPORTS);  /* exports (this binding) */\n\tduk_dup(ctx, DUK__IDX_FRESH_REQUIRE);  /* fresh require (argument) */\n\tduk_get_prop_string(ctx, DUK__IDX_MODULE, \"exports\");  /* relookup exports from module.exports in case it was changed by modSearch */\n\tduk_dup(ctx, DUK__IDX_MODULE);  /* module (argument) */\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 6);\n\n\t/* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module mod_func exports fresh_require exports module ] */\n\n\tpcall_rc = duk_pcall_method(ctx, 3 /*nargs*/);\n\tif (pcall_rc != DUK_EXEC_SUCCESS) {\n\t\t/* Module loading failed.  Node.js will forget the module\n\t\t * registration so that another require() will try to load\n\t\t * the module again.  Mimic that behavior.\n\t\t */\n\t\tgoto delete_rethrow;\n\t}\n\n\t/* [ requested_id require require.id resolved_id last_comp Duktape Duktape.modLoaded undefined fresh_require exports module result(ignored) ] */\n\tDUK__ASSERT_TOP(ctx, DUK__IDX_MODULE + 2);\n\n\t/* fall through */\n\n return_exports:\n\tduk_get_prop_string(ctx, DUK__IDX_MODULE, \"exports\");\n\tduk_compact(ctx, -1);  /* compact the exports table */\n\treturn 1;  /* return module.exports */\n\n delete_rethrow:\n\tduk_dup(ctx, DUK__IDX_RESOLVED_ID);\n\tduk_del_prop(ctx, DUK__IDX_MODLOADED);  /* delete Duktape.modLoaded[resolved_id] */\n\t(void) duk_throw(ctx);  /* rethrow original error */\n\treturn 0;  /* not reachable */\n}\n\nvoid duk_module_duktape_init(duk_context *ctx) {\n\t/* Stash 'Duktape' in case it's modified. */\n\tduk_push_global_stash(ctx);\n\tduk_get_global_string(ctx, \"Duktape\");\n\tduk_put_prop_string(ctx, -2, \"\\xff\" \"module:Duktape\");\n\tduk_pop(ctx);\n\n\t/* Register `require` as a global function. */\n\tduk_eval_string(ctx,\n\t\t\"(function(req){\"\n\t\t\"var D=Object.defineProperty;\"\n\t\t\"D(req,'name',{value:'require'});\"\n\t\t\"D(this,'require',{value:req,writable:true,configurable:true});\"\n\t\t\"D(Duktape,'modLoaded',{value:Object.create(null),writable:true,configurable:true});\"\n\t\t\"})\");\n\tduk_push_c_function(ctx, duk__require, 1 /*nargs*/);\n\tduk_call(ctx, 1);\n\tduk_pop(ctx);\n}\n\n#undef DUK__ASSERT\n#undef DUK__ASSERT_TOP\n#undef DUK__IDX_REQUESTED_ID\n#undef DUK__IDX_REQUIRE\n#undef DUK__IDX_REQUIRE_ID\n#undef DUK__IDX_RESOLVED_ID\n#undef DUK__IDX_LASTCOMP\n#undef DUK__IDX_DUKTAPE\n#undef DUK__IDX_MODLOADED\n#undef DUK__IDX_UNDEFINED\n#undef DUK__IDX_FRESH_REQUIRE\n#undef DUK__IDX_EXPORTS\n#undef DUK__IDX_MODULE\n"
  },
  {
    "path": "src/plugins/duktape-deps/duk_module_duktape.h",
    "content": "#if !defined(DUK_MODULE_DUKTAPE_H_INCLUDED)\n#define DUK_MODULE_DUKTAPE_H_INCLUDED\n\n#include \"duktape.h\"\n\n#if defined(__cplusplus)\nextern \"C\" {\n#endif\n\n/* Maximum length of CommonJS module identifier to resolve.  Length includes\n * both current module ID, requested (possibly relative) module ID, and a\n * slash in between.\n */\n#define  DUK_COMMONJS_MODULE_ID_LIMIT  256\n\nextern void duk_module_duktape_init(duk_context *ctx);\n\n#if defined(__cplusplus)\n}\n#endif  /* end 'extern \"C\"' wrapper */\n\n#endif  /* DUK_MODULE_DUKTAPE_H_INCLUDED */\n"
  },
  {
    "path": "src/plugins/janus_audiobridge.c",
    "content": "/*! \\file   janus_audiobridge.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus AudioBridge plugin\n * \\details Check the \\ref audiobridge for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page audiobridge AudioBridge plugin documentation\n * This is a plugin implementing an audio conference bridge for\n * Janus, specifically mixing Opus streams. This means that it replies\n * by providing in the SDP only support for Opus, and disabling video.\n * Opus encoding and decoding is implemented using libopus (http://opus.codec.org).\n * The plugin provides an API to allow peers to join and leave conference\n * rooms. Peers can then mute/unmute themselves by sending specific messages\n * to the plugin: any way a peer mutes/unmutes, an event is triggered\n * to the other participants, so that it can be rendered in the UI\n * accordingly.\n *\n * Rooms to make available are listed in the plugin configuration file.\n * A pre-filled configuration file is provided in \\c conf/janus.plugin.audiobridge.jcfg\n * and includes a demo room for testing.\n *\n * To add more rooms or modify the existing one, you can use the following\n * syntax:\n *\n * \\verbatim\nroom-<unique room ID>: {\n\tdescription = This is my awesome room\n\tis_private = true|false (private rooms don't appear when you do a 'list' request)\n\tsecret = <optional password needed for manipulating (e.g. destroying) the room>\n\tpin = <optional password needed for joining the room>\n\tsampling_rate = <sampling rate> (e.g., 16000 for wideband mixing)\n\tspatial_audio = true|false (if true, the mix will be stereo to spatially place users, default=false)\n\taudiolevel_ext = true|false (whether the ssrc-audio-level RTP extension must be\n\t\tnegotiated/used or not for new joins, default=true)\n\taudiolevel_event = true|false (whether to emit event to other users or not, default=false)\n\taudio_active_packets = 100 (number of packets with audio level, default=100, 2 seconds)\n\taudio_level_average = 25 (average value of audio level, 127=muted, 0='too loud', default=25)\n\tdefault_expectedloss = percent of packets we expect participants may miss, to help with outgoing FEC (default=0, max=20; automatically used for forwarders too)\n\tdefault_bitrate = default bitrate in bps to use for the all participants (default=0, which means libopus decides; automatically used for forwarders too)\n\tdenoise = true|false (whether denoising via RNNoise should be performed for each participant by default)\n\trecord = true|false (whether this room should be recorded, default=false)\n\trecord_file = /path/to/recording.wav (where to save the recording)\n\trecord_dir = /path/to/ (path to save the recording to, makes record_file a relative path if provided)\n\tmjrs = true|false (whether all participants in the room should be individually recorded to mjr files, default=false)\n\tmjrs_dir = \"/path/to/\" (path to save the mjr files to)\n\tallow_rtp_participants = true|false (whether participants should be allowed to join\n\t\tvia plain RTP as well, rather than just WebRTC, default=false)\n\tgroups = optional, non-hierarchical, array of groups to tag participants, for external forwarding purposes only\n\n\t\t[The following lines are only needed if you want the mixed audio\n\t\tto be automatically forwarded via plain RTP to an external component\n\t\t(e.g., an ffmpeg script, or a gstreamer pipeline) for processing.\n\t\tBy default plain RTP is used, SRTP must be configured if needed]\n\trtp_forward_id = numeric RTP forwarder ID for referencing it via API (optional: random ID used if missing)\n\trtp_forward_host = host address to forward RTP packets of mixed audio to\n\trtp_forward_host_family = ipv4|ipv6; by default, first family returned by DNS request\n\trtp_forward_port = port to forward RTP packets of mixed audio to\n\trtp_forward_ssrc = SSRC to use to use when streaming (optional: stream_id used if missing)\n\trtp_forward_codec = opus (default), pcma (A-Law) or pcmu (mu-Law)\n\trtp_forward_ptype = payload type to use when streaming (optional: only read for Opus, 100 used if missing)\n\trtp_forward_group = group of participants to forward, if enabled in the room (optional: forwards full mix if missing)\n\trtp_forward_srtp_suite = length of authentication tag, if SRTP is needed (32 or 80)\n\trtp_forward_srtp_crypto = key to use as crypto, if SRTP is needed (base64 encoded key as in SDES)\n\trtp_forward_always_on = true|false, whether silence should be forwarded when the room is empty (optional: false used if missing)\n}\n\\endverbatim\n *\n * \\section bridgeapi Audio Bridge API\n *\n * The Audio Bridge API supports several requests, some of which are\n * synchronous and some asynchronous. There are some situations, though,\n * (invalid JSON, invalid request) which will always result in a\n * synchronous error response even for asynchronous requests.\n *\n * \\c create, \\c edit, \\c destroy, \\c exists, \\c allowed, \\c kick, \\c list,\n * \\c mute, \\c unmute, \\c mute_room, \\c unmute_room, \\c listparticipants,\n * \\c listannouncements, \\c resetdecoder, \\c rtp_forward, \\c stop_rtp_forward,\n * \\c list_forwarders, \\c play_file, \\c is_playing, \\c stop_file and \\c stop_all_files\n * are synchronous requests, which means you'll get a response directly within\n * the context of the transaction. \\c create allows you to create a new audio\n * conference bridge dynamically, as an alternative to using the configuration file;\n * \\c edit allows you to dynamically edit some room properties (e.g., the PIN);\n * \\c destroy removes an audio conference bridge and destroys it, kicking\n * all the users out as part of the process; \\c exists allows you to\n * check whether a specific audio conference exists; \\c allowed allows\n * you to edit who's allowed to join a room via ad-hoc tokens; \\c list\n * lists all the available rooms, while \\c listparticipants lists all\n * the participants of a specific room and their details; \\c listannouncements\n * lists all playing announcements of a specific room and their details;\n * \\c resetdecoder marks the Opus decoder for the participant as invalid, and\n * forces it to be recreated (which might be needed if the audio for generated\n * by the participant becomes garbled); \\c rtp_forward allows you to forward\n * the mix of an AudioBridge room via RTP to a separate component (e.g.,\n * for broadcasting it to a wider audience, or for processing/recording),\n * whereas \\c stop_rtp_forward can remove an existing forwarder; a list\n * of configured forwarders for a room can be retrieved using the\n * \\c list_forwarders request; finally, \\c play_file allows you to\n * reproduce an audio .opus file in a mix (e.g., to play an announcement\n * or some background music), \\c is_playing checks if a specific file is\n * still playing, while \\c stop_file will stop such a playback instead and\n * \\c stop_all_files will stop all announcements.\n *\n * The \\c join , \\c configure , \\c changeroom and \\c leave requests\n * instead are all asynchronous, which means you'll get a notification\n * about their success or failure in an event. \\c join allows you to\n * join a specific audio conference bridge; \\c configure can be used\n * to modify some of the participation settings (e.g., mute/unmute);\n * \\c changeroom can be used to leave the current room and move to a\n * different one without having to tear down the PeerConnection and\n * recreate it again (useful for sidebars and \"waiting rooms\"); finally,\n * \\c leave allows you to leave an audio conference bridge for good.\n *\n * The AudioBridge plugin also allows you to forward the mix to an\n * external listener, e.g., a gstreamer/ffmpeg pipeline waiting to\n * process the mixer audio stream. You can add new RTP forwarders with\n * the \\c rtp_forward request; a \\c stop_rtp_forward request removes an\n * existing RTP forwarder; \\c listforwarders lists all the current RTP\n * forwarders on a specific AudioBridge room instance. As an alternative,\n * you can configure a single static RTP forwarder in the plugin\n * configuration file. A finer grained control of what to forward\n * externally, in terms of participants mix, can be achieved using\n * groups.\n *\n * \\c create can be used to create a new audio room, and has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"create\",\n\t\"room\" : <unique numeric ID, optional, chosen by plugin if missing>,\n\t\"permanent\" : <true|false, whether the room should be saved in the config file, default=false>,\n\t\"description\" : \"<pretty name of the room, optional>\",\n\t\"secret\" : \"<password required to edit/destroy the room, optional>\",\n\t\"pin\" : \"<password required to join the room, optional>\",\n\t\"is_private\" : <true|false, whether the room should appear in a list request>,\n\t\"allowed\" : [ array of string tokens users can use to join this room, optional],\n\t\"sampling_rate\" : <sampling rate of the room, optional, 16000 by default>,\n\t\"spatial_audio\" : <true|false, whether the mix should spatially place users, default=false>,\n\t\"audiolevel_ext\" : <true|false, whether the ssrc-audio-level RTP extension must be negotiated for new joins, default=true>,\n\t\"audiolevel_event\" : <true|false (whether to emit event to other users or not)>,\n\t\"audio_active_packets\" : <number of packets with audio level (default=100, 2 seconds)>,\n\t\"audio_level_average\" : <average value of audio level (127=muted, 0='too loud', default=25)>,\n\t\"default_expectedloss\" : <percent of packets we expect participants may miss, to help with outgoing FEC (default=0, max=20; automatically used for forwarders too)>,\n\t\"default_bitrate\" : <bitrate in bps to use for the all participants (default=0, which means libopus decides; automatically used for forwarders too)>,\n\t\"denoise\" : <true|false, whether denoising via RNNoise should be performed for each participant by default, default=false>,\n\t\"record\" : <true|false, whether to record the room or not, default=false>,\n\t\"record_file\" : \"</path/to/the/recording.wav, optional>\",\n\t\"record_dir\" : \"</path/to/, optional; makes record_file a relative path, if provided>\",\n\t\"mjrs\" : <true|false (whether all participants in the room should be individually recorded to mjr files, default=false)>,\n\t\"mjrs_dir\" : \"</path/to/, optional>\",\n\t\"allow_rtp_participants\" : <true|false, whether participants should be allowed to join via plain RTP as well, default=false>,\n\t\"groups\" : [ non-hierarchical array of string group names to use to gat participants, for external forwarding purposes only, optional]\n}\n\\endverbatim\n *\n * A successful creation procedure will result in a \\c created response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"created\",\n\t\"room\" : <unique numeric ID>,\n\t\"permanent\" : <true if saved to config file, false if not>\n}\n\\endverbatim\n *\n * If you requested a permanent room but a \\c false value is returned\n * instead, good chances are that there are permission problems.\n *\n * An error instead (and the same applies to all other requests, so this\n * won't be repeated) would provide both an error code and a more verbose\n * description of the cause of the issue:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"event\",\n\t\"error_code\" : <numeric ID, check Macros below>,\n\t\"error\" : \"<error description as a string>\"\n}\n\\endverbatim\n *\n * Notice that, in general, all users can create rooms. If you want to\n * limit this functionality, you can configure an admin \\c admin_key in\n * the plugin settings. When configured, only \"create\" requests that\n * include the correct \\c admin_key value in an \"admin_key\" property\n * will succeed, and will be rejected otherwise. Notice that you can\n * optionally extend this functionality to RTP forwarding as well, in\n * order to only allow trusted clients to use that feature.\n *\n * Once a room has been created, you can still edit some (but not all)\n * of its properties using the \\c edit request. This allows you to modify\n * the room description, secret, pin and whether it's private or not: you\n * won't be able to modify other more static properties, like the room ID,\n * the sampling rate, the extensions-related stuff and so on. If you're\n * interested in changing the ACL, instead, check the \\c allowed message.\n * An \\c edit request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"edit\",\n\t\"room\" : <unique numeric ID of the room to edit>,\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"new_description\" : \"<new pretty name of the room, optional>\",\n\t\"new_secret\" : \"<new password required to edit/destroy the room, optional>\",\n\t\"new_pin\" : \"<new PIN required to join the room, PIN will be removed if set to an empty string, optional>\",\n\t\"new_is_private\" : <true|false, whether the room should appear in a list request>,\n\t\"new_record_dir\" : \"<new path where new recording files should be saved>\",\n\t\"new_mjrs_dir\" : \"<new path where new MJR files should be saved>\",\n\t\"permanent\" : <true|false, whether the room should be also removed from the config file, default=false>\n}\n\\endverbatim\n *\n * A successful edit procedure will result in an \\c edited response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"edited\",\n\t\"room\" : <unique numeric ID>\n}\n\\endverbatim\n *\n * On the other hand, \\c destroy can be used to destroy an existing audio\n * room, whether created dynamically or statically, and has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"destroy\",\n\t\"room\" : <unique numeric ID of the room to destroy>,\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"permanent\" : <true|false, whether the room should be also removed from the config file, default=false>\n}\n\\endverbatim\n *\n * A successful destruction procedure will result in a \\c destroyed response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"destroyed\",\n\t\"room\" : <unique numeric ID>,\n\t\"permanent\" : <true|false, whether the room is also removed from the config file>\n}\n\\endverbatim\n *\n * This will also result in a \\c destroyed event being sent to all the\n * participants in the audio room, which will look like this:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"destroyed\",\n\t\"room\" : <unique numeric ID of the destroyed room>\n}\n\\endverbatim\n *\n * To enable or disable recording of mixed audio stream while the conference\n * is in progress, you can make use of the \\c enable_recording request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"enable_recording\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"secret\" : \"<room secret; mandatory if configured>\"\n\t\"record\" : <true|false, whether this room should be automatically recorded or not>,\n\t\"record_file\" : \"<file where audio recording should be saved (optional)>\",\n\t\"record_dir\" : \"<path where audio recording file should be saved (optional)>\"\n}\n\\endverbatim\n *\n * A room can also be recorded by saving the individual contributions of\n * participants to separate MJR files instead, in a format compatible with\n * the \\ref recordings. While a recording for each participant can be\n * enabled or disabled separately, there also is a request to enable or\n * disable them in bulk, thus implementing a feature similar to \\c enable_recording\n * but for MJR files, rather than for a \\c .wav mix. This can be done using\n * the \\c enable_mjrs request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"enable_mjrs\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"secret\" : \"<room secret; mandatory if configured>\"\n\t\"mjrs\" : <true|false, whether all participants in the room should be individually recorded to mjr files or not>,\n\t\"mjrs_dir\" : \"<path where all MJR files should be saved to (optional)>\"\n}\n\\endverbatim\n *\n *\n * You can check whether a room exists using the \\c exists request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"exists\",\n\t\"room\" : <unique numeric ID of the room to check>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"exists\" : <true|false>\n}\n\\endverbatim\n *\n * You can configure whether to check tokens or add/remove people who can join\n * a room using the \\c allowed request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"allowed\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"action\" : \"enable|disable|add|remove\",\n\t\"room\" : <unique numeric ID of the room to update>,\n\t\"allowed\" : [\n\t\t// Array of strings (tokens users might pass in \"join\", only for add|remove)\n\t]\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"allowed\" : [\n\t\t// Updated, complete, list of allowed tokens (only for enable|add|remove)\n\t]\n}\n\\endverbatim\n *\n * If you're the administrator of a room (that is, you created it and have access\n * to the secret) you can kick participants using the \\c kick request. Notice\n * that this only kicks the user out of the room, but does not prevent them from\n * re-joining: to ban them, you need to first remove them from the list of\n * authorized users (see \\c allowed request) and then \\c kick them. The \\c kick\n * request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"kick\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"id\" : <unique numeric ID of the participant to kick>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n}\n\\endverbatim\n *\n * If you're the administrator of a room (that is, you created it and have access\n * to the secret) you can kick all participants using the \\c kick_all request. Notice\n * that this only kicks all users out of the room, but does not prevent them from\n * re-joining: to ban them, you need to first remove them from the list of\n * authorized users (see \\c allowed request) and then perform \\c kick_all.\n * The \\c kick_all request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"kick_all\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n}\n\\endverbatim\n *\n * Another option available for administrators is suspending participants:\n * in that case, participants are not kicked from the room (they remain\n * in), but their contribution is not added to the mix, and they don't\n * receive any audio from the mix either. This is a useful option to\n * temporarily detach a participant from the room (e.g., because they'll\n * be busy somewhere else) while still allowing them to keep the existing\n * PeerConnection up and running, so that it can be quickly restored\n * when they're back; since they're not part of the mix and don't receive\n * any audio, the CPU resources to manage them are reduced as well. By\n * default these suspended users participants will still receive events\n * related to changes in the room (e.g., participants joining and leaving,\n * mutes and unmutes, etc.), but these can be disabled too in case saving\n * unnecessary signalling is desired: in that case, a suspended participant\n * will only receive a recap of the current status when resumed.\n * The \\c suspend request must be formatted as follows:\n *\n *\n\\verbatim\n{\n\t\"request\" : \"suspend\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"id\" : <unique numeric ID of the participant to suspend>,\n\t\"pause_events\" : <whether room events should be paused while suspended; optional, false by default>\n\t\"stop_record\" : <whether the MIR recording of this participant should be stopped too; optional, false by default>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n}\n\\endverbatim\n *\n * Resuming a suspended participant means bringing them back in the audio\n * mix, and allowing them to hear audio through the PeerConnection once more.\n * In case events were paused, they'll be resumed and a recap will be sent.\n * The \\c resume request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"resume\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"id\" : <unique numeric ID of the suspended participant to resume>,\n\t\"record\": <true|false, whether to record this resumed user's contribution to a .mjr file (mixer not involved); optional, false by default>,\n\t\"filename\": \"<basename of the file to record to, -audio.mjr will be added by the plugin; optional, will be relative to mjrs_dir, if configured in the room>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n}\n\\endverbatim\n *\n * Both \\c suspend and \\c resume on a participant will result in a\n * notification to the other participants in the room, which means they'll\n * all be notified about a participant being suspended or resumed.\n *\n * To get a list of the available rooms (excluded those configured or\n * created as private rooms) you can make use of the \\c list request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"list\"\n}\n\\endverbatim\n *\n * A successful request will produce a list of rooms in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"rooms\" : [\t\t// Array of room objects\n\t\t{\t// Room #1\n\t\t\t\"room\" : <unique numeric ID>,\n\t\t\t\"description\" : \"<Name of the room>\",\n\t\t\t\"pin_required\" : <true|false, whether a PIN is required to join this room>,\n\t\t\t\"sampling_rate\" : <sampling rate of the mixer>,\n\t\t\t\"spatial_audio\" : <true|false, whether the mix has spatial audio (stereo)>,\n\t\t\t\"record\" : <true|false, whether the room is being recorded>,\n\t\t\t\"num_participants\" : <count of the participants>\n\t\t},\n\t\t// Other rooms\n\t]\n}\n\\endverbatim\n *\n * To get a list of the available rooms (excluded those configured or\n * created as private rooms) you can make use of the \\c list request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"list\"\n}\n\\endverbatim\n *\n * A successful request will produce a list of rooms in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"rooms\" : [\t\t// Array of room objects\n\t\t{\t// Room #1\n\t\t\t\"room\" : <unique numeric ID>,\n\t\t\t\"description\" : \"<Name of the room>\",\n\t\t\t\"pin_required\" : <true|false, whether a PIN is required to join this room>,\n\t\t\t\"sampling_rate\" : <sampling rate of the mixer>,\n\t\t\t\"spatial_audio\" : <true|false, whether the mix has spatial audio (stereo)>,\n\t\t\t\"record\" : <true|false, whether the room is being recorded>,\n\t\t\t\"num_participants\" : <count of the participants>\n\t\t},\n\t\t// Other rooms\n\t]\n}\n\\endverbatim\n *\n * To get a list of the participants in a specific room, instead, you\n * can make use of the \\c listparticipants request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listparticipants\",\n\t\"room\" : <unique numeric ID of the room>\n}\n\\endverbatim\n *\n * A successful request will produce a list of participants in a\n * \\c participants response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"participants\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"participants\" : [\t\t// Array of participant objects\n\t\t{\t// Participant #1\n\t\t\t\"id\" : <unique numeric ID of the participant>,\n\t\t\t\"display\" : \"<display name of the participant, if any; optional>\",\n\t\t\t\"setup\" : <true|false, whether user successfully negotiate a WebRTC PeerConnection or not>,\n\t\t\t\"muted\" : <true|false, whether user is muted or not>,\n\t\t\t\"suspended\" : <true|false, whether user is suspended or not>,\n\t\t\t\"talking\" : <true|false, whether user is talking or not (only if audio levels are used)>,\n\t\t\t\"spatial_position\" : <in case spatial audio is used, the panning of this participant (0=left, 50=center, 100=right)>,\n\t\t},\n\t\t// Other participants\n\t]\n}\n\\endverbatim\n *\n * To mark the Opus decoder context for the current participant as\n * invalid and force it to be recreated, use the \\c resetdecoder request:\n *\n\\verbatim\n{\n\t\"request\" : \"resetdecoder\"\n}\n\\endverbatim\n *\n * A successful request will produce a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\"\n}\n\\endverbatim\n *\n * You can add a new RTP forwarder for an existing room using the\n * \\c rtp_forward request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"rtp_forward\",\n\t\"room\" : <unique numeric ID of the room to add the forwarder to>,\n\t\"group\" : \"<group to forward, if enabled in the room (forwards full mix if missing)>\",\n\t\"ssrc\" : <SSRC to use to use when streaming (optional: stream_id used if missing)>,\n\t\"codec\" : \"<opus (default), pcma (A-Law) or pcmu (mu-Law)>\",\n\t\"ptype\" : <payload type to use when streaming (optional: 100 used if missing)>,\n\t\"host\" : \"<host address to forward the RTP packets to>\",\n\t\"host_family\" : \"<ipv4|ipv6, if we need to resolve the host address to an IP; by default, whatever we get>\",\n\t\"port\" : <port to forward the RTP packets to>,\n\t\"srtp_suite\" : <length of authentication tag (32 or 80); optional>,\n\t\"srtp_crypto\" : \"<key to use as crypto (base64 encoded key as in SDES); optional>\",\n\t\"always_on\" : <true|false, whether silence should be forwarded when the room is empty>\n}\n\\endverbatim\n *\n * The concept of \"groups\" is particularly important, here, in case groups were\n * enabled when creating a room. By default, in fact, if a room has groups disabled,\n * then an RTP forwarder will simply relay the mix of all active participants;\n * sometimes, though, an external application may want to only receive the mix\n * of some of the participants, and not all of them. This is what groups are\n * for: if you tag participants with a specific group name, then creating a\n * new forwarder that explicitly references that group name will ensure that\n * only a mix of the participants tagged with that name will be forwarded.\n * As such, it's important to point out groups \\b only impact forwarders,\n * and \\c NOT participants or how they're mixed in main mix for the room itself.\n * Omitting a group name when creating a forwarder for a room where groups\n * are enabled will simply fall back to the default behaviour of forwarding\n * the full mix.\n *\n * Notice that, as explained above, in case you configured an \\c admin_key\n * property and extended it to RTP forwarding as well, you'll need to provide\n * it in the request as well or it will be rejected as unauthorized. By\n * default no limitation is posed on \\c rtp_forward .\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"group\" : \"<group to forward, same as request if provided>\",\n\t\"stream_id\" : <unique numeric ID assigned to the new RTP forwarder>,\n\t\"host\" : \"<host this forwarder is streaming to, same as request if not resolved>\",\n\t\"port\" : <audio port this forwarder is streaming to, same as request>\n}\n\\endverbatim\n *\n * To stop a previously created RTP forwarder and stop it, you can use\n * the \\c stop_rtp_forward request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"stop_rtp_forward\",\n\t\"room\" : <unique numeric ID of the room to remove the forwarder from>,\n\t\"stream_id\" : <unique numeric ID of the RTP forwarder>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"stream_id\" : <unique numeric ID, same as request>\n}\n\\endverbatim\n *\n * To get a list of the forwarders in a specific room, instead, you\n * can make use of the \\c listforwarders request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listforwarders\",\n\t\"room\" : <unique numeric ID of the room>\n}\n\\endverbatim\n *\n * A successful request will produce a list of RTP forwarders in a\n * \\c forwarders response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"forwarders\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"rtp_forwarders\" : [\t\t// Array of RTP forwarder objects\n\t\t{\t// RTP forwarder #1\n\t\t\t\"stream_id\" : <unique numeric ID of the forwarder>,\n\t\t\t\"group\" : \"<group that is being forwarded, if available>\",\n\t\t\t\"ip\" : \"<IP this forwarder is streaming to>\",\n\t\t\t\"port\" : <port this forwarder is streaming to>,\n\t\t\t\"ssrc\" : <SSRC this forwarder is using, if any>,\n\t\t\t\"codec\" : <codec this forwarder is using, if any>,\n\t\t\t\"ptype\" : <payload type this forwarder is using, if any>,\n\t\t\t\"srtp\" : <true|false, whether the RTP stream is encrypted>,\n\t\t\t\"always_on\" : <true|false, whether this forwarder works even when no participant is in or not>\n\t\t},\n\t\t// Other forwarders\n\t]\n}\n\\endverbatim\n *\n * As anticipated, while the AudioBridge is mainly meant to allow real users\n * to interact with each other by mixing their contributions, you can also\n * start the playback of one or more pre-recorded audio files in a mix:\n * this is especially useful whenever you have, for instance, to play\n * an announcement of some sort, or when maybe you want to play some\n * background music (e.g., some music on hold when the room is empty).\n * You can start the playback of an .opus file in an existing room using\n * the \\c play_file request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"play_file\",\n\t\"room\" : <unique numeric ID of the room to play the file in>,\n\t\"secret\" : \"<room password, if configured>\",\n\t\"group\" : \"<group to play in (for forwarding purposes only; optional, mandatory if enabled in the room)>\",\n\t\"file_id\": \"<unique string ID of the announcement; random if not provided>\",\n\t\"filename\": \"<path to the Opus file to play>\",\n\t\"loop\": <true|false, depending on whether or not the file should be played in a loop forever>\n}\n\\endverbatim\n *\n * Notice that, as explained above, in case you configured an \\c admin_key\n * property and extended it to RTP forwarding as well, you'll need to provide\n * it in the request as well or it will be rejected as unauthorized. By\n * default \\c play_file only requires the room secret, meaning only people\n * authorized to edit the room can start an audio playback.\n *\n * Also notice that the only supported files are .opus files: no other\n * audio format will be accepted. Besides, the file must be reachable\n * and available on the file system: network addresses (e.g., HTTP URL)\n * are NOT supported.\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"file_id\" : \"<unique string ID of the announcement, same as request if provided or randomly generated otherwise>\"\n}\n\\endverbatim\n *\n * As soon as the playback actually starts (usually immediately after\n * the request has been sent), an event is sent to all participants so\n * that they're aware something is being played back in the room besides\n * themselves:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"announcement-started\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"file_id\" : \"<unique string ID of the announcement>\"\n}\n\\endverbatim\n *\n * A similar event is also sent whenever the playback stops, whether it's\n * because the file ended and \\c loop was \\c FALSE (which will automatically\n * clear the resources) or because a \\c stop_file request asked for the\n * playback to be interrupted or \\c stop_all_files request asked for all\n * playbacks to be interrupted:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"announcement-stopped\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"file_id\" : \"<unique string ID of the announcement>\"\n}\n\\endverbatim\n *\n * You can check whether a specific playback is still going on in a room,\n * you can use the \\c is_playing request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"is_playing\",\n\t\"room\" : <unique numeric ID of the room where the playback is taking place>,\n\t\"secret\" : \"<room password, if configured>\",\n\t\"file_id\" : \"<unique string ID of the announcement>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"file_id\" : \"<unique string ID of the announcement>\",\n\t\"playing\" : <true|false>\n}\n\\endverbatim\n *\n * To get a list of the announcements in a specific room, you\n * can make use of the \\c listannouncements request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listannouncements\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>\n}\n\\endverbatim\n *\n * A successful request will produce a list of announcements in a\n * \\c announcements response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"announcements\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"announcements\" : [\t\t// Array of announcement objects\n\t\t{\t// Announcement #1\n\t\t\t\"file_id\" : \"<unique string ID of the announcement>\",\n\t\t\t\"filename\": \"<path to the Opus file to play>\",\n\t\t\t\"playing\" : <true|false, whether or not the file is playing>,\n\t\t\t\"loop\": <true|false, depending on whether or not the file is playing in a loop forever>\n\t\t}\n\t\t// Other announcements\n\t]\n}\n\\endverbatim\n *\n * As anticipated, when not looping a playback will automatically stop and\n * self-destruct when it reaches the end of the audio file. In case you\n * want to stop a playback sooner than that, or want to stop a looped\n * playback, you can use the \\c stop_file request:\n *\n\\verbatim\n{\n\t\"request\" : \"stop_file\",\n\t\"room\" : <unique numeric ID of the room where the playback is taking place>,\n\t\"secret\" : \"<room password, if configured>\",\n\t\"file_id\": \"<unique string ID of the announcement>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"file_id\" : \"<unique string ID of the now interrupted announcement>\"\n}\n\\endverbatim\n *\n * In case you want to stop all playbacks of a room immediatly,\n * you can use the \\c stop_all_files request:\n *\n\\verbatim\n{\n\t\"request\" : \"stop_all_files\",\n\t\"room\" : <unique numeric ID of the room where the playback is taking place>,\n\t\"secret\" : \"<room password, if configured>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"success\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"file_id_list\" : [\n\t\t// Array of file_id identifiers, as strings\n\t]\n}\n\\endverbatim\n *\n * That completes the list of synchronous requests you can send to the\n * AudioBridge plugin. As anticipated, though, there are also several\n * asynchronous requests you can send, specifically those related to\n * joining and updating one's presence as a participant in an audio room.\n *\n * The way you'd interact with the plugin is usually as follows:\n *\n * -# you use a \\c join request to join an audio room, and wait for the\n * \\c joined event; this event will also include a list of the other\n * participants, if any;\n * -# you send a \\c configure request attached to an audio-only JSEP offer\n * to start configuring your participation in the room (e.g., join unmuted\n * or muted), and wait for the related \\c event, which will be attached\n * to a JSEP answer by the plugin to complete the setup of the WebRTC\n * PeerConnection;\n * -# you send other \\c configure requests (without any JSEP-related\n * attachment) to mute/unmute yourself during the audio conference;\n * -# you intercept events originated by the plugin (\\c joined , \\c leaving )\n * to notify you about users joining/leaving/muting/unmuting;\n * -# you eventually send a \\c leave request to leave a room; if you leave the\n * PeerConnection instance intact, you can subsequently join a different\n * room without requiring a new negotiation (and so just use a \\c join + JSEP-less \\c configure to join).\n *\n * Notice that there's also a \\c changeroom request available: you can use\n * this request to immediately leave the room you're in and join a different\n * one, without requiring you to do a \\c leave + \\c join + \\c configure\n * round. Of course remember not to pass any JSEP-related payload when\n * doing a \\c changeroom as the same pre-existing PeerConnection will be\n * re-used for the purpose.\n *\n * Notice that you can also ask the AudioBridge plugin to send you an offer,\n * when you join, rather than providing one yourself: this means that the\n * SDP offer/answer roles would be reversed, and so you'd have to provide\n * an answer yourself in this case. Remember that, in case renegotiations\n * or restarts take place, they MUST follow the same negotiation pattern\n * as the one that originated the connection: it's an error to send an\n * SDP offer to the plugin to update a PeerConnection, if the plugin sent\n * you an offer originally. It's advised to let users generate the offer,\n * and let the plugin answer: this reverserd role is mostly here to\n * facilitate the setup of cascaded mixers, e.g., allow one AudioBridge\n * to connect to the other via WebRTC (which wouldn't be possible if\n * both expected an offer from the other). Refer to the \\ref aboffer\n * section for more details.\n *\n * About the syntax of all the above mentioned requests, \\c join has\n * to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"join\",\n\t\"room\" : <numeric ID of the room to join>,\n\t\"id\" : <unique ID to assign to the participant; optional, assigned by the plugin if missing>,\n\t\"group\" : \"<group to assign to this participant (for forwarding purposes only; optional, mandatory if enabled in the room)>\",\n\t\"pin\" : \"<password required to join the room, if any; optional>\",\n\t\"display\" : \"<display name to have in the room; optional>\",\n\t\"token\" : \"<invitation token, in case the room has an ACL; optional>\",\n\t\"muted\" : <true|false, whether to start unmuted or muted>,\n\t\"suspended\" : <true|false, whether to start suspended or not (false by default)>,\n\t\"pause_events\" : <whether room events should be paused, if the user is joining as suspended; optional, false by default>\n\t\"codec\" : \"<codec to use, among opus (default), pcma (A-Law) or pcmu (mu-Law)>\",\n\t\"bitrate\" : <bitrate to use for the Opus stream in bps; optional, default=0 (libopus decides)>,\n\t\"quality\" : <0-10, Opus-related complexity to use, the higher the value, the better the quality (but more CPU); optional, default is 4>,\n\t\"expected_loss\" : <0-20, a percentage of the expected loss (capped at 20%), only needed in case outgoing FEC is used; optional, default is 0 (FEC disabled even when negotiated) or the room default>,\n\t\"volume\" : <percent value, <100 reduces volume, >100 increases volume; optional, default is 100 (no volume change)>,\n\t\"spatial_position\" : <in case spatial audio is enabled for the room, panning of this participant (0=left, 50=center, 100=right)>,\n\t\"denoise\" : <true|false, whether denoising via RNNoise should be performed for this participant (default=room value)>,\n\t\"secret\" : \"<room management password; optional, if provided the user is an admin and can't be globally muted with mute_room>\",\n\t\"audio_level_average\" : \"<if provided, overrides the room audio_level_average for this user; optional>\",\n\t\"audio_active_packets\" : \"<if provided, overrides the room audio_active_packets for this user; optional>\",\n\t\"record\": <true|false, whether to record this user's contribution to a .mjr file (mixer not involved)>,\n\t\"filename\": \"<basename of the file to record to, -audio.mjr will be added by the plugin; will be relative to mjrs_dir, if configured in the room>\"\n}\n\\endverbatim\n *\n * A successful request will produce a \\c joined event:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"joined\",\n\t\"room\" : <numeric ID of the room>,\n\t\"id\" : <unique ID assigned to the participant>,\n\t\"display\" : \"<display name of the new participant>\",\n\t\"participants\" : [\n\t\t// Array of existing participants in the room\n\t]\n}\n\\endverbatim\n *\n * The other participants in the room will be notified about the new\n * participant by means of a different \\c joined event, which will only\n * include the \\c room and the new participant as the only object in\n * a \\c participants array.\n *\n * Notice that, while the AudioBridge assumes participants will exchange\n * media via WebRTC, there's a less known feature that allows you to use\n * plain RTP to join an AudioBridge room instead. This functionality may\n * be helpful in case you want, e.g., SIP based endpoints to join an\n * AudioBridge room, by crafting SDPs for the SIP dialogs yourself using\n * the info exchanged with the plugin. In order to do that, you keep on\n * using the API to join as a participant as explained above, but instead\n * of negotiating a PeerConnection as you usually would, you add an \\c rtp\n * object to the \\c join request, which needs to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"join\",\n\t[..]\n\t\"rtp\" : {\n\t\t\"ip\" : \"<IP address you want media to be sent to>\",\n\t\t\"port\" : <port you want media to be sent to>,\n\t\t\"payload_type\" : <payload type to use for RTP packets (optional; only needed in case Opus is used, automatic for G.711)>,\n\t\t\"audiolevel_ext\" : <ID of the audiolevel RTP extension, if used (optional)>,\n\t\t\"fec\" : <true|false, whether FEC from Janus to the user should be enabled for the Opus stream (optional; only needed in case Opus is used)>\n\t}\n}\n\\endverbatim\n *\n * In that case, the participant will be configured to use plain RTP to\n * exchange media with the room, and the \\c joined event will include an\n * \\c rtp object as well to complete the negotiation:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"joined\",\n\t[..]\n\t\"rtp\" : {\n\t\t\"ip\" : \"<IP address the AudioBridge will expect media to be sent to>\",\n\t\t\"port\" : <port the AudioBridge will expect media to be sent to>,\n\t\t\"payload_type\" : <payload type to use for RTP packets (optional; only needed in case Opus is used, automatic for G.711)>\n\t}\n}\n\\endverbatim\n *\n * Notice that if no ip/port are provided, it means this participant will\n * only send media, and will not receive any (e.g., for injecting audio).\n * This means the AudioBridge plugin will only start sending media via RTP\n * if it received a valid ip/port from the remote paricipant endpoint.\n * If you're interested in supporting scenarios where the AudioBridge\n * \"dials out\" (e.g., for outgoing INVITES to SIP endpoints) check the\n * \\ref aboffer section.\n *\n * At this point, whether the participant will be interacting via WebRTC\n * or plain RTP, the media-related settings of the participant can be\n * modified by means of a \\c configure request. The \\c configure request\n * has to be formatted as follows (notice that all parameters except\n * \\c request are optional, depending on what you want to change):\n *\n\\verbatim\n{\n\t\"request\" : \"configure\",\n\t\"muted\" : <true|false, whether to unmute or mute>,\n\t\"display\" : \"<new display name to have in the room>\",\n\t\"bitrate\" : <new bitrate to use for the Opus stream (see \"join\" for more info)>,\n\t\"quality\" : <new Opus-related complexity to use (see \"join\" for more info)>,\n\t\"expected_loss\" : <new value for the expected loss (see \"join\" for more info)>\n\t\"volume\" : <new volume percent value (see \"join\" for more info)>,\n\t\"spatial_position\" : <in case spatial audio is enabled for the room, new panning of this participant (0=left, 50=center, 100=right)>,\n\t\"denoise\" : <true|false, whether denoising via RNNoise should be performed for this participant (default=room value)>,\n\t\"record\": <true|false, whether to record this user's contribution to a .mjr file (mixer not involved),\n\t\"filename\": \"<basename of the file to record to, -audio.mjr will be added by the plugin; will be relative to mjrs_dir, if configured in the room>\",\n\t\"group\" : \"<new group to assign to this participant, if enabled in the room (for forwarding purposes)>\"\n}\n\\endverbatim\n *\n * \\c muted instructs the plugin to mute or unmute the participant;\n * \\c quality changes the complexity of the Opus encoder for the\n * participant; \\c record can be used to record this participant's contribution\n * to a Janus .mjr file, and \\c filename to provide a basename for the path to\n * save the file to (notice that this is different from the recording of a whole\n * room: this feature only records the packets this user is sending, and is not\n * related to the mixer stuff). A successful request will result in a \\c ok event:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"event\",\n\t\"room\" : <numeric ID of the room>,\n\t\"result\" : \"ok\"\n}\n\\endverbatim\n *\n * In case the \\c muted property was modified, the other participants in\n * the room will be notified about this by means of a \\c event notification,\n * which will only include the \\c room and the updated participant as the\n * only object in a \\c participants array.\n *\n * If you're the administrator of a room (that is, you created it and have access to the secret)\n * you can mute or unmute individual participants using the \\c mute or \\c unmute request\n *\n \\verbatim\n{\n\t\"request\" : \"<mute|unmute, whether to mute or unmute>\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"id\" : <unique numeric ID of the participant to mute|unmute>\n}\n\\endverbatim\n *\n * A successful request will result in a success response:\n *\n \\verbatim\n{\n\t\"audiobridge\" : \"success\",\n}\n\\endverbatim\n *\n * To mute/unmute the whole room, use \\c mute_room and \\c unmute_room instead.\n *\n \\verbatim\n{\n\t\"request\" : \"<mute_room|unmute_room, whether to mute or unmute>\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>\n}\n\\endverbatim\n *\n * A successful request will result in a success response:\n *\n \\verbatim\n{\n\t\"audiobridge\" : \"success\",\n}\n\\endverbatim\n *\n * As anticipated, you can leave an audio room using the \\c leave request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"leave\"\n}\n\\endverbatim\n *\n * The leaving user will receive a \\c left notification:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"left\",\n\t\"room\" : <numeric ID of the room>,\n\t\"id\" : <numeric ID of the participant who left>\n}\n\\endverbatim\n *\n * All the other participants will receive an \\c event notification with the\n * ID of the participant who just left:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"event\",\n\t\"room\" : <numeric ID of the room>,\n\t\"leaving\" : <numeric ID of the participant who left>\n}\n\\endverbatim\n *\n * For what concerns the \\c changeroom request, instead, it's pretty much\n * the same as a \\c join request and as such has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"changeroom\",\n\t\"room\" : <numeric ID of the room to move to>,\n\t\"id\" : <unique ID to assign to the participant; optional, assigned by the plugin if missing>,\n \t\"pin\" : \"<password required to join the room, optional>\",\n\t\"group\" : \"<group to assign to this participant (for forwarding purposes only; optional, mandatory if enabled in the new room)>\",\n\t\"display\" : \"<display name to have in the room; optional>\",\n\t\"token\" : \"<invitation token, in case the new room has an ACL; optional>\",\n\t\"muted\" : <true|false, whether to start unmuted or muted>,\n\t\"suspended\" : <true|false, whether to start suspended or not>,\n\t\"pause_events\" : <whether room events should be paused, if the user is joining as suspended; optional, false by default>\n\t\"bitrate\" : <bitrate to use for the Opus stream in bps; optional, default=0 (libopus decides)>,\n\t\"quality\" : <0-10, Opus-related complexity to use, higher is higher quality; optional, default is 4>,\n\t\"expected_loss\" : <0-20, a percentage of the expected loss (capped at 20%), only needed in case outgoing FEC is used; optional, default is 0 (FEC disabled even when negotiated) or the room default>,\n\t\"volume\" : <new volume percent value (see \"join\" for more info)>,\n\t\"spatial_position\" : <in case spatial audio is enabled for the room, new panning of this participant (0=left, 50=center, 100=right)>,\n\t\"denoise\" : <true|false, whether denoising via RNNoise should be performed for this participant (default=room value, or whether it was active before)>\n}\n\\endverbatim\n *\n * Such a request will trigger all the above-described leaving/joined\n * events to the other participants, as it is indeed wrapping a \\c leave\n * followed by a \\c join and as such the other participants in both rooms\n * need to be updated accordingly. The participant who switched room\n * instead will be sent a \\c roomchanged event which is pretty similar\n * to what \\c joined looks like:\n *\n * A successful request will produce a \\c joined event:\n *\n\\verbatim\n{\n\t\"audiobridge\" : \"roomchanged\",\n\t\"room\" : <numeric ID of the new room>,\n\t\"id\" : <unique ID assigned to the participant in the new room>,\n\t\"display\" : \"<display name of the new participant>\",\n\t\"participants\" : [\n\t\t// Array of existing participants in the new room\n\t]\n}\n\\endverbatim\n *\n * As a last note, notice that the AudioBridge plugin does support\n * renegotiations, mostly for the purpose of facilitating ICE restarts:\n * in fact, there isn't much need for renegotiations outside of that\n * context, as PeerConnections here will typically always contain a single\n * m-line for audio, and so adding/removing streams makes no sense; besides,\n * muting and unmuting is available via APIs, meaning that updating the\n * media direction via SDP renegotiations would be overkill.\n *\n * To force a renegotiation, all you need to do is send the new JSEP\n * offer together with a \\c configure request: this request doesn't need\n * to contain any directive at all, and can be empty. A JSEP answer will\n * be sent back along the result of the request, if successful.\n *\n * \\subsection aboffer AudioBridge-generated offers\n *\n * As anticipated in the previous sections, by default the AudioBridge\n * plugin expects an SDP offer from users interested to join a room, and\n * generates an SDP answer to complete the WebRTC negotiation process:\n * this SDP offer can be provided either in a \\c join request or a\n * \\c configure one, depending on how the app is constructed.\n *\n * It's worth pointing out that the AudioBridge plugin also supports\n * reversed roles when it comes to negotiation: that is, a user can ask\n * the plugin to generate an SDP offer first, to which they'd provide\n * an SDP answer to. This slightly changes the way the negotiation works\n * within the context of the AudioBridge API, as some messages may have\n * to be used in a different way. More specifically, if a user wants the\n * plugin to generate an offer, they'll have to include a:\n *\n\\verbatim\n\t[..]\n\t\"generate_offer\" : true,\n\t[..]\n}\n\\endverbatim\n *\n * property in the \\c join or \\c configure request used to setup the\n * PeerConnection. This means that the user will receive a JSEP SDP\n * offer as part of the related event: at this point, the user needs\n * to prepare to send a JSEP SDP answer and send it back to the plugin\n * to complete the negotiation. The user must use the \\c configure\n * request to provide this SDP answer: no need to provide additional\n * attributes in the request, unless it's needed for application related\n * purposes (e.g., to start muted).\n *\n * Notice that this does have an impact on renegotiations, e.g., for\n * ICE restarts or changes in the media direction. As a policy, plugins\n * in Janus tend to enforce the same negotiation pattern used to setup\n * the PeerConnection initially for renegotiations too, as it reduces\n * the risk of issues like glare: this means that users will NOT be able\n * to send an SDP offer to the AudioBridge plugin to update an existing\n * PeerConnection, if that PeerConnection had previously been originated\n * by a plugin offer instead. The plugin will treat this as an error.\n *\n * The \\c generate_offer feature also works for plain RTP participants.\n * If you want to use them in that context (e.g., because you want to\n * first obtain the AudioBridge's RTP details to craft a SIP INVITE,\n * and only later provide the remote RTP details back to the plugin),\n * set the \\c generate_offer property to \\c true and pass an \\c rtp\n * object without \\c ip and \\c port (an empty \\c rtp object will work\n * too). This will instruct the plugin to return its own connectivity\n * information first. When you are aware of the connectivity information\n * for the participant side (e.g., because you got a SIP 200 OK back),\n * pass a new \\c rtp object with \\c ip and \\c port in a \\c configure\n * request on the participant handle, which will finalize the media\n * establishment.\n *\n */\n\n#include \"plugin.h\"\n#ifdef __FreeBSD__\n#include <sys/socket.h>\n#include <netinet/in.h>\n#endif\n\n#include <jansson.h>\n#include <opus/opus.h>\n#ifdef HAVE_LIBOGG\n#include <ogg/ogg.h>\n#endif\n/* We ship our own version of the libspeex-dsp jitter buffer, since\n * the one available out of the box comes with a nasty memory leak */\n#include \"audiobridge-deps/speex/speex_jitter.h\"\n#include \"audiobridge-deps/speex/speex_resampler.h\"\n#ifdef HAVE_RNNOISE\n#include <rnnoise.h>\n#endif\n\n#include <arpa/inet.h>\n#include <net/if.h>\n#include <sys/socket.h>\n#include <netdb.h>\n#include <sys/time.h>\n#include <poll.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../rtp.h\"\n#include \"../rtpsrtp.h\"\n#include \"../rtcp.h\"\n#include \"../rtpfwd.h\"\n#include \"../record.h\"\n#include \"../sdp-utils.h\"\n#include \"../utils.h\"\n#include \"../ip-utils.h\"\n\n\n/* Plugin information */\n#define JANUS_AUDIOBRIDGE_VERSION\t\t\t13\n#define JANUS_AUDIOBRIDGE_VERSION_STRING\t\"0.0.13\"\n#define JANUS_AUDIOBRIDGE_DESCRIPTION\t\t\"This is a plugin implementing an audio conference bridge for Janus, mixing Opus streams.\"\n#define JANUS_AUDIOBRIDGE_NAME\t\t\t\t\"JANUS AudioBridge plugin\"\n#define JANUS_AUDIOBRIDGE_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_AUDIOBRIDGE_PACKAGE\t\t\t\"janus.plugin.audiobridge\"\n\n#define MIN_SEQUENTIAL \t\t\t\t\t\t2\n#define MAX_MISORDER\t\t\t\t\t\t50\n\n#define JANUS_AUDIOBRIDGE_MAX_GROUPS\t\t5\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_audiobridge_init(janus_callbacks *callback, const char *config_path);\nvoid janus_audiobridge_destroy(void);\nint janus_audiobridge_get_api_compatibility(void);\nint janus_audiobridge_get_version(void);\nconst char *janus_audiobridge_get_version_string(void);\nconst char *janus_audiobridge_get_description(void);\nconst char *janus_audiobridge_get_name(void);\nconst char *janus_audiobridge_get_author(void);\nconst char *janus_audiobridge_get_package(void);\nvoid janus_audiobridge_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_audiobridge_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_audiobridge_handle_admin_message(json_t *message);\nvoid janus_audiobridge_setup_media(janus_plugin_session *handle);\nvoid janus_audiobridge_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_audiobridge_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_audiobridge_hangup_media(janus_plugin_session *handle);\nvoid janus_audiobridge_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_audiobridge_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_audiobridge_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_audiobridge_init,\n\t\t.destroy = janus_audiobridge_destroy,\n\n\t\t.get_api_compatibility = janus_audiobridge_get_api_compatibility,\n\t\t.get_version = janus_audiobridge_get_version,\n\t\t.get_version_string = janus_audiobridge_get_version_string,\n\t\t.get_description = janus_audiobridge_get_description,\n\t\t.get_name = janus_audiobridge_get_name,\n\t\t.get_author = janus_audiobridge_get_author,\n\t\t.get_package = janus_audiobridge_get_package,\n\n\t\t.create_session = janus_audiobridge_create_session,\n\t\t.handle_message = janus_audiobridge_handle_message,\n\t\t.handle_admin_message = janus_audiobridge_handle_admin_message,\n\t\t.setup_media = janus_audiobridge_setup_media,\n\t\t.incoming_rtp = janus_audiobridge_incoming_rtp,\n\t\t.incoming_rtcp = janus_audiobridge_incoming_rtcp,\n\t\t.hangup_media = janus_audiobridge_hangup_media,\n\t\t.destroy_session = janus_audiobridge_destroy_session,\n\t\t.query_session = janus_audiobridge_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_AUDIOBRIDGE_NAME);\n\treturn &janus_audiobridge_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter adminkey_parameters[] = {\n\t{\"admin_key\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter room_parameters[] = {\n\t{\"room\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter roomopt_parameters[] = {\n\t{\"room\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter roomstr_parameters[] = {\n\t{\"room\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter roomstropt_parameters[] = {\n\t{\"room\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter id_parameters[] = {\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter idopt_parameters[] = {\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter idstr_parameters[] = {\n\t{\"id\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter idstropt_parameters[] = {\n\t{\"id\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter group_parameters[] = {\n\t{\"group\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter create_parameters[] = {\n\t{\"description\", JSON_STRING, 0},\n\t{\"secret\", JSON_STRING, 0},\n\t{\"pin\", JSON_STRING, 0},\n\t{\"is_private\", JANUS_JSON_BOOL, 0},\n\t{\"allowed\", JSON_ARRAY, 0},\n\t{\"sampling_rate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"sampling\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\t/* We keep this to be backwards compatible */\n\t{\"spatial_audio\", JANUS_JSON_BOOL, 0},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"record_file\", JSON_STRING, 0},\n\t{\"record_dir\", JSON_STRING, 0},\n\t{\"mjrs\", JANUS_JSON_BOOL, 0},\n\t{\"mjrs_dir\", JSON_STRING, 0},\n\t{\"allow_rtp_participants\", JANUS_JSON_BOOL, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0},\n\t{\"audiolevel_ext\", JANUS_JSON_BOOL, 0},\n\t{\"audiolevel_event\", JANUS_JSON_BOOL, 0},\n\t{\"audio_active_packets\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_level_average\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"default_expectedloss\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"default_bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"denoise\", JANUS_JSON_BOOL, 0},\n\t{\"groups\", JSON_ARRAY, 0}\n};\nstatic struct janus_json_parameter edit_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"new_description\", JSON_STRING, 0},\n\t{\"new_secret\", JSON_STRING, 0},\n\t{\"new_pin\", JSON_STRING, 0},\n\t{\"new_is_private\", JANUS_JSON_BOOL, 0},\n\t{\"new_record_dir\", JSON_STRING, 0},\n\t{\"new_mjrs_dir\", JSON_STRING, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter destroy_parameters[] = {\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter allowed_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"action\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"allowed\", JSON_ARRAY, 0}\n};\nstatic struct janus_json_parameter secret_parameters[] = {\n\t{\"secret\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter join_parameters[] = {\n\t{\"display\", JSON_STRING, 0},\n\t{\"token\", JSON_STRING, 0},\n\t{\"group\", JSON_STRING, 0},\n\t{\"muted\", JANUS_JSON_BOOL, 0},\n\t{\"suspended\", JANUS_JSON_BOOL, 0},\n\t{\"pause_events\", JANUS_JSON_BOOL, 0},\n\t{\"codec\", JSON_STRING, 0},\n\t{\"bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"quality\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"expected_loss\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"volume\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"spatial_position\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_level_average\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_active_packets\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"denoise\", JANUS_JSON_BOOL, 0},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"generate_offer\", JANUS_JSON_BOOL, 0},\n\t{\"rtp\", JSON_OBJECT, 0},\n\t{\"secret\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter record_parameters[] = {\n\t{\"record\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED},\n\t{\"record_file\", JSON_STRING, 0},\n\t{\"record_dir\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter mjrs_parameters[] = {\n\t{\"mjrs\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED},\n\t{\"mjrs_dir\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter rtp_parameters[] = {\n\t{\"ip\", JSON_STRING, 0},\n\t{\"port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"payload_type\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audiolevel_ext\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"fec\", JANUS_JSON_BOOL, 0},\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"muted\", JANUS_JSON_BOOL, 0},\n\t{\"bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"quality\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"expected_loss\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"volume\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"group\", JSON_STRING, 0},\n\t{\"spatial_position\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"denoise\", JANUS_JSON_BOOL, 0},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"display\", JSON_STRING, 0},\n\t{\"generate_offer\", JANUS_JSON_BOOL, 0},\n\t{\"rtp\", JSON_OBJECT, 0},\n\t{\"update\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter rtp_forward_parameters[] = {\n\t{\"group\", JSON_STRING, 0},\n\t{\"ssrc\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"codec\", JSON_STRING, 0},\n\t{\"ptype\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"port\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"host\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host_family\", JSON_STRING, 0},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0},\n\t{\"always_on\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter stop_rtp_forward_parameters[] = {\n\t{\"stream_id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\n#ifdef HAVE_LIBOGG\nstatic struct janus_json_parameter play_file_parameters[] = {\n\t{\"filename\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"file_id\", JSON_STRING, 0},\n\t{\"group\", JSON_STRING, 0},\n\t{\"loop\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter checkstop_file_parameters[] = {\n\t{\"file_id\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\n#endif\nstatic struct janus_json_parameter suspend_parameters[] = {\n\t{\"pause_events\", JANUS_JSON_BOOL, 0},\n\t{\"stop_record\", JANUS_JSON_BOOL, 0},\n};\nstatic struct janus_json_parameter resume_parameters[] = {\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n};\n\n/* Static configuration instance */\nstatic janus_config *config = NULL;\nstatic const char *config_folder = NULL;\nstatic janus_mutex config_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic gboolean string_ids = FALSE;\nstatic gboolean ipv6_disabled = FALSE;\nstatic janus_callbacks *gateway = NULL;\nstatic GThread *handler_thread;\nstatic void *janus_audiobridge_handler(void *data);\nstatic void janus_audiobridge_relay_rtp_packet(gpointer data, gpointer user_data);\nstatic void *janus_audiobridge_mixer_thread(void *data);\nstatic void *janus_audiobridge_participant_thread(void *data);\nstatic void janus_audiobridge_hangup_media_internal(janus_plugin_session *handle);\n\n/* Extension to add while recording (e.g., \"tmp\" --> \".wav.tmp\") */\nstatic char *rec_tempext = NULL;\n\n/* RTP range, in case we need to support plain RTP participants */\nstatic char *local_ip = NULL;\n#define JANUS_AUDIOBRIDGE_DEFAULT_RTP_RANGE_MIN 10000\n#define JANUS_AUDIOBRIDGE_DEFAULT_RTP_RANGE_MAX 60000\nstatic uint16_t rtp_range_min = JANUS_AUDIOBRIDGE_DEFAULT_RTP_RANGE_MIN;\nstatic uint16_t rtp_range_max = JANUS_AUDIOBRIDGE_DEFAULT_RTP_RANGE_MAX;\nstatic uint16_t rtp_range_slider = JANUS_AUDIOBRIDGE_DEFAULT_RTP_RANGE_MIN;\n\n/* Asynchronous API message to handle */\ntypedef struct janus_audiobridge_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_audiobridge_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_audiobridge_message exit_message;\n\n\n/* Structs */\ntypedef struct janus_audiobridge_room {\n\tguint64 room_id;\t\t\t/* Unique room ID (when using integers) */\n\tgchar *room_id_str;\t\t\t/* Unique room ID (when using strings) */\n\tgchar *room_name;\t\t\t/* Room description */\n\tgchar *room_secret;\t\t\t/* Secret needed to manipulate (e.g., destroy) this room */\n\tgchar *room_pin;\t\t\t/* Password needed to join this room, if any */\n\tuint32_t room_ssrc;\t\t\t/* SSRC we'll use for packets generated by the mixer */\n\tgboolean is_private;\t\t/* Whether this room is 'private' (as in hidden) or not */\n\tuint32_t sampling_rate;\t\t/* Sampling rate of the mix (e.g., 16000 for wideband; can be 8, 12, 16, 24 or 48kHz) */\n\tgboolean spatial_audio;\t\t/* Whether the mix will use spatial audio, using stereo */\n\tgboolean audiolevel_ext;\t/* Whether the ssrc-audio-level extension must be negotiated or not for new joins */\n\tgboolean audiolevel_event;\t/* Whether to emit event to other users about audiolevel */\n\tuint default_expectedloss;\t/* Percent of packets we expect participants may miss, to help with outgoing FEC: can be overridden per-participant */\n\tint32_t default_bitrate;\t/* Default bitrate to use for all Opus streams when encoding */\n\tint audio_active_packets;\t/* Amount of packets with audio level for checkup */\n\tint audio_level_average;\t/* Average audio level */\n#ifdef HAVE_RNNOISE\n\tgboolean denoise;\t\t\t/* Whether we should denoise participants by default */\n#endif\n\tvolatile gint record;\t\t/* Whether this room has to be recorded or not */\n\tgchar *record_file;\t\t\t/* Path of the recording file (absolute or relative, depending on record_dir) */\n\tgchar *record_dir;\t\t\t/* Folder to save the recording file to */\n\tgboolean mjrs;\t\t\t\t/* Whether all participants in the room should be individually recorded to mjr files or not */\n\tgchar *mjrs_dir;\t\t\t/* Folder to save the mjrs file to */\n\tFILE *recording;\t\t\t/* File to record the room into */\n\tgint64 record_lastupdate;\t/* Time when we last updated the wav header */\n\tvolatile gint wav_header_added;\t/* If wav header is added in recording file */\n\tgint64 rec_start_time;\t\t/* Time when recording started for generating file name */\n\tgboolean allow_plainrtp;\t/* Whether plain RTP participants are allowed*/\n\tgboolean destroy;\t\t\t/* Value to flag the room for destruction */\n\tGHashTable *participants;\t/* Map of participants */\n\tGHashTable *anncs;\t\t\t/* Map of announcements */\n\tgboolean check_tokens;\t\t/* Whether to check tokens when participants join (see below) */\n\tgboolean muted;\t\t\t\t/* Whether the room is globally muted (except for admins and played files) */\n\tGHashTable *allowed;\t\t/* Map of participants (as tokens) allowed to join */\n\tGThread *thread;\t\t\t/* Mixer thread for this room */\n\tvolatile gint destroyed;\t/* Whether this room has been destroyed */\n\tjanus_mutex mutex;\t\t\t/* Mutex to lock this room instance */\n\t/* RTP forwarders for this room's mix */\n\tGHashTable *groups;\t\t\t/* Forwarding groups supported in this room, indexed by name */\n\tGHashTable *groups_byid;\t/* Forwarding groups supported in this room, indexed by numeric ID */\n\tGHashTable *rtp_forwarders;\t/* RTP forwarders list (as a hashmap) */\n\tOpusEncoder *rtp_encoder;\t/* Opus encoder instance to use for all RTP forwarders */\n\tjanus_mutex rtp_mutex;\t\t/* Mutex to lock the RTP forwarders list */\n\tint rtp_udp_sock;\t\t\t/* UDP socket to use to forward RTP packets */\n\tjanus_refcount ref;\t\t\t/* Reference counter for this room */\n} janus_audiobridge_room;\nstatic GHashTable *rooms;\nstatic janus_mutex rooms_mutex = JANUS_MUTEX_INITIALIZER;\nstatic char *admin_key = NULL;\nstatic gboolean lock_rtpfwd = FALSE;\nstatic gboolean lock_playfile = FALSE;\n\ntypedef struct janus_audiobridge_session {\n\tjanus_plugin_session *handle;\n\tgint64 sdp_sessid;\n\tgint64 sdp_version;\n\tgboolean plugin_offer;\n\tgpointer participant;\n\tvolatile gint started;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_audiobridge_session;\nstatic GHashTable *sessions;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\n#ifdef HAVE_LIBOGG\n/* Helper struct to handle the playout of Opus files */\ntypedef struct janus_audiobridge_file {\n\tchar *id;\n\tchar *filename;\n\tFILE *file;\n\togg_sync_state sync;\n\togg_stream_state stream;\n\togg_page page;\n\togg_packet pkt;\n\tchar *oggbuf;\n\tgboolean started, loop;\n\tgint state, headers;\n} janus_audiobridge_file;\n/* Helper method to open an Opus file, and make sure it's valid */\nstatic int janus_audiobridge_file_init(janus_audiobridge_file *ctx) {\n\tif(ctx == NULL || ctx->file == NULL)\n\t\treturn -1;\n\tfseek(ctx->file, 0, SEEK_SET);\n\togg_stream_clear(&ctx->stream);\n\togg_sync_clear(&ctx->sync);\n\tif(ogg_sync_init(&ctx->sync) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Error re-initializing Ogg sync state...\\n\", ctx->id);\n\t\treturn -1;\n\t}\n\tctx->headers = 0;\n\treturn 0;\n}\n/* Helper method to check if an Ogg page begins with an Ogg stream */\nstatic gboolean janus_audiobridge_ogg_is_opus(ogg_page *page) {\n\togg_stream_state state;\n\togg_packet pkt;\n\togg_stream_init(&state, ogg_page_serialno(page));\n\togg_stream_pagein(&state, page);\n\tif(ogg_stream_packetout(&state, &pkt) == 1) {\n\t\tif(pkt.bytes >= 19 && !memcmp(pkt.packet, \"OpusHead\", 8)) {\n\t\t\togg_stream_clear(&state);\n\t\t\treturn 1;\n\t\t}\n\t}\n\togg_stream_clear(&state);\n\treturn FALSE;\n}\n/* Helper method to traverse the Opus file until we get a packet we can send */\nstatic int janus_audiobridge_file_read(janus_audiobridge_file *ctx, OpusDecoder *decoder, opus_int16 *buffer, int length) {\n\tif(ctx == NULL || ctx->file == NULL || decoder == NULL || buffer == NULL)\n\t\treturn -1;\n\t/* Check our current state in processing the Ogg file */\n\tint read = 0;\n\tif(ctx->state == 0) {\n\t\t/* Prepare a buffer, and read from the Ogg file... */\n\t\tctx->oggbuf = ogg_sync_buffer(&ctx->sync, 8192);\n\t\tif(ctx->oggbuf == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_sync_buffer failed...\\n\", ctx->id);\n\t\t\treturn -2;\n\t\t}\n\t\tread = fread(ctx->oggbuf, 1, 8192, ctx->file);\n\t\tif(read == 0 && feof(ctx->file)) {\n\t\t\t/* Check if we should rewind, or be done */\n\t\t\tif(!ctx->loop) {\n\t\t\t\t/* We're done */\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\t/* Rewind */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s] Rewind! (%s)\\n\", ctx->id, ctx->filename);\n\t\t\tif(janus_audiobridge_file_init(ctx) < 0)\n\t\t\t\treturn -3;\n\t\t\treturn janus_audiobridge_file_read(ctx, decoder, buffer, length);\n\t\t}\n\t\tif(ogg_sync_wrote(&ctx->sync, read) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_sync_wrote failed...\\n\", ctx->id);\n\t\t\treturn -4;\n\t\t}\n\t\t/* Next state: sync pageout */\n\t\tctx->state = 1;\n\t}\n\tif(ctx->state == 1) {\n\t\t/* Prepare an ogg_page out of the buffer */\n\t\twhile((read = ogg_sync_pageout(&ctx->sync, &ctx->page)) == 1) {\n\t\t\t/* Let's look for an Opus stream, first of all */\n\t\t\tif(ctx->headers == 0) {\n\t\t\t\tif(janus_audiobridge_ogg_is_opus(&ctx->page)) {\n\t\t\t\t\t/* This is the start of an Opus stream */\n\t\t\t\t\tif(ogg_stream_init(&ctx->stream, ogg_page_serialno(&ctx->page)) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_stream_init failed...\\n\", ctx->id);\n\t\t\t\t\t\treturn -5;\n\t\t\t\t\t}\n\t\t\t\t\tctx->headers++;\n\t\t\t\t} else if(!ogg_page_bos(&ctx->page)) {\n\t\t\t\t\t/* No Opus stream? */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] No Opus stream...\\n\", ctx->id);\n\t\t\t\t\treturn -6;\n\t\t\t\t} else {\n\t\t\t\t\t/* Still waiting for an Opus stream */\n\t\t\t\t\treturn janus_audiobridge_file_read(ctx, decoder, buffer, length);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Submit the page for packetization */\n\t\t\tif(ogg_stream_pagein(&ctx->stream, &ctx->page) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_stream_pagein failed...\\n\", ctx->id);\n\t\t\t\treturn -7;\n\t\t\t}\n\t\t\t/* Time to start reading packets */\n\t\t\tctx->state = 2;\n\t\t\tbreak;\n\t\t}\n\t\tif(read != 1) {\n\t\t\t/* Go back to reading from the file */\n\t\t\tctx->state = 0;\n\t\t\treturn janus_audiobridge_file_read(ctx, decoder, buffer, length);\n\t\t}\n\t}\n\tif(ctx->state == 2) {\n\t\t/* Read and process available packets */\n\t\tif(ogg_stream_packetout(&ctx->stream, &ctx->pkt) != 1) {\n\t\t\t/* Go back to reading pages */\n\t\t\tctx->state = 1;\n\t\t\treturn janus_audiobridge_file_read(ctx, decoder, buffer, length);\n\t\t} else {\n\t\t\t/* Skip header packets */\n\t\t\tif(ctx->headers == 1 && ctx->pkt.bytes >= 19 && !memcmp(ctx->pkt.packet, \"OpusHead\", 8)) {\n\t\t\t\tctx->headers++;\n\t\t\t\treturn janus_audiobridge_file_read(ctx, decoder, buffer, length);\n\t\t\t}\n\t\t\tif(ctx->headers == 2 && ctx->pkt.bytes >= 16 && !memcmp(ctx->pkt.packet, \"OpusTags\", 8)) {\n\t\t\t\tctx->headers++;\n\t\t\t\treturn janus_audiobridge_file_read(ctx, decoder, buffer, length);\n\t\t\t}\n\t\t\t/* Decode the audio */\n\t\t\tlength = opus_decode(decoder, ctx->pkt.packet, ctx->pkt.bytes,\n\t\t\t\t(opus_int16 *)buffer, length, 0);\n\t\t\treturn length;\n\t\t}\n\t}\n\t/* If we got here, continue with the iteration */\n\treturn -9;\n}\n/* Helper method to cleanup an Opus context */\nstatic void janus_audiobridge_file_free(janus_audiobridge_file *ctx) {\n\tif(ctx == NULL)\n\t\treturn;\n\tg_free(ctx->id);\n\tg_free(ctx->filename);\n\tif(ctx->file)\n\t\tfclose(ctx->file);\n\tif(ctx->headers > 0)\n\t\togg_stream_clear(&ctx->stream);\n\togg_sync_clear(&ctx->sync);\n\tg_free(ctx);\n}\n#endif\n\n/* In case we need to support plain RTP participants, this struct helps with that */\ntypedef struct janus_audiobridge_plainrtp_media {\n\tchar *remote_audio_ip;\n\tint ready:1;\n\tint audio_rtp_fd;\n\tint local_audio_rtp_port, remote_audio_rtp_port;\n\tguint32 audio_ssrc, audio_ssrc_peer;\n\tint audio_pt;\n\tgboolean audio_send;\n\tjanus_rtp_switching_context context;\n\tint pipefd[2];\n\tGThread *thread;\n\tvolatile int initialized;\n} janus_audiobridge_plainrtp_media;\nstatic void janus_audiobridge_plainrtp_media_cleanup(janus_audiobridge_plainrtp_media *media);\nstatic int janus_audiobridge_plainrtp_allocate_port(janus_audiobridge_plainrtp_media *media);\nstatic void *janus_audiobridge_plainrtp_relay_thread(void *data);\n\n/* AudioBridge participant */\ntypedef struct janus_audiobridge_participant {\n\tjanus_audiobridge_session *session;\n\tjanus_audiobridge_room *room;\t/* Room */\n\tguint64 user_id;\t\t/* Unique ID in the room */\n\tgchar *user_id_str;\t\t/* Unique ID in the room (when using strings) */\n\tgchar *display;\t\t\t/* Display name (opaque value, only meaningful to application) */\n\tgboolean admin;\t\t\t/* If the participant is an admin (can't be globally muted) */\n\tvolatile gint active;\t/* Whether this participant can receive media at all */\n\tvolatile gint encoding;\t/* Whether this participant is currently encoding */\n\tvolatile gint decoding;\t/* Whether this participant is currently decoding */\n\tgboolean muted;\t\t\t/* Whether this participant is muted */\n\tint volume_gain;\t\t/* Gain to apply to the input audio (in percentage) */\n\tint32_t opus_bitrate;\t/* Bitrate to use for the Opus stream */\n\tint opus_complexity;\t/* Complexity to use in the encoder (by default, DEFAULT_COMPLEXITY) */\n\tgboolean stereo;\t\t/* Whether stereo will be used for spatial audio */\n\tint spatial_position;\t/* Panning of this participant in the mix */\n#ifdef HAVE_RNNOISE\n#define DENOISER_FRAME_SIZE 480\n\tgboolean denoise;\t\t\t\t\t/* Whether we should denoise this participant */\n\tDenoiseState *rnnoise[2];\t\t\t/* RNNoise states (we'll need two for stereo) */\n\tuint32_t resampler_rate;\t\t\t/* Sampling rate of the resamplers */\n\tgboolean resampler_stereo;\t\t\t/* Whether the current resamplers are stereo */\n\tSpeexResamplerState *upsampler; \t/* Speex upsampler used for denoising */\n\tSpeexResamplerState *downsampler;\t/* Speex downsampler used for denoising*/\n\topus_int16 *upsample_buffer;\t\t/* Buffer for upsampling */\n\topus_int16 *downsample_buffer;\t\t/* Buffer for downsampling */\n\tfloat *denoiser_buffer[2];\t\t\t/* Buffer for denoising */\n#endif\n\t/* RTP stuff */\n\tJitterBuffer *jitter;\t/* Jitter buffer of incoming audio packets */\n\tgint64 jitter_next_check;\t/* Timestamp to perform next jitter buffer size check */\n\tGList *inbuf;\t\t\t/* Decoded audio from this participant, to feed to the mixer */\n\tGAsyncQueue *outbuf;\t/* Mixed audio to send to this participant */\n\tjanus_mutex qmutex;\t\t/* Incoming queue mutex */\n\tint opus_pt;\t\t\t/* Opus payload type */\n\tint extmap_id;\t\t\t/* Audio level RTP extension id, if any */\n\tint dBov_level;\t\t\t/* Value in dBov of the audio level (last value from extension) */\n\tint audio_active_packets;\t/* Participant's number of audio packets to accumulate */\n\tint audio_dBov_sum;\t    /* Participant's accumulated dBov value for audio level */\n\tint user_audio_active_packets; /* Participant's number of audio packets to evaluate */\n\tint user_audio_level_average;\t /* Participant's average level of dBov value */\n\tgboolean talking;\t\t/* Whether this participant is currently talking (uses audio levels extension) */\n\tjanus_rtp_switching_context context;\t/* Needed in case the participant changes room */\n\tjanus_audiocodec codec;\t/* Codec this participant is using (most often Opus, but G.711 is supported too) */\n\t/* Plain RTP, in case this is not a WebRTC participant */\n\tgboolean plainrtp;\t\t\t/* Whether this is a WebRTC participant, or a plain RTP one */\n\tjanus_audiobridge_plainrtp_media plainrtp_media;\n\tjanus_mutex pmutex;\n\t/* Opus stuff */\n\tuint32_t sampling_rate;\t\t/* Sampling rate to decode at */\n\tOpusEncoder *encoder;\t\t/* Opus encoder instance */\n\tOpusDecoder *decoder;\t\t/* Opus decoder instance */\n\tgboolean fec;\t\t\t\t/* Opus FEC (from Janus to user) status */\n\tint expected_loss;\t\t\t/* Percentage of expected loss, to configure libopus outgoing FEC behaviour (default=0, no FEC even if negotiated) */\n\tuint32_t last_timestamp;\t/* Last in seq timestamp */\n\tuint16_t last_seq; \t\t/* Last sequence number */\n\tgboolean reset;\t\t\t\t/* Whether or not the Opus context must be reset, without re-joining the room */\n\tGThread *thread;\t\t\t/* Encoding thread for this participant */\n\tgboolean mjr_active;\t\t/* Whether this participant has to be recorded to an mjr file or not */\n\tgchar *mjr_base;\t\t\t/* Base name for the mjr recording (e.g., /path/to/filename, will generate /path/to/filename-audio.mjr) */\n\tjanus_recorder *arc;\t\t/* The Janus recorder instance for this user's audio, if enabled */\n#ifdef HAVE_LIBOGG\n\tjanus_audiobridge_file *annc;\t/* In case this is a fake participant, a playable file */\n#endif\n\tuint group;\t\t\t\t\t/* Forwarding group index, if enabled in the room */\n\tjanus_mutex rec_mutex;\t\t/* Mutex to protect the recorder from race conditions */\n\tjanus_mutex suspend_cond_mutex;\n\tGCond suspend_cond;\n\tvolatile gint suspended;\t/* Whether this participant has been temporarily suspended */\n\tvolatile gint paused_events;/* Whether sending events to this participant has been paused because they're suspended */\n\tvolatile gint destroyed;\t/* Whether this participant has been destroyed */\n\tjanus_refcount ref;\t\t\t/* Reference counter for this participant */\n} janus_audiobridge_participant;\n\ntypedef struct janus_audiobridge_rtp_relay_packet {\n\tjanus_rtp_header *data;\n\tgint length;\n\tuint32_t ssrc;\n\tuint32_t timestamp;\n\tuint16_t seq_number;\n\tgboolean silence;\n} janus_audiobridge_rtp_relay_packet;\n\n/* Buffered audio/video packet */\ntypedef struct janus_audiobridge_buffer_packet {\n\t/* Pointer to the packet data, if RTP */\n\tjanus_plugin_rtp *rtp;\n\t/* Monotonic insert time */\n\tint64_t inserted;\n} janus_audiobridge_buffer_packet;\nstatic janus_audiobridge_buffer_packet *janus_audiobridge_buffer_packet_create(janus_plugin_rtp *rtp) {\n\tjanus_audiobridge_buffer_packet *pkt = g_malloc(sizeof(janus_audiobridge_buffer_packet));\n\tpkt->rtp = janus_plugin_rtp_duplicate(rtp);\n\tpkt->inserted = janus_get_monotonic_time();\n\treturn pkt;\n}\nstatic void janus_audiobridge_buffer_packet_destroy(janus_audiobridge_buffer_packet *pkt) {\n\tif(!pkt)\n\t\treturn;\n\tif(pkt->rtp)\n\t\tg_free(pkt->rtp->buffer);\n\tg_free(pkt->rtp);\n\tg_free(pkt);\n}\n\nstatic void janus_audiobridge_participant_istalking(janus_audiobridge_session *session,\n\tjanus_audiobridge_participant *participant, janus_plugin_rtp *packet, gboolean *silence);\n\nstatic void janus_audiobridge_participant_clear_jitter_buffer(janus_audiobridge_participant *participant) {\n\tif(participant->jitter) {\n\t\tjitter_buffer_reset(participant->jitter);\n\t}\n}\n\nstatic void janus_audiobridge_participant_clear_inbuf(janus_audiobridge_participant *participant) {\n\twhile(participant->inbuf) {\n\t\tGList *first = g_list_first(participant->inbuf);\n\t\tjanus_audiobridge_rtp_relay_packet *pkt = (janus_audiobridge_rtp_relay_packet *)first->data;\n\t\tparticipant->inbuf = g_list_delete_link(participant->inbuf, first);\n\t\tfirst = NULL;\n\t\tif(pkt == NULL)\n\t\t\tcontinue;\n\t\tg_free(pkt->data);\n\t\tpkt->data = NULL;\n\t\tg_free(pkt);\n\t\tpkt = NULL;\n\t}\n}\n\nstatic void janus_audiobridge_participant_clear_outbuf(janus_audiobridge_participant *participant) {\n\twhile(participant->outbuf && g_async_queue_length(participant->outbuf) > 0) {\n\t\tjanus_audiobridge_rtp_relay_packet *pkt = g_async_queue_try_pop(participant->outbuf);\n\t\tif(pkt) {\n\t\t\tg_free(pkt->data);\n\t\t\tg_free(pkt);\n\t\t}\n\t}\n}\n\nstatic void janus_audiobridge_participant_destroy(janus_audiobridge_participant *participant) {\n\tif(!participant)\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&participant->destroyed, 0, 1))\n\t\treturn;\n\t/* Decrease the counter */\n\tjanus_refcount_decrease(&participant->ref);\n}\n\nstatic void janus_audiobridge_participant_unref(janus_audiobridge_participant *participant) {\n\tif(!participant)\n\t\treturn;\n\t/* Just decrease the counter */\n\tjanus_refcount_decrease(&participant->ref);\n}\n\nstatic void janus_audiobridge_participant_free(const janus_refcount *participant_ref) {\n\tjanus_audiobridge_participant *participant = janus_refcount_containerof(participant_ref, janus_audiobridge_participant, ref);\n\t/* This participant can be destroyed, free all the resources */\n\tg_free(participant->user_id_str);\n\tg_free(participant->display);\n\tif(participant->encoder)\n\t\topus_encoder_destroy(participant->encoder);\n\tif(participant->decoder)\n\t\topus_decoder_destroy(participant->decoder);\n\tif(participant->jitter)\n\t\tjitter_buffer_destroy(participant->jitter);\n\tjanus_audiobridge_participant_clear_inbuf(participant);\n\tif(participant->outbuf != NULL) {\n\t\tjanus_audiobridge_participant_clear_outbuf(participant);\n\t\tg_async_queue_unref(participant->outbuf);\n\t}\n#ifdef HAVE_RNNOISE\n\tif(participant->rnnoise[0])\n\t\trnnoise_destroy(participant->rnnoise[0]);\n\tif(participant->rnnoise[1])\n\t\trnnoise_destroy(participant->rnnoise[1]);\n\tif(participant->denoiser_buffer[0])\n\t\tg_free(participant->denoiser_buffer[0]);\n\tif(participant->denoiser_buffer[1])\n\t\tg_free(participant->denoiser_buffer[1]);\n\tif(participant->upsampler)\n\t\tspeex_resampler_destroy(participant->upsampler);\n\tif(participant->downsampler)\n\t\tspeex_resampler_destroy(participant->downsampler);\n\tif(participant->upsample_buffer)\n\t\tg_free(participant->upsample_buffer);\n\tif(participant->downsample_buffer)\n\t\tg_free(participant->downsample_buffer);\n#endif\n\tg_free(participant->mjr_base);\n#ifdef HAVE_LIBOGG\n\tjanus_audiobridge_file_free(participant->annc);\n#endif\n\tjanus_mutex_lock(&participant->pmutex);\n\tjanus_audiobridge_plainrtp_media_cleanup(&participant->plainrtp_media);\n\tjanus_mutex_unlock(&participant->pmutex);\n\tjanus_mutex_destroy(&participant->pmutex);\n\tjanus_mutex_destroy(&participant->qmutex);\n\tjanus_mutex_destroy(&participant->rec_mutex);\n\tjanus_mutex_destroy(&participant->suspend_cond_mutex);\n\tjanus_condition_destroy(&participant->suspend_cond);\n\tg_free(participant);\n}\n\n#ifdef HAVE_RNNOISE\nstatic void janus_audiobridge_participant_denoise(janus_audiobridge_participant *participant, char *data, int len);\nstatic void janus_audiobridge_participant_upsample(janus_audiobridge_participant *participant, opus_int16 *input, int *in_len, opus_int16 *output, int *out_len);\nstatic void janus_audiobridge_participant_downsample(janus_audiobridge_participant *participant, opus_int16 *input, int *in_len, opus_int16 *output, int *out_len);\n#endif\n\nstatic void janus_audiobridge_session_destroy(janus_audiobridge_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_audiobridge_session_free(const janus_refcount *session_ref) {\n\tjanus_audiobridge_session *session = janus_refcount_containerof(session_ref, janus_audiobridge_session, ref);\n\t/* Destroy the participant instance, if any */\n\tif(session->participant)\n\t\tjanus_audiobridge_participant_destroy(session->participant);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tg_free(session);\n}\n\nstatic void janus_audiobridge_room_destroy(janus_audiobridge_room *audiobridge) {\n\tif(!audiobridge)\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&audiobridge->destroyed, 0, 1))\n\t\treturn;\n\t/* Decrease the counter */\n\tjanus_refcount_decrease(&audiobridge->ref);\n}\n\nstatic void janus_audiobridge_room_free(const janus_refcount *audiobridge_ref) {\n\tjanus_audiobridge_room *audiobridge = janus_refcount_containerof(audiobridge_ref, janus_audiobridge_room, ref);\n\t/* This room can be destroyed, free all the resources */\n\tg_free(audiobridge->room_id_str);\n\tg_free(audiobridge->room_name);\n\tg_free(audiobridge->room_secret);\n\tg_free(audiobridge->room_pin);\n\tg_free(audiobridge->record_file);\n\tg_free(audiobridge->record_dir);\n\tg_hash_table_destroy(audiobridge->participants);\n\tg_hash_table_destroy(audiobridge->anncs);\n\tg_hash_table_destroy(audiobridge->allowed);\n\tif(audiobridge->rtp_udp_sock > 0)\n\t\tclose(audiobridge->rtp_udp_sock);\n\tif(audiobridge->rtp_encoder)\n\t\topus_encoder_destroy(audiobridge->rtp_encoder);\n\tg_hash_table_destroy(audiobridge->rtp_forwarders);\n\tif(audiobridge->groups)\n\t\tg_hash_table_destroy(audiobridge->groups);\n\tif(audiobridge->groups_byid)\n\t\tg_hash_table_destroy(audiobridge->groups_byid);\n\tjanus_mutex_destroy(&audiobridge->mutex);\n\tjanus_mutex_destroy(&audiobridge->rtp_mutex);\n\tg_free(audiobridge);\n}\n\nstatic void janus_audiobridge_message_free(janus_audiobridge_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_audiobridge_session *session = (janus_audiobridge_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\n/* Start / stop recording */\nstatic void janus_audiobridge_recorder_create(janus_audiobridge_participant *participant);\nstatic void janus_audiobridge_recorder_close(janus_audiobridge_participant *participant);\n\n/* RTP forwarder metadata */\ntypedef struct janus_audiobridge_rtp_forwarder_metadata {\n\tjanus_audiocodec codec;\n\tuint32_t timestamp;\n\tuint16_t seq_number;\n\tuint group;\n\tgboolean always_on;\n} janus_audiobridge_rtp_forwarder_metadata;\n/* Helper to create a new RTP forwarder with the right metadata */\nstatic guint32 janus_audiobridge_rtp_forwarder_add_helper(janus_audiobridge_room *room,\n\t\tuint group, const gchar *host, uint16_t port, uint32_t ssrc, int pt,\n\t\tjanus_audiocodec codec, int srtp_suite, const char *srtp_crypto,\n\t\tgboolean always_on, guint32 stream_id) {\n\tif(room == NULL || host == NULL)\n\t\treturn 0;\n\t/* Create a new RTP forwarder */\n\tjanus_rtp_forwarder *rf = janus_rtp_forwarder_create(JANUS_AUDIOBRIDGE_NAME, stream_id,\n\t\troom->rtp_udp_sock, host, port, ssrc, pt, srtp_suite, srtp_crypto, FALSE, 0, FALSE, FALSE);\n\tif(rf == NULL)\n\t\treturn 0;\n\tif(codec == JANUS_AUDIOCODEC_PCMA)\n\t\trf->payload_type = 8;\n\telse if(codec == JANUS_AUDIOCODEC_PCMU)\n\t\trf->payload_type = 0;\n\t/* Fill in some metadata we'll need */\n\tjanus_audiobridge_rtp_forwarder_metadata *metadata = g_malloc0(sizeof(janus_audiobridge_rtp_forwarder_metadata));\n\tmetadata->codec = codec;\n\tmetadata->group = group;\n\tmetadata->always_on = always_on;\n\trf->metadata = metadata;\n\t/* Add the forwarder to the ones we have in the room */\n\tjanus_mutex_lock(&room->rtp_mutex);\n\tg_hash_table_insert(room->rtp_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\tjanus_mutex_unlock(&room->rtp_mutex);\n\t/* Done */\n\tJANUS_LOG(LOG_VERB, \"Added RTP forwarder to room %s: %s:%d (ID: %\"SCNu32\")\\n\",\n\t\troom->room_id_str, host, port, rf->stream_id);\n\treturn rf->stream_id;\n}\n\n/* Helper struct to generate and parse WAVE headers */\ntypedef struct wav_header {\n\tchar riff[4];\n\tuint32_t len;\n\tchar wave[4];\n\tchar fmt[4];\n\tuint32_t formatsize;\n\tuint16_t format;\n\tuint16_t channels;\n\tuint32_t samplerate;\n\tuint32_t avgbyterate;\n\tuint16_t samplebytes;\n\tuint16_t channelbits;\n\tchar data[4];\n\tuint32_t blocksize;\n} wav_header;\n\n\n/* In case we need mu-Law/a-Law support, these tables help us transcode */\nstatic uint8_t janus_audiobridge_g711_ulaw_enctable[256] = {\n\t0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,\n\t4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,\n\t5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n\t5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7\n};\nstatic int16_t janus_audiobridge_g711_ulaw_dectable[256] = {\n\t-32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,\n\t-23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,\n\t-15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,\n\t-11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316,\n\t-7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,\n\t-5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,\n\t-3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,\n\t-2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,\n\t-1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,\n\t-1372, -1308, -1244, -1180, -1116, -1052, -988, -924,\n\t-876, -844, -812, -780, -748, -716, -684, -652,\n\t-620, -588, -556, -524, -492, -460, -428, -396,\n\t-372, -356, -340, -324, -308, -292, -276, -260,\n\t-244, -228, -212, -196, -180, -164, -148, -132,\n\t-120, -112, -104, -96, -88, -80, -72, -64,\n\t-56, -48, -40, -32, -24, -16, -8, 0,\n\t32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,\n\t23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,\n\t15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,\n\t11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316,\n\t7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140,\n\t5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092,\n\t3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004,\n\t2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980,\n\t1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436,\n\t1372, 1308, 1244, 1180, 1116, 1052, 988, 924,\n\t876, 844, 812, 780, 748, 716, 684, 652,\n\t620, 588, 556, 524, 492, 460, 428, 396,\n\t372, 356, 340, 324, 308, 292, 276, 260,\n\t244, 228, 212, 196, 180, 164, 148, 132,\n\t120, 112, 104, 96, 88, 80, 72, 64,\n\t56, 48, 40, 32, 24, 16, 8, 0\n};\nstatic uint8_t janus_audiobridge_g711_ulaw_encode(int16_t sample) {\n\tuint8_t sign = (sample >> 8) & 0x80;\n\tif(sign)\n\t\tsample = -sample;\n\tif(sample > 32635)\n\t\tsample = 32635;\n\tsample = (int16_t)(sample + 0x84);\n\tuint8_t exponent = (int)janus_audiobridge_g711_ulaw_enctable[(sample>>7) & 0xFF];\n\tuint8_t mantissa = (sample >> (exponent+3)) & 0x0F;\n\tuint8_t encoded = ~ (sign | (exponent << 4) | mantissa);\n\treturn encoded;\n}\nstatic uint8_t janus_audiobridge_g711_alaw_enctable[128] = {\n\t1, 1, 2, 2, 3, 3, 3, 3,\n\t4, 4, 4, 4, 4, 4, 4, 4,\n\t5, 5, 5, 5, 5, 5, 5, 5,\n\t5, 5, 5, 5, 5, 5, 5, 5,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7\n};\nstatic int16_t janus_audiobridge_g711_alaw_dectable[256] = {\n\t-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,\n\t-7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,\n\t-2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,\n\t-3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,\n\t-22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944,\n\t-30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136,\n\t-11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472,\n\t-15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568,\n\t-344, -328, -376, -360, -280, -264, -312, -296,\n\t-472, -456, -504, -488, -408, -392, -440, -424,\n\t-88, -72, -120, -104, -24, -8, -56, -40,\n\t-216, -200, -248, -232, -152, -136, -184, -168,\n\t-1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,\n\t-1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,\n\t-688, -656, -752, -720, -560, -528, -624, -592,\n\t-944, -912, -1008, -976, -816, -784, -880, -848,\n\t5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736,\n\t7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784,\n\t2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368,\n\t3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392,\n\t22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,\n\t30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,\n\t11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472,\n\t15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,\n\t344, 328, 376, 360, 280, 264, 312, 296,\n\t472, 456, 504, 488, 408, 392, 440, 424,\n\t88, 72, 120, 104, 24, 8, 56, 40,\n\t216, 200, 248, 232, 152, 136, 184, 168,\n\t1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184,\n\t1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696,\n\t688, 656, 752, 720, 560, 528, 624, 592,\n\t944, 912, 1008, 976, 816, 784, 880, 848\n};\nstatic uint8_t janus_audiobridge_g711_alaw_encode(int16_t sample) {\n\tuint8_t sign = ((~sample) >> 8) & 0x80;\n\tuint8_t encoded = 0;\n\tif(!sign)\n\t\tsample = -sample;\n\tif(sample > 32635)\n\t\tsample = 32635;\n\tif(sample >= 256) {\n\t\tuint8_t exponent = janus_audiobridge_g711_alaw_enctable[(sample >> 8) & 0x7F];\n\t\tuint8_t mantissa = (sample >> (exponent + 3) ) & 0x0F;\n\t\tencoded = ((exponent << 4) | mantissa);\n\t} else {\n\t\tencoded = (uint8_t)(sample >> 4);\n\t}\n\tencoded ^= (sign ^ 0x55);\n\treturn encoded;\n}\n\n/* Ugly helper code to quickly resample (in case we're using G.711 anywhere) */\nstatic int janus_audiobridge_resample(int16_t *input, int input_num, int input_rate, int16_t *output, int output_rate) {\n\tif(input == NULL || output == NULL)\n\t\treturn 0;\n\tif((input_rate != 8000 && input_rate != 16000 && input_rate != 24000 && input_rate != 48000) ||\n\t\t\t(output_rate != 8000 && output_rate != 16000 && output_rate != 24000 && output_rate != 48000)) {\n\t\t/* Invalid sampling rate */\n\t\treturn 0;\n\t}\n\tif(input_rate != 8000 && output_rate != 8000) {\n\t\t/* We only use this for G.711, so one of the two MUST be 8000 */\n\t\treturn 0;\n\t}\n\tif(input_rate == output_rate) {\n\t\t/* Easy enough */\n\t\tmemcpy(output, input, input_num*sizeof(int16_t));\n\t\treturn input_num;\n\t} else if(input_rate < output_rate) {\n\t\t/* Upsample */\n\t\tint up = output_rate/input_rate, i = 0;\n\t\tmemset(output, 0, input_num*up);\n\t\tfor(i=0; i<input_num; i++) {\n\t\t\t*(output + i*up) = *(input + i);\n\t\t}\n\t\treturn input_num*up;\n\t} else {\n\t\t/* Downsample */\n\t\tint down = input_rate/output_rate, i = 0;\n\t\tfor(i=0; i<input_num; i++) {\n\t\t\t*(output + i) = *(input + i*down);\n\t\t}\n\t\treturn input_num/down;\n\t}\n}\n\n\n/* Opus settings */\n#define\tOPUS_SAMPLES\t960\n#define\tG711_SAMPLES\t160\n#define\tBUFFER_SAMPLES\tOPUS_SAMPLES*12\n#define DEFAULT_COMPLEXITY\t4\n\n\n/* Jitter Buffer and queue-in settings */\n#define JITTER_BUFFER_MIN_PACKETS 2\n#define JITTER_BUFFER_MAX_PACKETS 50\n#define JITTER_BUFFER_MAX_GAP_SIZE 20\n#define JITTER_BUFFER_CHECK_USECS 1*G_USEC_PER_SEC\n#define QUEUE_IN_MAX_PACKETS 4\n\n\n/* Error codes */\n#define JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR\t499\n#define JANUS_AUDIOBRIDGE_ERROR_NO_MESSAGE\t\t480\n#define JANUS_AUDIOBRIDGE_ERROR_INVALID_JSON\t481\n#define JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST\t482\n#define JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT\t483\n#define JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT\t484\n#define JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM\t485\n#define JANUS_AUDIOBRIDGE_ERROR_ROOM_EXISTS\t\t486\n#define JANUS_AUDIOBRIDGE_ERROR_NOT_JOINED\t\t487\n#define JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR\t488\n#define JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED\t489\n#define JANUS_AUDIOBRIDGE_ERROR_ID_EXISTS\t\t490\n#define JANUS_AUDIOBRIDGE_ERROR_ALREADY_JOINED\t491\n#define JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_USER\t492\n#define JANUS_AUDIOBRIDGE_ERROR_INVALID_SDP\t\t493\n#define JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_GROUP\t494\n\nstatic int janus_audiobridge_create_udp_socket_if_needed(janus_audiobridge_room *audiobridge) {\n\tif(audiobridge->rtp_udp_sock > 0) {\n\t\treturn 0;\n\t}\n\n\taudiobridge->rtp_udp_sock = socket(!ipv6_disabled ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\tif(audiobridge->rtp_udp_sock <= 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not open UDP socket for RTP forwarder (room %s), %d (%s)\\n\",\n\t\t\taudiobridge->room_id_str, errno, g_strerror(errno));\n\t\treturn -1;\n\t}\n\tif(!ipv6_disabled) {\n\t\tint v6only = 0;\n\t\tif(setsockopt(audiobridge->rtp_udp_sock, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not configure UDP socket for RTP forwarder (room %s), %d (%s))\\n\",\n\t\t\t\taudiobridge->room_id_str, errno, g_strerror(errno));\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\treturn 0;\n}\n\nstatic int janus_audiobridge_create_opus_encoder_if_needed(janus_audiobridge_room *audiobridge) {\n\tif(audiobridge->rtp_encoder != NULL) {\n\t\treturn 0;\n\t}\n\n\tint error = 0;\n\taudiobridge->rtp_encoder = opus_encoder_create(audiobridge->sampling_rate,\n\t\taudiobridge->spatial_audio ? 2 : 1, OPUS_APPLICATION_VOIP, &error);\n\tif(error != OPUS_OK) {\n\t\tJANUS_LOG(LOG_ERR, \"Error creating Opus encoder for RTP forwarder (room %s)\\n\", audiobridge->room_id_str);\n\t\treturn -1;\n\t}\n\n\tif(audiobridge->sampling_rate == 8000) {\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_NARROWBAND));\n\t} else if(audiobridge->sampling_rate == 12000) {\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_MEDIUMBAND));\n\t} else if(audiobridge->sampling_rate == 16000) {\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t} else if(audiobridge->sampling_rate == 24000) {\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_SUPERWIDEBAND));\n\t} else if(audiobridge->sampling_rate == 48000) {\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND));\n\t} else {\n\t\tJANUS_LOG(LOG_WARN, \"Unsupported sampling rate %d, setting 16kHz\\n\", audiobridge->sampling_rate);\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t}\n\n\t/* Check if we need FEC on the way out */\n\tif(audiobridge->default_expectedloss > 0) {\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_INBAND_FEC(TRUE));\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_PACKET_LOSS_PERC(audiobridge->default_expectedloss));\n\t}\n\t/* Also check if we need to enforce a bitrate */\n\tif(audiobridge->default_bitrate > 0)\n\t\topus_encoder_ctl(audiobridge->rtp_encoder, OPUS_SET_BITRATE(audiobridge->default_bitrate));\n\n\treturn 0;\n}\n\nstatic int janus_audiobridge_create_static_rtp_forwarder(janus_config_category *cat, janus_audiobridge_room *audiobridge) {\n\tguint32 forwarder_id = 0;\n\tjanus_config_item *forwarder_id_item = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_id\");\n\tif(forwarder_id_item != NULL && forwarder_id_item->value != NULL &&\n\t\t\tjanus_string_to_uint32(forwarder_id_item->value, &forwarder_id) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid forwarder ID\\n\");\n\t\treturn 0;\n\t}\n\n\tguint32 ssrc_value = 0;\n\tjanus_config_item *ssrc = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_ssrc\");\n\tif(ssrc != NULL && ssrc->value != NULL && janus_string_to_uint32(ssrc->value, &ssrc_value) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid SSRC (%s)\\n\", ssrc->value);\n\t\treturn 0;\n\t}\n\n\tjanus_audiocodec codec = JANUS_AUDIOCODEC_OPUS;\n\tjanus_config_item *rfcodec = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_codec\");\n\tif(rfcodec != NULL && rfcodec->value != NULL) {\n\t\tcodec = janus_audiocodec_from_name(rfcodec->value);\n\t\tif(codec != JANUS_AUDIOCODEC_OPUS && codec != JANUS_AUDIOCODEC_PCMA && codec != JANUS_AUDIOCODEC_PCMU) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec (%s)\\n\", rfcodec->value);\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\tint ptype = 100;\n\tjanus_config_item *pt = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_ptype\");\n\tif(pt != NULL && pt->value != NULL) {\n\t\tptype = atoi(pt->value);\n\t\tif(ptype < 0 || ptype > 127) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid payload type (%s)\\n\", pt->value);\n\t\t\treturn 0;\n\t\t}\n\t}\n\n\t/* If this room uses groups, check if a valid group name was provided */\n\tuint group = 0;\n\tif(audiobridge->groups != NULL) {\n\t\tjanus_config_item *group_name = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_group\");\n\t\tif(group_name != NULL && group_name->value != NULL) {\n\t\t\tgroup = GPOINTER_TO_UINT(g_hash_table_lookup(audiobridge->groups, group_name->value));\n\t\t\tif(group == 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid group name (%s)\\n\", group_name->value);\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t}\n\n\tjanus_config_item *port_item = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_port\");\n\tuint16_t port = 0;\n\tif(port_item != NULL && port_item->value != NULL && janus_string_to_uint16(port_item->value, &port) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s)\\n\", port_item->value);\n\t\treturn 0;\n\t}\n\tif(port == 0) {\n\t\treturn 0;\n\t}\n\n\tjanus_config_item *host_item = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_host\");\n\tif(host_item == NULL || host_item->value == NULL || strlen(host_item->value) == 0) {\n\t\treturn 0;\n\t}\n\tconst char *host = host_item->value, *resolved_host = NULL;\n\tint family = 0;\n\tjanus_config_item *host_family_item = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_host_family\");\n\tif(host_family_item != NULL && host_family_item->value != NULL) {\n\t\tconst char *host_family = host_family_item->value;\n\t\tif(host_family) {\n\t\t\tif(!strcasecmp(host_family, \"ipv4\")) {\n\t\t\t\tfamily = AF_INET;\n\t\t\t} else if(!strcasecmp(host_family, \"ipv6\")) {\n\t\t\t\tfamily = AF_INET6;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported protocol family (%s)\\n\", host_family);\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t}\n\t/* Check if we need to resolve this host address */\n\tstruct addrinfo *res = NULL, *start = NULL;\n\tjanus_network_address addr;\n\tjanus_network_address_string_buffer addr_buf;\n\tstruct addrinfo hints;\n\tmemset(&hints, 0, sizeof(hints));\n\tif(family != 0)\n\t\thints.ai_family = family;\n\tif(getaddrinfo(host, NULL, family != 0 ? &hints : NULL, &res) == 0) {\n\t\tstart = res;\n\t\twhile(res != NULL) {\n\t\t\tif(janus_network_address_from_sockaddr(res->ai_addr, &addr) == 0 &&\n\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t/* Resolved */\n\t\t\t\tresolved_host = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\tfreeaddrinfo(start);\n\t\t\t\tstart = NULL;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tres = res->ai_next;\n\t\t}\n\t}\n\tif(resolved_host == NULL) {\n\t\tif(start)\n\t\t\tfreeaddrinfo(start);\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s)...\\n\", host);\n\t\treturn 0;\n\t}\n\thost = resolved_host;\n\n\t/* We may need to SRTP-encrypt this stream */\n\tint srtp_suite = 0;\n\tconst char *srtp_crypto = NULL;\n\tjanus_config_item *s_suite = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_srtp_suite\");\n\tjanus_config_item *s_crypto = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_srtp_crypto\");\n\tif(s_suite && s_suite->value) {\n\t\tsrtp_suite = atoi(s_suite->value);\n\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't add static RTP forwarder for room %s, invalid SRTP suite...\\n\", audiobridge->room_id_str);\n\t\t\treturn 0;\n\t\t}\n\t\tif(s_crypto && s_crypto->value)\n\t\t\tsrtp_crypto = s_crypto->value;\n\t}\n\n\tjanus_config_item *always_on_item = janus_config_get(config, cat, janus_config_type_item, \"rtp_forward_always_on\");\n\tgboolean always_on = FALSE;\n\tif(always_on_item != NULL && always_on_item->value != NULL && strlen(always_on_item->value) > 0) {\n\t\talways_on = janus_is_true(always_on_item->value);\n\t}\n\n\t/* Update room */\n\tjanus_mutex_lock(&rooms_mutex);\n\tjanus_mutex_lock(&audiobridge->mutex);\n\n\tif(janus_audiobridge_create_udp_socket_if_needed(audiobridge)) {\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\treturn -1;\n\t}\n\n\tif(janus_audiobridge_create_opus_encoder_if_needed(audiobridge)) {\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\treturn -1;\n\t}\n\n\tjanus_audiobridge_rtp_forwarder_add_helper(audiobridge, group,\n\t\thost, port, ssrc_value, ptype, codec, srtp_suite, srtp_crypto,\n\t\talways_on, forwarder_id);\n\n\tjanus_mutex_unlock(&audiobridge->mutex);\n\tjanus_mutex_unlock(&rooms_mutex);\n\n\treturn 0;\n}\n\n/* Plugin implementation */\nint janus_audiobridge_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_AUDIOBRIDGE_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tconfig = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_AUDIOBRIDGE_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_AUDIOBRIDGE_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tconfig_folder = config_path;\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_audiobridge_session_destroy);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_audiobridge_message_free);\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n#ifdef HAVE_RNNOISE\n\tJANUS_LOG(LOG_INFO, \"Denoising via RNNoise supported (%d)\\n\", rnnoise_get_frame_size());\n#else\n\tJANUS_LOG(LOG_WARN, \"Denoising via RNNoise NOT supported\\n\");\n#endif\n\n\t/* Parse configuration to populate the rooms list */\n\tif(config != NULL) {\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\t/* Any admin key to limit who can \"create\"? */\n\t\tjanus_config_item *key = janus_config_get(config, config_general, janus_config_type_item, \"admin_key\");\n\t\tif(key != NULL && key->value != NULL)\n\t\t\tadmin_key = g_strdup(key->value);\n\t\tjanus_config_item *lrf = janus_config_get(config, config_general, janus_config_type_item, \"lock_rtp_forward\");\n\t\tif(admin_key && lrf != NULL && lrf->value != NULL)\n\t\t\tlock_rtpfwd = janus_is_true(lrf->value);\n\t\tjanus_config_item *lpf = janus_config_get(config, config_general, janus_config_type_item, \"lock_play_file\");\n\t\tif(admin_key && lpf != NULL && lpf->value != NULL)\n\t\t\tlock_playfile = janus_is_true(lpf->value);\n\t\tjanus_config_item *ext = janus_config_get(config, config_general, janus_config_type_item, \"record_tmp_ext\");\n\t\tif(ext != NULL && ext->value != NULL)\n\t\t\trec_tempext = g_strdup(ext->value);\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_AUDIOBRIDGE_NAME);\n\t\t}\n\t\tjanus_config_item *ids = janus_config_get(config, config_general, janus_config_type_item, \"string_ids\");\n\t\tif(ids != NULL && ids->value != NULL)\n\t\t\tstring_ids = janus_is_true(ids->value);\n\t\tif(string_ids) {\n\t\t\tJANUS_LOG(LOG_INFO, \"AudioBridge will use alphanumeric IDs, not numeric\\n\");\n\t\t}\n\t\tjanus_config_item *lip = janus_config_get(config, config_general, janus_config_type_item, \"local_ip\");\n\t\tif(lip && lip->value) {\n\t\t\t/* Verify that the address is valid */\n\t\t\tstruct ifaddrs *ifas = NULL;\n\t\t\tjanus_network_address iface;\n\t\t\tjanus_network_address_string_buffer ibuf;\n\t\t\tif(getifaddrs(&ifas) == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unable to acquire list of network devices/interfaces; some configurations may not work as expected... %d (%s)\\n\",\n\t\t\t\t\terrno, g_strerror(errno));\n\t\t\t} else {\n\t\t\t\tif(janus_network_lookup_interface(ifas, lip->value, &iface) != 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting local IP address to %s, falling back to detecting IP address...\\n\", lip->value);\n\t\t\t\t} else {\n\t\t\t\t\tif(janus_network_address_to_string_buffer(&iface, &ibuf) != 0 || janus_network_address_string_buffer_is_null(&ibuf)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error getting local IP address from %s, falling back to detecting IP address...\\n\", lip->value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlocal_ip = g_strdup(janus_network_address_string_from_buffer(&ibuf));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfreeifaddrs(ifas);\n\t\t\t}\n\t\t}\n\t\tjanus_config_item *rpr = janus_config_get(config, config_general, janus_config_type_item, \"rtp_port_range\");\n\t\tif(rpr && rpr->value) {\n\t\t\t/* Split in min and max port */\n\t\t\tchar *maxport = strrchr(rpr->value, '-');\n\t\t\tif(maxport != NULL) {\n\t\t\t\t*maxport = '\\0';\n\t\t\t\tmaxport++;\n\t\t\t\tif(janus_string_to_uint16(rpr->value, &rtp_range_min) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP min port value: %s (assuming 0)\\n\", rpr->value);\n\t\t\t\tif(janus_string_to_uint16(maxport, &rtp_range_max) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP max port value: %s (assuming 0)\\n\", maxport);\n\t\t\t\tmaxport--;\n\t\t\t\t*maxport = '-';\n\t\t\t}\n\t\t\tif(rtp_range_min > rtp_range_max) {\n\t\t\t\tuint16_t temp_port = rtp_range_min;\n\t\t\t\trtp_range_min = rtp_range_max;\n\t\t\t\trtp_range_max = temp_port;\n\t\t\t}\n\t\t\tif(rtp_range_min % 2)\n\t\t\t\trtp_range_min++;\t/* Pick an even port for RTP */\n\t\t\tif(rtp_range_min > rtp_range_max) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Incorrect port range (%u -- %u), switching min and max\\n\", rtp_range_min, rtp_range_max);\n\t\t\t\tuint16_t range_temp = rtp_range_max;\n\t\t\t\trtp_range_max = rtp_range_min;\n\t\t\t\trtp_range_min = range_temp;\n\t\t\t}\n\t\t\tif(rtp_range_max == 0)\n\t\t\t\trtp_range_max = 65535;\n\t\t\trtp_range_slider = rtp_range_min;\n\t\t\tJANUS_LOG(LOG_VERB, \"AudioBridge RTP port range: %u -- %u\\n\", rtp_range_min, rtp_range_max);\n\t\t}\n\t}\n\tif(local_ip == NULL) {\n\t\tlocal_ip = janus_network_detect_local_ip_as_string(janus_network_query_options_any_ip);\n\t\tif(local_ip == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find any address! using 127.0.0.1 as the local IP... (which is NOT going to work out of your machine)\\n\");\n\t\t\tlocal_ip = g_strdup(\"127.0.0.1\");\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Local IP set to %s\\n\", local_ip);\n\n\t/* Iterate on all rooms */\n\trooms = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_room_destroy);\n\tif(config != NULL) {\n\t\tGList *clist = janus_config_get_categories(config, NULL), *cl = clist;\n\t\twhile(cl != NULL) {\n\t\t\tjanus_config_category *cat = (janus_config_category *)cl->data;\n\t\t\tif(cat->name == NULL || !strcasecmp(cat->name, \"general\")) {\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Adding AudioBridge room '%s'\\n\", cat->name);\n\t\t\tjanus_config_item *desc = janus_config_get(config, cat, janus_config_type_item, \"description\");\n\t\t\tjanus_config_item *priv = janus_config_get(config, cat, janus_config_type_item, \"is_private\");\n\t\t\tjanus_config_item *sampling = janus_config_get(config, cat, janus_config_type_item, \"sampling_rate\");\n\t\t\tjanus_config_item *spatial = janus_config_get(config, cat, janus_config_type_item, \"spatial_audio\");\n\t\t\tjanus_config_item *audiolevel_ext = janus_config_get(config, cat, janus_config_type_item, \"audiolevel_ext\");\n\t\t\tjanus_config_item *audiolevel_event = janus_config_get(config, cat, janus_config_type_item, \"audiolevel_event\");\n\t\t\tjanus_config_item *audio_active_packets = janus_config_get(config, cat, janus_config_type_item, \"audio_active_packets\");\n\t\t\tjanus_config_item *audio_level_average = janus_config_get(config, cat, janus_config_type_item, \"audio_level_average\");\n\t\t\tjanus_config_item *default_expectedloss = janus_config_get(config, cat, janus_config_type_item, \"default_expectedloss\");\n\t\t\tjanus_config_item *default_bitrate = janus_config_get(config, cat, janus_config_type_item, \"default_bitrate\");\n\t\t\tjanus_config_item *denoise = janus_config_get(config, cat, janus_config_type_item, \"denoise\");\n\t\t\tjanus_config_item *secret = janus_config_get(config, cat, janus_config_type_item, \"secret\");\n\t\t\tjanus_config_item *pin = janus_config_get(config, cat, janus_config_type_item, \"pin\");\n\t\t\tjanus_config_array *groups = janus_config_get(config, cat, janus_config_type_array, \"groups\");\n\t\t\tjanus_config_item *record = janus_config_get(config, cat, janus_config_type_item, \"record\");\n\t\t\tjanus_config_item *recfile = janus_config_get(config, cat, janus_config_type_item, \"record_file\");\n\t\t\tjanus_config_item *recdir = janus_config_get(config, cat, janus_config_type_item, \"record_dir\");\n\t\t\tjanus_config_item *mjrs = janus_config_get(config, cat, janus_config_type_item, \"mjrs\");\n\t\t\tjanus_config_item *mjrsdir = janus_config_get(config, cat, janus_config_type_item, \"mjrs_dir\");\n\t\t\tjanus_config_item *allowrtp = janus_config_get(config, cat, janus_config_type_item, \"allow_rtp_participants\");\n\t\t\tif(sampling == NULL || sampling->value == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the AudioBridge room, missing mandatory information...\\n\");\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* Create the AudioBridge room */\n\t\t\tjanus_audiobridge_room *audiobridge = g_malloc0(sizeof(janus_audiobridge_room));\n\t\t\tjanus_refcount_init(&audiobridge->ref, janus_audiobridge_room_free);\n\t\t\tconst char *room_num = cat->name;\n\t\t\tif(strstr(room_num, \"room-\") == room_num)\n\t\t\t\troom_num += 5;\n\t\t\tif(!string_ids) {\n\t\t\t\taudiobridge->room_id = g_ascii_strtoull(room_num, NULL, 0);\n\t\t\t\tif(audiobridge->room_id == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the AudioBridge room, invalid ID 0...\\n\");\n\t\t\t\t\tjanus_audiobridge_room_destroy(audiobridge);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Make sure the ID is completely numeric */\n\t\t\t\tchar room_id_str[30];\n\t\t\t\tg_snprintf(room_id_str, sizeof(room_id_str), \"%\"SCNu64, audiobridge->room_id);\n\t\t\t\tif(strcmp(room_num, room_id_str)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the AudioBridge room, ID '%s' is not numeric...\\n\", room_num);\n\t\t\t\t\tjanus_audiobridge_room_destroy(audiobridge);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Let's make sure the room doesn't exist already */\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\tif(g_hash_table_lookup(rooms, string_ids ? (gpointer)room_num : (gpointer)&audiobridge->room_id) != NULL) {\n\t\t\t\t/* It does... */\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the AudioBridge room, room %s already exists...\\n\", room_num);\n\t\t\t\tjanus_audiobridge_room_destroy(audiobridge);\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\taudiobridge->room_id_str = g_strdup(room_num);\n\t\t\tchar *description = NULL;\n\t\t\tif(desc != NULL && desc->value != NULL && strlen(desc->value) > 0)\n\t\t\t\tdescription = g_strdup(desc->value);\n\t\t\telse\n\t\t\t\tdescription = g_strdup(cat->name);\n\t\t\taudiobridge->room_name = description;\n\t\t\taudiobridge->is_private = priv && priv->value && janus_is_true(priv->value);\n\t\t\taudiobridge->sampling_rate = atol(sampling->value);\n\t\t\tswitch(audiobridge->sampling_rate) {\n\t\t\t\tcase 8000:\n\t\t\t\tcase 12000:\n\t\t\t\tcase 16000:\n\t\t\t\tcase 24000:\n\t\t\t\tcase 48000:\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Sampling rate for mixing: %\"SCNu32\"\\n\", audiobridge->sampling_rate);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported sampling rate %\"SCNu32\"...\\n\", audiobridge->sampling_rate);\n\t\t\t\t\tjanus_audiobridge_room_destroy(audiobridge);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t\taudiobridge->spatial_audio = spatial && spatial->value && janus_is_true(spatial->value);\n\t\t\taudiobridge->audiolevel_ext = TRUE;\n\t\t\tif(audiolevel_ext != NULL && audiolevel_ext->value != NULL)\n\t\t\t\taudiobridge->audiolevel_ext = janus_is_true(audiolevel_ext->value);\n\t\t\taudiobridge->audiolevel_event = FALSE;\n\t\t\tif(audiolevel_event != NULL && audiolevel_event->value != NULL)\n\t\t\t\taudiobridge->audiolevel_event = janus_is_true(audiolevel_event->value);\n\t\t\tif(audiobridge->audiolevel_event) {\n\t\t\t\taudiobridge->audio_active_packets = 100;\n\t\t\t\tif(audio_active_packets != NULL && audio_active_packets->value != NULL){\n\t\t\t\t\tif(atoi(audio_active_packets->value) > 0) {\n\t\t\t\t\t\taudiobridge->audio_active_packets = atoi(audio_active_packets->value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_active_packets value provided, using default: %d\\n\", audiobridge->audio_active_packets);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\taudiobridge->audio_level_average = 25;\n\t\t\t\tif(audio_level_average != NULL && audio_level_average->value != NULL) {\n\t\t\t\t\tif(atoi(audio_level_average->value) > 0) {\n\t\t\t\t\t\taudiobridge->audio_level_average = atoi(audio_level_average->value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_level_average value provided, using default: %d\\n\", audiobridge->audio_level_average);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\taudiobridge->default_expectedloss = 0;\n\t\t\tif(default_expectedloss != NULL && default_expectedloss->value != NULL) {\n\t\t\t\tint expectedloss = atoi(default_expectedloss->value);\n\t\t\t\tif(expectedloss < 0 || expectedloss > 20) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid expectedloss value provided, using default: 0\\n\");\n\t\t\t\t} else {\n\t\t\t\t\taudiobridge->default_expectedloss = expectedloss;\n\t\t\t\t}\n\t\t\t}\n\t\t\taudiobridge->default_bitrate = 0;\n\t\t\tif(default_bitrate != NULL && default_bitrate->value != NULL) {\n\t\t\t\taudiobridge->default_bitrate = atoi(default_bitrate->value);\n\t\t\t\tif(audiobridge->default_bitrate < 500 || audiobridge->default_bitrate > 512000) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid bitrate %\"SCNi32\", falling back to auto\\n\", audiobridge->default_bitrate);\n\t\t\t\t\taudiobridge->default_bitrate = 0;\n\t\t\t\t}\n\t\t\t}\n#ifdef HAVE_RNNOISE\n\t\t\taudiobridge->denoise = denoise && denoise->value && janus_is_true(denoise->value);\n#else\n\t\t\tif(denoise && denoise->value && janus_is_true(denoise->value)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"RNNoise unavailable, denoising not supported\\n\");\n\t\t\t}\n#endif\n\t\t\taudiobridge->room_ssrc = janus_random_uint32();\n\t\t\tif(secret != NULL && secret->value != NULL) {\n\t\t\t\taudiobridge->room_secret = g_strdup(secret->value);\n\t\t\t}\n\t\t\tif(pin != NULL && pin->value != NULL) {\n\t\t\t\taudiobridge->room_pin = g_strdup(pin->value);\n\t\t\t}\n\t\t\tg_atomic_int_set(&audiobridge->record, 0);\n\t\t\tif(record && record->value && janus_is_true(record->value))\n\t\t\t\tg_atomic_int_set(&audiobridge->record, 1);\n\t\t\tif(recfile && recfile->value)\n\t\t\t\taudiobridge->record_file = g_strdup(recfile->value);\n\t\t\tif(recdir && recdir->value) {\n\t\t\t\taudiobridge->record_dir = g_strdup(recdir->value);\n\t\t\t\tif(janus_mkdir(audiobridge->record_dir, 0755) < 0) {\n\t\t\t\t\t/* FIXME Should this be fatal, when creating a room? */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"AudioBridge mkdir (%s) error: %d (%s)\\n\", audiobridge->record_dir, errno, g_strerror(errno));\n\t\t\t\t}\n\t\t\t}\n\t\t\taudiobridge->recording = NULL;\n\t\t\tif(mjrs && mjrs->value && janus_is_true(mjrs->value))\n\t\t\t\taudiobridge->mjrs = TRUE;\n\t\t\tif(mjrsdir && mjrsdir->value)\n\t\t\t\taudiobridge->mjrs_dir = g_strdup(mjrsdir->value);\n\t\t\taudiobridge->allow_plainrtp = FALSE;\n\t\t\tif(allowrtp && allowrtp->value)\n\t\t\t\taudiobridge->allow_plainrtp = janus_is_true(allowrtp->value);\n\t\t\taudiobridge->destroy = 0;\n\t\t\taudiobridge->participants = g_hash_table_new_full(\n\t\t\t\tstring_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref);\n\t\t\taudiobridge->anncs = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref);\n\t\t\taudiobridge->check_tokens = FALSE;\t/* Static rooms can't have an \"allowed\" list yet, no hooks to the configuration file */\n\t\t\taudiobridge->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\t\tif(groups != NULL) {\n\t\t\t\t/* Populate the group hashtable, and create the related indexes */\n\t\t\t\tGList *gl = groups->list;\n\t\t\t\tif(g_list_length(gl) > JANUS_AUDIOBRIDGE_MAX_GROUPS) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Too many groups specified in room %s (max %d allowed)\\n\", room_num, JANUS_AUDIOBRIDGE_MAX_GROUPS);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t}\n\t\t\t\tint count = 0;\n\t\t\t\taudiobridge->groups = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\t\t\taudiobridge->groups_byid = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)g_free);\n\t\t\t\twhile(gl) {\n\t\t\t\t\tjanus_config_item *g = (janus_config_item *)gl->data;\n\t\t\t\t\tif(g == NULL || g->type != janus_config_type_item || g->name != NULL || g->value == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid group item (not a string?), skipping in '%s'...\\n\", cat->name);\n\t\t\t\t\t\tgl = gl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst char *name = g->value;\n\t\t\t\t\tif(g_hash_table_lookup(audiobridge->groups, name)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicated group name '%s', skipping\\n\", name);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcount++;\n\t\t\t\t\t\tg_hash_table_insert(audiobridge->groups, g_strdup(name), GUINT_TO_POINTER(count));\n\t\t\t\t\t\tg_hash_table_insert(audiobridge->groups_byid, GUINT_TO_POINTER(count), g_strdup(name));\n\t\t\t\t\t}\n\t\t\t\t\tgl = gl->next;\n\t\t\t\t}\n\t\t\t\tif(count == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Empty or invalid groups array provided, groups will be disabled in '%s'...\\n\", cat->name);\n\t\t\t\t\tg_hash_table_destroy(audiobridge->groups);\n\t\t\t\t\tg_hash_table_destroy(audiobridge->groups_byid);\n\t\t\t\t\taudiobridge->groups = NULL;\n\t\t\t\t\taudiobridge->groups_byid = NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_atomic_int_set(&audiobridge->destroyed, 0);\n\t\t\tjanus_mutex_init(&audiobridge->mutex);\n\t\t\taudiobridge->rtp_forwarders = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\t\t\taudiobridge->rtp_encoder = NULL;\n\t\t\taudiobridge->rtp_udp_sock = -1;\n\t\t\tjanus_mutex_init(&audiobridge->rtp_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"Created AudioBridge room: %s (%s, %s, secret: %s, pin: %s)\\n\",\n\t\t\t\taudiobridge->room_id_str, audiobridge->room_name,\n\t\t\t\taudiobridge->is_private ? \"private\" : \"public\",\n\t\t\t\taudiobridge->room_secret ? audiobridge->room_secret : \"no secret\",\n\t\t\t\taudiobridge->room_pin ? audiobridge->room_pin : \"no pin\");\n\n\t\t\tif(janus_audiobridge_create_static_rtp_forwarder(cat, audiobridge)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating static RTP forwarder (room %s)\\n\", audiobridge->room_id_str);\n\t\t\t}\n\n\t\t\t/* We need a thread for the mix */\n\t\t\tGError *error = NULL;\n\t\t\tchar tname[16];\n\t\t\tg_snprintf(tname, sizeof(tname), \"mixer %s\", audiobridge->room_id_str);\n\t\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\t\taudiobridge->thread = g_thread_try_new(tname, &janus_audiobridge_mixer_thread, audiobridge, &error);\n\t\t\tif(error != NULL) {\n\t\t\t\t/* FIXME We should clear some resources... */\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the mixer thread...\\n\",\n\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\tg_error_free(error);\n\t\t\t} else {\n\t\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\t\tg_hash_table_insert(rooms,\n\t\t\t\t\tstring_ids ? (gpointer)g_strdup(audiobridge->room_id_str) : (gpointer)janus_uint64_dup(audiobridge->room_id),\n\t\t\t\t\taudiobridge);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t}\n\t\t\tcl = cl->next;\n\t\t}\n\t\tg_list_free(clist);\n\t\t/* Done: we keep the configuration file open in case we get a \"create\" or \"destroy\" with permanent=true */\n\t}\n\n\t/* Show available rooms */\n\tjanus_mutex_lock(&rooms_mutex);\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, rooms);\n\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_audiobridge_room *ar = value;\n\t\tJANUS_LOG(LOG_VERB, \"  ::: [%s][%s] %\"SCNu32\" (%s be recorded)\\n\",\n\t\t\tar->room_id_str, ar->room_name, ar->sampling_rate, g_atomic_int_get(&ar->record) ? \"will\" : \"will NOT\");\n\t}\n\tjanus_mutex_unlock(&rooms_mutex);\n\n\t/* Finally, let's check if IPv6 is disabled, as we may need to know for forwarders */\n\tint fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\tif(fd < 0) {\n\t\tipv6_disabled = TRUE;\n\t} else {\n\t\tint v6only = 0;\n\t\tif(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)\n\t\t\tipv6_disabled = TRUE;\n\t}\n\tif(fd >= 0)\n\t\tclose(fd);\n\tif(ipv6_disabled) {\n\t\tJANUS_LOG(LOG_WARN, \"IPv6 disabled, will only create VideoRoom forwarders to IPv4 addresses\\n\");\n\t}\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming messages */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"audiobridge handler\", janus_audiobridge_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the AudioBridge handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_config_destroy(config);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_AUDIOBRIDGE_NAME);\n\treturn 0;\n}\n\nvoid janus_audiobridge_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tjanus_mutex_lock(&rooms_mutex);\n\tg_hash_table_destroy(rooms);\n\trooms = NULL;\n\tjanus_mutex_unlock(&rooms_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\n\tjanus_config_destroy(config);\n\tg_free(admin_key);\n\tg_free(rec_tempext);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_AUDIOBRIDGE_NAME);\n}\n\nint janus_audiobridge_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_audiobridge_get_version(void) {\n\treturn JANUS_AUDIOBRIDGE_VERSION;\n}\n\nconst char *janus_audiobridge_get_version_string(void) {\n\treturn JANUS_AUDIOBRIDGE_VERSION_STRING;\n}\n\nconst char *janus_audiobridge_get_description(void) {\n\treturn JANUS_AUDIOBRIDGE_DESCRIPTION;\n}\n\nconst char *janus_audiobridge_get_name(void) {\n\treturn JANUS_AUDIOBRIDGE_NAME;\n}\n\nconst char *janus_audiobridge_get_author(void) {\n\treturn JANUS_AUDIOBRIDGE_AUTHOR;\n}\n\nconst char *janus_audiobridge_get_package(void) {\n\treturn JANUS_AUDIOBRIDGE_PACKAGE;\n}\n\nstatic janus_audiobridge_session *janus_audiobridge_lookup_session(janus_plugin_session *handle) {\n\tjanus_audiobridge_session *session = NULL;\n\tif(g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_audiobridge_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_audiobridge_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_audiobridge_session *session = g_malloc0(sizeof(janus_audiobridge_session));\n\tsession->handle = handle;\n\tg_atomic_int_set(&session->started, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\thandle->plugin_handle = session;\n\tjanus_refcount_init(&session->ref, janus_audiobridge_session_free);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_audiobridge_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_audiobridge_session *session = janus_audiobridge_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No AudioBridge session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Removing AudioBridge session...\\n\");\n\tjanus_audiobridge_hangup_media_internal(handle);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nstatic void janus_audiobridge_notify_participants(janus_audiobridge_room *room,\n\t\tjanus_audiobridge_participant *participant, json_t *msg, gboolean notify_source_participant) {\n\t/* room->participants_mutex has to be locked. */\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, room->participants);\n\twhile(!room->destroyed && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_audiobridge_participant *p = value;\n\t\tif(p && p->session && (p != participant || notify_source_participant) && !g_atomic_int_get(&p->paused_events)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, msg, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t}\n\t}\n}\n\njson_t *janus_audiobridge_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_audiobridge_session *session = janus_audiobridge_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Show the participant/room info, if any */\n\tjson_t *info = json_object();\n\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\tjson_object_set_new(info, \"state\", json_string(participant && participant->room ? \"inroom\" : \"idle\"));\n\tif(participant) {\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *room = participant->room;\n\t\tif(room != NULL)\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room->room_id_str) : json_integer(room->room_id));\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\tif(room && participant->group > 0 && room->groups_byid != NULL) {\n\t\t\tchar *name = g_hash_table_lookup(room->groups_byid, GUINT_TO_POINTER(participant->group));\n\t\t\tif(name != NULL)\n\t\t\t\tjson_object_set_new(info, \"group\", json_string(name));\n\t\t}\n\t\tif(participant->display)\n\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\tif(participant->admin)\n\t\t\tjson_object_set_new(info, \"admin\", json_true());\n\t\tjson_object_set_new(info, \"muted\", participant->muted ? json_true() : json_false());\n\t\tjson_object_set_new(info, \"active\", g_atomic_int_get(&participant->active) ? json_true() : json_false());\n\t\tjanus_mutex_lock(&participant->qmutex);\n\t\tspx_int32_t count = 0;\n\t\tif(participant->jitter)\n\t\t\tjitter_buffer_ctl(participant->jitter, JITTER_BUFFER_GET_AVALIABLE_COUNT, &count);\n\t\tjson_object_set_new(info, \"buffer-in\", json_integer(count));\n\t\tjson_object_set_new(info, \"queue-in\", json_integer(g_list_length(participant->inbuf)));\n\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\tif(participant->outbuf)\n\t\t\tjson_object_set_new(info, \"queue-out\", json_integer(g_async_queue_length(participant->outbuf)));\n\t\tif(participant->stereo)\n\t\t\tjson_object_set_new(info, \"spatial_position\", json_integer(participant->spatial_position));\n#ifdef HAVE_RNNOISE\n\t\tjson_object_set_new(info, \"denoise\",  participant->denoise ? json_true() : json_false());\n#endif\n\t\tif(participant->arc && participant->arc->filename)\n\t\t\tjson_object_set_new(info, \"audio-recording\", json_string(participant->arc->filename));\n\t\tif(participant->extmap_id > 0) {\n\t\t\tjson_object_set_new(info, \"audio-level-dBov\", json_integer(participant->dBov_level));\n\t\t\tjson_object_set_new(info, \"talking\", participant->talking ? json_true() : json_false());\n\t\t}\n\t\tjson_object_set_new(info, \"fec\", participant->fec ? json_true() : json_false());\n\t\tif(participant->fec)\n\t\t\tjson_object_set_new(info, \"expected-loss\", json_integer(participant->expected_loss));\n\t\tif(participant->opus_bitrate)\n\t\t\tjson_object_set_new(info, \"opus-bitrate\", json_integer(participant->opus_bitrate));\n\t\tif(participant->plainrtp && participant->plainrtp_media.audio_rtp_fd != -1) {\n\t\t\tjson_t *rtp = json_object();\n\t\t\tif(local_ip)\n\t\t\t\tjson_object_set_new(rtp, \"local-ip\", json_string(local_ip));\n\t\t\tif(participant->plainrtp_media.local_audio_rtp_port)\n\t\t\t\tjson_object_set_new(rtp, \"local-port\", json_integer(participant->plainrtp_media.local_audio_rtp_port));\n\t\t\tif(participant->plainrtp_media.remote_audio_ip)\n\t\t\t\tjson_object_set_new(rtp, \"remote-ip\", json_string(participant->plainrtp_media.remote_audio_ip));\n\t\t\tif(participant->plainrtp_media.remote_audio_rtp_port)\n\t\t\t\tjson_object_set_new(rtp, \"remote-port\", json_integer(participant->plainrtp_media.remote_audio_rtp_port));\n\t\t\tif(participant->plainrtp_media.audio_ssrc)\n\t\t\t\tjson_object_set_new(rtp, \"local-ssrc\", json_integer(participant->plainrtp_media.audio_ssrc));\n\t\t\tif(participant->plainrtp_media.audio_ssrc_peer)\n\t\t\t\tjson_object_set_new(rtp, \"remote-ssrc\", json_integer(participant->plainrtp_media.audio_ssrc_peer));\n\t\t\tjson_object_set_new(info, \"plain-rtp\", rtp);\n\t\t}\n\t\tjson_object_set_new(info, \"suspended\", g_atomic_int_get(&participant->suspended) ? json_true() : json_false());\n\t}\n\tif(session->plugin_offer)\n\t\tjson_object_set_new(info, \"plugin-offer\", json_true());\n\tjson_object_set_new(info, \"started\", g_atomic_int_get(&session->started) ? json_true() : json_false());\n\tjson_object_set_new(info, \"hangingup\", g_atomic_int_get(&session->hangingup) ? json_true() : json_false());\n\tjson_object_set_new(info, \"destroyed\", g_atomic_int_get(&session->destroyed) ? json_true() : json_false());\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstatic int janus_audiobridge_access_room(json_t *root, gboolean check_modify, janus_audiobridge_room **audiobridge, char *error_cause, int error_cause_size) {\n\t/* rooms_mutex has to be locked */\n\tint error_code = 0;\n\tjson_t *room = json_object_get(root, \"room\");\n\tguint64 room_id = 0;\n\tchar room_id_num[30], *room_id_str = NULL;\n\tif(!string_ids) {\n\t\troom_id = json_integer_value(room);\n\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\troom_id_str = room_id_num;\n\t} else {\n\t\troom_id_str = (char *)json_string_value(room);\n\t}\n\t*audiobridge = g_hash_table_lookup(rooms,\n\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\tif(*audiobridge == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\tif(error_cause)\n\t\t\tg_snprintf(error_cause, error_cause_size, \"No such room (%s)\", room_id_str);\n\t\treturn error_code;\n\t}\n\tif(g_atomic_int_get(&((*audiobridge)->destroyed))) {\n\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\tif(error_cause)\n\t\t\tg_snprintf(error_cause, error_cause_size, \"No such room (%s)\", room_id_str);\n\t\treturn error_code;\n\t}\n\tif(check_modify) {\n\t\tchar error_cause2[100];\n\t\tJANUS_CHECK_SECRET((*audiobridge)->room_secret, root, \"secret\", error_code, error_cause2,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tg_strlcpy(error_cause, error_cause2, error_cause_size);\n\t\t\treturn error_code;\n\t\t}\n\t}\n\treturn 0;\n}\n\n\n/* Helper method to process synchronous requests */\nstatic json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_session *session, json_t *message) {\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\n\t/* Parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\n\tif(!strcasecmp(request_text, \"create\")) {\n\t\t/* Create a new AudioBridge */\n\t\tJANUS_LOG(LOG_VERB, \"Creating a new AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, create_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomopt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstropt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\tjson_t *desc = json_object_get(root, \"description\");\n\t\tjson_t *secret = json_object_get(root, \"secret\");\n\t\tjson_t *pin = json_object_get(root, \"pin\");\n\t\tjson_t *is_private = json_object_get(root, \"is_private\");\n\t\tjson_t *allowed = json_object_get(root, \"allowed\");\n\t\tjson_t *sampling = json_object_get(root, \"sampling_rate\");\n\t\tif(sampling == NULL)\n\t\t\tsampling = json_object_get(root, \"sampling\");\n\t\tjson_t *spatial = json_object_get(root, \"spatial_audio\");\n\t\tjson_t *audiolevel_ext = json_object_get(root, \"audiolevel_ext\");\n\t\tjson_t *audiolevel_event = json_object_get(root, \"audiolevel_event\");\n\t\tjson_t *audio_active_packets = json_object_get(root, \"audio_active_packets\");\n\t\tjson_t *audio_level_average = json_object_get(root, \"audio_level_average\");\n\t\tjson_t *default_expectedloss = json_object_get(root, \"default_expectedloss\");\n\t\tjson_t *default_bitrate = json_object_get(root, \"default_bitrate\");\n\t\tjson_t *denoise = json_object_get(root, \"denoise\");\n\t\tjson_t *groups = json_object_get(root, \"groups\");\n\t\tjson_t *record = json_object_get(root, \"record\");\n\t\tjson_t *recfile = json_object_get(root, \"record_file\");\n\t\tjson_t *recdir = json_object_get(root, \"record_dir\");\n\t\tjson_t *mjrs = json_object_get(root, \"mjrs\");\n\t\tjson_t *mjrsdir = json_object_get(root, \"mjrs_dir\");\n\t\tjson_t *allowrtp = json_object_get(root, \"allow_rtp_participants\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tif(allowed) {\n\t\t\t/* Make sure the \"allowed\" array only contains strings */\n\t\t\tgboolean ok = TRUE;\n\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tjson_t *a = json_array_get(allowed, i);\n\t\t\t\t\tif(!a || !json_is_string(a)) {\n\t\t\t\t\t\tok = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!ok) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element in the allowed array (not a string)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element in the allowed array (not a string)\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tif(groups) {\n\t\t\t/* Make sure the \"groups\" array only contains strings */\n\t\t\tgboolean ok = TRUE;\n\t\t\tif(json_array_size(groups) > 0) {\n\t\t\t\tif(json_array_size(groups) > JANUS_AUDIOBRIDGE_MAX_GROUPS) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Too many groups specified (max %d allowed)\\n\", JANUS_AUDIOBRIDGE_MAX_GROUPS);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Too many groups specified (max %d allowed)\", JANUS_AUDIOBRIDGE_MAX_GROUPS);\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(groups); i++) {\n\t\t\t\t\tjson_t *a = json_array_get(groups, i);\n\t\t\t\t\tif(!a || !json_is_string(a)) {\n\t\t\t\t\t\tok = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!ok) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element in the groups array (not a string)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element in the groups array (not a string)\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't create permanent room\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't create permanent room\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tif(room_id == 0 && room_id_str == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Desired room ID is empty, which is not allowed... picking random ID instead\\n\");\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tif(room_id > 0 || room_id_str != NULL) {\n\t\t\t/* Let's make sure the room doesn't exist already */\n\t\t\tif(g_hash_table_lookup(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id) != NULL) {\n\t\t\t\t/* It does... */\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_ROOM_EXISTS;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Room %s already exists!\\n\", room_id_str);\n\t\t\t\tg_snprintf(error_cause, 512, \"Room %s already exists\", room_id_str);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\t/* Create the AudioBridge room */\n\t\tjanus_audiobridge_room *audiobridge = g_malloc0(sizeof(janus_audiobridge_room));\n\t\tjanus_refcount_init(&audiobridge->ref, janus_audiobridge_room_free);\n\t\t/* Generate a random ID, if needed */\n\t\tgboolean room_id_allocated = FALSE;\n\t\tif(!string_ids && room_id == 0) {\n\t\t\twhile(room_id == 0) {\n\t\t\t\troom_id = janus_random_uint64();\n\t\t\t\tif(g_hash_table_lookup(rooms, &room_id) != NULL) {\n\t\t\t\t\t/* Room ID already taken, try another one */\n\t\t\t\t\troom_id = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else if(string_ids && room_id_str == NULL) {\n\t\t\twhile(room_id_str == NULL) {\n\t\t\t\troom_id_str = janus_random_uuid();\n\t\t\t\tif(g_hash_table_lookup(rooms, room_id_str) != NULL) {\n\t\t\t\t\t/* Room ID already taken, try another one */\n\t\t\t\t\tg_clear_pointer(&room_id_str, g_free);\n\t\t\t\t}\n\t\t\t}\n\t\t\troom_id_allocated = TRUE;\n\t\t}\n\t\taudiobridge->room_id = room_id;\n\t\taudiobridge->room_id_str = room_id_str ? g_strdup(room_id_str) : NULL;\n\t\tchar *description = NULL;\n\t\tif(desc != NULL && strlen(json_string_value(desc)) > 0) {\n\t\t\tdescription = g_strdup(json_string_value(desc));\n\t\t} else {\n\t\t\tchar roomname[255];\n\t\t\tg_snprintf(roomname, 255, \"Room %s\", audiobridge->room_id_str);\n\t\t\tdescription = g_strdup(roomname);\n\t\t}\n\t\taudiobridge->room_name = description;\n\t\taudiobridge->is_private = is_private ? json_is_true(is_private) : FALSE;\n\t\tif(secret)\n\t\t\taudiobridge->room_secret = g_strdup(json_string_value(secret));\n\t\tif(pin)\n\t\t\taudiobridge->room_pin = g_strdup(json_string_value(pin));\n\t\tif(sampling)\n\t\t\taudiobridge->sampling_rate = json_integer_value(sampling);\n\t\telse\n\t\t\taudiobridge->sampling_rate = 16000;\n\t\taudiobridge->spatial_audio = spatial ? json_is_true(spatial) : FALSE;\n\t\taudiobridge->audiolevel_ext = audiolevel_ext ? json_is_true(audiolevel_ext) : TRUE;\n\t\taudiobridge->audiolevel_event = audiolevel_event ? json_is_true(audiolevel_event) : FALSE;\n\t\tif(audiobridge->audiolevel_event) {\n\t\t\taudiobridge->audio_active_packets = 100;\n\t\t\tif(json_integer_value(audio_active_packets) > 0) {\n\t\t\t\taudiobridge->audio_active_packets = json_integer_value(audio_active_packets);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_active_packets value provided, using default: %d\\n\",\n\t\t\t\t\taudiobridge->audio_active_packets);\n\t\t\t}\n\t\t\taudiobridge->audio_level_average = 25;\n\t\t\tif(json_integer_value(audio_level_average) > 0) {\n\t\t\t\taudiobridge->audio_level_average = json_integer_value(audio_level_average);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_level_average value provided, using default: %d\\n\",\n\t\t\t\t\taudiobridge->audio_level_average);\n\t\t\t}\n\t\t}\n\t\taudiobridge->default_expectedloss = 0;\n\t\tif(default_expectedloss != NULL) {\n\t\t\tint expectedloss = json_integer_value(default_expectedloss);\n\t\t\tif(expectedloss > 20) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid expectedloss value provided, using default: 0\\n\");\n\t\t\t} else {\n\t\t\t\taudiobridge->default_expectedloss = expectedloss;\n\t\t\t}\n\t\t}\n\t\taudiobridge->default_bitrate = 0;\n\t\tif(default_bitrate != NULL) {\n\t\t\taudiobridge->default_bitrate = json_integer_value(default_bitrate);\n\t\t\tif(audiobridge->default_bitrate < 500 || audiobridge->default_bitrate > 512000) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid bitrate %\"SCNi32\", falling back to auto\\n\", audiobridge->default_bitrate);\n\t\t\t\taudiobridge->default_bitrate = 0;\n\t\t\t}\n\t\t}\n#ifdef HAVE_RNNOISE\n\t\taudiobridge->denoise = denoise ? json_is_true(denoise) : FALSE;\n#else\n\t\tif(denoise && json_is_true(denoise)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"RNNoise unavailable, denoising not supported\\n\");\n\t\t}\n#endif\n\t\tswitch(audiobridge->sampling_rate) {\n\t\t\tcase 8000:\n\t\t\tcase 12000:\n\t\t\tcase 16000:\n\t\t\tcase 24000:\n\t\t\tcase 48000:\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Sampling rate for mixing: %\"SCNu32\"\\n\", audiobridge->sampling_rate);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tif(room_id_allocated)\n\t\t\t\t\tg_free(room_id_str);\n\t\t\t\tjanus_audiobridge_room_destroy(audiobridge);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported sampling rate %\"SCNu32\"...\\n\", audiobridge->sampling_rate);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported sampling rate %\"SCNu32, audiobridge->sampling_rate);\n\t\t\t\tjanus_audiobridge_room_destroy(audiobridge);\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\taudiobridge->room_ssrc = janus_random_uint32();\n\t\tg_atomic_int_set(&audiobridge->record, 0);\n\t\tif(record && json_is_true(record))\n\t\t\tg_atomic_int_set(&audiobridge->record, 1);\n\t\tif(recfile)\n\t\t\taudiobridge->record_file = g_strdup(json_string_value(recfile));\n\t\tif(recdir) {\n\t\t\taudiobridge->record_dir = g_strdup(json_string_value(recdir));\n\t\t\tif(janus_mkdir(audiobridge->record_dir, 0755) < 0) {\n\t\t\t\t/* FIXME Should this be fatal, when creating a room? */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"AudioBridge mkdir (%s) error: %d (%s)\\n\", audiobridge->record_dir, errno, g_strerror(errno));\n\t\t\t}\n\t\t}\n\t\taudiobridge->recording = NULL;\n\t\tif(mjrs && json_is_true(mjrs))\n\t\t\taudiobridge->mjrs = TRUE;\n\t\tif(mjrsdir)\n\t\t\taudiobridge->mjrs_dir = g_strdup(json_string_value(mjrsdir));\n\t\taudiobridge->allow_plainrtp = FALSE;\n\t\tif(allowrtp && json_is_true(allowrtp))\n\t\t\taudiobridge->allow_plainrtp = TRUE;\n\t\taudiobridge->destroy = 0;\n\t\taudiobridge->participants = g_hash_table_new_full(\n\t\t\tstring_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref);\n\t\taudiobridge->anncs = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref);\n\t\taudiobridge->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\tif(allowed != NULL) {\n\t\t\t/* Populate the \"allowed\" list as an ACL for people trying to join */\n\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tconst char *token = json_string_value(json_array_get(allowed, i));\n\t\t\t\t\tif(!g_hash_table_lookup(audiobridge->allowed, token))\n\t\t\t\t\t\tg_hash_table_insert(audiobridge->allowed, g_strdup(token), GINT_TO_POINTER(TRUE));\n\t\t\t\t}\n\t\t\t}\n\t\t\taudiobridge->check_tokens = TRUE;\n\t\t}\n\t\tg_atomic_int_set(&audiobridge->destroyed, 0);\n\t\tjanus_mutex_init(&audiobridge->mutex);\n\t\tif(groups != NULL && json_array_size(groups) > 0) {\n\t\t\t/* Populate the group hashtable, and create the related indexes */\n\t\t\taudiobridge->groups = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\t\taudiobridge->groups_byid = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)g_free);\n\t\t\tsize_t i = 0;\n\t\t\tint count = 0;\n\t\t\tfor(i=0; i<json_array_size(groups); i++) {\n\t\t\t\tconst char *name = json_string_value(json_array_get(groups, i));\n\t\t\t\tif(g_hash_table_lookup(audiobridge->groups, name)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicated group name '%s', skipping\\n\", name);\n\t\t\t\t} else {\n\t\t\t\t\tcount++;\n\t\t\t\t\tg_hash_table_insert(audiobridge->groups, g_strdup(name), GUINT_TO_POINTER(count));\n\t\t\t\t\tg_hash_table_insert(audiobridge->groups_byid, GUINT_TO_POINTER(count), g_strdup(name));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(count == 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Empty or invalid groups array provided, groups will be disabled\\n\");\n\t\t\t\tg_hash_table_destroy(audiobridge->groups);\n\t\t\t\tg_hash_table_destroy(audiobridge->groups_byid);\n\t\t\t\taudiobridge->groups = NULL;\n\t\t\t\taudiobridge->groups_byid = NULL;\n\t\t\t}\n\t\t}\n\t\taudiobridge->rtp_forwarders = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\t\taudiobridge->rtp_encoder = NULL;\n\t\taudiobridge->rtp_udp_sock = -1;\n\t\tjanus_mutex_init(&audiobridge->rtp_mutex);\n\t\tg_hash_table_insert(rooms,\n\t\t\tstring_ids ? (gpointer)g_strdup(audiobridge->room_id_str) : (gpointer)janus_uint64_dup(audiobridge->room_id),\n\t\t\taudiobridge);\n\t\tJANUS_LOG(LOG_VERB, \"Created AudioBridge: %s (%s, %s, secret: %s, pin: %s)\\n\",\n\t\t\taudiobridge->room_id_str, audiobridge->room_name,\n\t\t\taudiobridge->is_private ? \"private\" : \"public\",\n\t\t\taudiobridge->room_secret ? audiobridge->room_secret : \"no secret\",\n\t\t\taudiobridge->room_pin ? audiobridge->room_pin : \"no pin\");\n\t\t/* We need a thread for the mix */\n\t\tGError *error = NULL;\n\t\tchar tname[16];\n\t\tg_snprintf(tname, sizeof(tname), \"mixer %s\", audiobridge->room_id_str);\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\taudiobridge->thread = g_thread_try_new(tname, &janus_audiobridge_mixer_thread, audiobridge, &error);\n\t\tif(error != NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the mixer thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Got error %d (%s) trying to launch the mixer thread\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tg_hash_table_remove(rooms, string_ids ? (gpointer)audiobridge->room_id_str : (gpointer)&audiobridge->room_id);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tif(room_id_allocated)\n\t\t\t\tg_free(room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(save) {\n\t\t\t/* This room is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Saving room %s permanently in config file\\n\", audiobridge->room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ], value[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", audiobridge->room_id_str);\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, cat);\n\t\t\t/* Now for the values */\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", audiobridge->room_name));\n\t\t\tif(audiobridge->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, audiobridge->sampling_rate);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"sampling_rate\", value));\n\t\t\tif(audiobridge->room_secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", audiobridge->room_secret));\n\t\t\tif(audiobridge->room_pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", audiobridge->room_pin));\n\t\t\tif(audiobridge->audiolevel_ext) {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_ext\", \"true\"));\n\t\t\t\tif(audiobridge->audiolevel_event)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_event\", \"true\"));\n\t\t\t\tif(audiobridge->audio_active_packets > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", audiobridge->audio_active_packets);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_active_packets\", value));\n\t\t\t\t}\n\t\t\t\tif(audiobridge->audio_level_average > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", audiobridge->audio_level_average);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_level_average\", value));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(audiobridge->allow_plainrtp)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"allow_rtp_participants\", \"true\"));\n\t\t\tif(audiobridge->groups) {\n\t\t\t\t/* Save array of groups */\n\t\t\t\tjanus_config_array *gl = janus_config_array_create(\"groups\");\n\t\t\t\tjanus_config_add(config, c, gl);\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer key;\n\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->groups);\n\t\t\t\twhile(g_hash_table_iter_next(&iter, &key, NULL)) {\n\t\t\t\t\tjanus_config_add(config, gl, janus_config_item_create(NULL, (char *)key));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(audiobridge->record_file) {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record\", \"true\"));\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record_file\", audiobridge->record_file));\n\t\t\t}\n\t\t\tif(audiobridge->record_dir)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record_dir\", audiobridge->record_dir));\n\t\t\tif(audiobridge->mjrs)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"mjrs\", \"true\"));\n\t\t\tif(audiobridge->mjrs_dir)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"mjrs_dir\", audiobridge->mjrs_dir));\n\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"spatial_audio\", \"true\"));\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_AUDIOBRIDGE_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"created\"));\n\t\tjson_object_set_new(response, \"room\",\n\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"created\"));\n\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tif(room_id_allocated)\n\t\t\tg_free(room_id_str);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"edit\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to edit an existing AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, edit_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\t/* We only allow for a limited set of properties to be edited */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *desc = json_object_get(root, \"new_description\");\n\t\tjson_t *secret = json_object_get(root, \"new_secret\");\n\t\tjson_t *pin = json_object_get(root, \"new_pin\");\n\t\tjson_t *is_private = json_object_get(root, \"new_is_private\");\n\t\tjson_t *recdir = json_object_get(root, \"new_record_dir\");\n\t\tjson_t *mjrsdir = json_object_get(root, \"new_mjrs_dir\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't edit room permanently\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't edit room permanently\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Edit the room properties that were provided */\n\t\tif(desc != NULL && strlen(json_string_value(desc)) > 0) {\n\t\t\tchar *old_description = audiobridge->room_name;\n\t\t\tchar *new_description = g_strdup(json_string_value(desc));\n\t\t\taudiobridge->room_name = new_description;\n\t\t\tg_free(old_description);\n\t\t}\n\t\tif(is_private)\n\t\t\taudiobridge->is_private = json_is_true(is_private);\n\t\tif(secret && strlen(json_string_value(secret)) > 0) {\n\t\t\tchar *old_secret = audiobridge->room_secret;\n\t\t\tchar *new_secret = g_strdup(json_string_value(secret));\n\t\t\taudiobridge->room_secret = new_secret;\n\t\t\tg_free(old_secret);\n\t\t}\n\t\tif(pin) {\n\t\t\tchar *old_pin = audiobridge->room_pin;\n\t\t\tif(strlen(json_string_value(pin)) > 0) {\n\t\t\t\tchar *new_pin = g_strdup(json_string_value(pin));\n\t\t\t\taudiobridge->room_pin = new_pin;\n\t\t\t} else {\n\t\t\t\taudiobridge->room_pin = NULL;\n\t\t\t}\n\t\t\tg_free(old_pin);\n\t\t}\n\t\tif(recdir) {\n\t\t\tchar *old_record_dir = audiobridge->record_dir;\n\t\t\tchar *new_record_dir = g_strdup(json_string_value(recdir));\n\t\t\taudiobridge->record_dir = new_record_dir;\n\t\t\tg_free(old_record_dir);\n\t\t}\n\t\tif(mjrsdir) {\n\t\t\tchar *old_mjrs_dir = audiobridge->mjrs_dir;\n\t\t\tchar *new_mjrs_dir = g_strdup(json_string_value(mjrsdir));\n\t\t\taudiobridge->mjrs_dir = new_mjrs_dir;\n\t\t\tg_free(old_mjrs_dir);\n\t\t}\n\t\tif(save) {\n\t\t\t/* This change is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Modifying room %s permanently in config file\\n\", room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ], value[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", room_id_str);\n\t\t\t/* Remove the old category first */\n\t\t\tjanus_config_remove(config, NULL, cat);\n\t\t\t/* Now write the room details again */\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, cat);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", audiobridge->room_name));\n\t\t\tif(audiobridge->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, audiobridge->sampling_rate);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"sampling_rate\", value));\n\t\t\tif(audiobridge->room_secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", audiobridge->room_secret));\n\t\t\tif(audiobridge->room_pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", audiobridge->room_pin));\n\t\t\tif(audiobridge->audiolevel_ext) {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_ext\", \"true\"));\n\t\t\t\tif(audiobridge->audiolevel_event)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_event\", \"true\"));\n\t\t\t\tif(audiobridge->audio_active_packets > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", audiobridge->audio_active_packets);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_active_packets\", value));\n\t\t\t\t}\n\t\t\t\tif(audiobridge->audio_level_average > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", audiobridge->audio_level_average);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_level_average\", value));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(audiobridge->allow_plainrtp)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"allow_rtp_participants\", \"true\"));\n\t\t\tif(audiobridge->groups) {\n\t\t\t\t/* Save array of groups */\n\t\t\t\tjanus_config_array *gl = janus_config_array_create(\"groups\");\n\t\t\t\tjanus_config_add(config, c, gl);\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer key;\n\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->groups);\n\t\t\t\twhile(g_hash_table_iter_next(&iter, &key, NULL)) {\n\t\t\t\t\tjanus_config_add(config, gl, janus_config_item_create(NULL, (char *)key));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(audiobridge->record_file) {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record\", \"true\"));\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record_file\", audiobridge->record_file));\n\t\t\t}\n\t\t\tif(audiobridge->record_dir)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record_dir\", audiobridge->record_dir));\n\t\t\tif(audiobridge->mjrs)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"mjrs\", \"true\"));\n\t\t\tif(audiobridge->mjrs_dir)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"mjrs_dir\", audiobridge->mjrs_dir));\n\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"spatial_audio\", \"true\"));\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_AUDIOBRIDGE_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room changes are not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\t/* Prepare response/notification */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"edited\"));\n\t\tjson_object_set_new(response, \"room\",\n\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"edited\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t/* Done */\n\t\tJANUS_LOG(LOG_VERB, \"Audiobridge room edited\\n\");\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"destroy\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to destroy an existing AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, destroy_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't destroy room permanently\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't destroy room permanently\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Remove room */\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tg_hash_table_remove(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(save) {\n\t\t\t/* This change is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Destroying room %s permanently in config file\\n\", room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", room_id_str);\n\t\t\tjanus_config_remove(config, NULL, cat);\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_AUDIOBRIDGE_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room destruction is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\t/* Prepare response/notification */\n\t\tjson_t *destroyed = json_object();\n\t\tjson_object_set_new(destroyed, \"audiobridge\", json_string(\"destroyed\"));\n\t\tjson_object_set_new(destroyed, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t/* Notify all participants that the fun is over, and that they'll be kicked */\n\t\tJANUS_LOG(LOG_VERB, \"Notifying all participants\\n\");\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tif(p && p->session) {\n\t\t\t\tif(p->room) {\n\t\t\t\t\tp->room = NULL;\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t}\n\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, destroyed, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\t/* Get rid of queued packets */\n\t\t\t\tjanus_mutex_lock(&p->qmutex);\n\t\t\t\tg_atomic_int_set(&p->active, 0);\n\t\t\t\tjanus_audiobridge_participant_clear_jitter_buffer(p);\n\t\t\t\tjanus_audiobridge_participant_clear_inbuf(p);\n\t\t\t\tjanus_mutex_unlock(&p->qmutex);\n\t\t\t\tjanus_audiobridge_participant_clear_outbuf(p);\n\t\t\t\t/* Request a WebRTC hangup */\n\t\t\t\tgateway->close_pc(p->session->handle);\n\t\t\t}\n\t\t}\n\t\tjson_decref(destroyed);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"destroyed\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t/* Done */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"destroyed\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\tJANUS_LOG(LOG_VERB, \"Audiobridge room destroyed\\n\");\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"enable_recording\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, record_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *record = json_object_get(root, \"record\");\n\t\tjson_t *recfile = json_object_get(root, \"record_file\");\n\t\tjson_t *recdir = json_object_get(root, \"record_dir\");\n\t\tgboolean recording_active = json_is_true(record);\n\t\t/* Lookup room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = NULL;\n\t\terror_code = janus_audiobridge_access_room(root, TRUE, &audiobridge, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to access room\\n\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Set recording status */\n\t\tgint room_prev_recording_active = recording_active ? 1 : 0;\n\t\t/* Check if we need to create a folder */\n\t\tif(recording_active && recdir != NULL) {\n\t\t\tif(janus_mkdir(json_string_value(recdir), 0755) < 0) {\n\t\t\t\t/* FIXME Should this be fatal, when creating a room? */\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"AudioBridge mkdir (%s) error: %d (%s)\\n\", audiobridge->record_dir, errno, g_strerror(errno));\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"mkdir error: %d (%s)\", errno, g_strerror(errno));\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tif(room_prev_recording_active != g_atomic_int_get(&audiobridge->record)) {\n\t\t\t/* Room recording state has changed */\n\t\t\tJANUS_LOG(LOG_VERB, \"Recording status changed: prev=%d, curr=%d\\n\", g_atomic_int_get(&audiobridge->record), recording_active);\n\t\t\tg_atomic_int_set(&audiobridge->record, room_prev_recording_active);\n\t\t\tif(recfile && recording_active) {\n\t\t\t\tg_free(audiobridge->record_file);\n\t\t\t\taudiobridge->record_file = g_strdup(json_string_value(recfile));\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording file: %s\\n\", audiobridge->record_file);\n\t\t\t}\n\t\t\tif(recdir && recording_active) {\n\t\t\t\tg_free(audiobridge->record_dir);\n\t\t\t\taudiobridge->record_dir = g_strdup(json_string_value(recdir));\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording folder: %s\\n\", audiobridge->record_dir);\n\t\t\t}\n\t\t}\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"record\", json_boolean(g_atomic_int_get(&audiobridge->record)));\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"enable_mjrs\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, mjrs_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *mjrs = json_object_get(root, \"mjrs\");\n\t\tjson_t *mjrsdir = json_object_get(root, \"mjrs_dir\");\n\t\tgboolean mjrs_active = json_is_true(mjrs);\n\t\tJANUS_LOG(LOG_VERB, \"Enable MJR recording: %d\\n\", (mjrs_active ? 1 : 0));\n\t\t/* Lookup room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = NULL;\n\t\terror_code = janus_audiobridge_access_room(root, TRUE, &audiobridge, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to access room\\n\");\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t/* Set MJR recording status */\n\t\tif(mjrsdir) {\n\t\t\t/* Update the path where to save the MJR files */\n\t\t\tchar *old_mjrs_dir = audiobridge->mjrs_dir;\n\t\t\tchar *new_mjrs_dir = g_strdup(json_string_value(mjrsdir));\n\t\t\taudiobridge->mjrs_dir = new_mjrs_dir;\n\t\t\tg_free(old_mjrs_dir);\n\t\t}\n\t\tif(mjrs_active != audiobridge->mjrs) {\n\t\t\t/* Room recording state has changed */\n\t\t\taudiobridge->mjrs = mjrs_active;\n\t\t\t/* Iterate over all participants */\n\t\t\tgpointer value;\n\t\t\tGHashTableIter iter;\n\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_audiobridge_participant *participant = value;\n\t\t\t\tif(participant && participant->session) {\n\t\t\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\t\t\tgboolean prev_mjrs_active = participant->mjr_active;\n\t\t\t\t\tparticipant->mjr_active = mjrs_active;\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting MJR recording property: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->mjr_active ? \"true\" : \"false\", audiobridge->room_id_str, participant->user_id_str);\n\t\t\t\t\t/* Do we need to do something with the recordings right now? */\n\t\t\t\t\tif(participant->mjr_active != prev_mjrs_active) {\n\t\t\t\t\t\t/* Something changed */\n\t\t\t\t\t\tif(!participant->mjr_active) {\n\t\t\t\t\t\t\t/* Not recording (anymore?) */\n\t\t\t\t\t\t\tjanus_audiobridge_recorder_close(participant);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* We've started recording, send a PLI/FIR and go on */\n\t\t\t\t\t\t\tjanus_audiobridge_recorder_create(participant);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"mjrs\", json_boolean(mjrs_active));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"list\")) {\n\t\t/* List all rooms (but private ones) and their details (except for the secret, of course...) */\n\t\tJANUS_LOG(LOG_VERB, \"Request for the list for all audiobridge rooms\\n\");\n\t\tgboolean lock_room_list = TRUE;\n\t\tif(admin_key != NULL) {\n\t\t\tjson_t *admin_key_json = json_object_get(root, \"admin_key\");\n\t\t\t/* Verify admin_key if it was provided */\n\t\t\tif(admin_key_json != NULL && json_is_string(admin_key_json) && strlen(json_string_value(admin_key_json)) > 0) {\n\t\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t} else {\n\t\t\t\t\tlock_room_list = FALSE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjson_t *list = json_array();\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, rooms);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_room *room = value;\n\t\t\tif(!room || g_atomic_int_get(&room->destroyed))\n\t\t\t\tcontinue;\n\t\t\tjanus_refcount_increase(&room->ref);\n\t\t\tif(room->is_private && lock_room_list) {\n\t\t\t\t/* Skip private room if no valid admin_key was provided */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping private room '%s'\\n\", room->room_name);\n\t\t\t\tjanus_refcount_decrease(&room->ref);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *rl = json_object();\n\t\t\tjson_object_set_new(rl, \"room\", string_ids ? json_string(room->room_id_str) : json_integer(room->room_id));\n\t\t\tjson_object_set_new(rl, \"description\", json_string(room->room_name));\n\t\t\tjson_object_set_new(rl, \"sampling_rate\", json_integer(room->sampling_rate));\n\t\t\tjson_object_set_new(rl, \"spatial_audio\", room->spatial_audio ? json_true() : json_false());\n\t\t\tjson_object_set_new(rl, \"pin_required\", room->room_pin ? json_true() : json_false());\n\t\t\tjson_object_set_new(rl, \"record\", g_atomic_int_get(&room->record) ? json_true() : json_false());\n\t\t\tjson_object_set_new(rl, \"muted\", room->muted ? json_true() : json_false());\n\t\t\tjson_object_set_new(rl, \"num_participants\", json_integer(g_hash_table_size(room->participants)));\n\t\t\tjson_array_append_new(list, rl);\n\t\t\tjanus_refcount_decrease(&room->ref);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"list\", list);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"exists\")) {\n\t\t/* Check whether a given room exists or not, returns true/false */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tgboolean room_exists = g_hash_table_contains(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"exists\", room_exists ? json_true() : json_false());\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"allowed\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to edit the list of allowed participants in an existing AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, allowed_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *action = json_object_get(root, \"action\");\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *allowed = json_object_get(root, \"allowed\");\n\t\tconst char *action_text = json_string_value(action);\n\t\tif(strcasecmp(action_text, \"enable\") && strcasecmp(action_text, \"disable\") &&\n\t\t\t\tstrcasecmp(action_text, \"add\") && strcasecmp(action_text, \"remove\")) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported action '%s' (allowed)\\n\", action_text);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Unsupported action '%s' (allowed)\", action_text);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(!strcasecmp(action_text, \"enable\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Enabling the check on allowed authorization tokens for room %s\\n\", room_id_str);\n\t\t\taudiobridge->check_tokens = TRUE;\n\t\t} else if(!strcasecmp(action_text, \"disable\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Disabling the check on allowed authorization tokens for room %s (free entry)\\n\", room_id_str);\n\t\t\taudiobridge->check_tokens = FALSE;\n\t\t} else {\n\t\t\tgboolean add = !strcasecmp(action_text, \"add\");\n\t\t\tif(allowed) {\n\t\t\t\t/* Make sure the \"allowed\" array only contains strings */\n\t\t\t\tgboolean ok = TRUE;\n\t\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\t\tjson_t *a = json_array_get(allowed, i);\n\t\t\t\t\t\tif(!a || !json_is_string(a)) {\n\t\t\t\t\t\t\tok = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!ok) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element in the allowed array (not a string)\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element in the allowed array (not a string)\");\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tconst char *token = json_string_value(json_array_get(allowed, i));\n\t\t\t\t\tif(add) {\n\t\t\t\t\t\tif(!g_hash_table_lookup(audiobridge->allowed, token))\n\t\t\t\t\t\t\tg_hash_table_insert(audiobridge->allowed, g_strdup(token), GINT_TO_POINTER(TRUE));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tg_hash_table_remove(audiobridge->allowed, token);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\",\n\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\tjson_t *list = json_array();\n\t\tif(strcasecmp(action_text, \"disable\")) {\n\t\t\tif(g_hash_table_size(audiobridge->allowed) > 0) {\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer key;\n\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->allowed);\n\t\t\t\twhile(g_hash_table_iter_next(&iter, &key, NULL)) {\n\t\t\t\t\tchar *token = key;\n\t\t\t\t\tjson_array_append_new(list, json_string(token));\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_object_set_new(response, \"allowed\", list);\n\t\t}\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tJANUS_LOG(LOG_VERB, \"Audiobridge room allowed list updated\\n\");\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"mute\") || !strcasecmp(request_text, \"unmute\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to mute a participant from an existing AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, secret_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tgboolean muted = (!strcasecmp(request_text, \"mute\")) ? TRUE : FALSE;\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tguint64 user_id = 0;\n\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tuser_id = json_integer_value(id);\n\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\tuser_id_str = user_id_num;\n\t\t} else {\n\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_audiobridge_participant *participant = g_hash_table_lookup(audiobridge->participants,\n\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such user %s in room %s\\n\", user_id_str, room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_USER;\n\t\t\tg_snprintf(error_cause, 512, \"No such user %s in room %s\", user_id_str, room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tjanus_mutex_lock(&participant->qmutex);\n\t\tif(participant->muted == muted) {\n\t\t\t/* If someone trying to mute an already muted user, or trying to unmute a user that is not mute),\n\t\t\tthen we should do nothing */\n\n\t\t\t/* Nothing to do, just prepare response */\n\t\t\tresponse = json_object();\n\t\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\n\t\t\t/* Done */\n\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tparticipant->muted = muted;\n\t\tJANUS_LOG(LOG_VERB, \"Setting muted property: %s (room %s, user %s)\\n\",\n\t\t\tparticipant->muted ? \"true\" : \"false\", participant->room->room_id_str, participant->user_id_str);\n\t\tif(participant->muted) {\n\t\t\t/* Clear the queued packets waiting to be handled */\n\t\t\tjanus_audiobridge_participant_clear_jitter_buffer(participant);\n\t\t\tjanus_audiobridge_participant_clear_inbuf(participant);\n\t\t}\n\t\tjanus_mutex_unlock(&participant->qmutex);\n\n\t\tjson_t *list = json_array();\n\t\tjson_t *pl = json_object();\n\t\tjson_object_set_new(pl, \"id\",\n\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\tif(participant->display)\n\t\t\tjson_object_set_new(pl, \"display\", json_string(participant->display));\n\t\tjson_object_set_new(pl, \"setup\", g_atomic_int_get(&participant->session->started) ? json_true() : json_false());\n\t\tjson_object_set_new(pl, \"muted\", participant->muted ? json_true() : json_false());\n\t\tif(audiobridge->spatial_audio)\n\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(participant->spatial_position));\n\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\tjson_array_append_new(list, pl);\n\t\tjson_t *pub = json_object();\n\t\tjson_object_set_new(pub, \"audiobridge\", json_string(\"event\"));\n\t\tjson_object_set_new(pub, \"room\",\n\t\t\tstring_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(pub, \"participants\", list);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tif(g_atomic_int_get(&p->paused_events))\n\t\t\t\tcontinue;\n\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, pub, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t}\n\t\tjson_decref(pub);\n\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(request_text));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t}\n\n\t\tJANUS_LOG(LOG_VERB, \"%s user %s in room %s\\n\",\n\t\t\tmuted ? \"Muted\" : \"Unmuted\", user_id_str, room_id_str);\n\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"mute_room\") || !strcasecmp(request_text, \"unmute_room\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to mute all participants in an existing AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, secret_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tgboolean muted = (!strcasecmp(request_text, \"mute_room\")) ? TRUE : FALSE;\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tif(audiobridge->muted == muted) {\n\t\t\t/* If we're already in the right state, just prepare the response */\n\t\t\tresponse = json_object();\n\t\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\n\t\t\t/* Done */\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\taudiobridge->muted = muted;\n\n\t\t/* Prepare an event to notify all participants */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\tjson_object_set_new(event, \"room\",\n\t\t\tstring_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(event, \"muted\", audiobridge->muted ? json_true() : json_false());\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tjanus_mutex_lock(&p->qmutex);\n\t\t\tif(p->muted != audiobridge->muted) {\n\t\t\t\t/* Clear the queued packets waiting to be handled */\n\t\t\t\tjanus_audiobridge_participant_clear_jitter_buffer(p);\n\t\t\t\tjanus_audiobridge_participant_clear_inbuf(p);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&p->qmutex);\n\t\t\tif(g_atomic_int_get(&p->paused_events))\n\t\t\t\tcontinue;\n\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t}\n\t\tjson_decref(event);\n\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(request_text));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t}\n\n\t\tJANUS_LOG(LOG_VERB, \"%s all users in room %s\\n\", muted ? \"Muted\" : \"Unmuted\", room_id_str);\n\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tgoto prepare_response;\n#ifdef HAVE_RNNOISE\n\t} else if(!strcasecmp(request_text, \"denoise_enable\") || !strcasecmp(request_text, \"denoise_disable\")) {\n\t\tgboolean denoise = (!strcasecmp(request_text, \"denoise_enable\"));\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to %s denoising for a participant in an existing AudioBridge room\\n\",\n\t\t\tdenoise ? \"enable\" : \"disable\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, secret_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tguint64 user_id = 0;\n\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tuser_id = json_integer_value(id);\n\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\tuser_id_str = user_id_num;\n\t\t} else {\n\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_audiobridge_participant *participant = g_hash_table_lookup(audiobridge->participants,\n\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such user %s in room %s\\n\", user_id_str, room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_USER;\n\t\t\tg_snprintf(error_cause, 512, \"No such user %s in room %s\", user_id_str, room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tparticipant->denoise = denoise;\n\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tgoto prepare_response;\n#endif\n\t} else if(!strcasecmp(request_text, \"kick\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to kick a participant from an existing AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, secret_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 user_id = 0;\n\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tuser_id = json_integer_value(id);\n\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\tuser_id_str = user_id_num;\n\t\t} else {\n\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_audiobridge_participant *participant = g_hash_table_lookup(audiobridge->participants,\n\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such user %s in room %s\\n\", user_id_str, room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_USER;\n\t\t\tg_snprintf(error_cause, 512, \"No such user %s in room %s\", user_id_str, room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Notify all participants about the kick */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(event, \"kicked\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t}\n\t\tjson_decref(event);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"kicked\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\tif(participant && participant->session)\n\t\t\tgateway->close_pc(participant->session->handle);\n\t\tJANUS_LOG(LOG_VERB, \"Kicked user %s from room %s\\n\", user_id_str, room_id_str);\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"kick_all\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to kick all participants from an existing AudioBridge room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, secret_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tGHashTableIter kick_iter;\n\t\tgpointer kick_value;\n\t\tg_hash_table_iter_init(&kick_iter, audiobridge->participants);\n\t\twhile(g_hash_table_iter_next(&kick_iter, NULL, &kick_value)) {\n\t\t\tjanus_audiobridge_participant *participant = kick_value;\n\t\t\tJANUS_LOG(LOG_VERB, \"Kicking participant %s (%s)\\n\",\n\t\t\t\t\tparticipant->user_id_str, participant->display ? participant->display : \"??\");\n\t\t\tguint64 user_id = 0;\n\t\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\t\tif(string_ids) {\n\t\t\t\tuser_id_str = participant->user_id_str;\n\t\t\t} else {\n\t\t\t\tuser_id = participant->user_id;\n\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\tuser_id_str = user_id_num;\n\t\t\t}\n\t\t\t/* Notify all participants about the kick */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(event, \"kicked_all\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", participant->user_id_str, participant->display ? participant->display : \"??\");\n\t\t\tint ret = gateway->push_event(participant->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"kicked_all\"));\n\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t\t}\n\t\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\t\tif(participant && participant->session)\n\t\t\t\tgateway->close_pc(participant->session->handle);\n\t\t\tJANUS_LOG(LOG_VERB, \"Kicked user %s from room %s\\n\", user_id_str, room_id_str);\n\t\t}\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"listparticipants\")) {\n\t\t/* List all participants in a room */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\t/* Return a list of all participants */\n\t\tjson_t *list = json_array();\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\twhile(!g_atomic_int_get(&audiobridge->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tjson_t *pl = json_object();\n\t\t\tjson_object_set_new(pl, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\tif(p->display)\n\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\tjson_object_set_new(pl, \"setup\", g_atomic_int_get(&p->session->started) ? json_true() : json_false());\n\t\t\tjson_object_set_new(pl, \"muted\", p->muted ? json_true() : json_false());\n\t\t\tif(p->extmap_id > 0)\n\t\t\t\tjson_object_set_new(pl, \"talking\", p->talking ? json_true() : json_false());\n\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(p->spatial_position));\n\t\t\tif(g_atomic_int_get(&p->suspended))\n\t\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\t\tjson_array_append_new(list, pl);\n\t\t}\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"participants\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"participants\", list);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"resetdecoder\")) {\n\t\t/* Mark the Opus decoder for the participant invalid and recreate it */\n\t\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)(session ? session->participant : NULL);\n\t\tif(participant == NULL || participant->room == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't reset (not in a room)\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NOT_JOINED;\n\t\t\tg_snprintf(error_cause, 512, \"Can't reset (not in a room)\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tparticipant->reset = TRUE;\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"rtp_forward\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_forward_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_rtpfwd && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Parse arguments */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tguint32 ssrc_value = 0;\n\t\tjson_t *ssrc = json_object_get(root, \"ssrc\");\n\t\tif(ssrc)\n\t\t\tssrc_value = json_integer_value(ssrc);\n\t\tjanus_audiocodec codec = JANUS_AUDIOCODEC_OPUS;\n\t\tjson_t *rfc = json_object_get(root, \"codec\");\n\t\tif(rfc) {\n\t\t\tcodec = janus_audiocodec_from_name(json_string_value(rfc));\n\t\t\tif(codec != JANUS_AUDIOCODEC_OPUS && codec != JANUS_AUDIOCODEC_PCMA && codec != JANUS_AUDIOCODEC_PCMU) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec (%s)\\n\", json_string_value(rfc));\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported codec (%s)\", json_string_value(rfc));\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tint ptype = 100;\n\t\tjson_t *pt = json_object_get(root, \"ptype\");\n\t\tif(pt)\n\t\t\tptype = json_integer_value(pt);\n\t\tuint16_t port = json_integer_value(json_object_get(root, \"port\"));\n\t\tif(port == 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid port number (%d)\\n\", port);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Invalid port number (%d)\", port);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjson_t *json_host = json_object_get(root, \"host\");\n\t\tconst char *host = json_string_value(json_host), *resolved_host = NULL;\n\t\tjson_t *json_host_family = json_object_get(root, \"host_family\");\n\t\tconst char *host_family = json_string_value(json_host_family);\n\t\tint family = 0;\n\t\tif(host_family) {\n\t\t\tif(!strcasecmp(host_family, \"ipv4\")) {\n\t\t\t\tfamily = AF_INET;\n\t\t\t} else if(!strcasecmp(host_family, \"ipv6\")) {\n\t\t\t\tfamily = AF_INET6;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported protocol family (%s)\\n\", host_family);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported protocol family (%s)\", host_family);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\t/* Check if we need to resolve this host address */\n\t\tstruct addrinfo *res = NULL, *start = NULL;\n\t\tjanus_network_address addr;\n\t\tjanus_network_address_string_buffer addr_buf;\n\t\tstruct addrinfo hints;\n\t\tmemset(&hints, 0, sizeof(hints));\n\t\tif(family != 0)\n\t\t\thints.ai_family = family;\n\t\tif(getaddrinfo(host, NULL, family != 0 ? &hints : NULL, &res) == 0) {\n\t\t\tstart = res;\n\t\t\twhile(res != NULL) {\n\t\t\t\tif(janus_network_address_from_sockaddr(res->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t/* Resolved */\n\t\t\t\t\tresolved_host = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\tstart = NULL;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tres = res->ai_next;\n\t\t\t}\n\t\t}\n\t\tif(resolved_host == NULL) {\n\t\t\tif(start)\n\t\t\t\tfreeaddrinfo(start);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s)...\\n\", host);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Could not resolve address (%s)...\", host);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\thost = resolved_host;\n\t\tif(ipv6_disabled && strstr(host, \":\") != NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Attempt to create an IPv6 forwarder, but IPv6 networking is not available\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Attempt to create an IPv6 forwarder, but IPv6 networking is not available\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjson_t *always = json_object_get(root, \"always_on\");\n\t\tgboolean always_on = always ? json_is_true(always) : FALSE;\n\t\t/* Besides, we may need to SRTP-encrypt this stream */\n\t\tint srtp_suite = 0;\n\t\tconst char *srtp_crypto = NULL;\n\t\tjson_t *s_suite = json_object_get(root, \"srtp_suite\");\n\t\tjson_t *s_crypto = json_object_get(root, \"srtp_crypto\");\n\t\tif(s_suite && s_crypto) {\n\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP suite (%d)\\n\", srtp_suite);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP suite (%d)\", srtp_suite);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t}\n\t\t/* Update room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tif(audiobridge->destroyed) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* If this room uses groups, check if a valid group name was provided */\n\t\tuint group = 0;\n\t\tconst char *group_name = NULL;\n\t\tif(audiobridge->groups != NULL) {\n\t\t\tgroup_name = json_string_value(json_object_get(root, \"group\"));\n\t\t\tif(group_name != NULL) {\n\t\t\t\tgroup = GPOINTER_TO_UINT(g_hash_table_lookup(audiobridge->groups, group_name));\n\t\t\t\tif(group == 0) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_GROUP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such group\");\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif(janus_audiobridge_create_udp_socket_if_needed(audiobridge)) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Could not open UDP socket for RTP forwarder\");\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tif(janus_audiobridge_create_opus_encoder_if_needed(audiobridge)) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Error creating Opus encoder for RTP forwarder\");\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tguint32 stream_id = janus_audiobridge_rtp_forwarder_add_helper(audiobridge, group,\n\t\t\thost, port, ssrc_value, ptype, codec, srtp_suite, srtp_crypto, always_on, 0);\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* Done, prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tif(group_name != NULL)\n\t\t\tjson_object_set_new(response, \"group\", json_string(group_name));\n\t\tjson_object_set_new(response, \"stream_id\", json_integer(stream_id));\n\t\tjson_object_set_new(response, \"host\", json_string(host));\n\t\tjson_object_set_new(response, \"port\", json_integer(port));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"stop_rtp_forward\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, stop_rtp_forward_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_rtpfwd && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Parse parameters */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tguint32 stream_id = json_integer_value(json_object_get(root, \"stream_id\"));\n\t\t/* Update room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tif(audiobridge->destroyed) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->rtp_mutex);\n\t\tg_hash_table_remove(audiobridge->rtp_forwarders, GUINT_TO_POINTER(stream_id));\n\t\tjanus_mutex_unlock(&audiobridge->rtp_mutex);\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"stream_id\", json_integer(stream_id));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"listforwarders\")) {\n\t\t/* List all forwarders in a room */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(audiobridge->destroyed) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Return a list of all forwarders */\n\t\tjson_t *list = json_array();\n\t\tGHashTableIter iter;\n\t\tgpointer key, value;\n\t\tjanus_mutex_lock(&audiobridge->rtp_mutex);\n\t\tg_hash_table_iter_init(&iter, audiobridge->rtp_forwarders);\n\t\twhile(g_hash_table_iter_next(&iter, &key, &value)) {\n\t\t\tguint32 stream_id = GPOINTER_TO_UINT(key);\n\t\t\tjanus_rtp_forwarder *rf = (janus_rtp_forwarder *)value;\n\t\t\tjanus_audiobridge_rtp_forwarder_metadata *rfm = (janus_audiobridge_rtp_forwarder_metadata *)rf->metadata;\n\t\t\tjson_t *fl = json_object();\n\t\t\tjson_object_set_new(fl, \"stream_id\", json_integer(stream_id));\n\t\t\tif(rfm->group > 0 && audiobridge->groups_byid != NULL) {\n\t\t\t\tchar *name = g_hash_table_lookup(audiobridge->groups_byid, GUINT_TO_POINTER(rfm->group));\n\t\t\t\tif(name != NULL)\n\t\t\t\t\tjson_object_set_new(fl, \"group\", json_string(name));\n\t\t\t}\n\t\t\tchar address[100];\n\t\t\tif(rf->serv_addr.sin_family == AF_INET) {\n\t\t\t\tjson_object_set_new(fl, \"ip\", json_string(\n\t\t\t\t\tinet_ntop(AF_INET, &rf->serv_addr.sin_addr, address, sizeof(address))));\n\t\t\t} else {\n\t\t\t\tjson_object_set_new(fl, \"ip\", json_string(\n\t\t\t\t\tinet_ntop(AF_INET6, &rf->serv_addr6.sin6_addr, address, sizeof(address))));\n\t\t\t}\n\t\t\tjson_object_set_new(fl, \"port\", json_integer(ntohs(rf->serv_addr.sin_port)));\n\t\t\tjson_object_set_new(fl, \"ssrc\", json_integer(rf->ssrc ? rf->ssrc : stream_id));\n\t\t\tjson_object_set_new(fl, \"codec\", json_string(janus_audiocodec_name(rfm->codec)));\n\t\t\tjson_object_set_new(fl, \"ptype\", json_integer(rf->payload_type));\n\t\t\tif(rf->is_srtp)\n\t\t\t\tjson_object_set_new(fl, \"srtp\", json_true());\n\t\t\tjson_object_set_new(fl, \"always_on\", rfm->always_on ? json_true() : json_false());\n\t\t\tjson_array_append_new(list, fl);\n\t\t}\n\t\tjanus_mutex_unlock(&audiobridge->rtp_mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"forwarders\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"rtp_forwarders\", list);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"play_file\")) {\n#ifndef HAVE_LIBOGG\n\t\tJANUS_LOG(LOG_VERB, \"Playing files unsupported in this instance\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Playing files unsupported in this instance\");\n\t\tgoto prepare_response;\n#else\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, play_file_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_playfile && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Parse parameters */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\t/* Update room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tif(audiobridge->destroyed) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* If this room uses groups, make sure a valid group name was provided */\n\t\tuint group = 0;\n\t\tif(audiobridge->groups != NULL) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, group_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tconst char *group_name = json_string_value(json_object_get(root, \"group\"));\n\t\t\tgroup = GPOINTER_TO_UINT(g_hash_table_lookup(audiobridge->groups, group_name));\n\t\t\tif(group == 0) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No such group (%s)\\n\", group_name);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_GROUP;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such group (%s)\", group_name);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\t/* Check if an announcement ID has been provided, or generate a random one */\n\t\tjson_t *id = json_object_get(root, \"file_id\");\n\t\tchar *file_id = (char *)json_string_value(id);\n\t\tgboolean file_id_allocated = FALSE;\n\t\tif(file_id == NULL) {\n\t\t\t/* Generate a random ID */\n\t\t\twhile(file_id == NULL) {\n\t\t\t\tfile_id = janus_random_uuid();\n\t\t\t\tif(g_hash_table_lookup(audiobridge->anncs, file_id) != NULL) {\n\t\t\t\t\t/* ID already taken, try another one */\n\t\t\t\t\tg_clear_pointer(&file_id, g_free);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfile_id_allocated = TRUE;\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Announcement ID: %s\\n\", file_id);\n\t\t}\n\t\tif(g_hash_table_lookup(audiobridge->anncs, file_id) != NULL) {\n\t\t\t/* ID already taken */\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"File ID exists (%s)\\n\", file_id);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_ID_EXISTS;\n\t\t\tg_snprintf(error_cause, 512, \"File ID exists (%s)\", file_id);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* We \"abuse\" the participant struct for announcements too */\n\t\tjanus_audiobridge_participant *p = g_malloc0(sizeof(janus_audiobridge_participant));\n\t\tjanus_refcount_init(&p->ref, janus_audiobridge_participant_free);\n\t\tp->user_id_str = g_strdup(file_id);\n\t\tp->codec = JANUS_AUDIOCODEC_OPUS;\n\t\tp->volume_gain = 100;\n\t\t/* Open the file and check it's usable */\n\t\tp->annc = g_malloc0(sizeof(janus_audiobridge_file));\n\t\tp->annc->id = g_strdup(file_id);\n\t\tp->room = audiobridge;\n\t\tp->group = group;\n\t\tconst char *filename = json_string_value(json_object_get(root, \"filename\"));\n\t\tp->annc->filename = g_strdup(filename);\n\t\tp->annc->file = fopen(filename, \"rb\");\n\t\tif(p->annc->file == NULL || janus_audiobridge_file_init(p->annc) < 0) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tif(file_id_allocated)\n\t\t\t\tg_free(file_id);\n\t\t\tjanus_refcount_decrease(&p->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Error opening file\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Error opening file\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tp->annc->loop = json_is_true(json_object_get(root, \"loop\"));\n\t\t/* Setup the opus decoder */\n\t\tint opuserror = 0;\n\t\tp->stereo = audiobridge->spatial_audio;\n\t\tp->spatial_position = 50;\n\t\tp->decoder = opus_decoder_create(audiobridge->sampling_rate,\n\t\t\taudiobridge->spatial_audio ? 2 : 1, &opuserror);\n\t\tif(opuserror != OPUS_OK) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tif(file_id_allocated)\n\t\t\t\tg_free(file_id);\n\t\t\tjanus_refcount_decrease(&p->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating Opus decoder\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Error creating Opus decoder\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* We're done, add the announcement to the room */\n\t\tg_hash_table_insert(audiobridge->anncs, g_strdup(p->user_id_str), p);\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* Done, prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"file_id\", json_string(file_id));\n\t\tif(file_id_allocated)\n\t\t\tg_free(file_id);\n\t\tgoto prepare_response;\n#endif\n\t} else if(!strcasecmp(request_text, \"is_playing\")) {\n#ifndef HAVE_LIBOGG\n\t\tJANUS_LOG(LOG_VERB, \"Playing files unsupported in this instance\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Playing files unsupported in this instance\");\n\t\tgoto prepare_response;\n#else\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, checkstop_file_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_playfile && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Parse parameters */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\t/* Update room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tif(audiobridge->destroyed) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Check if there is such an announcement */\n\t\tjson_t *id = json_object_get(root, \"file_id\");\n\t\tchar *file_id = (char *)json_string_value(id);\n\t\tjanus_audiobridge_participant *p = g_hash_table_lookup(audiobridge->anncs, file_id);\n\t\tgboolean playing = (p && p->annc && p->annc->started);\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* Done, prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"file_id\", json_string(file_id));\n\t\tjson_object_set_new(response, \"playing\", playing ? json_true() : json_false());\n\t\tgoto prepare_response;\n#endif\n\t} else if(!strcasecmp(request_text, \"listannouncements\")) {\n#ifndef HAVE_LIBOGG\n\t\tJANUS_LOG(LOG_VERB, \"Listing announcements unsupported in this instance\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Listing announcements unsupported in this instance\");\n\t\tgoto prepare_response;\n#else\n\t\t/* List all announcements in a room */\n\t\tif(!string_ids) {\n\t\t    JANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n      \t\t\terror_code, error_cause, TRUE,\n      \t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t    JANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n      \t\t\terror_code, error_cause, TRUE,\n      \t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t    goto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t    room_id = json_integer_value(room);\n\t\t    g_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t    room_id_str = room_id_num;\n\t\t} else {\n\t\t    room_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t    string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t    janus_mutex_unlock(&rooms_mutex);\n\t\t    error_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t    JANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t    g_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t    goto prepare_response;\n\t\t}\n\t\tif(audiobridge->destroyed) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Return a list of all announcements */\n\t\tjson_t *list = json_array();\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, audiobridge->anncs);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tjson_t *pl = json_object();\n\t\t\tjson_object_set_new(pl, \"file_id\", json_string(p->annc->id));\n\t\t\tif(p->annc->filename)\n\t\t\t\tjson_object_set_new(pl, \"filename\", json_string(p->annc->filename));\n\t\t\tjson_object_set_new(pl, \"playing\", p->annc->started ? json_true() : json_false());\n\t\t\tjson_object_set_new(pl, \"loop\", p->annc->loop ? json_true() : json_false());\n\t\t\tjson_array_append_new(list, pl);\n\t\t}\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"announcements\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"announcements\", list);\n\t\tgoto prepare_response;\n#endif\n\t} else if(!strcasecmp(request_text, \"stop_file\")) {\n#ifndef HAVE_LIBOGG\n\t\tJANUS_LOG(LOG_VERB, \"Playing files unsupported in this instance\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Playing files unsupported in this instance\");\n\t\tgoto prepare_response;\n#else\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, checkstop_file_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_playfile && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Parse parameters */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\t/* Update room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tif(audiobridge->destroyed) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Get rid of the announcement: a notification will be sent by the mixer, if needed */\n\t\tjson_t *id = json_object_get(root, \"file_id\");\n\t\tchar *file_id = (char *)json_string_value(id);\n\t\tjanus_audiobridge_participant *p = g_hash_table_lookup(audiobridge->anncs, file_id);\n\t\tgboolean started = (p && p->annc && p->annc->started);\n\t\tif(p)\n\t\t\tjanus_refcount_increase(&p->ref);\n\t\tif(g_hash_table_remove(audiobridge->anncs, file_id) && started) {\n\t\t\t/* Send a notification that this announcement is over */\n\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Announcement stopped (%s)\\n\", audiobridge->room_id_str, file_id);\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"announcement-stopped\"));\n\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\tjson_object_set_new(event, \"file_id\", json_string(file_id));\n\t\t\tjanus_audiobridge_notify_participants(audiobridge, p, event, TRUE);\n\t\t\tjson_decref(event);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"announcement-stopped\"));\n\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjson_object_set_new(info, \"file_id\", json_string(file_id));\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, NULL, info);\n\t\t\t}\n\t\t}\n\t\tif(p)\n\t\t\tjanus_refcount_decrease(&p->ref);\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* Done, prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"file_id\", json_string(file_id));\n\t\tgoto prepare_response;\n#endif\n\t} else if(!strcasecmp(request_text, \"stop_all_files\")) {\n#ifndef HAVE_LIBOGG\n\t\tJANUS_LOG(LOG_VERB, \"Playing files unsupported in this instance\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Playing files unsupported in this instance\");\n\t\tgoto prepare_response;\n#else\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\n\t\tif(lock_playfile && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Parse parameters */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\t/* Update room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tif(audiobridge->destroyed) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\t/* Get list of started announcements and send a stop announcement notification */\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tjson_t *list_annc_removed = json_array();\n\t\tg_hash_table_iter_init(&iter, audiobridge->anncs);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tgboolean started = (p && p->annc && p->annc->started);\n\t\t\tif(p)\n\t\t\t\tjanus_refcount_increase(&p->ref);\n\t\t\tif(started) {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Announcement stopped (%s)\\n\", audiobridge->room_id_str, p->annc->id);\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"announcement-stopped\"));\n\t\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjson_object_set_new(event, \"file_id\", json_string(p->annc->id));\n\t\t\t\tjanus_audiobridge_notify_participants(audiobridge, p, event, TRUE);\n\t\t\t\tjson_decref(event);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"announcement-stopped\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\t\tjson_object_set_new(info, \"file_id\", json_string(p->annc->id));\n\t\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, NULL, info);\n\t\t\t\t}\n\t\t\t\tjson_array_append_new(list_annc_removed, json_string(p->annc->id));\n\t\t\t}\n\t\t\tg_hash_table_iter_remove(&iter);\n\t\t\tif(p)\n\t\t\t\tjanus_refcount_decrease(&p->ref);\n\t\t}\n\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\n\t\t/* Done, prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n \t\tjson_object_set_new(response, \"file_id_list\", list_annc_removed);\n\t\tgoto prepare_response;\n#endif\n\t} else if(!strcasecmp(request_text, \"suspend\") || !strcasecmp(request_text, \"resume\")) {\n\t\tgboolean suspend = !strcasecmp(request_text, \"suspend\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, secret_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(audiobridge == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tguint64 user_id = 0;\n\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tuser_id = json_integer_value(id);\n\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\tuser_id_str = user_id_num;\n\t\t} else {\n\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_audiobridge_participant *participant = g_hash_table_lookup(audiobridge->participants,\n\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such user %s in room %s\\n\", user_id_str, room_id_str);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_USER;\n\t\t\tg_snprintf(error_cause, 512, \"No such user %s in room %s\", user_id_str, room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&participant->ref);\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t/* A secret may be required for this action */\n\t\tif(session->participant != participant) {\n\t\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\t/* Change the suspend status of this participant */\n\t\tgboolean notify_participant = FALSE, recap = FALSE;\n\t\tif(suspend) {\n\t\t\t/* Validate the request */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, suspend_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\t/* Suspend this participant */\n\t\t\tjanus_mutex_lock(&participant->suspend_cond_mutex);\n\t\t\tif(g_atomic_int_compare_and_exchange(&participant->suspended, 0, 1)) {\n\t\t\t\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\t\t\t\tjson_t *pauseevs = json_object_get(root, \"pause_events\");\n\t\t\t\tif(pauseevs && json_is_true(pauseevs))\n\t\t\t\t\tg_atomic_int_set(&participant->paused_events, 1);\n\t\t\t\tnotify_participant = TRUE;\n\t\t\t\t/* Participant is now suspended, so clear the queued packets waiting to be handled */\n\t\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\t\tjanus_audiobridge_participant_clear_jitter_buffer(participant);\n\t\t\t\tjanus_audiobridge_participant_clear_inbuf(participant);\n\t\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\t\tjanus_audiobridge_participant_clear_outbuf(participant);\n\t\t\t\t/* Should we close the recording? */\n\t\t\t\tjson_t *stoprec = json_object_get(root, \"stop_record\");\n\t\t\t\tif(stoprec && json_is_true(stoprec)) {\n\t\t\t\t\t/* Stop recording (ignore if not recording) */\n\t\t\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\t\t\tjanus_audiobridge_recorder_close(participant);\n\t\t\t\t\tparticipant->mjr_active = FALSE;\n\t\t\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\t\t\t}\n\t\t} else {\n\t\t\t/* Validate the request */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, resume_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\t/* Resume this participant */\n\t\t\tjanus_mutex_lock(&participant->suspend_cond_mutex);\n\t\t\tif(g_atomic_int_compare_and_exchange(&participant->suspended, 1, 0)) {\n\t\t\t\tg_cond_signal(&participant->suspend_cond);\n\t\t\t\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\t\t\t\tnotify_participant = TRUE;\n\t\t\t\t/* Should we create a new recording? */\n\t\t\t\tjson_t *record = json_object_get(root, \"record\");\n\t\t\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\t\tif(record && json_is_true(record)) {\n\t\t\t\t\t/* Start recording (ignore if recording already) */\n\t\t\t\t\tif(participant->arc != NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Already recording participant's audio (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tparticipant->room->room_id_str, participant->user_id_str);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Starting recording of participant's audio (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tparticipant->room->room_id_str, participant->user_id_str);\n\t\t\t\t\t\tconst char *recording_base = json_string_value(recfile);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\tg_free(participant->mjr_base);\n\t\t\t\t\t\t\tparticipant->mjr_base = g_strdup(recording_base);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_audiobridge_recorder_create(participant);\n\t\t\t\t\t\tparticipant->mjr_active = TRUE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t\t/* Should we send a full updated state of the room to the participant? */\n\t\t\t\tif(g_atomic_int_compare_and_exchange(&participant->paused_events, 1, 0)) {\n\t\t\t\t\t/* Return a list of all available participants for the resumed participant now */\n\t\t\t\t\trecap = TRUE;\n\t\t\t\t\tjson_t *list = json_array();\n\t\t\t\t\tGHashTableIter iter;\n\t\t\t\t\tgpointer value;\n\t\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\t\t\tif(p == participant) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *pl = json_object();\n\t\t\t\t\t\tjson_object_set_new(pl, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\t\t\t\tif(p->display)\n\t\t\t\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\t\t\t\tjson_object_set_new(pl, \"setup\", g_atomic_int_get(&p->session->started) ? json_true() : json_false());\n\t\t\t\t\t\tjson_object_set_new(pl, \"muted\", p->muted ? json_true() : json_false());\n\t\t\t\t\t\tif(p->extmap_id > 0)\n\t\t\t\t\t\t\tjson_object_set_new(pl, \"talking\", p->talking ? json_true() : json_false());\n\t\t\t\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\t\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(p->spatial_position));\n\t\t\t\t\t\tif(g_atomic_int_get(&p->suspended))\n\t\t\t\t\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\t\t\t\t\tjson_array_append_new(list, pl);\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\tjson_object_set_new(event, \"resumed\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\t\t\tjson_object_set_new(event, \"participants\", list);\n\t\t\t\t\tint ret = gateway->push_event(participant->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\t\t\t}\n\t\t}\n\t\tif(notify_participant) {\n\t\t\t/* Notify all participants about the change */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(event, suspend ? \"suspended\" : \"resumed\",\n\t\t\t\tstring_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\tif((p == participant && recap) || (p != participant && g_atomic_int_get(&p->paused_events)))\n\t\t\t\t\tcontinue;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t}\n\t\t\tjson_decref(event);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(suspend ? \"suspended\" : \"resumed\"));\n\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session ? session->handle : NULL, info);\n\t\t\t}\n\t\t}\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"success\"));\n\t\t/* Done */\n\t\tjanus_refcount_decrease(&participant->ref);\n\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\tgoto prepare_response;\n\t} else {\n\t\t/* Not a request we recognize, don't do anything */\n\t\treturn NULL;\n\t}\n\nprepare_response:\n\t\t{\n\t\t\tif(error_code == 0 && !response) {\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nstruct janus_plugin_result *janus_audiobridge_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\t/* Pre-parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_audiobridge_session *session = janus_audiobridge_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No session associated with this handle...\");\n\t\tgoto plugin_response;\n\t}\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tJANUS_LOG(LOG_ERR, \"Session has already been marked as destroyed...\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"Session has already been marked as destroyed...\");\n\t\tgoto plugin_response;\n\t}\n\n\tif(message == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_MESSAGE;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\tgoto plugin_response;\n\t}\n\tif(!json_is_object(root)) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_JSON;\n\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\tgoto plugin_response;\n\t}\n\t/* Get the request first */\n\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\tjson_t *request = json_object_get(root, \"request\");\n\t/* Some requests ('create', 'destroy', 'exists', 'list') can be handled synchronously */\n\tconst char *request_text = json_string_value(request);\n\t/* We have a separate method to process synchronous requests, as those may\n\t * arrive from the Admin API as well, and so we handle them the same way */\n\tresponse = janus_audiobridge_process_synchronous_request(session, root);\n\tif(response != NULL) {\n\t\t/* We got a response, send it back */\n\t\tgoto plugin_response;\n\t} else if(!strcasecmp(request_text, \"join\") || !strcasecmp(request_text, \"configure\")\n\t\t\t|| !strcasecmp(request_text, \"changeroom\") || !strcasecmp(request_text, \"leave\")\n\t\t\t|| !strcasecmp(request_text, \"hangup\")) {\n\t\t/* These messages are handled asynchronously */\n\t\tjanus_audiobridge_message *msg = g_malloc(sizeof(janus_audiobridge_message));\n\t\tmsg->handle = handle;\n\t\tmsg->transaction = transaction;\n\t\tmsg->message = root;\n\t\tmsg->jsep = jsep;\n\n\t\tg_async_queue_push(messages, msg);\n\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code == 0 && !response) {\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\t\tresponse = event;\n\t\t\t}\n\t\t\tif(root != NULL)\n\t\t\t\tjson_decref(root);\n\t\t\tif(jsep != NULL)\n\t\t\t\tjson_decref(jsep);\n\t\t\tg_free(transaction);\n\n\t\t\tif(session != NULL)\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, response);\n\t\t}\n\n}\n\njson_t *janus_audiobridge_handle_admin_message(json_t *message) {\n\t/* Some requests (e.g., 'create' and 'destroy') can be handled via Admin API */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *response = NULL;\n\n\tJANUS_VALIDATE_JSON_OBJECT(message, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto admin_response;\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\tif((response = janus_audiobridge_process_synchronous_request(NULL, message)) != NULL) {\n\t\t/* We got a response, send it back */\n\t\tgoto admin_response;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nadmin_response:\n\t\t{\n\t\t\tif(!response) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"audiobridge\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nvoid janus_audiobridge_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_AUDIOBRIDGE_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_audiobridge_session *session = janus_audiobridge_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\tif(!participant) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n\t/* FIXME Only send this peer the audio mix when we get this event */\n\tg_atomic_int_set(&session->started, 1);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Notify all other participants that there's a new boy in town */\n\tjanus_mutex_lock(&rooms_mutex);\n\tjanus_audiobridge_room *audiobridge = participant->room;\n\tif(audiobridge == NULL) {\n\t\t/* No room..? Shouldn't happen */\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tJANUS_LOG(LOG_WARN, \"PeerConnection created, but AudioBridge participant not in a room...\\n\");\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&audiobridge->mutex);\n\tjson_t *list = json_array();\n\tjson_t *pl = json_object();\n\tjson_object_set_new(pl, \"id\",\n\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\tif(participant->display)\n\t\tjson_object_set_new(pl, \"display\", json_string(participant->display));\n\tjson_object_set_new(pl, \"setup\", json_true());\n\tjson_object_set_new(pl, \"muted\", participant->muted ? json_true() : json_false());\n\tif(audiobridge->spatial_audio)\n\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(participant->spatial_position));\n\tif(g_atomic_int_get(&participant->suspended))\n\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\tjson_array_append_new(list, pl);\n\tjson_t *pub = json_object();\n\tjson_object_set_new(pub, \"audiobridge\", json_string(\"event\"));\n\tjson_object_set_new(pub, \"room\",\n\t\tstring_ids ? json_string(participant->room->room_id_str) : json_integer(participant->room->room_id));\n\tjson_object_set_new(pub, \"participants\", list);\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_audiobridge_participant *p = value;\n\t\tif(p == participant || g_atomic_int_get(&p->paused_events)) {\n\t\t\tcontinue;\t/* Skip the new participant itself */\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, pub, NULL);\n\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t}\n\tjson_decref(pub);\n\tg_atomic_int_set(&participant->active, 1);\n\tjanus_mutex_unlock(&audiobridge->mutex);\n\tjanus_mutex_unlock(&rooms_mutex);\n}\n\nvoid janus_audiobridge_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_audiobridge_session *session = (janus_audiobridge_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || !session->participant)\n\t\treturn;\n\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\tif(!g_atomic_int_get(&participant->active) || participant->muted || g_atomic_int_get(&participant->suspended) ||\n\t\t\t(participant->codec == JANUS_AUDIOCODEC_OPUS && !participant->decoder) || !participant->room)\n\t\treturn;\n\tif(participant->room && participant->room->muted && !participant->admin)\n\t\treturn;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\t/* Save the frame if we're recording this leg */\n\tjanus_recorder_save_frame(participant->arc, buf, len);\n\tif(g_atomic_int_get(&participant->active) && (participant->codec != JANUS_AUDIOCODEC_OPUS ||\n\t\t\t(participant->codec == JANUS_AUDIOCODEC_OPUS && participant->decoder))) {\n\t\t/* First of all, check if a reset on the decoder is due */\n\t\tif(participant->reset && participant->codec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t/* Create a new decoder and get rid of the old one */\n\t\t\tint error = 0;\n\t\t\tOpusDecoder *decoder = opus_decoder_create(participant->room->sampling_rate,\n\t\t\t\tparticipant->stereo ? 2 : 1, &error);\n\t\t\tif(error != OPUS_OK) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error resetting Opus decoder...\\n\");\n\t\t\t} else {\n\t\t\t\tif(participant->decoder)\n\t\t\t\t\topus_decoder_destroy(participant->decoder);\n\t\t\t\tparticipant->decoder = decoder;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Opus decoder reset\\n\");\n\t\t\t}\n\t\t\tparticipant->reset = FALSE;\n\t\t}\n\t\t/* We'll need to decode the frame (Opus/G.711 -> slinear), so check the payload type */\n\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\t\tif((participant->codec == JANUS_AUDIOCODEC_PCMA && rtp->type != 8) ||\n\t\t\t\t(participant->codec == JANUS_AUDIOCODEC_PCMU && rtp->type != 0)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Wrong payload type (%d != %d), skipping audio packet\\n\",\n\t\t\t\trtp->type, participant->codec == JANUS_AUDIOCODEC_PCMA ? 8 : 0);\n\t\t\treturn;\n\t\t}\n\t\t/* Queue the audio packet in the jitter buffer (we won't decode now, there might be buffering involved) */\n\t\tif(participant->jitter) {\n\t\t\tjanus_audiobridge_buffer_packet *pkt = janus_audiobridge_buffer_packet_create(packet);\n\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\tJitterBufferPacket jbp = {0};\n\t\t\tjbp.data = (char *)pkt;\n\t\t\tjbp.len = 0;\n\t\t\tjbp.span = (participant->codec == JANUS_AUDIOCODEC_OPUS ? 960 : 160);\n\t\t\tjbp.timestamp = (uint32_t)ntohs(rtp->seq_number) * jbp.span;\n\t\t\tjitter_buffer_put(participant->jitter, &jbp);\n\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t}\n\t}\n}\n\nvoid janus_audiobridge_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\t/* FIXME Should we care? */\n}\n\nstatic void janus_audiobridge_recorder_create(janus_audiobridge_participant *participant) {\n\tif(participant == NULL || participant->room == NULL)\n\t\treturn;\n\tjanus_audiobridge_room *audiobridge = participant->room;\n\tchar filename[255];\n\tjanus_recorder *rc = NULL;\n\tgint64 now = janus_get_real_time();\n\tif(participant->arc == NULL) {\n\t\tmemset(filename, 0, 255);\n\t\tif(participant->mjr_base) {\n\t\t\t/* Use the filename and path we have been provided */\n\t\t\tg_snprintf(filename, 255, \"%s-audio\", participant->mjr_base);\n\t\t\trc = janus_recorder_create(audiobridge->mjrs_dir,\n\t\t\t\tjanus_audiocodec_name(participant->codec), filename);\n\t\t\tif(rc == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this participant!\\n\");\n\t\t\t}\n\t\t} else {\n\t\t\t/* Build a filename */\n\t\t\tg_snprintf(filename, 255, \"audiobridge-%s-user-%s-%\"SCNi64\"-audio\",\n\t\t\t\taudiobridge->room_id_str, participant->user_id_str, now);\n\t\t\trc = janus_recorder_create(audiobridge->mjrs_dir,\n\t\t\t\tjanus_audiocodec_name(participant->codec), filename);\n\t\t\tif(rc == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this participant!\\n\");\n\t\t\t}\n\t\t}\n\t\tif(participant->extmap_id > 0)\n\t\t\tjanus_recorder_add_extmap(rc, participant->extmap_id, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\tparticipant->arc = rc;\n\t}\n}\n\nstatic void janus_audiobridge_recorder_close(janus_audiobridge_participant *participant) {\n\tif(participant->arc) {\n\t\tjanus_recorder *rc = participant->arc;\n\t\tparticipant->arc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed user's audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n}\n\nvoid janus_audiobridge_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_AUDIOBRIDGE_PACKAGE, handle);\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_audiobridge_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_audiobridge_hangup_media_internal(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"No WebRTC media anymore\\n\");\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_audiobridge_session *session = janus_audiobridge_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->started, 0);\n\tif(session->participant == NULL)\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\t/* Get rid of participant */\n\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\t/* If this was a plain RTP participant, notify the thread that it's time to go */\n\tif(participant->plainrtp && participant->plainrtp_media.pipefd[1] > 0) {\n\t\tint code = 1;\n\t\tssize_t res = 0;\n\t\tdo {\n\t\t\tres = write(participant->plainrtp_media.pipefd[1], &code, sizeof(int));\n\t\t} while(res == -1 && errno == EINTR);\n\t}\n\tparticipant->plainrtp = FALSE;\n\tjanus_mutex_lock(&rooms_mutex);\n\tjanus_audiobridge_room *audiobridge = participant->room;\n\tgboolean removed = FALSE;\n\tif(audiobridge != NULL) {\n\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\tparticipant->room = NULL;\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\tjson_object_set_new(event, \"room\",\n\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\tjson_object_set_new(event, \"leaving\",\n\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\tremoved = g_hash_table_remove(audiobridge->participants,\n\t\t\tstring_ids ? (gpointer)participant->user_id_str : (gpointer)&participant->user_id);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\tif(p == participant) {\n\t\t\t\tcontinue;\t/* Skip the leaving participant itself */\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t}\n\t\tjson_decref(event);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"left\"));\n\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\tjson_object_set_new(info, \"id\",\n\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\t\tgateway->notify_event(&janus_audiobridge_plugin, NULL, info);\n\t\t}\n\t}\n\t/* Get rid of the recorders, if available */\n\tjanus_mutex_lock(&participant->rec_mutex);\n\tjanus_audiobridge_recorder_close(participant);\n\tparticipant->mjr_active = FALSE;\n\tjanus_mutex_unlock(&participant->rec_mutex);\n\t/* Free the participant resources */\n\tjanus_mutex_lock(&participant->qmutex);\n\tg_atomic_int_set(&participant->active, 0);\n\tparticipant->muted = TRUE;\n\tg_free(participant->display);\n\tparticipant->display = NULL;\n\t/* Make sure we're not using the encoder/decoder right now, we're going to destroy them */\n\twhile(!g_atomic_int_compare_and_exchange(&participant->encoding, 0, 1))\n\t\tg_usleep(5000);\n\tif(participant->encoder)\n\t\topus_encoder_destroy(participant->encoder);\n\tparticipant->encoder = NULL;\n\tg_atomic_int_set(&participant->encoding, 0);\n\twhile(!g_atomic_int_compare_and_exchange(&participant->decoding, 0, 1))\n\t\tg_usleep(5000);\n\tif(participant->decoder)\n\t\topus_decoder_destroy(participant->decoder);\n\tparticipant->decoder = NULL;\n\tg_atomic_int_set(&participant->decoding, 0);\n\tparticipant->reset = FALSE;\n\tparticipant->audio_active_packets = 0;\n\tparticipant->audio_dBov_sum = 0;\n\tparticipant->talking = FALSE;\n\tg_free(participant->mjr_base);\n\tparticipant->mjr_base = NULL;\n\t/* Get rid of queued packets */\n\tjanus_audiobridge_participant_clear_jitter_buffer(participant);\n\tjanus_audiobridge_participant_clear_inbuf(participant);\n\tjanus_mutex_unlock(&participant->qmutex);\n\tjanus_audiobridge_participant_clear_outbuf(participant);\n\tjanus_mutex_lock(&participant->suspend_cond_mutex);\n\tif(g_atomic_int_compare_and_exchange(&participant->suspended, 1, 0))\n\t\tg_cond_signal(&participant->suspend_cond);\n\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\tif(audiobridge != NULL) {\n\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\tif(removed) {\n\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t}\n\t}\n\tjanus_mutex_unlock(&rooms_mutex);\n\tsession->plugin_offer = FALSE;\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_audiobridge_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining AudioBridge handler thread\\n\");\n\tjanus_audiobridge_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_audiobridge_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_audiobridge_session *session = janus_audiobridge_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_audiobridge_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_audiobridge_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = NULL;\n\t\tif(msg->message == NULL) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\troot = msg->message;\n\t\t/* Get the request first */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tgoto error;\n\t\t}\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tjson_t *event = NULL;\n\t\tgboolean sdp_update = FALSE;\n\t\tif(json_object_get(msg->jsep, \"update\") != NULL)\n\t\t\tsdp_update = json_is_true(json_object_get(msg->jsep, \"update\"));\n\t\tgboolean got_offer = FALSE, got_answer = FALSE, generate_offer = FALSE;\n\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\tif(msg_sdp_type != NULL) {\n\t\t\tgot_offer = !strcasecmp(msg_sdp_type, \"offer\");\n\t\t\tgot_answer = !strcasecmp(msg_sdp_type, \"answer\");\n\t\t\tif(!got_offer && !got_answer) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported SDP type '%s'\\n\", msg_sdp_type);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported SDP type '%s'\\n\", msg_sdp_type);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t}\n\t\tif(!strcasecmp(request_text, \"join\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Configuring new participant\\n\");\n\t\t\tjanus_audiobridge_participant *participant = session->participant;\n\t\t\tif(participant != NULL && participant->room != NULL) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already in a room (use changeroom to join another one)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_ALREADY_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already in a room (use changeroom to join another one)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, join_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *rtp = json_object_get(root, \"rtp\");\n\t\t\tif(rtp != NULL) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(msg_sdp != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Added plain RTP details but negotiating a WebRTC PeerConnection: plain RTP will be ignored\\n\");\n\t\t\t\t\trtp = NULL;\n\t\t\t\t\tjson_object_del(root, \"rtp\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!string_ids) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!string_ids) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idopt_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstropt_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *room = json_object_get(root, \"room\");\n\t\t\tguint64 room_id = 0;\n\t\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\t\tif(!string_ids) {\n\t\t\t\troom_id = json_integer_value(room);\n\t\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\t\troom_id_str = room_id_num;\n\t\t\t} else {\n\t\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t\t}\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\t\tif(audiobridge == NULL) {\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tif(rtp != NULL && !audiobridge->allow_plainrtp) {\n\t\t\t\t/* Plain RTP participants are not allowed in this room */\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Plain RTP participants not allowed in this room\\n\");\n\t\t\t\tg_snprintf(error_cause, 512, \"Plain RTP participants not allowed in this room\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* A pin may be required for this action */\n\t\t\tJANUS_CHECK_SECRET(audiobridge->room_pin, root, \"pin\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* A token might be required too */\n\t\t\tif(audiobridge->check_tokens) {\n\t\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\t\tconst char *token_text = token ? json_string_value(token) : NULL;\n\t\t\t\tif(token_text == NULL || g_hash_table_lookup(audiobridge->allowed, token_text) == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized (not in the allowed list)\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Unauthorized (not in the allowed list)\");\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tgboolean admin = FALSE;\n\t\t\tif(json_object_get(root, \"secret\") != NULL) {\n\t\t\t\t/* The user is trying to present themselves as an admin */\n\t\t\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tadmin = TRUE;\n\t\t\t}\n\t\t\t/* If this room uses groups, make sure a valid group name was provided */\n\t\t\tuint group = 0;\n\t\t\tif(audiobridge->groups != NULL) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, group_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tconst char *group_name = json_string_value(json_object_get(root, \"group\"));\n\t\t\t\tgroup = GPOINTER_TO_UINT(g_hash_table_lookup(audiobridge->groups, group_name));\n\t\t\t\tif(group == 0) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such group (%s)\\n\", group_name);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_GROUP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such group (%s)\", group_name);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_t *display = json_object_get(root, \"display\");\n\t\t\tconst char *display_text = display ? json_string_value(display) : NULL;\n\t\t\tjson_t *muted = json_object_get(root, \"muted\");\n\t\t\tjson_t *suspended = json_object_get(root, \"suspended\");\n\t\t\tjson_t *gain = json_object_get(root, \"volume\");\n\t\t\tjson_t *spatial = json_object_get(root, \"spatial_position\");\n\t\t\tjson_t *bitrate = json_object_get(root, \"bitrate\");\n\t\t\tjson_t *quality = json_object_get(root, \"quality\");\n\t\t\tjson_t *exploss = json_object_get(root, \"expected_loss\");\n\t\t\tjson_t *acodec = json_object_get(root, \"codec\");\n\t\t\tjson_t *user_audio_level_average = json_object_get(root, \"audio_level_average\");\n\t\t\tjson_t *user_audio_active_packets = json_object_get(root, \"audio_active_packets\");\n\t\t\tjson_t *denoise = json_object_get(root, \"denoise\");\n\t\t\tjson_t *record = json_object_get(root, \"record\");\n\t\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\t\tjson_t *gen_offer = json_object_get(root, \"generate_offer\");\n\t\t\tint volume = gain ? json_integer_value(gain) : 100;\n\t\t\tint spatial_position = spatial ? json_integer_value(spatial) : 50;\n\t\t\tint32_t opus_bitrate = audiobridge->default_bitrate;\n\t\t\tif(bitrate) {\n\t\t\t\topus_bitrate = json_integer_value(bitrate);\n\t\t\t\tif(opus_bitrate < 500 || opus_bitrate > 512000) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid bitrate %\"SCNi32\", falling back to default/auto\\n\", opus_bitrate);\n\t\t\t\t\topus_bitrate = audiobridge->default_bitrate;\n\t\t\t\t}\n\t\t\t}\n\t\t\tint complexity = quality ? json_integer_value(quality) : DEFAULT_COMPLEXITY;\n\t\t\tif(complexity < 1 || complexity > 10) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (quality should be a positive integer between 1 and 10)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (quality should be a positive integer between 1 and 10)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tint expected_loss = exploss ? json_integer_value(exploss) : audiobridge->default_expectedloss;\n\t\t\tif(expected_loss > 20) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (expected_loss should be a positive integer between 0 and 20)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (expected_loss should be a positive integer between 0 and 20)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_audiocodec codec = JANUS_AUDIOCODEC_OPUS;\n\t\t\tif(acodec != NULL) {\n\t\t\t\tcodec = janus_audiocodec_from_name(json_string_value(acodec));\n\t\t\t\tif(codec != JANUS_AUDIOCODEC_OPUS && codec != JANUS_AUDIOCODEC_PCMA && codec != JANUS_AUDIOCODEC_PCMU) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (codec must opus, pcmu or pcma)\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (codec must opus, pcmu or pcma)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tguint64 user_id = 0;\n\t\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\t\tgboolean user_id_allocated = FALSE;\n\t\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t\tif(id) {\n\t\t\t\tif(!string_ids) {\n\t\t\t\t\tuser_id = json_integer_value(id);\n\t\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\t\tuser_id_str = user_id_num;\n\t\t\t\t} else {\n\t\t\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t\t\t}\n\t\t\t\tif(g_hash_table_lookup(audiobridge->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id) != NULL) {\n\t\t\t\t\t/* User ID already taken */\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_ID_EXISTS;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"User ID %s already exists\\n\", user_id_str);\n\t\t\t\t\tg_snprintf(error_cause, 512, \"User ID %s already exists\", user_id_str);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!string_ids) {\n\t\t\t\tif(user_id == 0) {\n\t\t\t\t\t/* Generate a random ID */\n\t\t\t\t\twhile(user_id == 0) {\n\t\t\t\t\t\tuser_id = janus_random_uint64();\n\t\t\t\t\t\tif(g_hash_table_lookup(audiobridge->participants, &user_id) != NULL) {\n\t\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\t\tuser_id = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\t\tuser_id_str = user_id_num;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID: %\"SCNu64\"\\n\", user_id);\n\t\t\t} else {\n\t\t\t\tif(user_id_str == NULL) {\n\t\t\t\t\t/* Generate a random ID */\n\t\t\t\t\twhile(user_id_str == NULL) {\n\t\t\t\t\t\tuser_id_str = janus_random_uuid();\n\t\t\t\t\t\tif(g_hash_table_lookup(audiobridge->participants, user_id_str) != NULL) {\n\t\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\t\tg_clear_pointer(&user_id_str, g_free);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tuser_id_allocated = TRUE;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID: %s\\n\", user_id_str);\n\t\t\t}\n\t\t\tif(participant == NULL) {\n\t\t\t\tparticipant = g_malloc0(sizeof(janus_audiobridge_participant));\n\t\t\t\tjanus_refcount_init(&participant->ref, janus_audiobridge_participant_free);\n\t\t\t\tg_atomic_int_set(&participant->active, 0);\n\t\t\t\tparticipant->codec = codec;\n\t\t\t\tparticipant->display = NULL;\n\t\t\t\tparticipant->jitter = jitter_buffer_init(participant->codec == JANUS_AUDIOCODEC_OPUS ? 960 : 160);\n\t\t\t\tjitter_buffer_ctl(participant->jitter, JITTER_BUFFER_SET_DESTROY_CALLBACK, &janus_audiobridge_buffer_packet_destroy);\n\t\t\t\tspx_int32_t min_buffer_size = participant->codec == JANUS_AUDIOCODEC_OPUS ? (JITTER_BUFFER_MIN_PACKETS * 960) : (JITTER_BUFFER_MIN_PACKETS * 160);\n\t\t\t\tjitter_buffer_ctl(participant->jitter, JITTER_BUFFER_SET_MARGIN, &min_buffer_size);\n\t\t\t\tspx_int32_t max_buffer_size = JITTER_BUFFER_MAX_PACKETS;\n\t\t\t\tjitter_buffer_ctl(participant->jitter, JITTER_BUFFER_SET_LIMIT, &max_buffer_size);\n\t\t\t\t/* Disable automatic adjustment */\n\t\t\t\tjitter_buffer_update_delay(participant->jitter, NULL, NULL);\n\t\t\t\tparticipant->inbuf = NULL;\n\t\t\t\tparticipant->outbuf = NULL;\n\t\t\t\tparticipant->encoder = NULL;\n\t\t\t\tparticipant->decoder = NULL;\n\t\t\t\tparticipant->reset = FALSE;\n\t\t\t\tparticipant->fec = FALSE;\n\t\t\t\tparticipant->last_timestamp = 0;\n\t\t\t\tparticipant->last_seq = 0;\n\t\t\t\tjanus_mutex_init(&participant->qmutex);\n\t\t\t\tparticipant->arc = NULL;\n\t\t\t\tjanus_audiobridge_plainrtp_media_cleanup(&participant->plainrtp_media);\n\t\t\t\tjanus_mutex_init(&participant->pmutex);\n\t\t\t\tjanus_mutex_init(&participant->rec_mutex);\n\t\t\t\tjanus_mutex_init(&participant->suspend_cond_mutex);\n\t\t\t\tjanus_condition_init(&participant->suspend_cond);\n\t\t\t}\n\t\t\tparticipant->session = session;\n\t\t\tparticipant->room = audiobridge;\n\t\t\tparticipant->user_id = user_id;\n\t\t\tparticipant->user_id_str = user_id_str ? g_strdup(user_id_str) : NULL;\n\t\t\tparticipant->group = group;\n\t\t\tg_free(participant->display);\n\t\t\tparticipant->admin = admin;\n\t\t\tparticipant->display = display_text ? g_strdup(display_text) : NULL;\n\t\t\tparticipant->muted = muted ? json_is_true(muted) : FALSE;\t/* By default, everyone's unmuted when joining */\n\t\t\tif(suspended && json_is_true(suspended)) {\n\t\t\t\tjanus_mutex_lock(&participant->suspend_cond_mutex);\n\t\t\t\tg_atomic_int_set(&participant->suspended, 1);\n\t\t\t\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\t\t\t\tjson_t *pauseevs = json_object_get(root, \"pause_events\");\n\t\t\t\tif(pauseevs && json_is_true(pauseevs))\n\t\t\t\t\tg_atomic_int_set(&participant->paused_events, 1);\n\t\t\t}\n\t\t\tparticipant->volume_gain = volume;\n\t\t\tparticipant->opus_complexity = complexity;\n\t\t\tparticipant->opus_bitrate = opus_bitrate;\n\t\t\tparticipant->expected_loss = expected_loss;\n\t\t\tparticipant->stereo = audiobridge->spatial_audio;\n\t\t\tif(participant->stereo) {\n\t\t\t\tif(spatial_position > 100)\n\t\t\t\t\tspatial_position = 100;\n\t\t\t\tparticipant->spatial_position = spatial_position;\n\t\t\t}\n\t\t\tparticipant->user_audio_active_packets = json_integer_value(user_audio_active_packets);\n\t\t\tparticipant->user_audio_level_average = json_integer_value(user_audio_level_average);\n\t\t\tif(participant->outbuf == NULL)\n\t\t\t\tparticipant->outbuf = g_async_queue_new();\n\t\t\tg_atomic_int_set(&participant->active, g_atomic_int_get(&session->started));\n\t\t\tif(!g_atomic_int_get(&session->started)) {\n\t\t\t\t/* Initialize the RTP context only if we're renegotiating */\n\t\t\t\tjanus_rtp_switching_context_reset(&participant->context);\n\t\t\t\tparticipant->opus_pt = 0;\n\t\t\t\tparticipant->extmap_id = 0;\n\t\t\t\tparticipant->dBov_level = 0;\n\t\t\t\tparticipant->talking = FALSE;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Creating Opus encoder/decoder (sampling rate %d)\\n\", audiobridge->sampling_rate);\n\t\t\t/* Opus encoder */\n\t\t\tint error = 0;\n\t\t\tif(participant->encoder == NULL) {\n\t\t\t\tparticipant->sampling_rate = audiobridge->sampling_rate;\n\t\t\t\tparticipant->encoder = opus_encoder_create(audiobridge->sampling_rate,\n\t\t\t\t\taudiobridge->spatial_audio ? 2 : 1, OPUS_APPLICATION_VOIP, &error);\n\t\t\t\tif(error != OPUS_OK) {\n\t\t\t\t\tif(user_id_allocated) {\n\t\t\t\t\t\tg_free(user_id_str);\n\t\t\t\t\t\tg_free(participant->user_id_str);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tg_free(participant->display);\n\t\t\t\t\tg_free(participant);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating Opus encoder\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error creating Opus encoder\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(audiobridge->sampling_rate == 8000) {\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_NARROWBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 12000) {\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_MEDIUMBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 16000) {\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 24000) {\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_SUPERWIDEBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 48000) {\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND));\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported sampling rate %d, setting 16kHz\\n\", audiobridge->sampling_rate);\n\t\t\t\t\taudiobridge->sampling_rate = 16000;\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t\t\t\t}\n\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_INBAND_FEC(participant->fec));\n\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_PACKET_LOSS_PERC(participant->expected_loss));\n\t\t\t}\n\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_COMPLEXITY(participant->opus_complexity));\n\t\t\tif(participant->opus_bitrate > 0)\n\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_BITRATE(participant->opus_bitrate));\n\t\t\tif(participant->decoder == NULL) {\n\t\t\t\t/* Opus decoder */\n\t\t\t\terror = 0;\n\t\t\t\tparticipant->decoder = opus_decoder_create(audiobridge->sampling_rate,\n\t\t\t\t\taudiobridge->spatial_audio ? 2 : 1, &error);\n\t\t\t\tif(error != OPUS_OK) {\n\t\t\t\t\tif(user_id_allocated) {\n\t\t\t\t\t\tg_free(user_id_str);\n\t\t\t\t\t\tg_free(participant->user_id_str);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tg_free(participant->display);\n\t\t\t\t\tif(participant->encoder)\n\t\t\t\t\t\topus_encoder_destroy(participant->encoder);\n\t\t\t\t\tparticipant->encoder = NULL;\n\t\t\t\t\tif(participant->decoder)\n\t\t\t\t\t\topus_decoder_destroy(participant->decoder);\n\t\t\t\t\tparticipant->decoder = NULL;\n\t\t\t\t\tg_free(participant);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating Opus decoder\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error creating Opus decoder\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n#ifdef HAVE_RNNOISE\n\t\t\tparticipant->denoise = denoise ? json_is_true(denoise) : audiobridge->denoise;\n#else\n\t\t\tif(denoise && json_is_true(denoise)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"RNNoise unavailable, denoising not supported\\n\");\n\t\t\t}\n#endif\n\t\t\tparticipant->reset = FALSE;\n\t\t\t/* If we need to generate an offer ourselves, do that */\n\t\t\tif(gen_offer != NULL)\n\t\t\t\tgenerate_offer = json_is_true(gen_offer);\n\t\t\tif(generate_offer)\n\t\t\t\tsession->plugin_offer = generate_offer;\n\t\t\t/* If this is a plain RTP participant, create the socket */\n\t\t\tif(rtp != NULL) {\n\t\t\t\tparticipant->plainrtp = TRUE;\n\t\t\t\tgenerate_offer = FALSE;\n\t\t\t\tconst char *ip = json_string_value(json_object_get(rtp, \"ip\"));\n\t\t\t\tuint16_t port = json_integer_value(json_object_get(rtp, \"port\"));\n\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\tint pt = json_integer_value(json_object_get(rtp, \"payload_type\"));\n\t\t\t\t\tif(pt == 0)\n\t\t\t\t\t\tpt = 100;\n\t\t\t\t\tparticipant->opus_pt = pt;\n\t\t\t\t}\n\t\t\t\tint audiolevel_ext_id = json_integer_value(json_object_get(rtp, \"audiolevel_ext\"));\n\t\t\t\tif(audiolevel_ext_id > 0)\n\t\t\t\t\tparticipant->extmap_id = audiolevel_ext_id;\n\t\t\t\tgboolean fec = json_is_true(json_object_get(rtp, \"fec\"));\n\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_OPUS && fec) {\n\t\t\t\t\tparticipant->fec = TRUE;\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_INBAND_FEC(participant->fec));\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_PACKET_LOSS_PERC(participant->expected_loss));\n\t\t\t\t}\n\t\t\t\t/* Create the socket */\n\t\t\t\tjanus_mutex_lock(&participant->pmutex);\n\t\t\t\tjanus_audiobridge_plainrtp_media_cleanup(&participant->plainrtp_media);\n\t\t\t\tif(janus_audiobridge_plainrtp_allocate_port(&participant->plainrtp_media) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Couldn't bind to local port\\n\", session);\n\t\t\t\t} else if(!generate_offer && ip != NULL && port > 0) {\n\t\t\t\t\t/* Connect the socket, if there's a remote address: if we're\n\t\t\t\t\t *generating the \"offer\" ourselves, that will happen later */\n\t\t\t\t\tg_free(participant->plainrtp_media.remote_audio_ip);\n\t\t\t\t\tparticipant->plainrtp_media.remote_audio_ip = g_strdup(ip);\n\t\t\t\t\tparticipant->plainrtp_media.remote_audio_rtp_port = port;\n\t\t\t\t\t/* Resolve the address */\n\t\t\t\t\tgboolean have_audio_server_ip = FALSE;\n\t\t\t\t\tstruct sockaddr_storage audio_server_addr = { 0 };\n\t\t\t\t\tif(janus_network_resolve_address(participant->plainrtp_media.remote_audio_ip, &audio_server_addr) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Couldn't get host '%s'\\n\", session,\n\t\t\t\t\t\t\tparticipant->plainrtp_media.remote_audio_ip);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Address resolved */\n\t\t\t\t\t\thave_audio_server_ip = TRUE;\n\t\t\t\t\t\tif(audio_server_addr.ss_family == AF_INET6) {\n\t\t\t\t\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)&audio_server_addr;\n\t\t\t\t\t\t\taddr6->sin6_port = htons(port);\n\t\t\t\t\t\t} else if(audio_server_addr.ss_family == AF_INET) {\n\t\t\t\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&audio_server_addr;\n\t\t\t\t\t\t\taddr->sin_port = htons(port);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(have_audio_server_ip) {\n\t\t\t\t\t\tif(connect(participant->plainrtp_media.audio_rtp_fd, (struct sockaddr *)&audio_server_addr, sizeof(audio_server_addr)) == -1) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Couldn't connect audio RTP? (%s:%d)\\n\", session,\n\t\t\t\t\t\t\t\tparticipant->plainrtp_media.remote_audio_ip, participant->plainrtp_media.remote_audio_rtp_port);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tparticipant->plainrtp_media.audio_send = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->pmutex);\n\t\t\t}\n\t\t\t/* Check if we need to record this participant right away */\n\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\tconst char *recording_base = json_string_value(recfile);\n\t\t\tif(recording_base) {\n\t\t\t\tg_free(participant->mjr_base);\n\t\t\t\tparticipant->mjr_base = g_strdup(recording_base);\n\t\t\t}\n\t\t\tif(audiobridge->mjrs || record) {\n\t\t\t\tif(audiobridge->mjrs || json_is_true(record)) {\n\t\t\t\t\t/* Start recording (ignore if recording already) */\n\t\t\t\t\tif(participant->arc != NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Already recording participant's audio (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tparticipant->room->room_id_str, participant->user_id_str);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Starting recording of participant's audio (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tparticipant->room->room_id_str, participant->user_id_str);\n\t\t\t\t\t\tjanus_audiobridge_recorder_create(participant);\n\t\t\t\t\t\tparticipant->mjr_active = TRUE;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Stop recording (ignore if not recording) */\n\t\t\t\t\tjanus_audiobridge_recorder_close(participant);\n\t\t\t\t\tparticipant->mjr_active = FALSE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t/* Finally, start the encoding thread if it hasn't already */\n\t\t\tif(participant->thread == NULL) {\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar roomtrunc[5], parttrunc[5];\n\t\t\t\tg_snprintf(roomtrunc, sizeof(roomtrunc), \"%s\", audiobridge->room_id_str);\n\t\t\t\tg_snprintf(parttrunc, sizeof(parttrunc), \"%s\", participant->user_id_str);\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"mixer %s %s\", roomtrunc, parttrunc);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tjanus_refcount_increase(&participant->ref);\n\t\t\t\tparticipant->thread = g_thread_try_new(tname, &janus_audiobridge_participant_thread, participant, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\t/* FIXME We should fail here... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the participant thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(participant->plainrtp && participant->plainrtp_media.audio_rtp_fd != -1 && participant->plainrtp_media.thread == NULL) {\n\t\t\t\t/* Spawn a thread for incoming plain RTP traffic too */\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar roomtrunc[5], parttrunc[5];\n\t\t\t\tg_snprintf(roomtrunc, sizeof(roomtrunc), \"%s\", audiobridge->room_id_str);\n\t\t\t\tg_snprintf(parttrunc, sizeof(parttrunc), \"%s\", participant->user_id_str);\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"rtp %s %s\", roomtrunc, parttrunc);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tjanus_refcount_increase(&participant->ref);\n\t\t\t\tparticipant->plainrtp_media.thread = g_thread_try_new(tname, &janus_audiobridge_plainrtp_relay_thread, participant, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\t/* FIXME We should fail here... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the plain RTP participant thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* If a PeerConnection exists, make sure to update the RTP headers */\n\t\t\tif(g_atomic_int_get(&session->started) == 1)\n\t\t\t\tparticipant->context.last_ssrc = 0;\n\n\t\t\t/* Done */\n\t\t\tsession->participant = participant;\n\t\t\tjanus_refcount_increase(&participant->ref);\n\t\t\tg_hash_table_insert(audiobridge->participants,\n\t\t\t\tstring_ids ? (gpointer)g_strdup(participant->user_id_str) : (gpointer)janus_uint64_dup(participant->user_id),\n\t\t\t\tparticipant);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* Notify the other participants */\n\t\t\tjson_t *newuser = json_object();\n\t\t\tjson_object_set_new(newuser, \"audiobridge\", json_string(\"joined\"));\n\t\t\tjson_object_set_new(newuser, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_t *newuserlist = json_array();\n\t\t\tjson_t *pl = json_object();\n\t\t\tjson_object_set_new(pl, \"id\",\n\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\tif(participant->display)\n\t\t\t\tjson_object_set_new(pl, \"display\", json_string(participant->display));\n\t\t\t/* Clarify we're still waiting for the user to negotiate a PeerConnection */\n\t\t\tjson_object_set_new(pl, \"setup\", json_false());\n\t\t\tjson_object_set_new(pl, \"muted\", participant->muted ? json_true() : json_false());\n\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(participant->spatial_position));\n\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\t\tjson_array_append_new(newuserlist, pl);\n\t\t\tjson_object_set_new(newuser, \"participants\", newuserlist);\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\tif(p == participant || g_atomic_int_get(&p->paused_events)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, newuser, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t}\n\t\t\tjson_decref(newuser);\n\t\t\t/* Return a list of all available participants for the new participant now */\n\t\t\tjson_t *list = json_array();\n\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\tif(p == participant || g_atomic_int_get(&p->paused_events)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjson_t *pl = json_object();\n\t\t\t\tjson_object_set_new(pl, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\t\tif(p->display)\n\t\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\t\tjson_object_set_new(pl, \"setup\", g_atomic_int_get(&p->session->started) ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(pl, \"muted\", p->muted ? json_true() : json_false());\n\t\t\t\tif(p->extmap_id > 0)\n\t\t\t\t\tjson_object_set_new(pl, \"talking\", p->talking ? json_true() : json_false());\n\t\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(p->spatial_position));\n\t\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\t\t\tjson_array_append_new(list, pl);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"joined\"));\n\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(event, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\tjson_object_set_new(event, \"participants\", list);\n\t\t\tif(participant->plainrtp_media.local_audio_rtp_port > 0) {\n\t\t\t\tjson_t *details = json_object();\n\t\t\t\tjson_object_set_new(details, \"ip\", json_string(local_ip));\n\t\t\t\tjson_object_set_new(details, \"port\", json_integer(participant->plainrtp_media.local_audio_rtp_port));\n\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_OPUS)\n\t\t\t\t\tjson_object_set_new(details, \"payload_type\", json_integer(participant->opus_pt));\n\t\t\t\telse\n\t\t\t\t\tjson_object_set_new(details, \"payload_type\", json_integer(participant->codec == JANUS_AUDIOCODEC_PCMA ? 8 : 0));\n\t\t\t\tjson_object_set_new(event, \"rtp\", details);\n\t\t\t}\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"joined\"));\n\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\t\t\tjson_object_set_new(info, \"setup\", g_atomic_int_get(&participant->session->started) ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"muted\", participant->muted ? json_true() : json_false());\n\t\t\t\tif(participant->stereo)\n\t\t\t\t\tjson_object_set_new(info, \"spatial_position\", json_integer(participant->spatial_position));\n\t\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\t\tjson_object_set_new(info, \"suspended\", json_true());\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session->handle, info);\n\t\t\t}\n\t\t\tif(user_id_allocated)\n\t\t\t\tg_free(user_id_str);\n\t\t\tif(participant->plainrtp && (!session->plugin_offer || participant->plainrtp_media.audio_send) &&\n\t\t\t\t\tg_atomic_int_compare_and_exchange(&participant->plainrtp_media.initialized, 0, 1)) {\n\t\t\t\t/* Plain RTP participant, simulate a setup_media event */\n\t\t\t\tjanus_audiobridge_setup_media(session->handle);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"configure\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* Handle this participant */\n\t\t\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\t\t\tif(participant == NULL || participant->room == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't configure (not in a room)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NOT_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't configure (not in a room)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Configure settings for this participant */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, configure_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *muted = json_object_get(root, \"muted\");\n\t\t\tjson_t *bitrate = json_object_get(root, \"bitrate\");\n\t\t\tjson_t *quality = json_object_get(root, \"quality\");\n\t\t\tjson_t *exploss = json_object_get(root, \"expected_loss\");\n\t\t\tjson_t *gain = json_object_get(root, \"volume\");\n\t\t\tjson_t *spatial = json_object_get(root, \"spatial_position\");\n\t\t\tjson_t *denoise = json_object_get(root, \"denoise\");\n\t\t\tjson_t *record = json_object_get(root, \"record\");\n\t\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\t\tjson_t *display = json_object_get(root, \"display\");\n\t\t\tjson_t *group = json_object_get(root, \"group\");\n\t\t\tjson_t *gen_offer = json_object_get(root, \"generate_offer\");\n\t\t\tjson_t *update = json_object_get(root, \"update\");\n\t\t\tjson_t *rtp = json_object_get(root, \"rtp\");\n\t\t\tif(rtp != NULL) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tgoto error;\n\t\t\t\tif(!participant->plainrtp || !session->plugin_offer) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring RTP details: not a plain RTP participant, or not using plugin offers\\n\");\n\t\t\t\t\trtp = NULL;\n\t\t\t\t\tjson_object_del(root, \"rtp\");\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* If this is a plain RTP participant, connect the socket */\n\t\t\tif(rtp != NULL) {\n\t\t\t\tconst char *ip = json_string_value(json_object_get(rtp, \"ip\"));\n\t\t\t\tuint16_t port = json_integer_value(json_object_get(rtp, \"port\"));\n\t\t\t\tjanus_mutex_lock(&participant->pmutex);\n\t\t\t\tif(ip == NULL || port == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Can't connect socket, no remote address provided\\n\", session);\n\t\t\t\t} else {\n\t\t\t\t\t/* Connect the socket using the info from the \"answer\" */\n\t\t\t\t\tg_free(participant->plainrtp_media.remote_audio_ip);\n\t\t\t\t\tparticipant->plainrtp_media.remote_audio_ip = g_strdup(ip);\n\t\t\t\t\tparticipant->plainrtp_media.remote_audio_rtp_port = port;\n\t\t\t\t\t/* Resolve the address */\n\t\t\t\t\tgboolean have_audio_server_ip = FALSE;\n\t\t\t\t\tstruct sockaddr_storage audio_server_addr = { 0 };\n\t\t\t\t\tif(janus_network_resolve_address(participant->plainrtp_media.remote_audio_ip, &audio_server_addr) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Couldn't get host '%s'\\n\", session,\n\t\t\t\t\t\t\tparticipant->plainrtp_media.remote_audio_ip);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Address resolved */\n\t\t\t\t\t\thave_audio_server_ip = TRUE;\n\t\t\t\t\t\tif(audio_server_addr.ss_family == AF_INET6) {\n\t\t\t\t\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)&audio_server_addr;\n\t\t\t\t\t\t\taddr6->sin6_port = htons(port);\n\t\t\t\t\t\t} else if(audio_server_addr.ss_family == AF_INET) {\n\t\t\t\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&audio_server_addr;\n\t\t\t\t\t\t\taddr->sin_port = htons(port);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(have_audio_server_ip) {\n\t\t\t\t\t\tif(connect(participant->plainrtp_media.audio_rtp_fd, (struct sockaddr *)&audio_server_addr, sizeof(audio_server_addr)) == -1) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Couldn't connect audio RTP? (%s:%d)\\n\", session,\n\t\t\t\t\t\t\t\tparticipant->plainrtp_media.remote_audio_ip, participant->plainrtp_media.remote_audio_rtp_port);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tparticipant->plainrtp_media.audio_send = TRUE;\n\t\t\t\t\t\t\tif(g_atomic_int_compare_and_exchange(&participant->plainrtp_media.initialized, 0, 1)) {\n\t\t\t\t\t\t\t\t/* Simulate a setup_media event */\n\t\t\t\t\t\t\t\tjanus_audiobridge_setup_media(session->handle);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->pmutex);\n\t\t\t}\n\t\t\tif(gain)\n\t\t\t\tparticipant->volume_gain = json_integer_value(gain);\n\t\t\tif(bitrate) {\n\t\t\t\tint32_t opus_bitrate = bitrate ? json_integer_value(bitrate) : 0;\n\t\t\t\tif(opus_bitrate < 500 || opus_bitrate > 512000) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid bitrate %\"SCNi32\", falling back to auto\\n\", opus_bitrate);\n\t\t\t\t\topus_bitrate = 0;\n\t\t\t\t}\n\t\t\t\tparticipant->opus_bitrate = opus_bitrate;\n\t\t\t\tif(participant->encoder)\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_BITRATE(participant->opus_bitrate ? participant->opus_bitrate : OPUS_AUTO));\n\t\t\t}\n\t\t\tif(quality) {\n\t\t\t\tint complexity = json_integer_value(quality);\n\t\t\t\tif(complexity < 1 || complexity > 10) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (quality should be a positive integer between 1 and 10)\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (quality should be a positive integer between 1 and 10)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tparticipant->opus_complexity = complexity;\n\t\t\t\tif(participant->encoder)\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_COMPLEXITY(participant->opus_complexity));\n\t\t\t}\n\t\t\tif(exploss) {\n\t\t\t\tint expected_loss = json_integer_value(exploss);\n\t\t\t\tif(expected_loss > 20) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (expected_loss should be a positive integer between 0 and 20)\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (expected_loss should be a positive integer between 0 and 20)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tparticipant->expected_loss = expected_loss;\n\t\t\t\tif(participant->encoder)\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_PACKET_LOSS_PERC(participant->expected_loss));\n\t\t\t}\n\t\t\tif(group && participant->room && participant->room->groups != NULL) {\n\t\t\t\tconst char *group_name = json_string_value(group);\n\t\t\t\tuint group_id = GPOINTER_TO_UINT(g_hash_table_lookup(participant->room->groups, group_name));\n\t\t\t\tif(group_id == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such group (%s)\\n\", group_name);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_GROUP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such group (%s)\", group_name);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tparticipant->group = group_id;\n\t\t\t}\n\t\t\tif(muted || display || (participant->stereo && spatial) || denoise) {\n\t\t\t\tif(muted) {\n\t\t\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\t\t\tif(participant->muted != json_is_true(muted)) {\n\t\t\t\t\t\tparticipant->muted = json_is_true(muted);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting muted property: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tparticipant->muted ? \"true\" : \"false\", participant->room->room_id_str, participant->user_id_str);\n\t\t\t\t\t\t/* Clear the queued packets waiting to be handled */\n\t\t\t\t\t\tjanus_audiobridge_participant_clear_jitter_buffer(participant);\n\t\t\t\t\t\tjanus_audiobridge_participant_clear_inbuf(participant);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\t\t}\n\t\t\t\tif(display) {\n\t\t\t\t\tchar *old_display = participant->display;\n\t\t\t\t\tchar *new_display = g_strdup(json_string_value(display));\n\t\t\t\t\tparticipant->display = new_display;\n\t\t\t\t\tg_free(old_display);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting display property: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->display, participant->room->room_id_str, participant->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(participant->stereo && spatial) {\n\t\t\t\t\tint spatial_position = json_integer_value(spatial);\n\t\t\t\t\tif(spatial_position > 100)\n\t\t\t\t\t\tspatial_position = 100;\n\t\t\t\t\tparticipant->spatial_position = spatial_position;\n\t\t\t\t}\n#ifdef HAVE_RNNOISE\n\t\t\t\tif(denoise)\n\t\t\t\t\tparticipant->denoise = json_is_true(denoise);\n#else\n\t\t\t\tif(denoise && json_is_true(denoise)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"RNNoise unavailable, denoising not supported\\n\");\n\t\t\t\t}\n#endif\n\t\t\t\t/* Notify all other participants */\n\t\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\t\tjanus_audiobridge_room *audiobridge = participant->room;\n\t\t\t\tif(audiobridge != NULL) {\n\t\t\t\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t\t\t\tjson_t *list = json_array();\n\t\t\t\t\tjson_t *pl = json_object();\n\t\t\t\t\tjson_object_set_new(pl, \"id\",\n\t\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t\tif(participant->display)\n\t\t\t\t\t\tjson_object_set_new(pl, \"display\", json_string(participant->display));\n\t\t\t\t\tjson_object_set_new(pl, \"setup\", g_atomic_int_get(&participant->session->started) ? json_true() : json_false());\n\t\t\t\t\tjson_object_set_new(pl, \"muted\", participant->muted ? json_true() : json_false());\n\t\t\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(participant->spatial_position));\n\t\t\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\t\t\t\tjson_array_append_new(list, pl);\n\t\t\t\t\tjson_t *pub = json_object();\n\t\t\t\t\tjson_object_set_new(pub, \"audiobridge\", json_string(\"event\"));\n\t\t\t\t\tjson_object_set_new(pub, \"room\",\n\t\t\t\t\t\tstring_ids ? json_string(participant->room->room_id_str) : json_integer(participant->room->room_id));\n\t\t\t\t\tjson_object_set_new(pub, \"participants\", list);\n\t\t\t\t\tGHashTableIter iter;\n\t\t\t\t\tgpointer value;\n\t\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\t\t\tif(p == participant || g_atomic_int_get(&p->paused_events)) {\n\t\t\t\t\t\t\tcontinue;\t/* Skip the new participant itself */\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\",\n\t\t\t\t\t\t\tp->user_id_str, p->display ? p->display : \"??\");\n\t\t\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, pub, NULL);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\t\t}\n\t\t\t\t\tjson_decref(pub);\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t}\n\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\tconst char *recording_base = json_string_value(recfile);\n\t\t\tif(recording_base) {\n\t\t\t\tg_free(participant->mjr_base);\n\t\t\t\tparticipant->mjr_base = g_strdup(recording_base);\n\t\t\t}\n\t\t\tif(record) {\n\t\t\t\tif(json_is_true(record)) {\n\t\t\t\t\t/* Start recording (ignore if recording already) */\n\t\t\t\t\tif(participant->arc != NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Already recording participant's audio (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tparticipant->room->room_id_str, participant->user_id_str);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Starting recording of participant's audio (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tparticipant->room->room_id_str, participant->user_id_str);\n\t\t\t\t\t\tjanus_audiobridge_recorder_create(participant);\n\t\t\t\t\t\tparticipant->mjr_active = TRUE;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Stop recording (ignore if not recording) */\n\t\t\t\t\tjanus_audiobridge_recorder_close(participant);\n\t\t\t\t\tparticipant->mjr_active = FALSE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\tgboolean do_update = update ? json_is_true(update) : FALSE;\n\t\t\tif(do_update && (!sdp_update || !session->plugin_offer || participant->plainrtp)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Got a 'update' request, but no SDP update? Ignoring...\\n\");\n\t\t\t}\n\t\t\t/* Done */\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"result\", json_string(\"ok\"));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjanus_audiobridge_room *audiobridge = participant->room;\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"configured\"));\n\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjson_object_set_new(info, \"id\",\n\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\t\t\tjson_object_set_new(info, \"muted\", participant->muted ? json_true() : json_false());\n\t\t\t\tif(participant->opus_bitrate > 0)\n\t\t\t\t\tjson_object_set_new(info, \"bitrate\", json_integer(participant->opus_bitrate));\n\t\t\t\tjson_object_set_new(info, \"quality\", json_integer(participant->opus_complexity));\n\t\t\t\tif(participant->stereo)\n\t\t\t\t\tjson_object_set_new(info, \"spatial_position\", json_integer(participant->spatial_position));\n\t\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\t\tjson_object_set_new(info, \"suspended\", json_true());\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* If we need to generate an offer ourselves, do that */\n\t\t\tif(!participant->plainrtp && do_update && session->plugin_offer) {\n\t\t\t\t/* We need an update and we originated an offer before, let's do it again */\n\t\t\t\tgenerate_offer = TRUE;\n\t\t\t} else if(!participant->plainrtp && gen_offer != NULL) {\n\t\t\t\tgenerate_offer = json_is_true(gen_offer);\n\t\t\t}\n\t\t\tif(generate_offer) {\n\t\t\t\t/* We should check if this conflicts with a user-generated offer from before */\n\t\t\t\tsession->plugin_offer = generate_offer;\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"changeroom\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* The participant wants to leave the current room and join another one without reconnecting (e.g., a sidebar) */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, join_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tif(!string_ids) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *room = json_object_get(root, \"room\");\n\t\t\tguint64 room_id = 0;\n\t\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\t\tif(!string_ids) {\n\t\t\t\troom_id = json_integer_value(room);\n\t\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\t\troom_id_str = room_id_num;\n\t\t\t} else {\n\t\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t\t}\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\t\t\tif(participant == NULL || participant->room == NULL) {\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't change room (not in a room in the first place)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NOT_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't change room (not in a room in the first place)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Is this the same room we're in? */\n\t\t\tif(participant->room && ((!string_ids && participant->room->room_id == room_id) ||\n\t\t\t\t\t(string_ids && participant->room->room_id_str && !strcmp(participant->room->room_id_str, room_id_str)))) {\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already in this room\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_ALREADY_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already in this room\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms,\n\t\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\t\tif(audiobridge == NULL) {\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t\t/* A pin may be required for this action */\n\t\t\tJANUS_CHECK_SECRET(audiobridge->room_pin, root, \"pin\", error_code, error_cause,\n\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* A token might be required too */\n\t\t\tif(audiobridge->check_tokens) {\n\t\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\t\tconst char *token_text = token ? json_string_value(token) : NULL;\n\t\t\t\tif(token_text == NULL || g_hash_table_lookup(audiobridge->allowed, token_text) == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized (not in the allowed list)\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Unauthorized (not in the allowed list)\");\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tgboolean admin = FALSE;\n\t\t\tif(json_object_get(root, \"secret\") != NULL) {\n\t\t\t\t/* The user is trying to present themselves as an admin */\n\t\t\t\tJANUS_CHECK_SECRET(audiobridge->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tadmin = TRUE;\n\t\t\t}\n\t\t\t/* If this room uses groups, make sure a valid group name was provided */\n\t\t\tuint group = 0;\n\t\t\tif(audiobridge->groups != NULL) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, group_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tconst char *group_name = json_string_value(json_object_get(root, \"group\"));\n\t\t\t\tgroup = GPOINTER_TO_UINT(g_hash_table_lookup(audiobridge->groups, group_name));\n\t\t\t\tif(group == 0) {\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such group (%s)\\n\", group_name);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_GROUP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such group (%s)\", group_name);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_t *display = json_object_get(root, \"display\");\n\t\t\tconst char *display_text = display ? json_string_value(display) : NULL;\n\t\t\tjson_t *muted = json_object_get(root, \"muted\");\n\t\t\tjson_t *suspended = json_object_get(root, \"suspended\");\n\t\t\tjson_t *gain = json_object_get(root, \"volume\");\n\t\t\tjson_t *spatial = json_object_get(root, \"spatial_position\");\n\t\t\tjson_t *bitrate = json_object_get(root, \"bitrate\");\n\t\t\tjson_t *quality = json_object_get(root, \"quality\");\n\t\t\tjson_t *exploss = json_object_get(root, \"expected_loss\");\n\t\t\tjson_t *denoise = json_object_get(root, \"denoise\");\n\t\t\tint volume = gain ? json_integer_value(gain) : 100;\n\t\t\tint spatial_position = spatial ? json_integer_value(spatial) : 50;\n\t\t\tint32_t opus_bitrate = audiobridge->default_bitrate;\n\t\t\tif(bitrate) {\n\t\t\t\topus_bitrate = json_integer_value(bitrate);\n\t\t\t\tif(opus_bitrate < 500 || opus_bitrate > 512000) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid bitrate %\"SCNi32\", falling back to default/auto\\n\", opus_bitrate);\n\t\t\t\t\topus_bitrate = audiobridge->default_bitrate;\n\t\t\t\t}\n\t\t\t}\n\t\t\tint complexity = quality ? json_integer_value(quality) : DEFAULT_COMPLEXITY;\n\t\t\tif(complexity < 1 || complexity > 10) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (quality should be a positive integer between 1 and 10)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (quality should be a positive integer between 1 and 10)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tint expected_loss = exploss ? json_integer_value(exploss) : audiobridge->default_expectedloss;\n\t\t\tif(expected_loss > 20) {\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (expected_loss should be a positive integer between 0 and 20)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (expected_loss should be a positive integer between 0 and 20)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tguint64 user_id = 0;\n\t\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\t\tgboolean user_id_allocated = FALSE;\n\t\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t\tif(id) {\n\t\t\t\tif(!string_ids) {\n\t\t\t\t\tuser_id = json_integer_value(id);\n\t\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\t\tuser_id_str = user_id_num;\n\t\t\t\t} else {\n\t\t\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t\t\t}\n\t\t\t\tif(g_hash_table_lookup(audiobridge->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id) != NULL) {\n\t\t\t\t\t/* User ID already taken */\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_ID_EXISTS;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"User ID %s already exists\\n\", user_id_str);\n\t\t\t\t\tg_snprintf(error_cause, 512, \"User ID %s already exists\", user_id_str);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!string_ids) {\n\t\t\t\tif(user_id == 0) {\n\t\t\t\t\t/* Generate a random ID */\n\t\t\t\t\twhile(user_id == 0) {\n\t\t\t\t\t\tuser_id = janus_random_uint64();\n\t\t\t\t\t\tif(g_hash_table_lookup(audiobridge->participants, &user_id) != NULL) {\n\t\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\t\tuser_id = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\t\tuser_id_str = user_id_num;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID in new room %\"SCNu64\": %\"SCNu64\"\\n\", room_id, user_id);\n\t\t\t} else {\n\t\t\t\tif(user_id_str == NULL) {\n\t\t\t\t\t/* Generate a random ID */\n\t\t\t\t\twhile(user_id_str == NULL) {\n\t\t\t\t\t\tuser_id_str = janus_random_uuid();\n\t\t\t\t\t\tif(g_hash_table_lookup(audiobridge->participants, user_id_str) != NULL) {\n\t\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\t\tg_clear_pointer(&user_id_str, g_free);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tuser_id_allocated = TRUE;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID in new room %s: %s\\n\", room_id_str, user_id_str);\n\t\t\t}\n\t\t\tparticipant->audio_active_packets = 0;\n\t\t\tparticipant->audio_dBov_sum = 0;\n\t\t\tparticipant->talking = FALSE;\n\t\t\t/* Is the sampling rate of the new room the same as the one in the old room, or should we update the decoder/encoder? */\n\t\t\tjanus_audiobridge_room *old_audiobridge = participant->room;\n\t\t\t/* Leave the old room first... */\n\t\t\tjanus_refcount_increase(&participant->ref);\n\t\t\tjanus_mutex_lock(&old_audiobridge->mutex);\n\t\t\tg_hash_table_remove(old_audiobridge->participants,\n\t\t\t\tstring_ids ? (gpointer)participant->user_id_str : (gpointer)&participant->user_id);\n\t\t\tif(old_audiobridge->sampling_rate != audiobridge->sampling_rate ||\n\t\t\t\t\told_audiobridge->spatial_audio != audiobridge->spatial_audio) {\n\t\t\t\t/* Create a new one that takes into account the sampling rate we want now */\n\t\t\t\tint error = 0;\n\t\t\t\tOpusEncoder *new_encoder = opus_encoder_create(audiobridge->sampling_rate,\n\t\t\t\t\taudiobridge->spatial_audio ? 2 : 1, OPUS_APPLICATION_VOIP, &error);\n\t\t\t\tif(error != OPUS_OK) {\n\t\t\t\t\tif(user_id_allocated)\n\t\t\t\t\t\tg_free(user_id_str);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tif(new_encoder)\n\t\t\t\t\t\topus_encoder_destroy(new_encoder);\n\t\t\t\t\tnew_encoder = NULL;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating Opus encoder\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error creating Opus encoder\");\n\t\t\t\t\t/* Join the old room again... */\n\t\t\t\t\tg_hash_table_insert(audiobridge->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)g_strdup(participant->user_id_str) : (gpointer)janus_uint64_dup(participant->user_id),\n\t\t\t\t\t\tparticipant);\n\t\t\t\t\tjanus_mutex_unlock(&old_audiobridge->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(audiobridge->sampling_rate == 8000) {\n\t\t\t\t\topus_encoder_ctl(new_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_NARROWBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 12000) {\n\t\t\t\t\topus_encoder_ctl(new_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_MEDIUMBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 16000) {\n\t\t\t\t\topus_encoder_ctl(new_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 24000) {\n\t\t\t\t\topus_encoder_ctl(new_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_SUPERWIDEBAND));\n\t\t\t\t} else if(audiobridge->sampling_rate == 48000) {\n\t\t\t\t\topus_encoder_ctl(new_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND));\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported sampling rate %d, setting 16kHz\\n\", audiobridge->sampling_rate);\n\t\t\t\t\taudiobridge->sampling_rate = 16000;\n\t\t\t\t\topus_encoder_ctl(new_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t\t\t\t}\n\t\t\t\topus_encoder_ctl(new_encoder, OPUS_SET_INBAND_FEC(participant->fec));\n\t\t\t\t/* Opus decoder */\n\t\t\t\terror = 0;\n\t\t\t\tOpusDecoder *new_decoder = opus_decoder_create(audiobridge->sampling_rate,\n\t\t\t\t\taudiobridge->spatial_audio ? 2 : 1, &error);\n\t\t\t\tif(error != OPUS_OK) {\n\t\t\t\t\tif(user_id_allocated)\n\t\t\t\t\t\tg_free(user_id_str);\n\t\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t\t\tif(new_encoder)\n\t\t\t\t\t\topus_encoder_destroy(new_encoder);\n\t\t\t\t\tnew_encoder = NULL;\n\t\t\t\t\tif(new_decoder)\n\t\t\t\t\t\topus_decoder_destroy(new_decoder);\n\t\t\t\t\tnew_decoder = NULL;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating Opus decoder\\n\");\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error creating Opus decoder\");\n\t\t\t\t\t/* Join the old room again... */\n\t\t\t\t\tg_hash_table_insert(audiobridge->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)g_strdup(participant->user_id_str) : (gpointer)janus_uint64_dup(participant->user_id),\n\t\t\t\t\t\tparticipant);\n\t\t\t\t\tjanus_mutex_unlock(&old_audiobridge->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tparticipant->reset = FALSE;\n\t\t\t\t/* Destroy the previous encoder/decoder and update the references */\n\t\t\t\twhile(!g_atomic_int_compare_and_exchange(&participant->encoding, 0, 1))\n\t\t\t\t\tg_usleep(5000);\n\t\t\t\tif(participant->encoder)\n\t\t\t\t\topus_encoder_destroy(participant->encoder);\n\t\t\t\tparticipant->sampling_rate = audiobridge->sampling_rate;\n\t\t\t\tparticipant->encoder = new_encoder;\n\t\t\t\tg_atomic_int_set(&participant->encoding, 0);\n\t\t\t\twhile(!g_atomic_int_compare_and_exchange(&participant->decoding, 0, 1))\n\t\t\t\t\tg_usleep(5000);\n\t\t\t\tif(participant->decoder)\n\t\t\t\t\topus_decoder_destroy(participant->decoder);\n\t\t\t\tparticipant->decoder = new_decoder;\n\t\t\t\tg_atomic_int_set(&participant->decoding, 0);\n\t\t\t}\n\t\t\tif(quality)\n\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_COMPLEXITY(participant->opus_complexity));\n\t\t\t/* Everything looks fine, start by telling the folks in the old room this participant is going away */\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\tstring_ids ? json_string(old_audiobridge->room_id_str) : json_integer(old_audiobridge->room_id));\n\t\t\tjson_object_set_new(event, \"leaving\",\n\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, old_audiobridge->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\tif(p == participant || g_atomic_int_get(&p->paused_events)) {\n\t\t\t\t\tcontinue;\t/* Skip the new participant itself */\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t}\n\t\t\tjson_decref(event);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"left\"));\n\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\tstring_ids ? json_string(old_audiobridge->room_id_str) : json_integer(old_audiobridge->room_id));\n\t\t\t\tjson_object_set_new(info, \"id\",\n\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session->handle, info);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&old_audiobridge->mutex);\n\t\t\t/* Stop recording, if we were (since this is a new room, a new recording would be required, so a new configure) */\n\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\tjanus_audiobridge_recorder_close(participant);\n\t\t\tparticipant->mjr_active = FALSE;\n\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\tjanus_refcount_decrease(&old_audiobridge->ref);\n\t\t\t/* Done, join the new one */\n\t\t\tparticipant->user_id = user_id;\n\t\t\tg_free(participant->user_id_str);\n\t\t\tparticipant->user_id_str = user_id_str ? g_strdup(user_id_str) : NULL;\n\t\t\tparticipant->group = group;\n\t\t\tparticipant->admin = admin;\n\t\t\tg_free(participant->display);\n\t\t\tparticipant->display = display_text ? g_strdup(display_text) : NULL;\n\t\t\tparticipant->room = audiobridge;\n\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\tparticipant->muted = muted ? json_is_true(muted) : FALSE;\t/* When switching to a new room, you're unmuted by default */\n\t\t\tjanus_audiobridge_participant_clear_jitter_buffer(participant);\n\t\t\tjanus_audiobridge_participant_clear_inbuf(participant);\n\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\tif(suspended && json_is_true(suspended)) {\n\t\t\t\tjanus_mutex_lock(&participant->suspend_cond_mutex);\n\t\t\t\tg_atomic_int_set(&participant->suspended, 1);\n\t\t\t\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\t\t\t\tjson_t *pauseevs = json_object_get(root, \"pause_events\");\n\t\t\t\tif(pauseevs && json_is_true(pauseevs))\n\t\t\t\t\tg_atomic_int_set(&participant->paused_events, 1);\n\t\t\t}\n\t\t\tparticipant->audio_active_packets = 0;\n\t\t\tparticipant->audio_dBov_sum = 0;\n\t\t\tparticipant->talking = FALSE;\n\t\t\tparticipant->volume_gain = volume;\n\t\t\tparticipant->stereo = audiobridge->spatial_audio;\n\t\t\tparticipant->spatial_position = spatial_position;\n\t\t\tif(participant->spatial_position < 0)\n\t\t\t\tparticipant->spatial_position = 0;\n\t\t\telse if(participant->spatial_position > 100)\n\t\t\t\tparticipant->spatial_position = 100;\n\t\t\tparticipant->opus_bitrate = opus_bitrate;\n\t\t\tif(participant->encoder)\n\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_BITRATE(participant->opus_bitrate ? participant->opus_bitrate : OPUS_AUTO));\n\t\t\tif(quality) {\n\t\t\t\tparticipant->opus_complexity = complexity;\n\t\t\t\tif(participant->encoder)\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_COMPLEXITY(participant->opus_complexity));\n\t\t\t}\n\t\t\tif(exploss) {\n\t\t\t\tparticipant->expected_loss = expected_loss;\n\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_PACKET_LOSS_PERC(participant->expected_loss));\n\t\t\t}\n#ifdef HAVE_RNNOISE\n\t\t\t/* Check if a denoiser is needed now */\n\t\t\tparticipant->denoise = denoise ? json_is_true(denoise) : audiobridge->denoise;\n#else\n\t\t\tif(denoise && json_is_true(denoise)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"RNNoise unavailable, denoising not supported\\n\");\n\t\t\t}\n#endif\n\t\t\tg_hash_table_insert(audiobridge->participants,\n\t\t\t\tstring_ids ? (gpointer)g_strdup(participant->user_id_str) : (gpointer)janus_uint64_dup(participant->user_id),\n\t\t\t\tparticipant);\n\t\t\t/* Notify the other participants */\n\t\t\tjson_t *newuser = json_object();\n\t\t\tjson_object_set_new(newuser, \"audiobridge\", json_string(\"joined\"));\n\t\t\tjson_object_set_new(newuser, \"room\",\n\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\tjson_t *newuserlist = json_array();\n\t\t\tjson_t *pl = json_object();\n\t\t\tjson_object_set_new(pl, \"id\",\n\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\tif(participant->display)\n\t\t\t\tjson_object_set_new(pl, \"display\", json_string(participant->display));\n\t\t\tjson_object_set_new(pl, \"setup\", g_atomic_int_get(&participant->session->started) ? json_true() : json_false());\n\t\t\tjson_object_set_new(pl, \"muted\", participant->muted ? json_true() : json_false());\n\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(participant->spatial_position));\n\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\t\tjson_array_append_new(newuserlist, pl);\n\t\t\tjson_object_set_new(newuser, \"participants\", newuserlist);\n\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\tif(p == participant || g_atomic_int_get(&p->paused_events)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, newuser, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t}\n\t\t\tjson_decref(newuser);\n\t\t\t/* Return a list of all available participants for the new participant now */\n\t\t\tjson_t *list = json_array();\n\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\tif(p == participant) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjson_t *pl = json_object();\n\t\t\t\tjson_object_set_new(pl, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\t\tif(p->display)\n\t\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\t\tjson_object_set_new(pl, \"setup\", g_atomic_int_get(&p->session->started) ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(pl, \"muted\", p->muted ? json_true() : json_false());\n\t\t\t\tif(p->extmap_id > 0)\n\t\t\t\t\tjson_object_set_new(pl, \"talking\", p->talking ? json_true() : json_false());\n\t\t\t\tif(audiobridge->spatial_audio)\n\t\t\t\t\tjson_object_set_new(pl, \"spatial_position\", json_integer(p->spatial_position));\n\t\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\t\tjson_object_set_new(pl, \"suspended\", json_true());\n\t\t\t\tjson_array_append_new(list, pl);\n\t\t\t}\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"roomchanged\"));\n\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\tjson_object_set_new(event, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\tjson_object_set_new(event, \"participants\", list);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"joined\"));\n\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjson_object_set_new(info, \"id\",\n\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\t\t\tjson_object_set_new(info, \"muted\", participant->muted ? json_true() : json_false());\n\t\t\t\tif(participant->stereo)\n\t\t\t\t\tjson_object_set_new(info, \"spatial_position\", json_integer(participant->spatial_position));\n\t\t\t\tif(g_atomic_int_get(&participant->suspended))\n\t\t\t\t\tjson_object_set_new(info, \"suspended\", json_true());\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session->handle, info);\n\t\t\t}\n\t\t\tif(user_id_allocated)\n\t\t\t\tg_free(user_id_str);\n\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t} else if(!strcasecmp(request_text, \"hangup\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* Get rid of an ongoing session */\n\t\t\tgateway->close_pc(session->handle);\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"hangingup\"));\n\t\t} else if(!strcasecmp(request_text, \"leave\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* This participant is leaving */\n\t\t\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\t\t\tif(participant == NULL || participant->room == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't leave (not in a room)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NOT_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't leave (not in a room)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Tell everybody */\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\tjanus_audiobridge_room *audiobridge = participant->room;\n\t\t\tgboolean removed = FALSE;\n\t\t\tif(audiobridge != NULL) {\n\t\t\t\tjanus_refcount_increase(&audiobridge->ref);\n\t\t\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjson_object_set_new(event, \"leaving\",\n\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->participants);\n\t\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\tjanus_audiobridge_participant *p = value;\n\t\t\t\t\tif(p == participant || g_atomic_int_get(&p->paused_events)) {\n\t\t\t\t\t\tcontinue;\t/* Skip the new participant itself */\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, event, NULL);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\t}\n\t\t\t\tjson_decref(event);\n\t\t\t\t/* Actually leave the room... */\n\t\t\t\tremoved = g_hash_table_remove(audiobridge->participants,\n\t\t\t\t\tstring_ids ? (gpointer)participant->user_id_str : (gpointer)&participant->user_id);\n\t\t\t\tparticipant->room = NULL;\n\t\t\t}\n\t\t\t/* Get rid of queued packets */\n\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\tg_atomic_int_set(&participant->active, 0);\n\t\t\tjanus_audiobridge_participant_clear_jitter_buffer(participant);\n\t\t\tjanus_audiobridge_participant_clear_inbuf(participant);\n\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\tjanus_audiobridge_participant_clear_outbuf(participant);\n\t\t\t/* Stop recording, if we were */\n\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\tjanus_audiobridge_recorder_close(participant);\n\t\t\tparticipant->mjr_active = FALSE;\n\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"left\"));\n\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjson_object_set_new(info, \"id\",\n\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* Done */\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"left\"));\n\t\t\tif(audiobridge != NULL) {\n\t\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t}\n\t\t\tjson_object_set_new(event, \"id\",\n\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tif(removed) {\n\t\t\t\t/* Only decrease the counter if we were still there */\n\t\t\t\tjanus_refcount_decrease(&audiobridge->ref);\n\t\t\t}\n\t\t} else {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"Unknown request '%s'\\n\", request_text);\n\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Prepare JSON event */\n\t\tJANUS_LOG(LOG_VERB, \"Preparing JSON event as a reply\\n\");\n\t\t/* Any SDP to handle? */\n\t\tif(!msg_sdp && !generate_offer) {\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_audiobridge_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t} else {\n\t\t\tif(msg_sdp) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well:\\n%s\\n\", msg_sdp_type, msg_sdp);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation: generating offer\\n\");\n\t\t\t}\n\t\t\t/* Prepare an SDP offer or answer */\n\t\t\tif(msg_sdp && json_is_true(json_object_get(msg->jsep, \"e2ee\"))) {\n\t\t\t\t/* Media is encrypted, but we need unencrypted media frames to decode and mix */\n\t\t\t\tjson_decref(event);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Media encryption unsupported by this plugin\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Media encryption unsupported by this plugin\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* We answer by default, unless the user asked the plugin for an offer */\n\t\t\tif(msg_sdp && got_offer && session->plugin_offer) {\n\t\t\t\tjson_decref(event);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Received an offer on a plugin-offered session\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Received an offer on a plugin-offered session\");\n\t\t\t\tgoto error;\n\t\t\t} else if(msg_sdp && got_answer && !session->plugin_offer) {\n\t\t\t\tjson_decref(event);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Received an answer when we didn't send an offer\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Received an answer when we didn't send an offer\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tconst char *type = session->plugin_offer ? \"offer\" : \"answer\";\n\t\t\tchar error_str[512];\n\t\t\tjanus_sdp *sdp = NULL;\n\t\t\tif(msg_sdp != NULL) {\n\t\t\t\tsdp = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str));\n\t\t\t\tif(sdp == NULL) {\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing %s: %s\\n\", msg_sdp, error_str);\n\t\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing %s: %s\", msg_sdp, error_str);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(got_offer) {\n\t\t\t\tif(sdp_update) {\n\t\t\t\t\t/* Renegotiation */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Request to update existing connection\\n\");\n\t\t\t\t\tsession->sdp_version++;\t\t/* This needs to be increased when it changes */\n\t\t\t\t} else {\n\t\t\t\t\t/* New PeerConnection */\n\t\t\t\t\tsession->sdp_version = 1;\t/* This needs to be increased when it changes */\n\t\t\t\t\tsession->sdp_sessid = janus_get_real_time();\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* What is the Opus payload type? */\n\t\t\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;\n\t\t\tif(sdp != NULL) {\n\t\t\t\tparticipant->opus_pt = janus_sdp_get_codec_pt(sdp, -1, \"opus\");\n\t\t\t\tif(participant->opus_pt > 0 && strstr(msg_sdp, \"useinbandfec=1\")){\n\t\t\t\t\t/* Opus codec, inband FEC (from Janus to user) set */\n\t\t\t\t\tparticipant->fec = TRUE;\n\t\t\t\t\topus_encoder_ctl(participant->encoder, OPUS_SET_INBAND_FEC(participant->fec));\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Opus payload type is %d, outgoing FEC %s\\n\", participant->opus_pt, participant->fec ? \"enabled\" : \"disabled\");\n\t\t\t}\n\t\t\t/* Check if the audio level extension was offered */\n\t\t\tint extmap_id = generate_offer ? 2 : -1;\n\t\t\tif(sdp != NULL) {\n\t\t\t\tGList *temp = sdp->m_lines;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\t\tGList *ma = m->attributes;\n\t\t\t\t\t\twhile(ma) {\n\t\t\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\t\t\t\tif(a->value) {\n\t\t\t\t\t\t\t\tif(strstr(a->value, JANUS_RTP_EXTMAP_AUDIO_LEVEL)) {\n\t\t\t\t\t\t\t\t\textmap_id = atoi(a->value);\n\t\t\t\t\t\t\t\t\tif(extmap_id < 0)\n\t\t\t\t\t\t\t\t\t\textmap_id = 0;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tma = ma->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* If we're just processing an answer, we're done */\n\t\t\tif(got_answer) {\n\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\tint res = gateway->push_event(msg->handle, &janus_audiobridge_plugin, msg->transaction, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\tjson_decref(event);\n\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\tif(msg)\n\t\t\t\t\tjanus_audiobridge_message_free(msg);\n\t\t\t\tmsg = NULL;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(participant == NULL || participant->room == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't handle SDP (not in a room)\\n\");\n\t\t\t\terror_code = JANUS_AUDIOBRIDGE_ERROR_NOT_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't handle SDP (not in a room)\");\n\t\t\t\tif(sdp)\n\t\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* We use a custom session name in the SDP */\n\t\t\tchar s_name[100];\n\t\t\tg_snprintf(s_name, sizeof(s_name), \"AudioBridge %s\", participant->room->room_id_str);\n\t\t\t/* Prepare a fmtp string too */\n\t\t\tchar fmtp[100];\n\t\t\tg_snprintf(fmtp, sizeof(fmtp), \"%d maxplaybackrate=%\"SCNu32\"; stereo=%d; sprop-stereo=%d; useinbandfec=%d\\r\\n\",\n\t\t\t\tparticipant->opus_pt, participant->room->sampling_rate,\n\t\t\t\tparticipant->stereo ? 1 : 0, participant->stereo ? 1 : 0, participant->fec ? 1 : 0);\n\t\t\t/* If we got an offer, we need to answer */\n\t\t\tjanus_sdp *offer = NULL, *answer = NULL;\n\t\t\tif(got_offer) {\n\t\t\t\tanswer = janus_sdp_generate_answer(sdp);\n\t\t\t\t/* Only accept the first audio line, and reject everything else if offered */\n\t\t\t\tGList *temp = sdp->m_lines;\n\t\t\t\tgboolean accepted = FALSE;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO && !accepted) {\n\t\t\t\t\t\taccepted = TRUE;\n\t\t\t\t\t\tjanus_sdp_generate_answer_mline(sdp, answer, m,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(participant->codec),\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_AUDIO_LEVEL,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\t/* Replace the session name */\n\t\t\t\tg_free(answer->s_name);\n\t\t\t\tanswer->s_name = g_strdup(s_name);\n\t\t\t\t/* Add an fmtp attribute if this is Opus */\n\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\tjanus_sdp_mline *m = janus_sdp_mline_find(answer, JANUS_SDP_AUDIO);\n\t\t\t\t\tif(m != NULL) {\n\t\t\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"fmtp\", \"%s\", fmtp);\n\t\t\t\t\t\tjanus_sdp_attribute_add_to_mline(m, a);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No audio m-line found in SDP answer\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Let's overwrite a couple o= fields, in case this is a renegotiation */\n\t\t\t\tanswer->o_sessid = session->sdp_sessid;\n\t\t\t\tanswer->o_version = session->sdp_version;\n\t\t\t} else if(generate_offer) {\n\t\t\t\t/* We need to generate an offer ourselves */\n\t\t\t\tint pt = 100;\n\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_PCMU)\n\t\t\t\t\tpt = 0;\n\t\t\t\telse if(participant->codec == JANUS_AUDIOCODEC_PCMA)\n\t\t\t\t\tpt = 8;\n\t\t\t\toffer = janus_sdp_generate_offer(\n\t\t\t\t\ts_name, \"1.1.1.1\",\n\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(participant->codec),\n\t\t\t\t\t\tJANUS_SDP_OA_PT, pt,\n\t\t\t\t\t\tJANUS_SDP_OA_FMTP, (participant->codec == JANUS_AUDIOCODEC_OPUS ? fmtp : NULL),\n\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDRECV,\n\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_MID, 1,\n\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_AUDIO_LEVEL, extmap_id,\n\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t/* Let's overwrite a couple o= fields, in case this is a renegotiation */\n\t\t\t\tif(session->sdp_version == 1) {\n\t\t\t\t\tsession->sdp_sessid = offer->o_sessid;\n\t\t\t\t} else {\n\t\t\t\t\toffer->o_sessid = session->sdp_sessid;\n\t\t\t\t\toffer->o_version = session->sdp_version;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Was the audio level extension negotiated? */\n\t\t\tparticipant->extmap_id = 0;\n\t\t\tparticipant->dBov_level = 0;\n\t\t\tif(extmap_id > -1 && participant->room && participant->room->audiolevel_ext) {\n\t\t\t\t/* Add an extmap attribute too */\n\t\t\t\tparticipant->extmap_id = extmap_id;\n\t\t\t\t/* If there's a recording, add the extension there */\n\t\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\t\tif(participant->arc != NULL)\n\t\t\t\t\tjanus_recorder_add_extmap(participant->arc, participant->extmap_id, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t}\n\t\t\t/* Prepare the response */\n\t\t\tchar *new_sdp = janus_sdp_write(answer ? answer : offer);\n\t\t\tjanus_sdp_destroy(sdp);\n\t\t\tjanus_sdp_destroy(answer ? answer : offer);\n\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", type, \"sdp\", new_sdp);\n\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\tint res = gateway->push_event(msg->handle, &janus_audiobridge_plugin, msg->transaction, event, jsep);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(jsep);\n\t\t\tg_free(new_sdp);\n\t\t\tif(res != JANUS_OK) {\n\t\t\t\t/* TODO Failed to negotiate? We should remove this participant */\n\t\t\t} else {\n\t\t\t\t/* We'll notify all other participants when the PeerConnection has been established */\n\t\t\t}\n\t\t}\n\t\tif(msg)\n\t\t\tjanus_audiobridge_message_free(msg);\n\t\tmsg = NULL;\n\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_audiobridge_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_audiobridge_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving AudioBridge handler thread\\n\");\n\treturn NULL;\n}\nstatic void janus_audiobridge_rec_add_wav_header(janus_audiobridge_room *audiobridge) {\n\t/* Do we need to record the mix? */\n\tchar filename[255];\n\tgint64 now = janus_get_real_time();\n\taudiobridge->rec_start_time = now;\n\tif(audiobridge->record_file) {\n\t\tg_snprintf(filename, 255, \"%s%s%s%s%s\",\n\t\t\taudiobridge->record_dir ? audiobridge->record_dir : \"\",\n\t\t\taudiobridge->record_dir ? \"/\" : \"\",\n\t\t\taudiobridge->record_file,\n\t\t\trec_tempext ? \".\" : \"\", rec_tempext ? rec_tempext : \"\");\n\t} else {\n\t\tg_snprintf(filename, 255, \"%s%sjanus-audioroom-%s-%\"SCNi64\".wav%s%s\",\n\t\t\taudiobridge->record_dir ? audiobridge->record_dir : \"\",\n\t\t\taudiobridge->record_dir ? \"/\" : \"\",\n\t\t\taudiobridge->room_id_str, now,\n\t\t\trec_tempext ? \".\" : \"\", rec_tempext ? rec_tempext : \"\");\n\t}\n\taudiobridge->recording = fopen(filename, \"wb\");\n\tif(audiobridge->recording == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Recording requested, but could NOT open file %s for writing, giving up...\\n\", filename);\n\t\tg_atomic_int_set(&audiobridge->record, 0);\n\t\tg_atomic_int_set(&audiobridge->wav_header_added, 0);\n\t\tg_free(audiobridge->record_file);\n\t\taudiobridge->record_file = NULL;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Recording requested, opened file %s for writing\\n\", filename);\n\t\t/* Write WAV header */\n\t\twav_header header = {\n\t\t\t{'R', 'I', 'F', 'F'},\n\t\t\t0,\n\t\t\t{'W', 'A', 'V', 'E'},\n\t\t\t{'f', 'm', 't', ' '},\n\t\t\t16,\n\t\t\t1,\n\t\t\taudiobridge->spatial_audio ? 2 : 1,\n\t\t\taudiobridge->sampling_rate,\n\t\t\taudiobridge->sampling_rate * 2 * (audiobridge->spatial_audio ? 2 : 1),\n\t\t\t2,\n\t\t\t16,\n\t\t\t{'d', 'a', 't', 'a'},\n\t\t\t0\n\t\t};\n\t\tif(fwrite(&header, 1, sizeof(header), audiobridge->recording) != sizeof(header)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error writing WAV header...\\n\");\n\t\t}\n\t\tfflush(audiobridge->recording);\n\t\taudiobridge->record_lastupdate = janus_get_monotonic_time();\n\t\tg_atomic_int_set(&audiobridge->wav_header_added, 1);\n\t}\n}\nstatic void janus_audiobridge_update_wav_header(janus_audiobridge_room *audiobridge) {\n\t/* Update the length in the header */\n\tif(audiobridge->recording == NULL)\n\t\treturn;\n\tfseek(audiobridge->recording, 0, SEEK_END);\n\tlong int size = ftell(audiobridge->recording);\n\tif(size >= 8) {\n\t\tsize -= 8;\n\t\tfseek(audiobridge->recording, 4, SEEK_SET);\n\t\tfwrite(&size, sizeof(uint32_t), 1, audiobridge->recording);\n\t\tsize += 8;\n\t\tfseek(audiobridge->recording, 40, SEEK_SET);\n\t\tfwrite(&size, sizeof(uint32_t), 1, audiobridge->recording);\n\t\tfflush(audiobridge->recording);\n\t}\n\tfclose(audiobridge->recording);\n\taudiobridge->recording = NULL;\n\tg_atomic_int_set(&audiobridge->wav_header_added, 0);\n\n\tchar filename[255];\n\tif(audiobridge->record_file) {\n\t\tg_snprintf(filename, 255, \"%s%s%s\",\n\t\t\taudiobridge->record_dir ? audiobridge->record_dir : \"\",\n\t\t\taudiobridge->record_dir ? \"/\" : \"\",\n\t\t\taudiobridge->record_file);\n\t} else {\n\t\tg_snprintf(filename, 255, \"%s%sjanus-audioroom-%s-%\"SCNi64\".wav\",\n\t\t\taudiobridge->record_dir ? audiobridge->record_dir : \"\",\n\t\t\taudiobridge->record_dir ? \"/\" : \"\",\n\t\t\taudiobridge->room_id_str, audiobridge->rec_start_time);\n\t}\n\tif(rec_tempext) {\n\t\t/* We need to rename the file, to remove the temporary extension */\n\t\tchar extfilename[255];\n\t\tif(audiobridge->record_file) {\n\t\t\tg_snprintf(extfilename, 255, \"%s%s%s.%s\",\n\t\t\t\taudiobridge->record_dir ? audiobridge->record_dir : \"\",\n\t\t\t\taudiobridge->record_dir ? \"/\" : \"\",\n\t\t\t\taudiobridge->record_file, rec_tempext);\n\t\t} else {\n\t\t\tg_snprintf(extfilename, 255, \"%s%sjanus-audioroom-%s-%\"SCNi64\".wav.%s\",\n\t\t\t\taudiobridge->record_dir ? audiobridge->record_dir : \"\",\n\t\t\t\taudiobridge->record_dir ? \"/\" : \"\",\n\t\t\t\taudiobridge->room_id_str,\n\t\t\t\taudiobridge->rec_start_time, rec_tempext);\n\t\t}\n\t\tif(rename(extfilename, filename) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error renaming %s to %s...\\n\", extfilename, filename);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_INFO, \"Recording renamed: %s\\n\", filename);\n\t\t}\n\t}\n\t/* Also notify event handlers */\n\tif(notify_events && gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(\"recordingdone\"));\n\t\tjson_object_set_new(info, \"room\",\n\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\tjson_object_set_new(info, \"record_file\", json_string(filename));\n\t\tgateway->notify_event(&janus_audiobridge_plugin, NULL, info);\n\t}\n}\n\n/* Thread to mix the contributions from all participants */\nstatic void *janus_audiobridge_mixer_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Audio bridge thread starting...\\n\");\n\tjanus_audiobridge_room *audiobridge = (janus_audiobridge_room *)data;\n\tif(!audiobridge) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid room!\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Thread is for mixing room %s (%s) at rate %\"SCNu32\"...\\n\",\n\t\taudiobridge->room_id_str, audiobridge->room_name, audiobridge->sampling_rate);\n\n\t/* Buffer (we allocate assuming 48kHz, although we'll likely use less than that) */\n\tint samples = audiobridge->sampling_rate/50;\n\tif(audiobridge->spatial_audio)\n\t\tsamples = samples*2;\n\topus_int32 buffer[audiobridge->spatial_audio ? OPUS_SAMPLES*2 : OPUS_SAMPLES],\n\t\tsumBuffer[audiobridge->spatial_audio ? OPUS_SAMPLES*2 : OPUS_SAMPLES];\n\topus_int16 outBuffer[audiobridge->spatial_audio ? OPUS_SAMPLES*2 : OPUS_SAMPLES],\n\t\tresampled[audiobridge->spatial_audio ? OPUS_SAMPLES*2 : OPUS_SAMPLES], *curBuffer = NULL;\n\tmemset(buffer, 0, OPUS_SAMPLES*(audiobridge->spatial_audio ? 8 : 4));\n\tmemset(sumBuffer, 0, OPUS_SAMPLES*(audiobridge->spatial_audio ? 8 : 4));\n\tmemset(outBuffer, 0, OPUS_SAMPLES*(audiobridge->spatial_audio ? 4 : 2));\n\tmemset(resampled, 0, OPUS_SAMPLES*(audiobridge->spatial_audio ? 4 : 2));\n\n\t/* In case forwarding groups are enabled, we need additional buffers */\n\tuint groups_num = audiobridge->groups ? g_hash_table_size(audiobridge->groups) : 0, index = 0;\n\topus_int32 *groupBuffers = NULL;\n\tuint32_t groupBufferSize = 0, groupBuffersSize = 0;\n\tOpusEncoder **groupEncoders = NULL;\n\tif(groups_num > 0) {\n\t\t/* Create buffers */\n\t\tgroupBufferSize = (audiobridge->spatial_audio ? OPUS_SAMPLES*2 : OPUS_SAMPLES) * sizeof(opus_int32);\n\t\tgroupBuffersSize = groups_num * groupBufferSize;\n\t\tgroupBuffers = g_malloc(groupBuffersSize);\n\t\tgroupEncoders = g_malloc(groups_num * sizeof(OpusEncoder *));\n\t\t/* Create separate encoders */\n\t\tfor(index=0; index<groups_num; index++) {\n\t\t\tint error = 0;\n\t\t\tOpusEncoder *rtp_encoder = opus_encoder_create(audiobridge->sampling_rate,\n\t\t\t\taudiobridge->spatial_audio ? 2 : 1, OPUS_APPLICATION_VOIP, &error);\n\t\t\tif(audiobridge->sampling_rate == 8000) {\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_NARROWBAND));\n\t\t\t} else if(audiobridge->sampling_rate == 12000) {\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_MEDIUMBAND));\n\t\t\t} else if(audiobridge->sampling_rate == 16000) {\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t\t\t} else if(audiobridge->sampling_rate == 24000) {\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_SUPERWIDEBAND));\n\t\t\t} else if(audiobridge->sampling_rate == 48000) {\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND));\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported sampling rate %d, setting 16kHz\\n\", audiobridge->sampling_rate);\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));\n\t\t\t}\n\t\t\t/* Check if we need outgoing FEC */\n\t\t\tif(audiobridge->default_expectedloss > 0) {\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_INBAND_FEC(TRUE));\n\t\t\t\topus_encoder_ctl(rtp_encoder, OPUS_SET_PACKET_LOSS_PERC(audiobridge->default_expectedloss));\n\t\t\t}\n\t\t\tgroupEncoders[index] = rtp_encoder;\n\t\t}\n\t}\n\n\t/* Base RTP packets, in case there are forwarders involved */\n\tgboolean have_opus[JANUS_AUDIOBRIDGE_MAX_GROUPS+1],\n\t\thave_alaw[JANUS_AUDIOBRIDGE_MAX_GROUPS+1],\n\t\thave_ulaw[JANUS_AUDIOBRIDGE_MAX_GROUPS+1];\n\tunsigned char *rtpbuffer = g_malloc0(1500 * (groups_num+1));\n\tjanus_rtp_header *rtph = NULL;\n\t/* In case we need G.711 forwarders */\n\tuint8_t *rtpalaw = g_malloc0((12+G711_SAMPLES) * (groups_num+1)),\n\t\t\t*rtpulaw = g_malloc0((12+G711_SAMPLES) * (groups_num+1));\n\n\t/* Timer */\n\tstruct timeval now, before;\n\tgettimeofday(&before, NULL);\n\tnow.tv_sec = before.tv_sec;\n\tnow.tv_usec = before.tv_usec;\n\ttime_t passed, d_s, d_us;\n\n\t/* RTP */\n\tguint16 seq = 0;\n\tguint32 ts = 0;\n\n\tg_atomic_int_set(&audiobridge->wav_header_added, 0);\n\t/* Loop */\n\tint i=0;\n\tint count = 0, rf_count = 0, pf_count = 0, prev_count = 0;\n\tint lgain = 0, rgain = 0, diff = 0;\n\twhile(!g_atomic_int_get(&stopping) && !g_atomic_int_get(&audiobridge->destroyed)) {\n\t\t/* See if it's time to prepare a frame */\n\t\tgettimeofday(&now, NULL);\n\t\td_s = now.tv_sec - before.tv_sec;\n\t\td_us = now.tv_usec - before.tv_usec;\n\t\tif(d_us < 0) {\n\t\t\td_us += 1000000;\n\t\t\t--d_s;\n\t\t}\n\t\tpassed = d_s*1000000 + d_us;\n\t\tif(passed < 15000) {\t/* Let's wait about 15ms at max */\n\t\t\tg_usleep(5000);\n\t\t\tcontinue;\n\t\t}\n\t\t/* If we're recording to a wav file, update the info */\n\t\tif(g_atomic_int_get(&audiobridge->record) && !g_atomic_int_get(&audiobridge->wav_header_added)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Adding WAV header for recording %s (%s)...\\n\", audiobridge->room_id_str, audiobridge->room_name);\n\t\t\tjanus_audiobridge_rec_add_wav_header(audiobridge);\n\t\t}\n\t\tif(!g_atomic_int_get(&audiobridge->record) && g_atomic_int_get(&audiobridge->wav_header_added)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Updating WAV header for recording %s (%s)...\\n\", audiobridge->room_id_str, audiobridge->room_name);\n\t\t\tjanus_audiobridge_update_wav_header(audiobridge);\n\t\t}\n\t\t/* Update the reference time */\n\t\tbefore.tv_usec += 20000;\n\t\tif(before.tv_usec > 1000000) {\n\t\t\tbefore.tv_sec++;\n\t\t\tbefore.tv_usec -= 1000000;\n\t\t}\n\t\t/* Do we need to mix at all? */\n\t\tjanus_mutex_lock_nodebug(&audiobridge->mutex);\n\t\tcount = g_hash_table_size(audiobridge->participants);\n\t\trf_count = g_hash_table_size(audiobridge->rtp_forwarders);\n\t\tpf_count = g_hash_table_size(audiobridge->anncs);\n\t\tif((count+rf_count+pf_count) == 0) {\n\t\t\tjanus_mutex_unlock_nodebug(&audiobridge->mutex);\n\t\t\t/* No participant and RTP forwarders, do nothing */\n\t\t\tif(prev_count > 0) {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Last user/forwarder/file just left room %s, going idle...\\n\", audiobridge->room_id_str);\n\t\t\t\tprev_count = 0;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif(prev_count == 0) {\n\t\t\tJANUS_LOG(LOG_INFO, \"First user/forwarder/file just joined room %s, waking it up...\\n\", audiobridge->room_id_str);\n\t\t}\n\t\tprev_count = count+rf_count+pf_count;\n\t\t/* Update RTP header information */\n\t\tseq++;\n\t\tts += OPUS_SAMPLES;\n\t\t/* Mix all contributions */\n\t\tGList *participants_list = g_hash_table_get_values(audiobridge->participants);\n\t\t/* Add a reference to all these participants, in case some leave while we're mixing */\n\t\tGList *ps = participants_list;\n\t\twhile(ps) {\n\t\t\tjanus_audiobridge_participant *p = (janus_audiobridge_participant *)ps->data;\n\t\t\tjanus_refcount_increase(&p->ref);\n\t\t\tps = ps->next;\n\t\t}\n\t\t/* Do the same for announcements */\n\t\tGList *anncs_list = g_hash_table_get_values(audiobridge->anncs);\n\t\tps = anncs_list;\n\t\twhile(ps) {\n\t\t\tjanus_audiobridge_participant *annc = (janus_audiobridge_participant *)ps->data;\n\t\t\tjanus_refcount_increase(&annc->ref);\n\t\t\tps = ps->next;\n\t\t}\n\t\tjanus_mutex_unlock_nodebug(&audiobridge->mutex);\n\t\tfor(i=0; i<samples; i++)\n\t\t\tbuffer[i] = 0;\n\t\tif(groups_num > 0)\n\t\t\tmemset(groupBuffers, 0, groupBuffersSize);\n\t\tps = participants_list;\n\t\twhile(ps) {\n\t\t\tjanus_audiobridge_participant *p = (janus_audiobridge_participant *)ps->data;\n\t\t\tjanus_mutex_lock(&p->qmutex);\n\t\t\tif(g_atomic_int_get(&p->destroyed) || !p->session || !g_atomic_int_get(&p->session->started) ||\n\t\t\t\t\t!g_atomic_int_get(&p->active) || p->muted || g_atomic_int_get(&p->suspended) || !p->inbuf) {\n\t\t\t\tjanus_mutex_unlock(&p->qmutex);\n\t\t\t\tps = ps->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tGList *peek = g_list_first(p->inbuf);\n\t\t\tjanus_audiobridge_rtp_relay_packet *pkt = (janus_audiobridge_rtp_relay_packet *)(peek ? peek->data : NULL);\n\t\t\tif(pkt != NULL && !pkt->silence) {\n\t\t\t\tif(p->codec != JANUS_AUDIOCODEC_OPUS && audiobridge->sampling_rate != 8000) {\n\t\t\t\t\t/* Upsample this to whatever the mixer needs */\n\t\t\t\t\tpkt->length = janus_audiobridge_resample((opus_int16 *)pkt->data, 160, 8000, resampled, audiobridge->sampling_rate);\n\t\t\t\t\tif(pkt->length == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[G.711] Error upsampling to %d, skipping audio packet\\n\", audiobridge->sampling_rate);\n\t\t\t\t\t\tjanus_mutex_unlock(&p->qmutex);\n\t\t\t\t\t\tps = ps->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tmemcpy(pkt->data, resampled, pkt->length*2);\n\t\t\t\t}\n\t\t\t\tcurBuffer = (opus_int16 *)pkt->data;\n\t\t\t\tif(groups_num == 0) {\n\t\t\t\t\t/* Add to the main mix */\n\t\t\t\t\tif(!p->stereo) {\n\t\t\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\tbuffer[i] += curBuffer[i];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tbuffer[i] += (curBuffer[i]*p->volume_gain)/100;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdiff = 50 - p->spatial_position;\n\t\t\t\t\t\tlgain = 50 + diff;\n\t\t\t\t\t\trgain = 50 - diff;\n\t\t\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\t\t\tif(i%2 == 0) {\n\t\t\t\t\t\t\t\tif(lgain == 100) {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += curBuffer[i];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += (curBuffer[i]*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += (curBuffer[i]*lgain)/100;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += (((curBuffer[i]*lgain)/100)*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif(rgain == 100) {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += curBuffer[i];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += (curBuffer[i]*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += (curBuffer[i]*rgain)/100;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tbuffer[i] += (((curBuffer[i]*rgain)/100)*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Add to the group submix */\n\t\t\t\t\tint index = p->group-1;\n\t\t\t\t\tif(!p->stereo) {\n\t\t\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += curBuffer[i];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (curBuffer[i]*p->volume_gain)/100;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdiff = 50 - p->spatial_position;\n\t\t\t\t\t\tlgain = 50 + diff;\n\t\t\t\t\t\trgain = 50 - diff;\n\t\t\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\t\t\tif(i%2 == 0) {\n\t\t\t\t\t\t\t\tif(lgain == 100) {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += curBuffer[i];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (curBuffer[i]*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (curBuffer[i]*lgain)/100;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (((curBuffer[i]*lgain)/100)*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif(rgain == 100) {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += curBuffer[i];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (curBuffer[i]*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (curBuffer[i]*rgain)/100;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (((curBuffer[i]*rgain)/100)*p->volume_gain)/100;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&p->qmutex);\n\t\t\tps = ps->next;\n\t\t}\n#ifdef HAVE_LIBOGG\n\t\t/* If there are announcements playing, mix those too */\n\t\tif(anncs_list != NULL) {\n\t\t\tps = anncs_list;\n\t\t\twhile(ps) {\n\t\t\t\tjanus_audiobridge_participant *p = (janus_audiobridge_participant *)ps->data;\n\t\t\t\tif(p->annc == NULL || g_atomic_int_get(&p->destroyed)) {\n\t\t\t\t\tps = ps->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tint read = janus_audiobridge_file_read(p->annc, p->decoder, resampled, sizeof(resampled));\n\t\t\t\tif(read <= 0) {\n\t\t\t\t\t/* Playback over or broken */\n\t\t\t\t\tif(p->annc->started) {\n\t\t\t\t\t\t/* Send a notification that this announcement is over */\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Announcement stopped (%s)\\n\", audiobridge->room_id_str, p->user_id_str);\n\t\t\t\t\t\tjanus_mutex_lock_nodebug(&audiobridge->mutex);\n\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"announcement-stopped\"));\n\t\t\t\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\t\t\tjson_object_set_new(event, \"file_id\", json_string(p->user_id_str));\n\t\t\t\t\t\tjanus_audiobridge_notify_participants(audiobridge, p, event, TRUE);\n\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"announcement-stopped\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"file_id\", json_string(p->user_id_str));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Remove the announcement */\n\t\t\t\t\t\tg_hash_table_remove(audiobridge->anncs, p->user_id_str);\n\t\t\t\t\t\tjanus_mutex_unlock_nodebug(&audiobridge->mutex);\n\t\t\t\t\t}\n\t\t\t\t\tps = ps->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(!p->annc->started) {\n\t\t\t\t\t/* This announcement just started, notify the participants */\n\t\t\t\t\tp->annc->started = TRUE;\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Announcement started (%s)\\n\", audiobridge->room_id_str, p->user_id_str);\n\t\t\t\t\tjanus_mutex_lock_nodebug(&audiobridge->mutex);\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(\"announcement-started\"));\n\t\t\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\t\tjson_object_set_new(event, \"file_id\", json_string(p->user_id_str));\n\t\t\t\t\tjanus_audiobridge_notify_participants(audiobridge, p, event, TRUE);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"announcement-started\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"file_id\", json_string(p->user_id_str));\n\t\t\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, NULL, info);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock_nodebug(&audiobridge->mutex);\n\t\t\t\t}\n\t\t\t\tif(groups_num == 0) {\n\t\t\t\t\t/* Add to the main mix */\n\t\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\tbuffer[i] += resampled[i];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbuffer[i] += (resampled[i]*p->volume_gain)/100;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Add to the group submix */\n\t\t\t\t\tindex = p->group-1;\n\t\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\t\tif(p->volume_gain == 100) {\n\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += resampled[i];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t*(groupBuffers + index*samples + i) += (resampled[i]*p->volume_gain)/100;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tps = ps->next;\n\t\t\t}\n\t\t\tg_list_free_full(anncs_list, (GDestroyNotify)janus_audiobridge_participant_unref);\n\t\t}\n#endif\n\t\t/* If groups are in use, put them together in the main mix */\n\t\tif(groups_num > 0) {\n\t\t\t/* Mix all submixes */\n\t\t\tfor(index=0; index<groups_num; index++) {\n\t\t\t\tfor(i=0; i<samples; i++)\n\t\t\t\t\tbuffer[i] += *(groupBuffers + index*samples + i);\n\t\t\t}\n\t\t}\n\t\t/* Are we recording the mix? (only do it if there's someone in, though...) */\n\t\tif(audiobridge->recording != NULL && g_list_length(participants_list) > 0) {\n\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t/* FIXME Smoothen/Normalize instead of truncating? */\n\t\t\t\toutBuffer[i] = buffer[i];\n\t\t\t}\n\t\t\tfwrite(outBuffer, sizeof(opus_int16), samples, audiobridge->recording);\n\t\t\t/* Every 5 seconds we update the wav header */\n\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\tif(now - audiobridge->record_lastupdate >= 5*G_USEC_PER_SEC) {\n\t\t\t\taudiobridge->record_lastupdate = now;\n\t\t\t\t/* Update the length in the header */\n\t\t\t\tfseek(audiobridge->recording, 0, SEEK_END);\n\t\t\t\tlong int size = ftell(audiobridge->recording);\n\t\t\t\tif(size >= 8) {\n\t\t\t\t\tsize -= 8;\n\t\t\t\t\tfseek(audiobridge->recording, 4, SEEK_SET);\n\t\t\t\t\tfwrite(&size, sizeof(uint32_t), 1, audiobridge->recording);\n\t\t\t\t\tsize += 8;\n\t\t\t\t\tfseek(audiobridge->recording, 40, SEEK_SET);\n\t\t\t\t\tfwrite(&size, sizeof(uint32_t), 1, audiobridge->recording);\n\t\t\t\t\tfflush(audiobridge->recording);\n\t\t\t\t\tfseek(audiobridge->recording, 0, SEEK_END);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Send proper packet to each participant (remove own contribution) */\n\t\tps = participants_list;\n\t\twhile(ps) {\n\t\t\tjanus_audiobridge_participant *p = (janus_audiobridge_participant *)ps->data;\n\t\t\tif(g_atomic_int_get(&p->destroyed) || !p->session || !g_atomic_int_get(&p->session->started) ||\n\t\t\t\t\tg_atomic_int_get(&p->suspended)) {\n\t\t\t\tjanus_refcount_decrease(&p->ref);\n\t\t\t\tps = ps->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_audiobridge_rtp_relay_packet *pkt = NULL;\n\t\t\tjanus_mutex_lock(&p->qmutex);\n\t\t\tif(g_atomic_int_get(&p->active) && !p->muted && p->inbuf) {\n\t\t\t\tGList *first = g_list_first(p->inbuf);\n\t\t\t\tpkt = (janus_audiobridge_rtp_relay_packet *)(first ? first->data : NULL);\n\t\t\t\tp->inbuf = g_list_delete_link(p->inbuf, first);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&p->qmutex);\n\t\t\t/* Remove the participant's own contribution */\n\t\t\tcurBuffer = (opus_int16 *)((pkt && pkt->length && !pkt->silence) ? pkt->data : NULL);\n\t\t\tif(!p->stereo) {\n\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\tif(p->volume_gain == 100)\n\t\t\t\t\t\tsumBuffer[i] = buffer[i] - (curBuffer ? (curBuffer[i]) : 0);\n\t\t\t\t\telse\n\t\t\t\t\t\tsumBuffer[i] = buffer[i] - (curBuffer ? (curBuffer[i]*p->volume_gain)/100 : 0);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdiff = 50 - p->spatial_position;\n\t\t\t\tlgain = 50 + diff;\n\t\t\t\trgain = 50 - diff;\n\t\t\t\tfor(i=0; i<samples; i++) {\n\t\t\t\t\tif(i%2 == 0) {\n\t\t\t\t\t\tif(lgain == 100) {\n\t\t\t\t\t\t\tsumBuffer[i] = buffer[i] - (curBuffer ? (curBuffer[i]) : 0);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsumBuffer[i] = buffer[i] - (curBuffer ? (curBuffer[i]*lgain)/100 : 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif(rgain == 100) {\n\t\t\t\t\t\t\tsumBuffer[i] = buffer[i] - (curBuffer ? (curBuffer[i]) : 0);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsumBuffer[i] = buffer[i] - (curBuffer ? (curBuffer[i]*rgain)/100 : 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor(i=0; i<samples; i++)\n\t\t\t\t/* FIXME Smoothen/Normalize instead of truncating? */\n\t\t\t\toutBuffer[i] = sumBuffer[i];\n\t\t\t/* Enqueue this mixed frame for encoding in the participant thread */\n\t\t\tjanus_audiobridge_rtp_relay_packet *mixedpkt = g_malloc(sizeof(janus_audiobridge_rtp_relay_packet));\n\t\t\tmixedpkt->data = g_malloc(samples*2);\n\t\t\tif(p->codec != JANUS_AUDIOCODEC_OPUS && audiobridge->sampling_rate != 8000) {\n\t\t\t\t/* Downsample this from whatever the mixer uses */\n\t\t\t\ti = janus_audiobridge_resample(outBuffer, samples, audiobridge->sampling_rate, (int16_t *)mixedpkt->data, 8000);\n\t\t\t\tif(i == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[G.711] Error downsampling from %d, skipping audio packet\\n\", audiobridge->sampling_rate);\n\t\t\t\t\tg_free(mixedpkt->data);\n\t\t\t\t\tg_free(mixedpkt);\n\t\t\t\t\tjanus_refcount_decrease(&p->ref);\n\t\t\t\t\tps = ps->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Just copy */\n\t\t\t\tmemcpy(mixedpkt->data, outBuffer, samples*2);\n\t\t\t}\n\t\t\tmixedpkt->length = samples;\t/* We set the number of samples here, not the data length */\n\t\t\tmixedpkt->timestamp = ts;\n\t\t\tmixedpkt->seq_number = seq;\n\t\t\tmixedpkt->ssrc = audiobridge->room_ssrc;\n\t\t\tmixedpkt->silence = FALSE;\n\t\t\tg_async_queue_push(p->outbuf, mixedpkt);\n\t\t\tif(pkt) {\n\t\t\t\tg_free(pkt->data);\n\t\t\t\tpkt->data = NULL;\n\t\t\t\tg_free(pkt);\n\t\t\t\tpkt = NULL;\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&p->ref);\n\t\t\tps = ps->next;\n\t\t}\n\t\tg_list_free(participants_list);\n\t\t/* Forward the mixed packet as RTP to any RTP forwarder that may be listening */\n\t\tjanus_mutex_lock(&audiobridge->rtp_mutex);\n\t\tif(g_hash_table_size(audiobridge->rtp_forwarders) > 0 && audiobridge->rtp_encoder) {\n\t\t\t/* If the room is empty, check if there's any RTP forwarder with an \"always on\" option */\n\t\t\tgboolean go_on = FALSE;\n\t\t\tif(count == 0 && pf_count == 0) {\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->rtp_forwarders);\n\t\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\tjanus_rtp_forwarder *rf = (janus_rtp_forwarder *)value;\n\t\t\t\t\tjanus_audiobridge_rtp_forwarder_metadata *rfm = (janus_audiobridge_rtp_forwarder_metadata *)rf->metadata;\n\t\t\t\t\tif(rfm->always_on) {\n\t\t\t\t\t\tgo_on = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tgo_on = TRUE;\n\t\t\t}\n\t\t\tif(go_on) {\n\t\t\t\t/* By default, let's send the mixed frame to everybody */\n\t\t\t\tif(groups_num == 0) {\n\t\t\t\t\tfor(i=0; i<samples; i++)\n\t\t\t\t\t\toutBuffer[i] = buffer[i];\n\t\t\t\t\thave_opus[0] = FALSE;\n\t\t\t\t\thave_alaw[0] = FALSE;\n\t\t\t\t\thave_ulaw[0] = FALSE;\n\t\t\t\t} else {\n\t\t\t\t\tfor(index=0; index <= groups_num; index++) {\n\t\t\t\t\t\thave_opus[index] = FALSE;\n\t\t\t\t\t\thave_alaw[index] = FALSE;\n\t\t\t\t\t\thave_ulaw[index] = FALSE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer key, value;\n\t\t\t\tg_hash_table_iter_init(&iter, audiobridge->rtp_forwarders);\n\t\t\t\topus_int32 length = 0;\n\t\t\t\twhile(audiobridge->rtp_udp_sock > 0 && g_hash_table_iter_next(&iter, &key, &value)) {\n\t\t\t\t\tjanus_rtp_forwarder *rf = (janus_rtp_forwarder *)value;\n\t\t\t\t\tjanus_audiobridge_rtp_forwarder_metadata *rfm = (janus_audiobridge_rtp_forwarder_metadata *)rf->metadata;\n\t\t\t\t\tif(count == 0 && pf_count == 0 && !rfm->always_on)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t/* Check if we're forwarding the main mix or a specific group */\n\t\t\t\t\tif(groups_num > 0) {\n\t\t\t\t\t\tif(rfm->group == 0) {\n\t\t\t\t\t\t\t/* We're forwarding the main mix */\n\t\t\t\t\t\t\tfor(i=0; i<samples; i++)\n\t\t\t\t\t\t\t\toutBuffer[i] = buffer[i];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* We're forwarding a group mix */\n\t\t\t\t\t\t\tindex = rfm->group-1;\n\t\t\t\t\t\t\tfor(i=0; i<samples; i++)\n\t\t\t\t\t\t\t\toutBuffer[i] = *(groupBuffers + index*samples + i);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(rfm->codec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\t\t/* This is an Opus forwarder, check if we have a version for that already */\n\t\t\t\t\t\tif(!have_opus[rfm->group]) {\n\t\t\t\t\t\t\t/* We don't, encode now */\n\t\t\t\t\t\t\tOpusEncoder *rtp_encoder = (rfm->group == 0 ? audiobridge->rtp_encoder : groupEncoders[rfm->group-1]);\n\t\t\t\t\t\t\tlength = opus_encode(rtp_encoder, outBuffer,\n\t\t\t\t\t\t\t\taudiobridge->spatial_audio ? samples/2 : samples,\n\t\t\t\t\t\t\t\trtpbuffer + rfm->group*1500 + 12, 1500-12);\n\t\t\t\t\t\t\tif(length < 0) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[Opus] Ops! got an error encoding the Opus frame: %d (%s)\\n\", length, opus_strerror(length));\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\thave_opus[rfm->group] = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t\trtph = (janus_rtp_header *)(rtpbuffer + rfm->group*1500);\n\t\t\t\t\t\trtph->version = 2;\n\t\t\t\t\t} else if(rfm->codec == JANUS_AUDIOCODEC_PCMA || rfm->codec == JANUS_AUDIOCODEC_PCMU) {\n\t\t\t\t\t\t/* This is a G.711 forwarder, check if we have a version for that already */\n\t\t\t\t\t\tif((rfm->codec == JANUS_AUDIOCODEC_PCMA && !have_alaw[rfm->group]) ||\n\t\t\t\t\t\t\t\t(rfm->codec == JANUS_AUDIOCODEC_PCMU && !have_ulaw[rfm->group])) {\n\t\t\t\t\t\t\t/* We don't, encode now */\n\t\t\t\t\t\t\tif(audiobridge->sampling_rate != 8000) {\n\t\t\t\t\t\t\t\t/* Downsample this from whatever the mixer uses */\n\t\t\t\t\t\t\t\ti = janus_audiobridge_resample(outBuffer, samples, audiobridge->sampling_rate, resampled, 8000);\n\t\t\t\t\t\t\t\tif(i == 0) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[G.711] Error downsampling from %d, skipping audio packet\\n\", audiobridge->sampling_rate);\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t/* Just copy */\n\t\t\t\t\t\t\t\tmemcpy(resampled, outBuffer, samples*2);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tint i = 0;\n\t\t\t\t\t\t\tif(rfm->codec == JANUS_AUDIOCODEC_PCMA) {\n\t\t\t\t\t\t\t\tuint8_t *rtpalaw_buffer = rtpalaw + rfm->group*G711_SAMPLES + 12;\n\t\t\t\t\t\t\t\tfor(i=0; i<160; i++)\n\t\t\t\t\t\t\t\t\trtpalaw_buffer[i] = janus_audiobridge_g711_alaw_encode(resampled[i]);\n\t\t\t\t\t\t\t\thave_alaw[rfm->group] = TRUE;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tuint8_t *rtpulaw_buffer = rtpulaw + rfm->group*G711_SAMPLES + 12;\n\t\t\t\t\t\t\t\tfor(i=0; i<160; i++)\n\t\t\t\t\t\t\t\t\trtpulaw_buffer[i] = janus_audiobridge_g711_ulaw_encode(resampled[i]);\n\t\t\t\t\t\t\t\thave_ulaw[rfm->group] = TRUE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\trtph = (janus_rtp_header *)(rfm->codec == JANUS_AUDIOCODEC_PCMA ?\n\t\t\t\t\t\t\t(rtpalaw + rfm->group*G711_SAMPLES) : (rtpulaw + rfm->group*G711_SAMPLES));\n\t\t\t\t\t\trtph->version = 2;\n\t\t\t\t\t\tlength = 160;\n\t\t\t\t\t}\n\t\t\t\t\t/* Update header */\n\t\t\t\t\trtph->ssrc = htonl(rf->stream_id);\n\t\t\t\t\trfm->seq_number++;\n\t\t\t\t\trtph->seq_number = htons(rfm->seq_number);\n\t\t\t\t\trfm->timestamp += (rfm->codec == JANUS_AUDIOCODEC_OPUS ? OPUS_SAMPLES : G711_SAMPLES);\n\t\t\t\t\trtph->timestamp = htonl(rfm->timestamp);\n\t\t\t\t\t/* Forward the packet */\n\t\t\t\t\tjanus_rtp_forwarder_send_rtp(rf, (char *)rtph, length+12, -1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&audiobridge->rtp_mutex);\n\t}\n\t/* Close the recording file */\n\tif(audiobridge->recording != NULL && g_atomic_int_get(&audiobridge->wav_header_added)) {\n\t\tJANUS_LOG(LOG_VERB, \"Update wave header for recording %s (%s)...\\n\", audiobridge->room_id_str, audiobridge->room_name);\n\t\tjanus_audiobridge_update_wav_header(audiobridge);\n\t}\n\tg_free(rtpbuffer);\n\tg_free(rtpalaw);\n\tg_free(rtpulaw);\n\tg_free(groupBuffers);\n\tif(groupEncoders) {\n\t\tfor(index=0; index<groups_num; index++) {\n\t\t\tif(groupEncoders[index])\n\t\t\t\topus_encoder_destroy(groupEncoders[index]);\n\t\t}\n\t\tg_free(groupEncoders);\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving mixer thread for room %s (%s)...\\n\", audiobridge->room_id_str, audiobridge->room_name);\n\n\tjanus_refcount_decrease(&audiobridge->ref);\n\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\n/* Thread to encode a mixed frame and send it to a specific participant */\nstatic void *janus_audiobridge_participant_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"AudioBridge Participant thread starting...\\n\");\n\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)data;\n\tif(!participant) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid participant!\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Thread is for participant %s (%s)\\n\",\n\t\tparticipant->user_id_str, participant->display ? participant->display : \"??\");\n\tjanus_audiobridge_session *session = participant->session;\n\n\t/* Output buffer */\n\tjanus_audiobridge_rtp_relay_packet *outpkt = g_malloc(sizeof(janus_audiobridge_rtp_relay_packet));\n\toutpkt->data = g_malloc0(1500);\n\toutpkt->ssrc = 0;\n\toutpkt->timestamp = 0;\n\toutpkt->seq_number = 0;\n\toutpkt->length = 0;\n\toutpkt->silence = FALSE;\n\tuint8_t *payload = (uint8_t *)outpkt->data;\n\n\tJitterBufferPacket jbp = {0};\n\tjanus_audiobridge_buffer_packet *bpkt = NULL;\n\tjanus_audiobridge_rtp_relay_packet *pkt = NULL;\n\tjanus_audiobridge_rtp_relay_packet *mixedpkt = NULL;\n\tint jitter_ticks = 0;\n\tjanus_rtp_header *rtp = NULL;\n\tgint64 now = janus_get_monotonic_time(), before = now;\n\tgboolean first = TRUE;\n\tint ret = 0;\n\tint lost_packets_gap = 0;\n\n\t/* Start working: check both the incoming queue (to decode and queue) and the outgoing one (to encode and send) */\n\twhile(!g_atomic_int_get(&stopping) && g_atomic_int_get(&session->destroyed) == 0) {\n\t\tjanus_mutex_lock(&participant->suspend_cond_mutex);\n\t\twhile(g_atomic_int_get(&participant->suspended)) {\n\t\t\tg_cond_wait(&participant->suspend_cond, &participant->suspend_cond_mutex);\n\t\t\tbefore = janus_get_monotonic_time();\n\t\t\tparticipant->context.seq_reset = TRUE;\n\t\t\tfirst = TRUE;\n\t\t\t/* Clear the output queue since it might contain old packets and break RTP sequence */\n\t\t\tjanus_audiobridge_participant_clear_outbuf(participant);\n\t\t}\n\t\tjanus_mutex_unlock(&participant->suspend_cond_mutex);\n\t\t/* Start with packets to decode and queue for the mixer */\n\t\tnow = janus_get_monotonic_time();\n\t\t/* Start by reading packets to decode from the jitter buffer on a clock */\n\t\tif(now - before >= 18000) {\n\t\t\tbefore += 20000;\n\t\t\tif(participant->jitter) {\n\t\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\t\tret = jitter_buffer_get(participant->jitter, &jbp, participant->codec == JANUS_AUDIOCODEC_OPUS ? 960 : 160, NULL);\n\t\t\t\tjitter_ticks++;\n\t\t\t\t/* Adjust the buffer size every 50 ticks (~1 second) */\n\t\t\t\tif(jitter_ticks == JITTER_BUFFER_MAX_PACKETS) {\n\t\t\t\t\tjitter_buffer_update_delay(participant->jitter, NULL, NULL);\n\t\t\t\t\tjitter_ticks = 0;\n\t\t\t\t}\n\t\t\t\tjitter_buffer_tick(participant->jitter);\n\t\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\t\tif(ret != JITTER_BUFFER_OK) {\n\t\t\t\t\t/* We didn't get a packet: check if PLC can help */\n\t\t\t\t\tif(!first && participant->codec == JANUS_AUDIOCODEC_OPUS && lost_packets_gap <= JITTER_BUFFER_MAX_GAP_SIZE && !participant->muted) {\n\t\t\t\t\t\tlost_packets_gap++;\n\t\t\t\t\t\tif(!g_atomic_int_compare_and_exchange(&participant->decoding, 0, 1)) {\n\t\t\t\t\t\t\t/* This means we're cleaning up, so don't try to decode */\n\t\t\t\t\t\t\tjanus_audiobridge_buffer_packet_destroy(bpkt);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tint32_t output_samples = 0;\n\t\t\t\t\t\topus_decoder_ctl(participant->decoder, OPUS_GET_LAST_PACKET_DURATION(&output_samples));\n\t\t\t\t\t\t/* Allocate a fake packet we can queue */\n\t\t\t\t\t\tpkt = g_malloc(sizeof(janus_audiobridge_rtp_relay_packet));\n\t\t\t\t\t\tpkt->data = g_malloc0(BUFFER_SAMPLES * sizeof(opus_int16));\n\t\t\t\t\t\tpkt->ssrc = 0;\n\t\t\t\t\t\tpkt->timestamp = participant->last_timestamp + OPUS_SAMPLES;\n\t\t\t\t\t\tpkt->seq_number = participant->last_seq + 1;\n\t\t\t\t\t\t/* This is a redundant packet, so we can't parse any extension info */\n\t\t\t\t\t\tpkt->silence = FALSE;\n\t\t\t\t\t\tjanus_audiobridge_participant_istalking(session, participant, NULL, NULL);\n\t\t\t\t\t\tpkt->length = opus_decode(participant->decoder, NULL, 0, (opus_int16 *)pkt->data, output_samples, 0);\n#ifdef HAVE_RNNOISE\n\t\t\t\t\t\t/* Check if we need to denoise this packet */\n\t\t\t\t\t\tif(participant->denoise)\n\t\t\t\t\t\t\tjanus_audiobridge_participant_denoise(participant, (char *)pkt->data, pkt->length);\n#endif\n\t\t\t\t\t\t/* Update the details */\n\t\t\t\t\t\tparticipant->last_seq = pkt->seq_number;\n\t\t\t\t\t\tparticipant->last_timestamp = pkt->timestamp;\n\t\t\t\t\t\tg_atomic_int_set(&participant->decoding, 0);\n\t\t\t\t\t\tif(pkt->length < 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[Opus] Ops! got an error decoding the Opus frame: %d (%s)\\n\", pkt->length, opus_strerror(pkt->length));\n\t\t\t\t\t\t\tg_free(pkt->data);\n\t\t\t\t\t\t\tg_free(pkt);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Queue the decoded packet for the mixer */\n\t\t\t\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\t\t\t\t/* Do not let queue-in grow too much */\n\t\t\t\t\t\tguint count = g_list_length(participant->inbuf);\n\t\t\t\t\t\tif((int) count > QUEUE_IN_MAX_PACKETS) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Participant queue-in contains too many packets, clearing now (count=%u)\\n\", count);\n\t\t\t\t\t\t\tjanus_audiobridge_participant_clear_inbuf(participant);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparticipant->inbuf = g_list_append(participant->inbuf, pkt);\n\t\t\t\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* No packet in the jitter buffer? Move on the talking detection, if needed */\n\t\t\t\t\t\tjanus_audiobridge_participant_istalking(session, participant, NULL, NULL);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Decode the audio packet */\n\t\t\t\t\tbpkt = (janus_audiobridge_buffer_packet *)jbp.data;\n\t\t\t\t\tif(!g_atomic_int_compare_and_exchange(&participant->decoding, 0, 1)) {\n\t\t\t\t\t\t/* This means we're cleaning up, so don't try to decode */\n\t\t\t\t\t\tjanus_audiobridge_buffer_packet_destroy(bpkt);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t/* Access the payload */\n\t\t\t\t\tchar *buffer = bpkt->rtp ? bpkt->rtp->buffer : NULL;\n\t\t\t\t\tuint16_t len = bpkt->rtp ? bpkt->rtp->length : 0;\n\t\t\t\t\tint plen = 0;\n\t\t\t\t\tconst unsigned char *payload = (const unsigned char *)janus_rtp_payload(buffer, len, &plen);\n\t\t\t\t\tif(!payload) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Ops! got an error accessing the RTP payload\\n\",\n\t\t\t\t\t\t\tparticipant->codec == JANUS_AUDIOCODEC_OPUS ? \"Opus\" : \"G.711\");\n\t\t\t\t\t\tg_atomic_int_set(&participant->decoding, 0);\n\t\t\t\t\t\tjanus_audiobridge_buffer_packet_destroy(bpkt);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\trtp = (janus_rtp_header *)buffer;\n\t\t\t\t\tfirst = FALSE;\n\t\t\t\t\tlost_packets_gap = 0;\n\t\t\t\t\t/* Decode the packet */\n\t\t\t\t\tpkt = g_malloc(sizeof(janus_audiobridge_rtp_relay_packet));\n\t\t\t\t\tpkt->data = g_malloc0(BUFFER_SAMPLES*sizeof(opus_int16));\n\t\t\t\t\tpkt->ssrc = 0;\n\t\t\t\t\tpkt->timestamp = ntohl(rtp->timestamp);\n\t\t\t\t\tpkt->seq_number = ntohs(rtp->seq_number);\n\t\t\t\t\t/* Check the audio level extension to see if this is silence */\n\t\t\t\t\tpkt->silence = FALSE;\n\t\t\t\t\tjanus_audiobridge_participant_istalking(session, participant, bpkt->rtp, &pkt->silence);\n\t\t\t\t\tpkt->length = 0;\n\t\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\t\t/* Opus */\n\t\t\t\t\t\tpkt->length = opus_decode(participant->decoder, payload, plen, (opus_int16 *)pkt->data, BUFFER_SAMPLES, 0);\n\t\t\t\t\t} else if(participant->codec == JANUS_AUDIOCODEC_PCMA || participant->codec == JANUS_AUDIOCODEC_PCMU) {\n\t\t\t\t\t\t/* G.711 */\n\t\t\t\t\t\tif(plen != 160) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[G.711] Wrong packet size (expected 160, got %d), skipping audio packet\\n\", plen);\n\t\t\t\t\t\t\tg_atomic_int_set(&participant->decoding, 0);\n\t\t\t\t\t\t\tjanus_audiobridge_buffer_packet_destroy(bpkt);\n\t\t\t\t\t\t\tg_free(pkt->data);\n\t\t\t\t\t\t\tg_free(pkt);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tint i = 0;\n\t\t\t\t\t\tuint16_t *samples = (uint16_t *)pkt->data;\n\t\t\t\t\t\tif(rtp->type == 0) {\n\t\t\t\t\t\t\t/* mu-law */\n\t\t\t\t\t\t\tfor(i=0; i<plen; i++)\n\t\t\t\t\t\t\t\t*(samples+i) = janus_audiobridge_g711_ulaw_dectable[*(payload+i)];\n\t\t\t\t\t\t} else if(rtp->type == 8) {\n\t\t\t\t\t\t\t/* a-law */\n\t\t\t\t\t\t\tfor(i=0; i<plen; i++)\n\t\t\t\t\t\t\t\t*(samples+i) = janus_audiobridge_g711_alaw_dectable[*(payload+i)];\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpkt->length = 320;\n\t\t\t\t\t}\n#ifdef HAVE_RNNOISE\n\t\t\t\t\t/* Check if we need to denoise this packet */\n\t\t\t\t\tif(participant->denoise)\n\t\t\t\t\t\tjanus_audiobridge_participant_denoise(participant, (char *)pkt->data, pkt->length);\n#endif\n\t\t\t\t\t/* Get rid of the buffered packet */\n\t\t\t\t\tjanus_audiobridge_buffer_packet_destroy(bpkt);\n\t\t\t\t\t/* Update the details */\n\t\t\t\t\tparticipant->last_seq = pkt->seq_number;\n\t\t\t\t\tparticipant->last_timestamp = pkt->timestamp;\n\t\t\t\t\tg_atomic_int_set(&participant->decoding, 0);\n\t\t\t\t\tif(pkt->length < 0) {\n\t\t\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[Opus] Ops! got an error decoding the Opus frame: %d (%s)\\n\", pkt->length, opus_strerror(pkt->length));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[G.711] Ops! got an error decoding the audio frame\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg_free(pkt->data);\n\t\t\t\t\t\tg_free(pkt);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Queue the decoded packet for the mixer */\n\t\t\t\t\tjanus_mutex_lock(&participant->qmutex);\n\t\t\t\t\t/* Do not let queue-in grow too much */\n\t\t\t\t\tguint count = g_list_length(participant->inbuf);\n\t\t\t\t\tif(count > QUEUE_IN_MAX_PACKETS) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Participant queue-in contains too many packets, clearing now (count=%u)\\n\", count);\n\t\t\t\t\t\tjanus_audiobridge_participant_clear_inbuf(participant);\n\t\t\t\t\t}\n\t\t\t\t\tparticipant->inbuf = g_list_append(participant->inbuf, pkt);\n\t\t\t\t\tjanus_mutex_unlock(&participant->qmutex);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Now check if there's packets to encode */\n\t\tmixedpkt = g_async_queue_try_pop(participant->outbuf);\n\t\tif(mixedpkt != NULL && g_atomic_int_get(&session->destroyed) == 0 && g_atomic_int_get(&session->started)) {\n\t\t\tif(g_atomic_int_get(&participant->active) && (participant->codec == JANUS_AUDIOCODEC_PCMA ||\n\t\t\t\t\tparticipant->codec == JANUS_AUDIOCODEC_PCMU) && g_atomic_int_compare_and_exchange(&participant->encoding, 0, 1)) {\n\t\t\t\t/* Encode using G.711 */\n\t\t\t\tif(mixedpkt->length != 320) {\n\t\t\t\t\t/* TODO Resample */\n\t\t\t\t}\n\t\t\t\tint i = 0;\n\t\t\t\topus_int16 *outBuffer = (opus_int16 *)mixedpkt->data;\n\t\t\t\tif(participant->codec == JANUS_AUDIOCODEC_PCMA) {\n\t\t\t\t\t/* A-law */\n\t\t\t\t\tfor(i=0; i<160; i++)\n\t\t\t\t\t\t*(payload+12+i) = janus_audiobridge_g711_alaw_encode(outBuffer[i]);\n\t\t\t\t} else {\n\t\t\t\t\t/* Mu-Law */\n\t\t\t\t\tfor(i=0; i<160; i++)\n\t\t\t\t\t\t*(payload+12+i) = janus_audiobridge_g711_ulaw_encode(outBuffer[i]);\n\t\t\t\t}\n\t\t\t\tg_atomic_int_set(&participant->encoding, 0);\n\t\t\t\toutpkt->length = 172;\t/* Take the RTP header into consideration */\n\t\t\t\t/* Update RTP header */\n\t\t\t\toutpkt->data->version = 2;\n\t\t\t\toutpkt->data->markerbit = 0;\t/* FIXME Should be 1 for the first packet */\n\t\t\t\toutpkt->data->seq_number = htons(mixedpkt->seq_number);\n\t\t\t\toutpkt->data->timestamp = htonl(mixedpkt->timestamp/6);\n\t\t\t\toutpkt->data->ssrc = htonl(mixedpkt->ssrc);\t/* The Janus core will fix this anyway */\n\t\t\t\t/* Backup the actual timestamp and sequence number set by the audiobridge, in case a room is changed */\n\t\t\t\toutpkt->ssrc = mixedpkt->ssrc;\n\t\t\t\toutpkt->timestamp = mixedpkt->timestamp/6;\n\t\t\t\toutpkt->seq_number = mixedpkt->seq_number;\n\t\t\t\tjanus_audiobridge_relay_rtp_packet(participant->session, outpkt);\n\t\t\t} else if(g_atomic_int_get(&participant->active) && participant->encoder &&\n\t\t\t\t\tg_atomic_int_compare_and_exchange(&participant->encoding, 0, 1)) {\n\t\t\t\t/* Encode raw frame to Opus */\n\t\t\t\topus_int16 *outBuffer = (opus_int16 *)mixedpkt->data;\n\t\t\t\toutpkt->length = opus_encode(participant->encoder, outBuffer,\n\t\t\t\t\tparticipant->stereo ? mixedpkt->length/2 : mixedpkt->length, payload+12, 1500-12);\n\t\t\t\tg_atomic_int_set(&participant->encoding, 0);\n\t\t\t\tif(outpkt->length < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[Opus] Ops! got an error encoding the Opus frame: %d (%s)\\n\", outpkt->length, opus_strerror(outpkt->length));\n\t\t\t\t} else {\n\t\t\t\t\toutpkt->length += 12;\t/* Take the RTP header into consideration */\n\t\t\t\t\t/* Update RTP header */\n\t\t\t\t\toutpkt->data->version = 2;\n\t\t\t\t\toutpkt->data->markerbit = 0;\t/* FIXME Should be 1 for the first packet */\n\t\t\t\t\toutpkt->data->seq_number = htons(mixedpkt->seq_number);\n\t\t\t\t\toutpkt->data->timestamp = htonl(mixedpkt->timestamp);\n\t\t\t\t\toutpkt->data->ssrc = htonl(mixedpkt->ssrc);\t/* The Janus core will fix this anyway */\n\t\t\t\t\t/* Backup the actual timestamp and sequence number set by the audiobridge, in case a room is changed */\n\t\t\t\t\toutpkt->ssrc = mixedpkt->ssrc;\n\t\t\t\t\toutpkt->timestamp = mixedpkt->timestamp;\n\t\t\t\t\toutpkt->seq_number = mixedpkt->seq_number;\n\t\t\t\t\tjanus_audiobridge_relay_rtp_packet(participant->session, outpkt);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(mixedpkt) {\n\t\t\tg_free(mixedpkt->data);\n\t\t\tg_free(mixedpkt);\n\t\t}\n\t\tg_usleep(2500);\n\t}\n\t/* We're done, get rid of the resources */\n\tg_free(outpkt->data);\n\tg_free(outpkt);\n\tJANUS_LOG(LOG_VERB, \"AudioBridge Participant thread leaving...\\n\");\n\n\tjanus_refcount_decrease(&participant->ref);\n\tjanus_refcount_decrease(&session->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\nstatic void janus_audiobridge_relay_rtp_packet(gpointer data, gpointer user_data) {\n\tjanus_audiobridge_rtp_relay_packet *packet = (janus_audiobridge_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_audiobridge_session *session = (janus_audiobridge_session *)data;\n\tif(!session || !session->handle) {\n\t\t/* JANUS_LOG(LOG_ERR, \"Invalid session...\\n\"); */\n\t\treturn;\n\t}\n\tif(!g_atomic_int_get(&session->started)) {\n\t\t/* JANUS_LOG(LOG_ERR, \"Streaming not started yet for this session...\\n\"); */\n\t\treturn;\n\t}\n\tjanus_audiobridge_participant *participant = session->participant;\n\t/* Set the payload type */\n\tif(participant->codec == JANUS_AUDIOCODEC_OPUS)\n\t\tpacket->data->type = participant->opus_pt;\n\telse\n\t\tpacket->data->type = (participant->codec == JANUS_AUDIOCODEC_PCMA ? 8 : 0);\n\t/* Fix sequence number and timestamp (room switching may be involved) */\n\tjanus_rtp_header_update(packet->data, &participant->context, FALSE, 0);\n\tif(participant->plainrtp_media.audio_rtp_fd > 0) {\n\t\tif(participant->plainrtp_media.audio_ssrc == 0)\n\t\t\tparticipant->plainrtp_media.audio_ssrc = ntohl(packet->ssrc);\n\t\tif(participant->plainrtp_media.audio_send) {\n\t\t\tint ret = send(participant->plainrtp_media.audio_rtp_fd, (char *)packet->data, packet->length, 0);\n\t\t\tif(ret < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Error sending plain RTP packet: %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t}\n\t\t}\n\t} else if(gateway != NULL) {\n\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = FALSE, .buffer = (char *)packet->data, .length = packet->length };\n\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t/* FIXME Should we add our own audio level extension? */\n\t\tgateway->relay_rtp(session->handle, &rtp);\n\t}\n\t/* Restore the timestamp and sequence number to what the mixer set them to */\n\tpacket->data->timestamp = htonl(packet->timestamp);\n\tpacket->data->seq_number = htons(packet->seq_number);\n}\n\n/* Plain RTP stuff */\nstatic void janus_audiobridge_plainrtp_media_cleanup(janus_audiobridge_plainrtp_media *media) {\n\tif(media == NULL)\n\t\treturn;\n\tmedia->ready = FALSE;\n\tmedia->audio_pt = -1;\n\tmedia->audio_send = FALSE;\n\tif(media->audio_rtp_fd > 0)\n\t\tclose(media->audio_rtp_fd);\n\tmedia->audio_rtp_fd = -1;\n\tmedia->local_audio_rtp_port = 0;\n\tmedia->remote_audio_rtp_port = 0;\n\tg_free(media->remote_audio_ip);\n\tmedia->remote_audio_ip = NULL;\n\tmedia->audio_ssrc = 0;\n\tmedia->audio_ssrc_peer = 0;\n\tif(media->pipefd[0] > 0)\n\t\tclose(media->pipefd[0]);\n\tmedia->pipefd[0] = -1;\n\tif(media->pipefd[1] > 0)\n\t\tclose(media->pipefd[1]);\n\tmedia->pipefd[1] = -1;\n}\nstatic int janus_audiobridge_plainrtp_allocate_port(janus_audiobridge_plainrtp_media *media) {\n\t/* Read global slider */\n\tuint16_t rtp_port_next = rtp_range_slider;\n\tuint16_t rtp_port_start = rtp_port_next;\n\tgboolean rtp_port_wrap = FALSE;\n\t/* Find a port we can use */\n\tint rtp_fd = -1;\n\twhile(1) {\n\t\tif(rtp_port_wrap && rtp_port_next >= rtp_port_start) {\n\t\t\t/* Full range scanned */\n\t\t\tJANUS_LOG(LOG_ERR, \"No ports available in range: %u -- %u\\n\", rtp_range_min, rtp_range_max);\n\t\t\tbreak;\n\t\t}\n\t\tif(rtp_fd == -1)\n\t\t\trtp_fd = socket(!ipv6_disabled ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);\n\t\tif(rtp_fd == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating socket... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\tbreak;\n\t\t}\n\t\tint v6only = 0;\n\t\tif(!ipv6_disabled && setsockopt(rtp_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"setsockopt on socket failed... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\tbreak;\n\t\t}\n\t\tint rtp_port = rtp_port_next;\n\t\tif((uint32_t)(rtp_port_next + 2UL) < rtp_range_max) {\n\t\t\t/* Advance to next value */\n\t\t\trtp_port_next += 2;\n\t\t} else {\n\t\t\trtp_port_next = rtp_range_min;\n\t\t\trtp_port_wrap = TRUE;\n\t\t}\n\t\tstruct sockaddr_storage rtp_address = { 0 };\n\t\tif(!ipv6_disabled) {\n\t\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)&rtp_address;\n\t\t\taddr->sin6_family = AF_INET6;\n\t\t\taddr->sin6_port = htons(rtp_port);\n\t\t\taddr->sin6_addr = in6addr_any;\n\t\t} else {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&rtp_address;\n\t\t\taddr->sin_family = AF_INET;\n\t\t\taddr->sin_port = htons(rtp_port);\n\t\t\taddr->sin_addr.s_addr = INADDR_ANY;\n\t\t}\n\t\tif(bind(rtp_fd, (struct sockaddr *)(&rtp_address), sizeof(rtp_address)) < 0) {\n\t\t\t/* rtp_fd still unbound, reuse it in the next iteration */\n\t\t} else {\n\t\t\tmedia->audio_rtp_fd = rtp_fd;\n\t\t\tmedia->local_audio_rtp_port = rtp_port;\n\t\t\trtp_range_slider = rtp_port_next;\t\t/* Update global slider */\n\t\t\treturn 0;\n\t\t}\n\t}\n\tif(rtp_fd != -1) {\n\t\tclose(rtp_fd);\n\t}\n\treturn -1;\n}\n/* Thread to relay RTP/RTCP frames coming from the peer */\nstatic void *janus_audiobridge_plainrtp_relay_thread(void *data) {\n\tjanus_audiobridge_participant *participant = (janus_audiobridge_participant *)data;\n\tif(!participant) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid participant!\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tjanus_audiobridge_session *session = participant->session;\n\tJANUS_LOG(LOG_INFO, \"[AudioBridge-%p] Starting Plain RTP participant thread\\n\", session);\n\n\t/* File descriptors */\n\tsocklen_t addrlen;\n\tstruct sockaddr_storage remote = { 0 };\n\tint resfd = 0, bytes = 0, pollerrs = 0;\n\tstruct pollfd fds[2];\n\tint pipe_fd = participant->plainrtp_media.pipefd[0];\n\tchar buffer[1500];\n\tmemset(buffer, 0, 1500);\n\t/* Loop */\n\tint num = 0;\n\tgboolean goon = TRUE;\n\n\t/* Fake RTP packet */\n\tjanus_plugin_rtp packet = { .video = FALSE, .buffer = buffer, .length = 0 };\n\tjanus_plugin_rtp_extensions_reset(&packet.extensions);\n\n\twhile(goon && session != NULL && !g_atomic_int_get(&session->destroyed) && !g_atomic_int_get(&session->hangingup)) {\n\t\t/* Prepare poll */\n\t\tnum = 0;\n\t\tif(participant->plainrtp_media.audio_rtp_fd != -1) {\n\t\t\tfds[num].fd = participant->plainrtp_media.audio_rtp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(pipe_fd != -1) {\n\t\t\tfds[num].fd = pipe_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\t/* Wait for some data */\n\t\tresfd = poll(fds, num, 1000);\n\t\tif(resfd < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[AudioBridge-%p] Got an EINTR (%s), ignoring...\\n\", session, g_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Error polling...\\n\", session);\n\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t\tbreak;\n\t\t} else if(resfd == 0) {\n\t\t\t/* No data, keep going */\n\t\t\tcontinue;\n\t\t}\n\t\tif(session == NULL || g_atomic_int_get(&session->destroyed))\n\t\t\tbreak;\n\t\tint i = 0;\n\t\tfor(i=0; i<num; i++) {\n\t\t\tif(fds[i].revents & (POLLERR | POLLHUP)) {\n\t\t\t\t/* Check the socket error */\n\t\t\t\tint error = 0;\n\t\t\t\tsocklen_t errlen = sizeof(error);\n\t\t\t\tgetsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen);\n\t\t\t\tif(error == 0) {\n\t\t\t\t\t/* Maybe not a breaking error after all? */\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* FIXME Should we be more tolerant of ICMP errors on RTP sockets as well? */\n\t\t\t\tpollerrs++;\n\t\t\t\tif(pollerrs < 100)\n\t\t\t\t\tcontinue;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p] Too many errors polling %d (socket #%d): %s...\\n\", session,\n\t\t\t\t\tfds[i].fd, i, fds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\");\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[AudioBridge-%p]   -- %d (%s)\\n\", session, error, g_strerror(error));\n\t\t\t\t/* Can we assume it's pretty much over, after a POLLERR? */\n\t\t\t\tgoon = FALSE;\n\t\t\t\t/* Close the channel */\n\t\t\t\tjanus_audiobridge_hangup_media(session->handle);\n\t\t\t\tbreak;\n\t\t\t} else if(fds[i].revents & POLLIN) {\n\t\t\t\tif(pipe_fd != -1 && fds[i].fd == pipe_fd) {\n\t\t\t\t\t/* Poll interrupted for a reason, go on */\n\t\t\t\t\tint code = 0;\n\t\t\t\t\t(void)read(pipe_fd, &code, sizeof(int));\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* Got an RTP packet */\n\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\tif(bytes < 0) {\n\t\t\t\t\t/* Failed to read? */\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Audio RTP */\n\t\t\t\tif(!janus_is_rtp(buffer, bytes)) {\n\t\t\t\t\t/* Not an RTP packet? */\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Handle the packet */\n\t\t\t\tpollerrs = 0;\n\t\t\t\trtp_header *header = (rtp_header *)buffer;\n\t\t\t\tif(participant->plainrtp_media.audio_ssrc_peer != ntohl(header->ssrc)) {\n\t\t\t\t\tparticipant->plainrtp_media.audio_ssrc_peer = ntohl(header->ssrc);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[AudioBridge-%p] Got peer audio SSRC: %\"SCNu32\"\\n\",\n\t\t\t\t\t\tsession, participant->plainrtp_media.audio_ssrc_peer);\n\t\t\t\t}\n\t\t\t\t/* Check if the SSRC changed (e.g., after a re-INVITE or UPDATE) */\n\t\t\t\tjanus_rtp_header_update(header, &participant->plainrtp_media.context, FALSE, 0);\n\t\t\t\t/* Handle as a WebRTC RTP packet */\n\t\t\t\tpacket.length = bytes;\n\t\t\t\tjanus_audiobridge_incoming_rtp(session->handle, &packet);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t}\n\t/* Cleanup the media session */\n\tparticipant->plainrtp_media.thread = NULL;\n\tjanus_mutex_lock(&participant->pmutex);\n\tparticipant->plainrtp = FALSE;\n\tjanus_audiobridge_plainrtp_media_cleanup(&participant->plainrtp_media);\n\tjanus_mutex_unlock(&participant->pmutex);\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"[AudioBridge-%p] Leaving Plain RTP participant thread\\n\", session);\n\tjanus_refcount_decrease(&participant->ref);\n\tjanus_refcount_decrease(&session->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\nstatic void janus_audiobridge_participant_istalking(janus_audiobridge_session *session,\n\t\tjanus_audiobridge_participant *participant, janus_plugin_rtp *packet, gboolean *silence) {\n\t/* Check the audio levels, in case we need to notify participants about who's talking */\n\tif(participant == NULL || participant->extmap_id < 1)\n\t\treturn;\n\tint level = packet ? packet->extensions.audio_level : 127;\n\tif(level == -1)\n\t\treturn;\n\tif(level == 127 && silence)\n\t\t*silence = TRUE;\n\tif(participant->room && participant->room->audiolevel_event) {\n\t\t/* We need to detect who's talking: update our monitoring stuff */\n\t\tint audio_active_packets = participant->room ? participant->room->audio_active_packets : 100;\n\t\tint audio_level_average = participant->room ? participant->room->audio_level_average : 25;\n\t\t/* Check if we need to override those with user specific properties */\n\t\tif(participant->user_audio_active_packets > 0)\n\t\t\taudio_active_packets = participant->user_audio_active_packets;\n\t\tif(participant->user_audio_level_average > 0)\n\t\t\taudio_level_average = participant->user_audio_level_average;\n\t\tparticipant->audio_dBov_sum += level;\n\t\tparticipant->audio_active_packets++;\n\t\tparticipant->dBov_level = level;\n\t\tif(participant->audio_active_packets > 0 && participant->audio_active_packets == audio_active_packets) {\n\t\t\tgboolean notify_talk_event = FALSE;\n\t\t\tif((float) participant->audio_dBov_sum / (float) participant->audio_active_packets < audio_level_average) {\n\t\t\t\t/* Participant talking, should we notify all participants? */\n\t\t\t\tif(!participant->talking)\n\t\t\t\t\tnotify_talk_event = TRUE;\n\t\t\t\tparticipant->talking = TRUE;\n\t\t\t} else {\n\t\t\t\t/* Participant not talking anymore, should we notify all participants? */\n\t\t\t\tif(participant->talking)\n\t\t\t\t\tnotify_talk_event = TRUE;\n\t\t\t\tparticipant->talking = FALSE;\n\t\t\t}\n\t\t\tparticipant->audio_active_packets = 0;\n\t\t\tparticipant->audio_dBov_sum = 0;\n\t\t\t/* Only notify in case of state changes */\n\t\t\tjanus_audiobridge_room *audiobridge = participant->room;\n\t\t\tif(audiobridge && notify_talk_event) {\n\t\t\t\tjanus_mutex_lock(&audiobridge->mutex);\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"audiobridge\", json_string(participant->talking ? \"talking\" : \"stopped-talking\"));\n\t\t\t\tjson_object_set_new(event, \"room\",\n\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\tjson_object_set_new(event, \"id\",\n\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t/* Notify the speaker this event is related to as well */\n\t\t\t\tjanus_audiobridge_notify_participants(audiobridge, participant, event, TRUE);\n\t\t\t\tjson_decref(event);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"audiobridge\", json_string(participant->talking ? \"talking\" : \"stopped-talking\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\tstring_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id));\n\t\t\t\t\tjson_object_set_new(info, \"id\",\n\t\t\t\t\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t\tgateway->notify_event(&janus_audiobridge_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&audiobridge->mutex);\n\t\t\t}\n\t\t}\n\t}\n}\n\n#ifdef HAVE_RNNOISE\nstatic void janus_audiobridge_participant_denoise(janus_audiobridge_participant *participant, char *data, int len) {\n\tif(len < 0 || data == NULL)\n\t\treturn;\n\t/* Create a denoiser if we still don't have one */\n\tif(participant->rnnoise[0] == NULL) {\n\t\t/* Create RNNoise context */\n\t\tparticipant->rnnoise[0] = rnnoise_create(NULL);\n\t\t/* If we still don't have a denoiser, give up */\n\t\tif(participant->rnnoise[0] == NULL)\n\t\t\treturn;\n\t\t/* Allocate the buffer for the denoiser */\n\t\tif(participant->denoiser_buffer[0] == NULL)\n\t\t\tparticipant->denoiser_buffer[0] = g_malloc(DENOISER_FRAME_SIZE * sizeof(float));\n\t}\n\t/* Check if we need a denoiser for stereo channel too */\n\tif(participant->stereo && participant->rnnoise[1] == NULL) {\n\t\t/* Create RNNoise context */\n\t\tparticipant->rnnoise[1] = rnnoise_create(NULL);\n\t\t/* If we still don't have a denoiser, give up */\n\t\tif(participant->rnnoise[1] == NULL)\n\t\t\treturn;\n\t\t/* Allocate the buffer for the denoiser */\n\t\tif(participant->denoiser_buffer[1] == NULL)\n\t\t\tparticipant->denoiser_buffer[1] = g_malloc(DENOISER_FRAME_SIZE * sizeof(float));\n\t}\n\t/* Check if we need to (re)create resamplers too */\n\tif(participant->sampling_rate != participant->resampler_rate ||\n\t\t\tparticipant->stereo != participant->resampler_stereo) {\n\t\tparticipant->resampler_rate = participant->sampling_rate;\n\t\tparticipant->resampler_stereo = participant->stereo;\n\t\tif(participant->upsampler)\n\t\t\tspeex_resampler_destroy(participant->upsampler);\n\t\tparticipant->upsampler = NULL;\n\t\tif(participant->downsampler)\n\t\t\tspeex_resampler_destroy(participant->downsampler);\n\t\tparticipant->downsampler = NULL;\n\t\t/* We need resamplers only if rate is not 48kHz */\n\t\tif(participant->resampler_rate != 48000) {\n\t\t\tspx_uint32_t channels = !participant->resampler_stereo ? 1 : 2;\n\t\t\tspx_uint32_t from_rate = participant->resampler_rate;\n\t\t\tspx_uint32_t to_rate = 48000;\n\t\t\tint quality = 8, error = 0;\n\t\t\tparticipant->upsampler = speex_resampler_init(channels, from_rate, to_rate, quality, &error);\n\t\t\tif(participant->upsampler != NULL) {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Created %s resampler from %d to %d (channels=%d, quality=%d)\\n\",\n\t\t\t\t\t(participant->resampler_stereo ? \"stereo\" : \"mono\"), from_rate, to_rate, channels, quality);\n\t\t\t} else {\n\t\t\t\t/* We couldn't create a resampler, don't do anything */\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tparticipant->downsampler = speex_resampler_init(channels, to_rate, from_rate, quality, &error);\n\t\t\tif(participant->downsampler != NULL) {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Created %s resampler from %d to %d (channels=%d, quality=%d)\\n\",\n\t\t\t\t\t(participant->resampler_stereo ? \"stereo\" : \"mono\"), to_rate, from_rate, channels, quality);\n\t\t\t} else {\n\t\t\t\t/* We couldn't create a resampler, don't do anything */\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(participant->upsample_buffer == NULL)\n\t\t\t\tparticipant->upsample_buffer = g_malloc(2 * OPUS_SAMPLES * sizeof(opus_int16));\n\t\t\tif(participant->downsample_buffer == NULL)\n\t\t\t\tparticipant->downsample_buffer = g_malloc(2 * OPUS_SAMPLES * sizeof(opus_int16));\n\t\t}\n\t}\n\n\t/* Opus int16 original samples */\n\topus_int16 *samples = (opus_int16 *)data;\n\t/* Number of original samples, should be: 160 (8kHz), 320 (16kHz), 480 (24kHz), 960 (48kHz) */\n\tint samples_count = len;\n\tif(samples_count > (2 * OPUS_SAMPLES))\n\t\tsamples_count = (2 * OPUS_SAMPLES);\n\t/* Actual length of the resampled array (double size for stereo) */\n\tconst int samples_len = !participant->resampler_stereo ? samples_count : 2*samples_count;\n\n\t/* Should be 960 */\n\tint upsample_buffer_count = len * (48000/participant->resampler_rate);\n\t/* Upsampled buffer */\n\topus_int16 *upsample_buffer = samples;\n\n\t/* Downsampled data samples count is equal to original samples */\n\tint downsample_buffer_count = samples_count;\n\t/* Downsampled buffer */\n\topus_int16 *downsample_buffer = upsample_buffer;\n\n\t/* Upsample */\n\tif(participant->resampler_rate != 48000) {\n\t\tupsample_buffer = participant->upsample_buffer;\n\t\tjanus_audiobridge_participant_upsample(participant, samples, &samples_count, upsample_buffer, &upsample_buffer_count);\n\t}\n\n\tint i = 0, j = 0;\n\tfloat *denoiser_buffer = participant->denoiser_buffer[0];\n\tfloat *denoiser_buffer_alt = participant->denoiser_buffer[1];\n\n\t/* Denoise in chunks of 480 samples */\n\tif(!participant->resampler_stereo) {\n\t\tfor(i=0; i<upsample_buffer_count; i+= DENOISER_FRAME_SIZE) {\n\t\t\tfor(j=0; j<DENOISER_FRAME_SIZE; j++) {\n\t\t\t\tdenoiser_buffer[j] = upsample_buffer[i + j];\n\t\t\t}\n\t\t\trnnoise_process_frame(participant->rnnoise[0], denoiser_buffer, denoiser_buffer);\n\t\t\tfor(j=0; j<DENOISER_FRAME_SIZE; j++) {\n\t\t\t\tupsample_buffer[i + j] = denoiser_buffer[j];\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor(i=0; i<upsample_buffer_count; i+= DENOISER_FRAME_SIZE) {\n\t\t\tfor(j=0; j<DENOISER_FRAME_SIZE; j++) {\n\t\t\t\tdenoiser_buffer[j] = upsample_buffer[2*i + 2*j];\n\t\t\t\tdenoiser_buffer_alt[j] = upsample_buffer[2*i + 2*j + 1];\n\t\t\t}\n\t\t\trnnoise_process_frame(participant->rnnoise[0], denoiser_buffer, denoiser_buffer);\n\t\t\trnnoise_process_frame(participant->rnnoise[1], denoiser_buffer_alt, denoiser_buffer_alt);\n\t\t\tfor(j=0; j<DENOISER_FRAME_SIZE; j++) {\n\t\t\t\tupsample_buffer[2*i + 2*j] = denoiser_buffer[j];\n\t\t\t\tupsample_buffer[2*i + 2*j + 1] = denoiser_buffer_alt[j];\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Downsample */\n\tif(participant->resampler_rate != 48000) {\n\t\tdownsample_buffer = participant->downsample_buffer;\n\t\tjanus_audiobridge_participant_downsample(participant, upsample_buffer, &upsample_buffer_count, downsample_buffer, &downsample_buffer_count);\n\t}\n\n\t/* Copy denoised and downsampled data back */\n\tmemcpy(samples, downsample_buffer, samples_len*sizeof(opus_int16));\n}\n\nstatic void janus_audiobridge_participant_upsample(janus_audiobridge_participant *participant, opus_int16 *input, int *in_len, opus_int16 *output, int *out_len) {\n\tif(!participant->resampler_stereo) {\n\t\tint err = speex_resampler_process_int(participant->upsampler, 0, (spx_int16_t *)input, (spx_uint32_t *)in_len, (spx_int16_t *)output, (spx_uint32_t *)out_len);\n\t\tif(err != 0) {\n\t\t\t//TODO\n\t\t}\n\t} else {\n\t\tint err = speex_resampler_process_interleaved_int(participant->upsampler, (spx_int16_t *)input, (spx_uint32_t *)in_len, (spx_int16_t *)output, (spx_uint32_t *)out_len);\n\t\tif(err != 0) {\n\t\t\t//TODO\n\t\t}\n\t}\n}\nstatic void janus_audiobridge_participant_downsample(janus_audiobridge_participant *participant, opus_int16 *input, int *in_len, opus_int16 *output, int *out_len) {\n\tif(!participant->resampler_stereo) {\n\t\tint err = speex_resampler_process_int(participant->downsampler, 0, (spx_int16_t *)input, (spx_uint32_t *)in_len, (spx_int16_t *)output, (spx_uint32_t *)out_len);\n\t\tif(err != 0) {\n\t\t\t//TODO\n\t\t}\n\t} else {\n\t\tint err = speex_resampler_process_interleaved_int(participant->downsampler, (spx_int16_t *)input, (spx_uint32_t *)in_len, (spx_int16_t *)output, (spx_uint32_t *)out_len);\n\t\tif(err != 0) {\n\t\t\t//TODO\n\t\t}\n\t}\n}\n#endif\n"
  },
  {
    "path": "src/plugins/janus_duktape.c",
    "content": "/*! \\file   janus_duktape.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus JavaScript plugin (via Duktape)\n * \\details Check the \\ref duktape for more details.\n *\n * \\ingroup plugins\n * \\ingroup jspapi\n * \\ref plugins\n * \\ref jspapi\n *\n * \\page duktape JavaScript (Duktape) plugin documentation\n * This is a plugin that implements a simple bridge to JavaScript via\n * Duktape. While the plugin implements low level stuff like media\n * manipulation, routing, recording, etc., all the logic is demanded\n * to an external JavaScript script. This means that the C code exposes functions\n * to the JavaScript script (e.g., to dictate what to do with media, whether\n * recording should be done, sending PLIs, etc.), while JavaScript exposes\n * functions to be notified by the C code about important events (e.g.,\n * new users, WebRTC state, incoming messages, etc.).\n *\n * Considering the C code and the JavaScript script will need some sort of\n * \"contract\" in order to be able to properly interact with each other,\n * the interface (as in method names) must be consistent, but the logic\n * in the JavaScript script can be completely customized, so that it fits\n * whatever requirement one has (e.g., something like the EchoTest, or\n * something like the VideoRoom).\n *\n * \\section jsapi JavaScript interfaces\n *\n * Every JavaScript script that wants to implement a Janus plugin must provide\n * the following functions as callbacks:\n *\n * - \\c init(): called when janus_duktape.c is initialized;\n * - \\c destroy(): called when janus_duktape.c is deinitialized (Janus shutting down);\n * - \\c createSession(): called when a new user attaches to the Janus Duktape plugin;\n * - \\c destroySession(): called when an attached user detaches from the Janus Duktape plugin;\n * - \\c querySession(): called when an Admin API query for a specific user gets to the Janus Duktape plugin;\n * - \\c handleMessage(): called when a user sends a message to the Janus Duktape plugin;\n * - \\c setupMedia(): called when a users's WebRTC PeerConnection goes up;\n * - \\c hangupMedia(): called when a users's WebRTC PeerConnection goes down;\n * - \\c resumeScheduler(): called by the C scheduler to resume coroutines.\n *\n * While \\c init() expects a path to a config file (which you can ignore if\n * unneeded), and \\c destroy() and \\c resumeScheduler() don't need any\n * argument, all other functions expect at the very least a numeric session\n * identifier, that will uniquely address a user in the plugin. Such a\n * value is created dynamically by the C code, and so all the JavaScript script\n * needs to do is track it as a unique session identifier when handling\n * requests and pushing responses/events/actions towards the C code.\n * Refer to the existing examples (e.g., \\c echotest.js) to see the\n * exact signature for all the above callbacks.\n *\n * \\note Notice that, along the above mentioned callbacks, JavaScript scripts\n * can also implement functions like \\c incomingRtp() \\c incomingRtcp()\n * \\c incomingTextData() and \\c incomingBinaryData() to handle those packets\n * directly, instead of letting the C code worry about relaying/processing\n * them. While it might make sense to handle incoming data channel messages\n * with \\c incomingTextData() or \\c incomingBinaryData\n * though, the performance impact of directly processing and manipulating\n * RTP an RTCP packets is probably too high, and so their usage is currently\n * discouraged. The \\c dataReady() callback can be used to figure out when\n * data can be sent. As an additional note, JavaScript scripts can also decide to\n * implement the functions that return information about the plugin itself,\n * namely \\c getVersion() \\c getVersionString() \\c getDescription()\n * \\c getName() \\c getAuthor() and \\c getPackage(). If not implemented,\n * the JavaScript plugin will return its own info (i.e., \"janus.plugin.javascript\", etc.).\n * Most of the times, JavaScript scripts will not need to override this information,\n * unless they really want to register their own name spaces and versioning.\n * JavaScript scripts can also receive information on slow links via the\n * \\c slowLink() callback, in order to react accordingly: e.g., reduce\n * the bitrate of a video sender if they, or their viewers, are experiencing\n * issues. Finally, in case simulcast is used, JavaScript scripts may\n * receive events on substream and/or temporal layer changes happening\n * for receiving sessions via the \\c substreamChanged() and the\n * \\c temporalLayerChanged() callbacks: this may be useful to track\n * which layer is actually being sent, vs. what was requested.\n *\n * \\section dtcapi C interfaces\n *\n * Just as the JavaScript script needs to expose callbacks that the C code can\n * invoke, the C code exposes methods as JavaScript functions accessible from\n * the JavaScript script. This includes means to push events, configure how\n * media should be routed without handling each packet in JavaScript, sending\n * RTCP feedback, start/stop recording and so on.\n *\n * The following are the functions the C code exposes:\n *\n * - \\c pushEvent(): push an event to the user via Janus API;\n * - \\c eventsIsEnabled(): check if Event Handlers are enabled in the core;\n * - \\c notifyEvent(): send an event to Event Handlers;\n * - \\c closePc(): force the closure of a PeerConnection;\n * - \\c configureMedium(): specify whether audio/video/data can be received/sent;\n * - \\c addRecipient(): specify which user should receive a user's media;\n * - \\c removeRecipient(): specify which user should not receive a user's media anymore;\n * - \\c setBitrate(): specify the bitrate to force on a user via REMB feedback;\n * - \\c setPliFreq(): specify how often the plugin should send a PLI to this user;\n * - \\c setSubstream(): set the target simulcast substream;\n * - \\c setTemporalLayer(): set the target simulcast temporal layer;\n * - \\c sendPli(): send a PLI (keyframe request);\n * - \\c startRecording(): start recording audio, video and or data for a user;\n * - \\c stopRecording(): start recording audio, video and or data for a user;\n * - \\c pokeScheduler(): notify the C code that there's a coroutine to resume;\n * - \\c timeCallback(): trigger the execution of a JavaScript function after X milliseconds.\n *\n * As anticipated in the previous section, almost all these methods also\n * expect the unique session identifier to address a specific user in the\n * plugin. This is true for all the above methods expect \\c eventsIsEnabled\n * and, more importantly, both \\c timeCallback() and \\c pokeScheduler() which,\n * together with JavaScript's \\c resumeScheduler(), will be clearer in the next section.\n *\n * \\section jcoroutines JavaScript/C coroutines scheduler\n *\n * Duktape is a single threaded environment. While it has a concept similar\n * to threads called coroutines, these are not threads as known in C.\n * In order to allow for an easy to implement asynchronous behaviour in\n * JavaScript scripts, you can leverage a scheduler implemented in the C code.\n *\n * More specifically, when the plugin starts a dedicated thread is devoted\n * to the only purpose of acting as a scheduler for JavaScript coroutines. This\n * means that, whenever this C scheduler is awaken, it will call the\n * \\c resumeScheduler() function in the JavaScript script, thus allowing the\n * JavaScript script to execute one or more pending coroutines. The C scheduler\n * only acts when triggered, which means it's up to the JavaScript script to\n * tell it when to wake up: this is possible via the \\c pokeScheduler()\n * function, which does nothing more than sending a simple signal to the\n * C scheduler to wake it up. As such, it's easy for the JavaScript script to\n * implement asynchronous behaviour, e.g.:\n *\n * 1. JavaScript script needs to do something asynchronously;\n * 2. JavaScript script creates coroutine, and takes note of it somewhere;\n * 3. JavaScript script calls \\c pokeScheduler();\n * 4. C code sends signal to the thread acting as a scheduler;\n * 5. when the scheduling thread wakes up, it calls \\c resumeScheduler();\n * 6. JavaScript script resumes the previously queued coroutine.\n *\n * This simple mechanism is what the sample JavaScript scripts provided in this\n * repo use, for instance, to handle incoming messages asynchronously,\n * so you can refer to those to have an idea of how it can be used. The\n * next section will address \\ref jtimers instead.\n *\n * \\note You can implement asynchronous behaviour any way you want, and\n * you're not required to use this C scheduler. Anyway, you must implement\n * a method called \\c resumeScheduler() anyway, as the C code checks for\n * its presence and fails if it's not there. If you don't need it, just\n * create an empty function that does nothing and you'll be fine.\n *\n * \\section jtimers JavaScript/C time-based scheduler\n *\n * Another helpful way to implement asynchronous behaviour is with the\n * help of the \\c timeCallback() function. Specifically, this function\n * implements a mechanism to ask for a specific JavaScript method to be invoked\n * after a provided amount of time. To specify the function to invoke,\n * an optional argument to pass (which MUST be a string) and the time to\n * wait to do that. This is particularly helpful when you're handling\n * asynchronous behaviour that you want to inspect on a regular basis.\n *\n * The \\c timeCallback() function expects three arguments:\n *\n * \\verbatim\ntimeCallback(function, argument, milliseconds);\n\\endverbatim\n *\n * The only mandatory parameter is \\c function: if you set \\c argument\n * to \\c null no argument will be passed to \\c function when it's executed;\n * it \\c milliseconds is 0, \\c function will be executed as soon as possible.\n *\n * \\verbatim\n// This will cause an error (timeCallback needs a function)\ntimeCallback();\n// Invoke test() in 500 milliseconds\ntimeCallback(\"test\", null, 500);\n// Invoke test(\"ciccio\") in 2 seconds\ntimeCallback(\"test\", \"ciccio\", 2000);\n\\endverbatim\n *\n * Notice that \\c timeCallback() allows you to formally recreate the\n * mechanism \\c pokeScheduler() and \\c resumeScheduler() implement, as\n * the following is pretty much an equivalent of that:\n *\n * \\verbatim\ntimeCallback(\"resumeScheduler\", null, 0);\n\\endverbatim\n *\n * Anyway, \\c pokeScheduler() and \\c resumeScheduler() is much more\n * compact and less verbose, and as such is preferred in cases where\n * timing and opaque arguments are not needed.\n *\n * Refer to the \\ref jspapi section for more information on how you\n * can register your own C functions.\n */\n\n#include <jansson.h>\n\n/* Session definition and hashtable */\n#include \"janus_duktape_data.h\"\n/* Extra/custom C hooks and code */\n#include \"janus_duktape_extra.h\"\n\n\n/* Plugin information */\n#define JANUS_DUKTAPE_VERSION\t\t\t1\n#define JANUS_DUKTAPE_VERSION_STRING\t\"0.0.1\"\n#define JANUS_DUKTAPE_DESCRIPTION\t\t\"A custom plugin for implementing the logic in JavaScript, via Duktape.\"\n#define JANUS_DUKTAPE_NAME\t\t\t\t\"Janus JavaScript plugin (Duktape)\"\n#define JANUS_DUKTAPE_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_DUKTAPE_PACKAGE\t\t\t\"janus.plugin.duktape\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_duktape_init(janus_callbacks *callback, const char *config_path);\nvoid janus_duktape_destroy(void);\nint janus_duktape_get_api_compatibility(void);\nint janus_duktape_get_version(void);\nconst char *janus_duktape_get_version_string(void);\nconst char *janus_duktape_get_description(void);\nconst char *janus_duktape_get_name(void);\nconst char *janus_duktape_get_author(void);\nconst char *janus_duktape_get_package(void);\nvoid janus_duktape_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_duktape_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_duktape_handle_admin_message(json_t *message);\nvoid janus_duktape_setup_media(janus_plugin_session *handle);\nvoid janus_duktape_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_duktape_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_duktape_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet);\nvoid janus_duktape_data_ready(janus_plugin_session *handle);\nvoid janus_duktape_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink);\nvoid janus_duktape_hangup_media(janus_plugin_session *handle);\nvoid janus_duktape_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_duktape_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_duktape_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_duktape_init,\n\t\t.destroy = janus_duktape_destroy,\n\n\t\t.get_api_compatibility = janus_duktape_get_api_compatibility,\n\t\t.get_version = janus_duktape_get_version,\n\t\t.get_version_string = janus_duktape_get_version_string,\n\t\t.get_description = janus_duktape_get_description,\n\t\t.get_name = janus_duktape_get_name,\n\t\t.get_author = janus_duktape_get_author,\n\t\t.get_package = janus_duktape_get_package,\n\n\t\t.create_session = janus_duktape_create_session,\n\t\t.handle_message = janus_duktape_handle_message,\n\t\t.handle_admin_message = janus_duktape_handle_admin_message,\n\t\t.setup_media = janus_duktape_setup_media,\n\t\t.incoming_rtp = janus_duktape_incoming_rtp,\n\t\t.incoming_rtcp = janus_duktape_incoming_rtcp,\n\t\t.incoming_data = janus_duktape_incoming_data,\n\t\t.data_ready = janus_duktape_data_ready,\n\t\t.slow_link = janus_duktape_slow_link,\n\t\t.hangup_media = janus_duktape_hangup_media,\n\t\t.destroy_session = janus_duktape_destroy_session,\n\t\t.query_session = janus_duktape_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_DUKTAPE_NAME);\n\treturn &janus_duktape_plugin;\n}\n\n/* Useful stuff */\nvolatile gint duktape_initialized = 0, duktape_stopping = 0;\njanus_callbacks *duktape_janus_core = NULL;\nstatic char *duktape_folder = NULL;\n\n/* Duktape stuff */\nduk_context *duktape_ctx = NULL;\njanus_mutex duktape_mutex = JANUS_MUTEX_INITIALIZER;\nstatic const char *duktape_functions[] = {\n\t\"init\", \"destroy\", \"resumeScheduler\",\n\t\"createSession\", \"destroySession\", \"querySession\",\n\t\"handleMessage\",\n\t\"setupMedia\", \"hangupMedia\"\n};\nstatic uint duktape_funcsize = sizeof(duktape_functions)/sizeof(*duktape_functions);\n/* Some bindings are optional */\nstatic gboolean has_get_version = FALSE;\nstatic int duktape_script_version = -1;\nstatic gboolean has_get_version_string = FALSE;\nstatic char *duktape_script_version_string = NULL;\nstatic gboolean has_get_description = FALSE;\nstatic char *duktape_script_description = NULL;\nstatic gboolean has_get_name = FALSE;\nstatic char *duktape_script_name = NULL;\nstatic gboolean has_get_author = FALSE;\nstatic char *duktape_script_author = NULL;\nstatic gboolean has_get_package = FALSE;\nstatic char *duktape_script_package = NULL;\nstatic gboolean has_handle_admin_message = FALSE;\nstatic gboolean has_incoming_rtp = FALSE;\nstatic gboolean has_incoming_rtcp = FALSE;\nstatic gboolean has_incoming_data_legacy = FALSE,\t/* Legacy callback */\n\thas_incoming_text_data = FALSE,\n\thas_incoming_binary_data = FALSE;\nstatic gboolean has_data_ready = FALSE;\nstatic gboolean has_slow_link = FALSE;\nstatic gboolean has_substream_changed = FALSE;\nstatic gboolean has_temporal_changed = FALSE;\n/* JavaScript C scheduler (for coroutines) */\nstatic GThread *scheduler_thread = NULL;\nstatic void *janus_duktape_scheduler(void *data);\nstatic GAsyncQueue *events = NULL;\ntypedef enum janus_duktape_event {\n\tjanus_duktape_event_none = 0,\n\tjanus_duktape_event_resume,\t\t/* Resume one or more pending coroutines */\n\tjanus_duktape_event_exit\t\t/* Break the scheduler loop */\n} janus_duktape_event;\n/* JavaScript timer loop (for scheduled callbacks) */\nstatic GMainContext *timer_context = NULL;\nstatic GMainLoop *timer_loop = NULL;\nstatic GThread *timer_thread = NULL;\nstatic void *janus_duktape_timer(void *data);\nstatic gboolean janus_duktape_timer_cb(void *data);\ntypedef struct janus_duktape_callback {\n\tguint id;\n\tuint32_t ms;\n\tGSource *source;\n\tchar *function;\n\tchar *argument;\n} janus_duktape_callback;\nstatic GHashTable *callbacks = NULL;\nstatic void janus_duktape_callback_free(janus_duktape_callback *cb) {\n\tif(!cb)\n\t\treturn;\n\tg_source_destroy(cb->source);\n\tg_source_unref(cb->source);\n\tg_free(cb->function);\n\tg_free(cb->argument);\n\tg_free(cb);\n}\n\n/* Helper function to sample the number of occupied slots into JavaScript stack */\nstatic void janus_duktape_stackdump(duk_context *ctx) {\n\tint top = duk_get_top(ctx);\n\tJANUS_LOG(LOG_HUGE, \"Total in Duktape stack: %d\\n\", top);\n}\n\n/* janus_duktape_session is defined in janus_duktape_data.h, but it's managed here */\nGHashTable *duktape_sessions, *duktape_ids;\njanus_mutex duktape_sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_duktape_session_destroy(janus_duktape_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n}\n\nstatic void janus_duktape_session_free(const janus_refcount *session_ref) {\n\tjanus_duktape_session *session = janus_refcount_containerof(session_ref, janus_duktape_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tg_hash_table_remove(duktape_ids, GUINT_TO_POINTER(session->id));\n\tjanus_recorder_destroy(session->arc);\n\tjanus_recorder_destroy(session->vrc);\n\tjanus_recorder_destroy(session->drc);\n\tjanus_mutex_destroy(&session->rec_mutex);\n\tjanus_mutex_destroy(&session->recipients_mutex);\n\tjanus_mutex_destroy(&session->rid_mutex);\n\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\tg_free(session);\n}\n\n/* Packet data and routing */\ntypedef struct janus_duktape_rtp_relay_packet {\n\tjanus_duktape_session *sender;\n\tjanus_rtp_header *data;\n\tgint length;\n\tjanus_plugin_rtp_extensions extensions;\n\tgboolean is_rtp;\t/* This may be a data packet and not RTP */\n\tgboolean is_video;\n\tuint32_t ssrc[3];\n\tuint32_t timestamp;\n\tuint16_t seq_number;\n\t/* The following is only relevant for datachannels */\n\tgboolean textdata;\n} janus_duktape_rtp_relay_packet;\nstatic void janus_duktape_relay_rtp_packet(gpointer data, gpointer user_data);\nstatic void janus_duktape_relay_data_packet(gpointer data, gpointer user_data);\n\n\n/* Helper struct to address outgoing notifications, e.g., involving PeerConnections */\ntypedef enum janus_duktape_async_event_type {\n\tjanus_duktape_async_event_type_none = 0,\n\tjanus_duktape_async_event_type_pushevent\n} janus_duktape_async_event_type;\ntypedef struct janus_duktape_async_event {\n\tjanus_duktape_session *session;\t\t\t/* Who this event is for */\n\tjanus_duktape_async_event_type type;\t/* What this event is about */\n\tchar *transaction;\t\t\t\t\t/* Notification transaction, if any */\n\tjson_t *event;\t\t\t\t\t\t/* Content of the notification, if any */\n\tjson_t *jsep;\t\t\t\t\t\t/* Content of JSEP SDP, if any */\n} janus_duktape_async_event;\n/* Helper thread to push events that need to be asynchronous, e.g., for those\n * that would keep the Duktape context busy longer than usual and cause delays,\n * or those that might actually result in a deadlock if done synchronously */\nstatic void *janus_duktape_async_event_helper(void *data) {\n\tjanus_duktape_async_event *asev = (janus_duktape_async_event *)data;\n\tif(asev == NULL)\n\t\treturn NULL;\n\tif(asev->type == janus_duktape_async_event_type_pushevent) {\n\t\t/* Send the event */\n\t\tduktape_janus_core->push_event(asev->session->handle, &janus_duktape_plugin, asev->transaction, asev->event, asev->jsep);\n\t}\n\tjson_decref(asev->event);\n\tjson_decref(asev->jsep);\n\tg_free(asev->transaction);\n\tjanus_refcount_decrease(&asev->session->ref);\n\tg_free(asev);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\n\n/* Helper method to stringify Duktape types */\n#define DUK_CASE_STR(type) case type: return #type\nstatic const char *janus_duktape_type_string(int type) {\n\tswitch(type) {\n\t\tDUK_CASE_STR(DUK_TYPE_NONE);\n\t\tDUK_CASE_STR(DUK_TYPE_UNDEFINED);\n\t\tDUK_CASE_STR(DUK_TYPE_NULL);\n\t\tDUK_CASE_STR(DUK_TYPE_BOOLEAN);\n\t\tDUK_CASE_STR(DUK_TYPE_NUMBER);\n\t\tDUK_CASE_STR(DUK_TYPE_STRING);\n\t\tDUK_CASE_STR(DUK_TYPE_OBJECT);\n\t\tDUK_CASE_STR(DUK_TYPE_BUFFER);\n\t\tDUK_CASE_STR(DUK_TYPE_POINTER);\n\t\tDUK_CASE_STR(DUK_TYPE_LIGHTFUNC);\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\n\n/* Methods that we expose to the JavaScript script */\nstatic duk_ret_t janus_duktape_method_getmodulesfolder(duk_context *ctx) {\n\t/* This method returns the folder that was configured in the settings, for modules */\n\tduk_push_string(ctx, duktape_folder ? duktape_folder : \".\");\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_getversion(duk_context *ctx) {\n\tduk_push_int(ctx, DUK_VERSION);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_readfile(duk_context *ctx) {\n\t/* Helper method to read a text file and return its content as a string */\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tconst char *filename = duk_get_string(ctx, 0);\n\tFILE *f = fopen(filename, \"rb\");\n\tif(f == NULL) {\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Error opening file: %s\\n\", filename);\n\t\treturn duk_throw(ctx);\n\t}\n\tfseek(f, 0, SEEK_END);\n\tint len = (int)ftell(f);\n\tif(len < 0) {\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Error opening file: %s\\n\", g_strerror(errno));\n\t\tfclose(f);\n\t\treturn duk_throw(ctx);\n\t}\n\tfseek(f, 0, SEEK_SET);\n\tchar *text = g_malloc(len);\n\tsize_t offset = 0, r = 0, t = len;\n\twhile(t > 0) {\n\t\tr = fread(text+offset, 1, t, f);\n\t\tif(r == 0) {\n\t\t\tfclose(f);\n\t\t\tg_free(text);\n\t\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Error reading file: %s\\n\", filename);\n\t\t\treturn duk_throw(ctx);\n\t\t}\n\t\tt -= r;\n\t}\n\tduk_push_lstring(ctx, text, len);\n\tfclose(f);\n\tg_free(text);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_pokescheduler(duk_context *ctx) {\n\t/* This method allows the JavaScript script to poke the scheduler and have it wake up ASAP */\n\tg_async_queue_push(events, GUINT_TO_POINTER(janus_duktape_event_resume));\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_timecallback(duk_context *ctx) {\n\t/* This method allows the JS script to schedule a callback after a specified amount of time */\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_STRING && duk_get_type(ctx, 1) != DUK_TYPE_UNDEFINED) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 2) != DUK_TYPE_NUMBER && duk_get_type(ctx, 2) != DUK_TYPE_UNDEFINED) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 2)));\n\t\treturn duk_throw(ctx);\n\t}\n\tconst char *function = duk_get_string(ctx, 0);\n\tconst char *argument = duk_get_string(ctx, 1);\n\tuint32_t ms = (uint32_t)duk_get_number(ctx, 2);\n\t/* Create a callback instance */\n\tjanus_duktape_callback *cb = g_malloc0(sizeof(janus_duktape_callback));\n\tcb->function = g_strdup(function);\n\tif(argument != NULL)\n\t\tcb->argument = g_strdup(argument);\n\tcb->ms = ms;\n\tcb->source = g_timeout_source_new(ms);\n\tg_source_set_callback(cb->source, janus_duktape_timer_cb, cb, NULL);\n\tg_hash_table_insert(callbacks, cb, cb);\n\tcb->id = g_source_attach(cb->source, timer_context);\n\tJANUS_LOG(LOG_VERB, \"Created scheduled callback (%\"SCNu32\"ms) with ID %u\\n\", cb->ms, cb->id);\n\t/* Done */\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_pushevent(duk_context *ctx) {\n\t/* Get the arguments from the provided context */\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_STRING &&\n\t\t\tduk_get_type(ctx, 1) != DUK_TYPE_UNDEFINED && duk_get_type(ctx, 1) != DUK_TYPE_NULL) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 2) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 2)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 3) != DUK_TYPE_STRING &&\n\t\t\tduk_get_type(ctx, 3) != DUK_TYPE_UNDEFINED && duk_get_type(ctx, 3) != DUK_TYPE_NULL) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 3)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tconst char *transaction = duk_get_string(ctx, 1);\n\tconst char *event_text = duk_get_string(ctx, 2);\n\tconst char *jsep_text = duk_get_string(ctx, 3);\n\t/* Parse the event/jsep strings to Jansson objects */\n\tjson_error_t error;\n\tjson_t *event = json_loads(event_text, 0, &error);\n\tif(!event) {\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\treturn duk_throw(ctx);\n\t}\n\tjson_t *jsep = NULL;\n\tif(jsep_text != NULL) {\n\t\tjsep = json_loads(jsep_text, 0, &error);\n\t\tif(!jsep) {\n\t\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\t\tjson_decref(event);\n\t\t\treturn duk_throw(ctx);\n\t\t}\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tjson_decref(event);\n\t\tif(jsep)\n\t\t\tjson_decref(jsep);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* If there's an SDP attached, create a thread to send the event asynchronously:\n\t * sending it here would keep the locked Duktape context busy much longer than intended */\n\tif(jsep != NULL) {\n\t\t/* Let's parse the SDP first, though */\n\t\tconst char *sdp = json_string_value(json_object_get(jsep, \"sdp\"));\n\t\tconst char *sdp_type = json_string_value(json_object_get(jsep, \"type\"));\n\t\tchar error_str[512];\n\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(sdp, error_str, sizeof(error_str));\n\t\tif(parsed_sdp == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing answer: %s\\n\", error_str);\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(jsep);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Error parsing answer: %s\", error_str);\n\t\t\treturn duk_throw(ctx);\n\t\t}\n\t\tjanus_duktape_async_event *asev = g_malloc0(sizeof(janus_duktape_async_event));\n\t\tasev->session = session;\n\t\tasev->type = janus_duktape_async_event_type_pushevent;\n\t\tasev->transaction = transaction ? g_strdup(transaction) : NULL;\n\t\tasev->event = event;\n\t\tasev->jsep = jsep;\n\t\tif(json_is_true(json_object_get(jsep, \"e2ee\")))\n\t\t\tsession->e2ee = TRUE;\n\t\tif(sdp_type && !strcasecmp(sdp_type, \"answer\")) {\n\t\t\t/* Take note of which video codec were negotiated */\n\t\t\tconst char *vcodec = NULL;\n\t\t\tjanus_sdp_find_first_codec(parsed_sdp, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\tif(vcodec)\n\t\t\t\tsession->vcodec = janus_videocodec_from_name(vcodec);\n\t\t}\n\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t/* Send asynchronously */\n\t\tGError *error = NULL;\n\t\tg_thread_try_new(\"duktape pushevent\", janus_duktape_async_event_helper, asev, &error);\n\t\tif(error != NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Duktape pushevent thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(jsep);\n\t\t\tg_free(asev->transaction);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tg_free(asev);\n\t\t}\n\t\t/* Return a success/error right away */\n\t\tif(error) {\n\t\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Error spawning pushevent thread\");\n\t\t\treturn duk_throw(ctx);\n\t\t}\n\t\tduk_push_int(ctx, 0);\n\t\treturn 1;\n\t}\n\t/* No SDP, send the event now */\n\tint res = duktape_janus_core->push_event(session->handle, &janus_duktape_plugin, transaction, event, NULL);\n\tjanus_refcount_decrease(&session->ref);\n\tjson_decref(event);\n\tduk_push_int(ctx, res);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_notifyevent(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tconst char *event_text = duk_get_string(ctx, 1);\n\tif(event_text == NULL)\n\t\treturn duk_throw(ctx);\n\t/* Get the arguments from the provided context */\n\tif(!duktape_janus_core->events_is_enabled()) {\n\t\t/* Event handlers are disabled in the core, ignoring */\n\t\tduk_push_int(ctx, 0);\n\t\treturn 1;\n\t}\n\t/* Parse the event/jsep strings to Jansson objects */\n\tjson_error_t error;\n\tjson_t *event = json_loads(event_text, 0, &error);\n\tif(!event) {\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\treturn duk_throw(ctx);\n\t}\n\t/* Find the session (optional) */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session != NULL)\n\t\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Notify the event */\n\tduktape_janus_core->notify_event(&janus_duktape_plugin, session ? session->handle : NULL, event);\n\tif(session != NULL)\n\t\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_eventsisenabled(duk_context *ctx) {\n\t/* Return info on whether event handlers are enabled in the core or not */\n\tduk_push_int(ctx, duktape_janus_core->events_is_enabled());\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_closepc(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Close the PeerConnection */\n\tduktape_janus_core->close_pc(session->handle);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_endsession(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Close the plugin handle */\n\tduktape_janus_core->end_session(session->handle);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_configuremedium(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 2) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 2)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 3) != DUK_TYPE_BOOLEAN) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_BOOLEAN), janus_duktape_type_string(duk_get_type(ctx, 3)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tconst char *medium = duk_get_string(ctx, 1);\n\tconst char *direction = duk_get_string(ctx, 2);\n\tint enabled = duk_get_boolean(ctx, 3);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Modify the session media property */\n\tif(medium && direction) {\n\t\tif(!strcasecmp(medium, \"audio\")) {\n\t\t\tif(!strcasecmp(direction, \"in\")) {\n\t\t\t\tsession->accept_audio = enabled ? TRUE : FALSE;\n\t\t\t} else {\n\t\t\t\tsession->send_audio = enabled ? TRUE : FALSE;\n\t\t\t}\n\t\t} else if(!strcasecmp(medium, \"video\")) {\n\t\t\tif(!strcasecmp(direction, \"in\")) {\n\t\t\t\tsession->accept_video = enabled ? TRUE : FALSE;\n\t\t\t} else {\n\t\t\t\tsession->send_video = enabled ? TRUE : FALSE;\n\t\t\t}\n\t\t} else if(!strcasecmp(medium, \"data\")) {\n\t\t\tif(!strcasecmp(direction, \"in\")) {\n\t\t\t\tsession->accept_data = enabled ? TRUE : FALSE;\n\t\t\t} else {\n\t\t\t\tsession->send_data = enabled ? TRUE : FALSE;\n\t\t\t}\n\t\t}\n\t}\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_addrecipient(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tuint32_t rid = (uint32_t)duk_get_number(ctx, 1);\n\t/* Find the sessions */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->recipients_mutex);\n\tjanus_duktape_session *recipient = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(rid));\n\tif(recipient == NULL || g_atomic_int_get(&recipient->destroyed)) {\n\t\tjanus_mutex_unlock(&session->recipients_mutex);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Recipient session %\"SCNu32\" doesn't exist\", rid);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&recipient->ref);\n\t/* Add to the list of recipients */\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(g_slist_find(session->recipients, recipient) == NULL) {\n\t\tjanus_refcount_increase(&session->ref);\n\t\tjanus_refcount_increase(&recipient->ref);\n\t\tsession->recipients = g_slist_append(session->recipients, recipient);\n\t\trecipient->sender = session;\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tjanus_refcount_decrease(&recipient->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_removerecipient(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tuint32_t rid = (uint32_t)duk_get_number(ctx, 1);\n\t/* Find the sessions */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->recipients_mutex);\n\tjanus_duktape_session *recipient = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(rid));\n\tif(recipient == NULL) {\n\t\tjanus_mutex_unlock(&session->recipients_mutex);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Recipient session %\"SCNu32\" doesn't exist\", rid);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&recipient->ref);\n\t/* Remove from the list of recipients */\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tgboolean unref = FALSE;\n\tif(g_slist_find(session->recipients, recipient) != NULL) {\n\t\tsession->recipients = g_slist_remove(session->recipients, recipient);\n\t\trecipient->sender = NULL;\n\t\tunref = TRUE;\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\tif(unref) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&recipient->ref);\n\t}\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tjanus_refcount_decrease(&recipient->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_setbitrate(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tuint32_t bitrate = (uint32_t)duk_get_number(ctx, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tsession->bitrate = bitrate;\n\t/* Send a REMB right away too, if the PeerConnection is up */\n\tif(g_atomic_int_get(&session->started)) {\n\t\t/* No limit ~= 10000000 */\n\t\tduktape_janus_core->send_remb(session->handle, session->bitrate ? session->bitrate : 10000000);\n\t}\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_setplifreq(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tuint16_t pli_freq = (uint16_t)duk_get_number(ctx, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tsession->pli_freq = pli_freq;\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_setsubstream(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tuint16_t substream = (uint16_t)duk_get_number(ctx, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(substream <= 2)\n\t\tsession->sim_context.substream_target = substream;\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_settemporallayer(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tuint16_t temporal = (uint16_t)duk_get_number(ctx, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(temporal <= 2)\n\t\tsession->sim_context.templayer_target = temporal;\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_sendpli(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Send a PLI */\n\tsession->pli_latest = janus_get_monotonic_time();\n\tJANUS_LOG(LOG_HUGE, \"Sending PLI to session %\"SCNu32\"\\n\", session->id);\n\tduktape_janus_core->send_pli(session->handle);\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_relayrtp(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_BOOLEAN) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_BOOLEAN), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 2) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 2)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 3) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 3)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tint is_video = duk_get_boolean(ctx, 1);\n\tconst char *payload = duk_get_string(ctx, 2);\n\tint len = (int)duk_get_number(ctx, 3);\n\tif(payload == NULL || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid payload\\n\");\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Invalid payload of declared size %d\", len);\n\t\treturn duk_throw(ctx);\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Send the RTP packet */\n\tjanus_plugin_rtp rtp = { .mindex = -1, .video = is_video, .buffer = (char *)payload, .length = len };\n\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\tduktape_janus_core->relay_rtp(session->handle, &rtp);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_relayrtcp(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_BOOLEAN) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_BOOLEAN), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 2) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 2)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 3) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 3)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tint is_video = duk_get_boolean(ctx, 1);\n\tconst char *payload = duk_get_string(ctx, 2);\n\tint len = (int)duk_get_number(ctx, 3);\n\tif(payload == NULL || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid payload\\n\");\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Invalid payload of declared size %d\", len);\n\t\treturn duk_throw(ctx);\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Send the RTCP packet */\n\tjanus_plugin_rtcp rtcp = { .video = is_video, .buffer = (char *)payload, .length = len };\n\tduktape_janus_core->relay_rtcp(session->handle, &rtcp);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_relaytextdata(duk_context *ctx) {\n\tint n = duk_get_top(ctx);\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 2) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 2)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(n > 3 && duk_get_type(ctx, 3) != DUK_TYPE_STRING &&\n\t\t\tduk_get_type(ctx, 3) != DUK_TYPE_UNDEFINED && duk_get_type(ctx, 3) != DUK_TYPE_NULL) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 3)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(n > 4 && duk_get_type(ctx, 4) != DUK_TYPE_STRING &&\n\t\t\tduk_get_type(ctx, 4) != DUK_TYPE_UNDEFINED && duk_get_type(ctx, 4) != DUK_TYPE_NULL) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 4)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tconst char *payload = duk_get_string(ctx, 1);\n\tint len = (int)duk_get_number(ctx, 2);\n\tif(payload == NULL || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid data\\n\");\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Invalid payload of declared size %d\", len);\n\t\treturn duk_throw(ctx);\n\t}\n\t/* Check if label and/or protocol were provided as well */\n\tconst char *label = NULL, *protocol = NULL;\n\tif(n > 3 && duk_get_type(ctx, 3) == DUK_TYPE_STRING)\n\t\tlabel = duk_get_string(ctx, 3);\n\tif(n > 4 && duk_get_type(ctx, 4) == DUK_TYPE_STRING)\n\t\tprotocol = duk_get_string(ctx, 4);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(!g_atomic_int_get(&session->dataready)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Datachannel not ready yet for session %\"SCNu32, id);\n\t\treturn duk_throw(ctx);\n\t}\n\t/* Send the data */\n\tjanus_plugin_data data = {\n\t\t.label = (char *)label,\n\t\t.protocol = (char *)protocol,\n\t\t.binary = FALSE,\n\t\t.buffer = (char *)payload,\n\t\t.length = len\n\t};\n\tduktape_janus_core->relay_data(session->handle, &data);\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_relaybinarydata(duk_context *ctx) {\n\tint n = duk_get_top(ctx);\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 1) != DUK_TYPE_STRING) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 1)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(duk_get_type(ctx, 2) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 2)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(n > 3 && duk_get_type(ctx, 3) != DUK_TYPE_STRING &&\n\t\t\tduk_get_type(ctx, 3) != DUK_TYPE_UNDEFINED && duk_get_type(ctx, 3) != DUK_TYPE_NULL) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 3)));\n\t\treturn duk_throw(ctx);\n\t}\n\tif(n > 4 && duk_get_type(ctx, 4) != DUK_TYPE_STRING &&\n\t\t\tduk_get_type(ctx, 4) != DUK_TYPE_UNDEFINED && duk_get_type(ctx, 4) != DUK_TYPE_NULL) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_STRING), janus_duktape_type_string(duk_get_type(ctx, 4)));\n\t\treturn duk_throw(ctx);\n\t}\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\tconst char *payload = duk_get_string(ctx, 1);\n\tint len = (int)duk_get_number(ctx, 2);\n\tif(payload == NULL || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid data\\n\");\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Invalid payload of declared size %d\", len);\n\t\treturn duk_throw(ctx);\n\t}\n\t/* Check if label and/or protocol were provided as well */\n\tconst char *label = NULL, *protocol = NULL;\n\tif(n > 3 && duk_get_type(ctx, 3) == DUK_TYPE_STRING)\n\t\tlabel = duk_get_string(ctx, 3);\n\tif(n > 4 && duk_get_type(ctx, 4) == DUK_TYPE_STRING)\n\t\tprotocol = duk_get_string(ctx, 4);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(!g_atomic_int_get(&session->dataready)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Datachannel not ready yet for session %\"SCNu32, id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_plugin_data data = {\n\t\t.label = (char *)label,\n\t\t.protocol = (char *)protocol,\n\t\t.binary = TRUE,\n\t\t.buffer = (char *)payload,\n\t\t.length = len\n\t};\n\tduktape_janus_core->relay_data(session->handle, &data);\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic int janus_duktape_method_relaydata(duk_context *ctx) {\n\tJANUS_LOG(LOG_WARN, \"Deprecated function 'relayData' called, invoking 'relayTextData' instead\\n\");\n\treturn janus_duktape_method_relaytextdata(ctx);\n}\n\nstatic duk_ret_t janus_duktape_method_startrecording(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tint n = duk_get_top(ctx);\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Iterate on all arguments, to see what we're being asked to record */\n\tint recordings = 0;\n\tn--;\n\tint i = 0;\n\tjanus_recorder *arc = NULL, *vrc = NULL, *drc = NULL;\n\twhile(n > 0) {\n\t\ti++; n--;\n\t\tconst char *type = duk_get_string(ctx, i);\n\t\ti++; n--;\n\t\tconst char *codec = duk_get_string(ctx, i);\n\t\ti++; n--;\n\t\tconst char *folder = duk_get_string(ctx, i);\n\t\ti++; n--;\n\t\tconst char *filename = duk_get_string(ctx, i);\n\t\tif(type == NULL || codec == NULL) {\n\t\t\t/* No type or codec provided, skip this */\n\t\t\tcontinue;\n\t\t}\n\t\t/* Check if the codec contains some fmtp stuff too */\n\t\tconst char *c = codec, *f = NULL;\n\t\tgchar **parts = NULL;\n\t\tif(strstr(codec, \"/fmtp=\") != NULL) {\n\t\t\tparts = g_strsplit(codec, \"/fmtp=\", 2);\n\t\t\tc = parts[0];\n\t\t\tf = parts[1];\n\t\t}\n\t\t/* Create the recorder */\n\t\tjanus_recorder *rc = janus_recorder_create_full(folder, c, f, filename);\n\t\tif(parts != NULL)\n\t\t\tg_strfreev(parts);\n\t\tif(rc == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating '%s' recorder...\\n\", type);\n\t\t\tgoto error;\n\t\t}\n\t\tif(!strcasecmp(type, \"audio\")) {\n\t\t\tif(arc != NULL || session->arc != NULL) {\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicate audio recording, skipping\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\tif(session->e2ee)\n\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\tarc = rc;\n\t\t} else if(!strcasecmp(type, \"video\")) {\n\t\t\tif(vrc != NULL || session->vrc != NULL) {\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicate video recording, skipping\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_rtp_switching_context_reset(&session->rec_ctx);\n\t\t\tjanus_rtp_simulcasting_context_reset(&session->rec_simctx);\n\t\t\tsession->rec_simctx.substream_target = 2;\n\t\t\tsession->rec_simctx.templayer_target = 2;\n\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\tif(session->e2ee)\n\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\tvrc = rc;\n\t\t} else if(!strcasecmp(type, \"data\")) {\n\t\t\tif(drc != NULL || session->drc != NULL) {\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicate data recording, skipping\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdrc = rc;\n\t\t}\n\t\trecordings++;\n\t}\n\tif(recordings == 0)\n\t\tgoto error;\n\tif(arc) {\n\t\tsession->arc = arc;\n\t}\n\tif(vrc) {\n\t\tsession->vrc = vrc;\n\t\t/* Also send a keyframe request */\n\t\tsession->pli_latest = janus_get_monotonic_time();\n\t\tJANUS_LOG(LOG_HUGE, \"Sending PLI to session %\"SCNu32\"\\n\", session->id);\n\t\tduktape_janus_core->send_pli(session->handle);\n\t}\n\tif(drc) {\n\t\tsession->drc = drc;\n\t}\n\tjanus_refcount_decrease(&session->ref);\n\tgoto done;\n\nerror:\n\tjanus_recorder_destroy(arc);\n\tjanus_recorder_destroy(vrc);\n\tjanus_recorder_destroy(drc);\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Something went wrong */\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Error starting the recording of session %\"SCNu32, id);\n\treturn duk_throw(ctx);\n\ndone:\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Done */\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\nstatic duk_ret_t janus_duktape_method_stoprecording(duk_context *ctx) {\n\tif(duk_get_type(ctx, 0) != DUK_TYPE_NUMBER) {\n\t\tduk_push_error_object(ctx, DUK_RET_TYPE_ERROR, \"Invalid argument (expected %s, got %s)\\n\",\n\t\t\tjanus_duktape_type_string(DUK_TYPE_NUMBER), janus_duktape_type_string(duk_get_type(ctx, 0)));\n\t\treturn duk_throw(ctx);\n\t}\n\tint n = duk_get_top(ctx);\n\tuint32_t id = (uint32_t)duk_get_number(ctx, 0);\n\t/* Find the session */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tduk_push_error_object(ctx, DUK_ERR_ERROR, \"Session %\"SCNu32\" doesn't exist\", id);\n\t\treturn duk_throw(ctx);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Iterate on all arguments, to see what which recording we're being asked to stop */\n\tn--;\n\tint i = 0;\n\twhile(n > 0) {\n\t\ti++; n--;\n\t\tconst char *type = duk_get_string(ctx, i);\n\t\tif(type == NULL)\n\t\t\tcontinue;\n\t\tif(!strcasecmp(type, \"audio\")) {\n\t\t\tif(session->arc != NULL) {\n\t\t\t\tjanus_recorder *rc = session->arc;\n\t\t\t\tsession->arc = NULL;\n\t\t\t\tjanus_recorder_close(rc);\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t}\n\t\t} else if(!strcasecmp(type, \"video\")) {\n\t\t\tif(session->vrc != NULL) {\n\t\t\t\tjanus_recorder *rc = session->vrc;\n\t\t\t\tsession->vrc = NULL;\n\t\t\t\tjanus_recorder_close(rc);\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t}\n\t\t} else if(!strcasecmp(type, \"data\")) {\n\t\t\tif(session->drc != NULL) {\n\t\t\t\tjanus_recorder *rc = session->drc;\n\t\t\t\tsession->drc = NULL;\n\t\t\t\tjanus_recorder_close(rc);\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t}\n\t\t}\n\t}\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tduk_push_int(ctx, 0);\n\treturn 1;\n}\n\n\n/* Plugin implementation */\nint janus_duktape_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&duktape_stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_DUKTAPE_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_DUKTAPE_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_DUKTAPE_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config == NULL) {\n\t\t/* No config means no JS script */\n\t\tJANUS_LOG(LOG_ERR, \"Failed to load configuration file for Duktape plugin...\\n\");\n\t\treturn -1;\n\t}\n\tjanus_config_print(config);\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\tjanus_config_item *folder = janus_config_get(config, config_general, janus_config_type_item, \"path\");\n\tif(folder && folder->value)\n\t\tduktape_folder = g_strdup(folder->value);\n\tjanus_config_item *script = janus_config_get(config, config_general, janus_config_type_item, \"script\");\n\tif(script == NULL || script->value == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Missing script path in Duktape plugin configuration...\\n\");\n\t\tjanus_config_destroy(config);\n\t\tg_free(duktape_folder);\n\t\treturn -1;\n\t}\n\tchar *duktape_file = g_strdup(script->value);\n\tchar *duktape_config = NULL;\n\tjanus_config_item *conf = janus_config_get(config, config_general, janus_config_type_item, \"config\");\n\tif(conf && conf->value)\n\t\tduktape_config = g_strdup(conf->value);\n\tjanus_config_destroy(config);\n\n\t/* Initialize Duktape */\n\tduktape_ctx = duk_create_heap_default();\n\tif(duktape_ctx == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error creating Duktape heap...\\n\");\n\t\tg_free(duktape_folder);\n\t\treturn -1;\n\t}\n\tduk_console_init(duktape_ctx, DUK_CONSOLE_PROXY_WRAPPER);\n\tduk_module_duktape_init(duktape_ctx);\n\n\t/* Register our functions */\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_getmodulesfolder, 0);\n\tduk_put_global_string(duktape_ctx, \"getModulesFolder\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_readfile, 1);\n\tduk_put_global_string(duktape_ctx, \"readFile\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_pokescheduler, 0);\n\tduk_put_global_string(duktape_ctx, \"pokeScheduler\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_timecallback, 3);\n\tduk_put_global_string(duktape_ctx, \"timeCallback\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_pushevent, 4);\n\tduk_put_global_string(duktape_ctx, \"pushEvent\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_notifyevent, 2);\n\tduk_put_global_string(duktape_ctx, \"notifyEvent\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_eventsisenabled, 0);\n\tduk_put_global_string(duktape_ctx, \"eventsIsEnabled\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_closepc, 1);\n\tduk_put_global_string(duktape_ctx, \"closePc\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_endsession, 1);\n\tduk_put_global_string(duktape_ctx, \"endSession\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_configuremedium, 4);\n\tduk_put_global_string(duktape_ctx, \"configureMedium\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_addrecipient, 2);\n\tduk_put_global_string(duktape_ctx, \"addRecipient\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_removerecipient, 2);\n\tduk_put_global_string(duktape_ctx, \"removeRecipient\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_setbitrate, 2);\n\tduk_put_global_string(duktape_ctx, \"setBitrate\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_setplifreq, 2);\n\tduk_put_global_string(duktape_ctx, \"setPliFreq\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_setsubstream, 2);\n\tduk_put_global_string(duktape_ctx, \"setSubstream\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_settemporallayer, 2);\n\tduk_put_global_string(duktape_ctx, \"setTemporalLayer\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_sendpli, 1);\n\tduk_put_global_string(duktape_ctx, \"sendPli\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_relayrtp, 4);\n\tduk_put_global_string(duktape_ctx, \"relayRtp\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_relayrtcp, 4);\n\tduk_put_global_string(duktape_ctx, \"relayRtcp\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_relaydata, 5);\t/* Legacy function, deprecated */\n\tduk_put_global_string(duktape_ctx, \"relayData\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_relaytextdata, 5);\n\tduk_put_global_string(duktape_ctx, \"relayTextData\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_relaybinarydata, 5);\n\tduk_put_global_string(duktape_ctx, \"relayBinaryData\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_startrecording, 13);\n\tduk_put_global_string(duktape_ctx, \"startRecording\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_stoprecording, 4);\n\tduk_put_global_string(duktape_ctx, \"stopRecording\");\n\tduk_push_c_function(duktape_ctx, janus_duktape_method_getversion, 0);\n\tduk_put_global_string(duktape_ctx, \"getDuktapeVersion\");\n\t/* Register all extra functions, if any were added */\n\tjanus_duktape_register_extra_functions(duktape_ctx);\n\n\t/* Now load the script (FIXME badly) */\n\tFILE *f = fopen(duktape_file, \"rb\");\n\tif(f == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error loading JS script %s: no such file\\n\", duktape_file);\n\t\tduk_destroy_heap(duktape_ctx);\n\t\tg_free(duktape_folder);\n\t\tg_free(duktape_file);\n\t\treturn -1;\n\t}\n\tfseek(f, 0, SEEK_END);\n\tlong int fs = ftell(f);\n\tif(fs < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Error loading JS script %s: empty file\\n\", duktape_file);\n\t\tfclose(f);\n\t\tduk_destroy_heap(duktape_ctx);\n\t\tg_free(duktape_folder);\n\t\tg_free(duktape_file);\n\t\treturn -1;\n\t}\n\tsize_t len = fs;\n\tchar *buf = (char *)g_malloc0(len);\n\tfseek(f, 0, SEEK_SET);\n\tif(fread((void *)buf, 1, len, f) < len) {\n\t\tJANUS_LOG(LOG_ERR, \"Error reading JS script %s: %s\\n\", duktape_file, g_strerror(errno));\n\t\tg_free(buf);\n\t\tfclose(f);\n\t\tduk_destroy_heap(duktape_ctx);\n\t\tg_free(duktape_folder);\n\t\tg_free(duktape_file);\n\t\treturn -1;\n\t}\n\tfclose(f);\n\tduk_push_lstring(duktape_ctx, (const char *)buf, (duk_size_t)len);\n\tg_free(buf);\n\tif(duk_peval(duktape_ctx) != 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error loading JS script %s: %s\\n\", duktape_file, duk_safe_to_string(duktape_ctx, -1));\n\t\tduk_destroy_heap(duktape_ctx);\n\t\tg_free(duktape_folder);\n\t\tg_free(duktape_file);\n\t\treturn -1;\n\t}\n\tduk_pop(duktape_ctx);\n\t/* Make sure that all the functions we need are there */\n\tuint i=0;\n\tfor(i=0; i<duktape_funcsize; i++) {\n\t\tduk_get_global_string(duktape_ctx, duktape_functions[i]);\n\t\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) == 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Function '%s' is missing in %s\\n\", duktape_functions[i], duktape_file);\n\t\t\tduk_destroy_heap(duktape_ctx);\n\t\t\tg_free(duktape_folder);\n\t\t\tg_free(duktape_file);\n\t\t\treturn -1;\n\t\t}\n\t}\n\t/* Some JS functions are optional (e.g., those to directly handle RTP, RTCP and\n\t * data, as those will typically be kept at a C level, with JavaScript only dictating\n\t * the logic, or those overriding the plugin namespace and versioning information */\n\tduk_get_global_string(duktape_ctx, \"getVersion\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_get_version = TRUE;\n\tduk_get_global_string(duktape_ctx, \"getVersionString\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_get_version_string = TRUE;\n\tduk_get_global_string(duktape_ctx, \"getDescription\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_get_description = TRUE;\n\tduk_get_global_string(duktape_ctx, \"getName\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_get_name = TRUE;\n\tduk_get_global_string(duktape_ctx, \"getAuthor\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_get_author = TRUE;\n\tduk_get_global_string(duktape_ctx, \"getPackage\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_get_package = TRUE;\n\tduk_get_global_string(duktape_ctx, \"handleAdminMessage\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_handle_admin_message = TRUE;\n\tduk_get_global_string(duktape_ctx, \"incomingRtp\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_incoming_rtp = TRUE;\n\tduk_get_global_string(duktape_ctx, \"incomingRtcp\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_incoming_rtcp = TRUE;\n\tduk_get_global_string(duktape_ctx, \"incomingData\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0) {\n\t\thas_incoming_data_legacy = TRUE;\n\t\tJANUS_LOG(LOG_WARN, \"The Duktape script contains the deprecated 'incomingData' callback: update it \"\n\t\t\t\"to use 'incomingTextData' and/or 'incomingBinaryData' in the future (see PR #1878)\\n\");\n\t}\n\tduk_get_global_string(duktape_ctx, \"incomingTextData\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_incoming_text_data = TRUE;\n\tduk_get_global_string(duktape_ctx, \"incomingBinaryData\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_incoming_binary_data = TRUE;\n\tduk_get_global_string(duktape_ctx, \"dataReady\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_data_ready = TRUE;\n\tduk_get_global_string(duktape_ctx, \"slowLink\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_slow_link = TRUE;\n\tduk_get_global_string(duktape_ctx, \"substreamChanged\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_substream_changed = TRUE;\n\tduk_get_global_string(duktape_ctx, \"temporalLayerChanged\");\n\tif(duk_is_function(duktape_ctx, duk_get_top(duktape_ctx)-1) != 0)\n\t\thas_temporal_changed = TRUE;\n\n\tduktape_sessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_duktape_session_destroy);\n\tduktape_ids = g_hash_table_new(NULL, NULL);\n\tevents = g_async_queue_new();\n\tcallbacks = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_duktape_callback_free);\n\n\tg_atomic_int_set(&duktape_initialized, 1);\n\n\t/* Launch the scheduler thread (which will be responsible for resuming asynchronous coroutines) */\n\tGError *error = NULL;\n\tscheduler_thread = g_thread_try_new(\"duktape scheduler\", janus_duktape_scheduler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&duktape_initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Duktape scheduler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tduk_destroy_heap(duktape_ctx);\n\t\tg_free(duktape_folder);\n\t\tg_free(duktape_file);\n\t\tg_free(duktape_config);\n\t\treturn -1;\n\t}\n\t/* Launch the timer loop thread (which will be responsible for scheduling timed callbacks) */\n\ttimer_context = g_main_context_new();\n\ttimer_loop = g_main_loop_new(timer_context, FALSE);\n\ttimer_thread = g_thread_try_new(\"duktape timer\", janus_duktape_timer, timer_loop, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&duktape_initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Duktape timer loop thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tif(timer_loop != NULL)\n\t\t\tg_main_loop_unref(timer_loop);\n\t\tif(timer_context != NULL)\n\t\t\tg_main_context_unref(timer_context);\n\t\tduk_destroy_heap(duktape_ctx);\n\t\tg_free(duktape_folder);\n\t\tg_free(duktape_file);\n\t\tg_free(duktape_config);\n\t\treturn -1;\n\t}\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tduktape_janus_core = callback;\n\n\t/* Init the JS script, in case it's needed */\n\tduk_get_global_string(duktape_ctx, \"init\");\n\tduk_push_string(duktape_ctx, duktape_config);\n\tint res = duk_pcall(duktape_ctx, 1);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\tg_atomic_int_set(&duktape_initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(duktape_ctx, -1));\n\t\tduk_pop(duktape_ctx);\n\t\tif(timer_loop != NULL)\n\t\t\tg_main_loop_unref(timer_loop);\n\t\tif(timer_context != NULL)\n\t\t\tg_main_context_unref(timer_context);\n\t\tduk_destroy_heap(duktape_ctx);\n\t\tg_free(duktape_folder);\n\t\tg_free(duktape_file);\n\t\tg_free(duktape_config);\n\t\treturn -1;\n\t}\n\n\tg_free(duktape_file);\n\tg_free(duktape_config);\n\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_DUKTAPE_NAME);\n\treturn 0;\n}\n\nvoid janus_duktape_destroy(void) {\n\tif(!g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tg_atomic_int_set(&duktape_stopping, 1);\n\n\tg_async_queue_push(events, GUINT_TO_POINTER(janus_duktape_event_exit));\n\tif(scheduler_thread != NULL) {\n\t\tg_thread_join(scheduler_thread);\n\t\tscheduler_thread = NULL;\n\t}\n\tif(timer_loop != NULL)\n\t\tg_main_loop_quit(timer_loop);\n\tif(timer_thread != NULL) {\n\t\tg_thread_join(timer_thread);\n\t\ttimer_thread = NULL;\n\t}\n\tif(timer_loop != NULL) {\n\t\tg_main_loop_unref(timer_loop);\n\t\ttimer_loop = NULL;\n\t}\n\tif(timer_context != NULL) {\n\t\tg_main_context_unref(timer_context);\n\t\ttimer_context = NULL;\n\t}\n\n\t/* Deinit the JS script, in case it's needed */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_get_global_string(duktape_ctx, \"destroy\");\n\tint res = duk_pcall(duktape_ctx, 0);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(duktape_ctx, -1));\n\t\tduk_pop(duktape_ctx);\n\t}\n\tg_hash_table_destroy(callbacks);\n\tcallbacks = NULL;\n\tjanus_mutex_unlock(&duktape_mutex);\n\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tg_hash_table_destroy(duktape_sessions);\n\tduktape_sessions = NULL;\n\tg_hash_table_destroy(duktape_ids);\n\tduktape_ids = NULL;\n\tg_async_queue_unref(events);\n\tevents = NULL;\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_destroy_heap(duktape_ctx);\n\tduktape_ctx = NULL;\n\tjanus_mutex_unlock(&duktape_mutex);\n\n\tg_free(duktape_script_version_string);\n\tg_free(duktape_script_description);\n\tg_free(duktape_script_name);\n\tg_free(duktape_script_author);\n\tg_free(duktape_script_package);\n\n\tg_free(duktape_folder);\n\n\tg_atomic_int_set(&duktape_initialized, 0);\n\tg_atomic_int_set(&duktape_stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_DUKTAPE_NAME);\n}\n\nint janus_duktape_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_duktape_get_version(void) {\n\t/* Check if the JS script wants to override this method and return info itself */\n\tif(has_get_version) {\n\t\t/* Yep, pass the request to the JS script and return the info */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tif(duktape_script_version != -1) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn duktape_script_version;\n\t\t}\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"getVersion\");\n\t\tint res = duk_pcall(t, 0);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... return the Janus Duktape plugin info */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\tduk_pop(t);\n\t\t\tduk_pop(duktape_ctx);\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn JANUS_DUKTAPE_VERSION;\n\t\t}\n\t\tduktape_script_version = (int)duk_get_number(t, -1);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn duktape_script_version;\n\t}\n\t/* No override, return the Janus Duktape plugin info */\n\treturn JANUS_DUKTAPE_VERSION;\n}\n\nconst char *janus_duktape_get_version_string(void) {\n\t/* Check if the JS script wants to override this method and return info itself */\n\tif(has_get_version_string) {\n\t\t/* Yep, pass the request to the JS script and return the info */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tif(duktape_script_version_string != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn duktape_script_version_string;\n\t\t}\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"getVersionString\");\n\t\tint res = duk_pcall(t, 0);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... return the Janus Duktape plugin info */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\tduk_pop(t);\n\t\t\tduk_pop(duktape_ctx);\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn JANUS_DUKTAPE_VERSION_STRING;\n\t\t}\n\t\tconst char *version = duk_get_string(t, -1);\n\t\tif(version != NULL)\n\t\t\tduktape_script_version_string = g_strdup(version);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn duktape_script_version_string;\n\t}\n\t/* No override, return the Janus Duktape plugin info */\n\treturn JANUS_DUKTAPE_VERSION_STRING;\n}\n\nconst char *janus_duktape_get_description(void) {\n\t/* Check if the JS script wants to override this method and return info itself */\n\tif(has_get_description) {\n\t\t/* Yep, pass the request to the JS script and return the info */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tif(duktape_script_description != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn duktape_script_description;\n\t\t}\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"getDescription\");\n\t\tint res = duk_pcall(t, 0);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... return the Janus Duktape plugin info */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\tduk_pop(t);\n\t\t\tduk_pop(duktape_ctx);\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn JANUS_DUKTAPE_DESCRIPTION;\n\t\t}\n\t\tconst char *description = duk_get_string(t, -1);\n\t\tif(description != NULL)\n\t\t\tduktape_script_description = g_strdup(description);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn duktape_script_description;\n\t}\n\t/* No override, return the Janus Duktape plugin info */\n\treturn JANUS_DUKTAPE_DESCRIPTION;\n}\n\nconst char *janus_duktape_get_name(void) {\n\t/* Check if the JS script wants to override this method and return info itself */\n\tif(has_get_name) {\n\t\t/* Yep, pass the request to the JS script and return the info */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tif(duktape_script_name != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn duktape_script_name;\n\t\t}\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"getName\");\n\t\tint res = duk_pcall(t, 0);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... return the Janus Duktape plugin info */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\tduk_pop(t);\n\t\t\tduk_pop(duktape_ctx);\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn JANUS_DUKTAPE_NAME;\n\t\t}\n\t\tconst char *name = duk_get_string(t, -1);\n\t\tif(name != NULL)\n\t\t\tduktape_script_name = g_strdup(name);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn duktape_script_name;\n\t}\n\t/* No override, return the Janus Duktape plugin info */\n\treturn JANUS_DUKTAPE_NAME;\n}\n\nconst char *janus_duktape_get_author(void) {\n\t/* Check if the JS script wants to override this method and return info itself */\n\tif(has_get_author) {\n\t\t/* Yep, pass the request to the JS script and return the info */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tif(duktape_script_author != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn duktape_script_author;\n\t\t}\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"getAuthor\");\n\t\tint res = duk_pcall(t, 0);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... return the Janus Duktape plugin info */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\tduk_pop(t);\n\t\t\tduk_pop(duktape_ctx);\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn JANUS_DUKTAPE_AUTHOR;\n\t\t}\n\t\tconst char *author = duk_get_string(t, -1);\n\t\tif(author != NULL)\n\t\t\tduktape_script_author = g_strdup(author);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn duktape_script_author;\n\t}\n\t/* No override, return the Janus Duktape plugin info */\n\treturn JANUS_DUKTAPE_AUTHOR;\n}\n\nconst char *janus_duktape_get_package(void) {\n\t/* Check if the JS script wants to override this method and return info itself */\n\tif(has_get_package) {\n\t\t/* Yep, pass the request to the JS script and return the info */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tif(duktape_script_package != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn duktape_script_package;\n\t\t}\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"getPackage\");\n\t\tint res = duk_pcall(t, 0);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... return the Janus Duktape plugin info */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\tduk_pop(t);\n\t\t\tduk_pop(duktape_ctx);\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\treturn JANUS_DUKTAPE_PACKAGE;\n\t\t}\n\t\tconst char *package = duk_get_string(t, -1);\n\t\tif(package != NULL)\n\t\t\tduktape_script_package = g_strdup(package);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn duktape_script_package;\n\t}\n\t/* No override, return the Janus Duktape plugin info */\n\treturn JANUS_DUKTAPE_PACKAGE;\n}\n\njanus_duktape_session *janus_duktape_lookup_session(janus_plugin_session *handle) {\n\tjanus_duktape_session *session = NULL;\n\tif (g_hash_table_contains(duktape_sessions, handle)) {\n\t\tsession = (janus_duktape_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_duktape_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tuint32_t id = 0;\n\twhile(id == 0) {\n\t\tid = janus_random_uint32();\n\t\tif(g_hash_table_lookup(duktape_ids, GUINT_TO_POINTER(id))) {\n\t\t\tid = 0;\n\t\t\tcontinue;\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Creating new Duktape session %\"SCNu32\"...\\n\", id);\n\tjanus_duktape_session *session = (janus_duktape_session *)g_malloc0(sizeof(janus_duktape_session));\n\tsession->handle = handle;\n\tsession->id = id;\n\tjanus_rtp_switching_context_reset(&session->artpctx);\n\tjanus_rtp_switching_context_reset(&session->vrtpctx);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tsession->sim_context.substream_target = 2;\n\tsession->sim_context.templayer_target = 2;\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tsession->rid_extmap_id = -1;\n\tjanus_mutex_init(&session->rid_mutex);\n\tjanus_mutex_init(&session->recipients_mutex);\n\tjanus_mutex_init(&session->rec_mutex);\n\tsession->vcodec = JANUS_VIDEOCODEC_NONE;\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tjanus_refcount_init(&session->ref, janus_duktape_session_free);\n\thandle->plugin_handle = session;\n\tg_hash_table_insert(duktape_sessions, handle, session);\n\tg_hash_table_insert(duktape_ids, GUINT_TO_POINTER(session->id), session);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\n\t/* Notify the JS script */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, \"createSession\");\n\tduk_push_number(t, session->id);\n\tint res = duk_pcall(t, 1);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\t/* Something went wrong... this session will likely be broken */\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t}\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\tjanus_mutex_unlock(&duktape_mutex);\n\n\treturn;\n}\n\nvoid janus_duktape_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = janus_duktape_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tuint32_t id = session->id;\n\tJANUS_LOG(LOG_VERB, \"Removing Duktape session %\"SCNu32\"...\\n\", id);\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\n\t/* Notify the JS script */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, \"destroySession\");\n\tduk_push_number(t, id);\n\tint res = duk_pcall(t, 1);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t}\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\tjanus_mutex_unlock(&duktape_mutex);\n\n\t/* Get any rid references recipients of this sessions may have */\n\tjanus_mutex_lock(&session->recipients_mutex);\n\twhile(session->recipients != NULL) {\n\t\tjanus_duktape_session *recipient = (janus_duktape_session *)session->recipients->data;\n\t\tif(recipient != NULL) {\n\t\t\trecipient->sender = NULL;\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tjanus_refcount_decrease(&recipient->ref);\n\t\t}\n\t\tsession->recipients = g_slist_remove(session->recipients, recipient);\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\n\t/* Finally, remove from the hashtable */\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tg_hash_table_remove(duktape_sessions, handle);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tjanus_refcount_decrease(&session->ref);\n\n\treturn;\n}\n\njson_t *janus_duktape_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = janus_duktape_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t/* Ask the JS script for information on this session */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, \"querySession\");\n\tduk_push_number(t, session->id);\n\tint res = duk_pcall(t, 1);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\t/* Something went wrong... return this error */\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\tjson_t *json = json_object();\n\t\tjson_object_set_new(json, \"error\", json_string(duk_safe_to_string(t, -1)));\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn json;\n\t}\n\tjanus_refcount_decrease(&session->ref);\n\tconst char *info = duk_get_string(t, -1);\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\t/* We need a Jansson object */\n\tjson_error_t error;\n\tjson_t *json = json_loads(info, 0, &error);\n\tjanus_mutex_unlock(&duktape_mutex);\n\tif(!json) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\treturn NULL;\n\t}\n\treturn json;\n}\n\nstruct janus_plugin_result *janus_duktape_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&duktape_stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = janus_duktape_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\n\t/* Processing the message is up to the JS script: serialize the Jansson objects to strings */\n\tchar *message_text = message ? json_dumps(message, JSON_INDENT(0) | JSON_PRESERVE_ORDER) : NULL;\n\tjson_decref(message);\n\tif(message == NULL || message_text == NULL) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_ERR, \"Invalid message..?\\n\");\n\t\tif(jsep != NULL)\n\t\t\tjson_decref(jsep);\n\t\tg_free(transaction);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\t}\n\tchar *jsep_text = jsep ? json_dumps(jsep, JSON_INDENT(0) | JSON_PRESERVE_ORDER) : NULL;\n\tif(jsep != NULL) {\n\t\tjson_t *simulcast = json_object_get(jsep, \"simulcast\");\n\t\tif(simulcast && json_array_size(simulcast) > 0) {\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(simulcast); i++) {\n\t\t\t\tjson_t *s = json_array_get(simulcast, i);\n\t\t\t\t/* Clear existing RIDs in case this is a renegotiation */\n\t\t\t\tjanus_mutex_lock(&session->rid_mutex);\n\t\t\t\tjanus_rtp_simulcasting_cleanup(&session->rid_extmap_id, NULL, session->rid, NULL);\n\t\t\t\tjanus_rtp_simulcasting_prepare(s,\n\t\t\t\t\t&session->rid_extmap_id,\n\t\t\t\t\tsession->ssrc, session->rid);\n\t\t\t\tjanus_mutex_unlock(&session->rid_mutex);\n\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst char *sdp_type = json_string_value(json_object_get(jsep, \"type\"));\n\t\tif(sdp_type && !strcasecmp(sdp_type, \"answer\")) {\n\t\t\tchar error_str[512];\n\t\t\tconst char *sdp = json_string_value(json_object_get(jsep, \"sdp\"));\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(sdp, error_str, sizeof(error_str));\n\t\t\tconst char *vcodec = NULL;\n\t\t\tjanus_sdp_find_first_codec(parsed_sdp, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\tif(vcodec)\n\t\t\t\tsession->vcodec = janus_videocodec_from_name(vcodec);\n\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t}\n\t\tif(json_is_true(json_object_get(jsep, \"e2ee\")))\n\t\t\tsession->e2ee = TRUE;\n\t\tjson_decref(jsep);\n\t}\n\t/* Invoke the script function */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, \"handleMessage\");\n\tduk_push_number(t, session->id);\n\tduk_push_string(t, transaction);\n\tduk_push_string(t, message_text);\n\tduk_push_string(t, jsep_text);\n\tint res = duk_pcall(t, 4);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"Duktape error\", NULL);\n\t}\n\tjanus_refcount_decrease(&session->ref);\n\tif(message_text != NULL)\n\t\tfree(message_text);\n\tif(jsep_text != NULL)\n\t\tfree(jsep_text);\n\tg_free(transaction);\n\t/* Check if this is a synchronous or asynchronous response */\n\tif(duk_get_type(t, 0) == DUK_TYPE_NUMBER) {\n\t\t/* Either an error or an asynchronous response */\n\t\tint res = (int)duk_get_number(t, 0);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\tif(res < 0) {\n\t\t\t/* We got an error */\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"Duktape error\", NULL);\n\t\t}\n\t\t/* If we got here, it's an asynchronous response */\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n\t} else if(duk_get_type(t, 0) == DUK_TYPE_STRING) {\n\t\t/* If it's not a number, it's a string, and so a synchronous response */\n\t\tconst char *response = duk_get_string(t, 0);\n\t\tjson_error_t error;\n\t\tjson_t *json = json_loads(response, 0, &error);\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\tif(!json) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"Duktape error\", NULL);\n\t\t}\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, json);\n\t}\n\t/* If we got here, we didn't get what we expect */\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\tjanus_mutex_unlock(&duktape_mutex);\n\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"Duktape error\", NULL);\n}\n\njson_t *janus_duktape_handle_admin_message(json_t *message) {\n\tif(!has_handle_admin_message || message == NULL)\n\t\treturn NULL;\n\tchar *message_text = json_dumps(message, JSON_INDENT(0) | JSON_PRESERVE_ORDER);\n\tif(message_text == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\treturn NULL;\n\t}\n\t/* Invoke the script function */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, \"handleAdminMessage\");\n\tduk_push_string(t, message_text);\n\tint res = duk_pcall(t, 1);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn NULL;\n\t}\n\tif(message_text != NULL)\n\t\tfree(message_text);\n\t/* Get the response */\n\tconst char *response = duk_get_string(t, 0);\n\tjson_error_t error;\n\tjson_t *json = json_loads(response, 0, &error);\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\tjanus_mutex_unlock(&duktape_mutex);\n\tif(!json) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\treturn NULL;\n\t}\n\treturn json;\n}\n\nvoid janus_duktape_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"WebRTC media is now available\\n\");\n\tif(g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = janus_duktape_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->started, 1);\n\tsession->pli_latest = janus_get_monotonic_time();\n\n\t/* Notify the JS script */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, \"setupMedia\");\n\tduk_push_number(t, session->id);\n\tint res = duk_pcall(t, 1);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\t/* Something went wrong... this media session will likely be broken */\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t}\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\tjanus_mutex_unlock(&duktape_mutex);\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_duktape_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *rtp_packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tjanus_duktape_session *session = (janus_duktape_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tgboolean video = rtp_packet->video;\n\tchar *buf = rtp_packet->buffer;\n\tuint16_t len = rtp_packet->length;\n\t/* Check if the JS script wants to handle/manipulate RTP packets itself */\n\tif(has_incoming_rtp) {\n\t\t/* Yep, pass the data to the JS script and return */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"incomingRtp\");\n\t\tduk_push_number(t, session->id);\n\t\tduk_push_boolean(t, video);\n\t\tduk_push_lstring(t, buf, len);\n\t\tduk_push_number(t, len);\n\t\tint res = duk_pcall(t, 4);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t}\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn;\n\t}\n\t/* Is this session allowed to send media? */\n\tif((video && !session->send_video) || (!video && !session->send_audio))\n\t\treturn;\n\t/* Handle the packet */\n\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\t/* Check if we're simulcasting, and if so, keep track of the \"layer\" */\n\tint sc = video ? 0 : -1;\n\tif(video && (session->ssrc[0] != 0 || session->rid[0] != NULL)) {\n\t\tuint32_t ssrc = ntohl(rtp->ssrc);\n\t\tif(ssrc == session->ssrc[0])\n\t\t\tsc = 0;\n\t\telse if(ssrc == session->ssrc[1])\n\t\t\tsc = 1;\n\t\telse if(ssrc == session->ssrc[2])\n\t\t\tsc = 2;\n\t\telse if(session->rid_extmap_id > 0) {\n\t\t\t/* We may not know the SSRC yet, try the rid RTP extension */\n\t\t\tjanus_mutex_lock(&session->rid_mutex);\n\t\t\tchar sdes_item[16];\n\t\t\tif(janus_rtp_header_extension_parse_rid(buf, len, session->rid_extmap_id, sdes_item, sizeof(sdes_item)) == 0) {\n\t\t\t\tif(session->rid[0] != NULL && !strcmp(session->rid[0], sdes_item)) {\n\t\t\t\t\tsession->ssrc[0] = ssrc;\n\t\t\t\t\tsc = 0;\n\t\t\t\t} else if(session->rid[1] != NULL && !strcmp(session->rid[1], sdes_item)) {\n\t\t\t\t\tsession->ssrc[1] = ssrc;\n\t\t\t\t\tsc = 1;\n\t\t\t\t} else if(session->rid[2] != NULL && !strcmp(session->rid[2], sdes_item)) {\n\t\t\t\t\tsession->ssrc[2] = ssrc;\n\t\t\t\t\tsc = 2;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rid_mutex);\n\t\t}\n\t}\n\t/* Are we recording? */\n\tif(!video || (session->ssrc[0] == 0 && session->rid[0] == NULL)) {\n\t\tjanus_recorder_save_frame(video ? session->vrc : session->arc, buf, len);\n\t} else {\n\t\t/* We're simulcasting, save the best video quality */\n\t\tgboolean save = janus_rtp_simulcasting_context_process_rtp(&session->rec_simctx,\n\t\t\tbuf, len, NULL, 0, session->ssrc, session->rid, session->vcodec, &session->rec_ctx, &session->rid_mutex);\n\t\tif(save) {\n\t\t\tuint32_t seq_number = ntohs(rtp->seq_number);\n\t\t\tuint32_t timestamp = ntohl(rtp->timestamp);\n\t\t\tuint32_t ssrc = ntohl(rtp->ssrc);\n\t\t\tjanus_rtp_header_update(rtp, &session->rec_ctx, TRUE, 0);\n\t\t\t/* We use a fixed SSRC for the whole recording */\n\t\t\trtp->ssrc = session->ssrc[0];\n\t\t\tjanus_recorder_save_frame(session->vrc, buf, len);\n\t\t\t/* Restore the header, as it will be needed by recipients of this packet */\n\t\t\trtp->ssrc = htonl(ssrc);\n\t\t\trtp->timestamp = htonl(timestamp);\n\t\t\trtp->seq_number = htons(seq_number);\n\t\t}\n\t}\n\tjanus_duktape_rtp_relay_packet packet;\n\tpacket.sender = session;\n\tpacket.data = rtp;\n\tpacket.length = len;\n\tpacket.is_video = video;\n\tpacket.extensions = rtp_packet->extensions;\n\tpacket.ssrc[0] = (sc != -1 ? session->ssrc[0] : 0);\n\tpacket.ssrc[1] = (sc != -1 ? session->ssrc[1] : 0);\n\tpacket.ssrc[2] = (sc != -1 ? session->ssrc[2] : 0);\n\t/* Backup the actual timestamp and sequence number set by the publisher, in case switching is involved */\n\tpacket.timestamp = ntohl(packet.data->timestamp);\n\tpacket.seq_number = ntohs(packet.data->seq_number);\n\t/* Relay to all recipients */\n\tjanus_mutex_lock_nodebug(&session->recipients_mutex);\n\tg_slist_foreach(session->recipients, janus_duktape_relay_rtp_packet, &packet);\n\tjanus_mutex_unlock_nodebug(&session->recipients_mutex);\n\n\t/* Check if we need to send any PLI to this media source */\n\tif(video && session->pli_freq > 0) {\n\t\t/* We send a FIR every tot seconds, depending on what the JS script configured */\n\t\tgint64 now = janus_get_monotonic_time();\n\t\tif((now-session->pli_latest) >= ((gint64)session->pli_freq*G_USEC_PER_SEC)) {\n\t\t\tsession->pli_latest = now;\n\t\t\tduktape_janus_core->send_pli(handle);\n\t\t}\n\t}\n}\n\nvoid janus_duktape_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tjanus_duktape_session *session = (janus_duktape_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tgboolean video = packet->video;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\t/* Check if the JS script wants to handle/manipulate RTCP packets itself */\n\tif(has_incoming_rtcp) {\n\t\t/* Yep, pass the data to the JS script and return */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"incomingRtcp\");\n\t\tduk_push_number(t, session->id);\n\t\tduk_push_boolean(t, video);\n\t\tduk_push_lstring(t, buf, len);\n\t\tduk_push_number(t, len);\n\t\tint res = duk_pcall(t, 4);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t}\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn;\n\t}\n\t/* If a REMB arrived, make sure we cap it to our configuration, and send it as a video RTCP */\n\tuint32_t bitrate = janus_rtcp_get_remb(buf, len);\n\tif(bitrate > 0) {\n\t\t/* No limit ~= 10000000 */\n\t\tduktape_janus_core->send_remb(handle, session->bitrate ? session->bitrate : 10000000);\n\t}\n\t/* If there's an incoming PLI, instead, relay it to the source of the media if any */\n\tif(janus_rtcp_has_pli(buf, len)) {\n\t\tif(session->sender != NULL) {\n\t\t\tjanus_mutex_lock_nodebug(&session->sender->recipients_mutex);\n\t\t\t/* Send a PLI */\n\t\t\tsession->sender->pli_latest = janus_get_monotonic_time();\n\t\t\tJANUS_LOG(LOG_HUGE, \"Sending PLI to session %\"SCNu32\"\\n\", session->sender->id);\n\t\t\tduktape_janus_core->send_pli(session->sender->handle);\n\t\t\tjanus_mutex_unlock_nodebug(&session->sender->recipients_mutex);\n\t\t}\n\t}\n}\n\nvoid janus_duktape_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tjanus_duktape_session *session = (janus_duktape_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\tchar *label = packet->label;\n\tchar *protocol = packet->protocol;\n\t/* Are we recording? */\n\tjanus_recorder_save_frame(session->drc, buf, len);\n\t/* Check if the JS script wants to handle/manipulate data channel packets itself */\n\tif((!packet->binary && (has_incoming_data_legacy || has_incoming_text_data)) || (packet->binary && has_incoming_binary_data)) {\n\t\t/* Yep, pass the data to the JS script and return */\n\t\tif(packet->binary && !has_incoming_text_data)\n\t\t\tJANUS_LOG(LOG_WARN, \"Missing 'incomingTextData', invoking deprecated function 'incomingData' instead\\n\");\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, packet->binary ? \"incomingBinaryData\" : (has_incoming_text_data ? \"incomingTextData\" : \"incomingData\"));\n\t\tduk_push_number(t, session->id);\n\t\t/* We use a string for both text and binary data */\n\t\tduk_push_lstring(t, buf, len);\n\t\tduk_push_number(t, len);\n\t\tduk_push_lstring(t, label, label ? strlen(label) : 0);\n\t\tduk_push_lstring(t, protocol, protocol ? strlen(protocol) : 0);\n\t\tint res = duk_pcall(t, 5);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t}\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn;\n\t}\n\t/* Is this session allowed to send data? */\n\tif(!session->send_data)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"Got a %s DataChannel message (%d bytes) to forward\\n\",\n\t\tpacket->binary ? \"binary\" : \"text\", len);\n\t/* Relay to all recipients */\n\tjanus_duktape_rtp_relay_packet pkt;\n\tpkt.sender = session;\n\tpkt.data = (janus_rtp_header *)buf;\n\tpkt.length = len;\n\tpkt.is_rtp = FALSE;\n\tpkt.textdata = !packet->binary;\n\tjanus_mutex_lock_nodebug(&session->recipients_mutex);\n\t/* FIXME We should add support for labels, here */\n\tg_slist_foreach(session->recipients, janus_duktape_relay_data_packet, &pkt);\n\tjanus_mutex_unlock_nodebug(&session->recipients_mutex);\n}\n\nvoid janus_duktape_data_ready(janus_plugin_session *handle) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tjanus_duktape_session *session = (janus_duktape_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tif(g_atomic_int_compare_and_exchange(&session->dataready, 0, 1)) {\n\t\tJANUS_LOG(LOG_INFO, \"[%s-%p] Data channel available\\n\", JANUS_DUKTAPE_PACKAGE, handle);\n\t}\n\t/* Check if the JS script wants to receive this event */\n\tif(has_data_ready) {\n\t\t/* Yep, pass the event to the JS script and return */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"dataReady\");\n\t\tduk_push_number(t, session->id);\n\t\tint res = duk_pcall(t, 1);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t}\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\treturn;\n\t}\n}\n\nvoid janus_duktape_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = janus_duktape_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\t/* Check if the Duktape script wants to handle such events */\n\tjanus_refcount_increase(&session->ref);\n\tif(has_slow_link) {\n\t\t/* Notify the JS script */\n\t\tjanus_mutex_lock(&duktape_mutex);\n\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\tduk_get_global_string(t, \"slowLink\");\n\t\tduk_push_number(t, session->id);\n\t\tduk_push_boolean(t, uplink);\n\t\tduk_push_boolean(t, video);\n\t\tint res = duk_pcall(t, 3);\n\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t}\n\t\tduk_pop(t);\n\t\tduk_pop(duktape_ctx);\n\t\tjanus_mutex_unlock(&duktape_mutex);\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_duktape_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_DUKTAPE_PACKAGE, handle);\n\tif(g_atomic_int_get(&duktape_stopping) || !g_atomic_int_get(&duktape_initialized))\n\t\treturn;\n\tjanus_mutex_lock(&duktape_sessions_mutex);\n\tjanus_duktape_session *session = janus_duktape_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&duktape_sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->started, 0);\n\tg_atomic_int_set(&session->dataready, 0);\n\n\t/* Reset the media properties */\n\tsession->accept_audio = FALSE;\n\tsession->accept_video = FALSE;\n\tsession->accept_data = FALSE;\n\tsession->send_audio = FALSE;\n\tsession->send_video = FALSE;\n\tsession->send_data = FALSE;\n\tsession->bitrate = 0;\n\tsession->pli_freq = 0;\n\tsession->pli_latest = 0;\n\tsession->e2ee = FALSE;\n\tjanus_rtp_switching_context_reset(&session->artpctx);\n\tjanus_rtp_switching_context_reset(&session->vrtpctx);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tsession->sim_context.substream_target = 2;\n\tsession->sim_context.templayer_target = 2;\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tsession->vcodec = JANUS_VIDEOCODEC_NONE;\n\tjanus_rtp_simulcasting_cleanup(&session->rid_extmap_id, session->ssrc, session->rid, &session->rid_mutex);\n\n\t/* Get rid of the recipients */\n\tjanus_mutex_lock(&session->recipients_mutex);\n\twhile(session->recipients) {\n\t\tjanus_duktape_session *recipient = (janus_duktape_session *)session->recipients->data;\n\t\tsession->recipients = g_slist_remove(session->recipients, recipient);\n\t\trecipient->sender = NULL;\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&recipient->ref);\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\n\t/* Notify the JS script */\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, \"hangupMedia\");\n\tduk_push_number(t, session->id);\n\tint res = duk_pcall(t, 1);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t}\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\tjanus_mutex_unlock(&duktape_mutex);\n\tjanus_refcount_decrease(&session->ref);\n}\n\n/* Helpers to quickly relay RTP and data packets to the intended recipients */\nstatic void janus_duktape_relay_rtp_packet(gpointer data, gpointer user_data) {\n\tjanus_duktape_rtp_relay_packet *packet = (janus_duktape_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_duktape_session *sender = (janus_duktape_session *)packet->sender;\n\tjanus_duktape_session *session = (janus_duktape_session *)data;\n\tif(!session || !session->handle || !g_atomic_int_get(&session->started)) {\n\t\treturn;\n\t}\n\n\t/* Check if this recipient is willing/allowed to receive this medium */\n\tif((packet->is_video && !session->accept_video) || (!packet->is_video && !session->accept_audio)) {\n\t\t/* Nope, don't relay */\n\t\treturn;\n\t}\n\tif(packet->ssrc[0] != 0) {\n\t\t/* Handle simulcast: make sure we have a payload to work with */\n\t\tint plen = 0;\n\t\tchar *payload = janus_rtp_payload((char *)packet->data, packet->length, &plen);\n\t\tif(payload == NULL)\n\t\t\treturn;\n\t\t/* Process this packet: don't relay if it's not the SSRC/layer we wanted to handle */\n\t\tgboolean relay = janus_rtp_simulcasting_context_process_rtp(&session->sim_context,\n\t\t\t(char *)packet->data, packet->length, packet->extensions.dd_content, packet->extensions.dd_len,\n\t\t\tpacket->ssrc, NULL, sender->vcodec, &session->vrtpctx, NULL);\n\t\tif(session->sim_context.need_pli && sender->handle) {\n\t\t\t/* Send a PLI */\n\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the simulcast context\\n\");\n\t\t\tduktape_janus_core->send_pli(sender->handle);\n\t\t}\n\t\t/* Do we need to drop this? */\n\t\tif(!relay)\n\t\t\treturn;\n\t\t/* Any event we should notify? */\n\t\tif(session->sim_context.changed_substream) {\n\t\t\t/* Notify the script about the substream change */\n\t\t\tif(has_substream_changed) {\n\t\t\t\tjanus_mutex_lock(&duktape_mutex);\n\t\t\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\t\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\t\t\tduk_get_global_string(t, \"substreamChanged\");\n\t\t\t\tduk_push_number(t, session->id);\n\t\t\t\tduk_push_number(t, session->sim_context.substream);\n\t\t\t\tint res = duk_pcall(t, 2);\n\t\t\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t\t\t/* Something went wrong... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\t\t}\n\t\t\t\tduk_pop(t);\n\t\t\t\tduk_pop(duktape_ctx);\n\t\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\t}\n\t\t}\n\t\tif(session->sim_context.changed_temporal) {\n\t\t\t/* Notify the user about the temporal layer change */\n\t\t\tif(has_substream_changed) {\n\t\t\t\tjanus_mutex_lock(&duktape_mutex);\n\t\t\t\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\t\t\t\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\t\t\t\tduk_get_global_string(t, \"temporalLayerChanged\");\n\t\t\t\tduk_push_number(t, session->id);\n\t\t\t\tduk_push_number(t, session->sim_context.templayer);\n\t\t\t\tint res = duk_pcall(t, 2);\n\t\t\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t\t\t/* Something went wrong... */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t\t\t\t}\n\t\t\t\tduk_pop(t);\n\t\t\t\tduk_pop(duktape_ctx);\n\t\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t\t}\n\t\t}\n\t\t/* If we got here, update the RTP header and send the packet */\n\t\tjanus_rtp_header_update(packet->data, &session->vrtpctx, TRUE, 0);\n\t\tchar vp8pd[6];\n\t\tif(sender->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t/* For VP8, we save the original payload descriptor, to restore it after */\n\t\t\tmemcpy(vp8pd, payload, sizeof(vp8pd));\n\t\t\tjanus_vp8_simulcast_descriptor_update(payload, plen, &session->vp8_context,\n\t\t\t\tsession->sim_context.changed_substream);\n\t\t}\n\t\t/* Send the packet */\n\t\tif(duktape_janus_core != NULL) {\n\t\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = packet->is_video,\n\t\t\t\t.buffer = (char *)packet->data, .length = packet->length, .extensions = packet->extensions };\n\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\tduktape_janus_core->relay_rtp(session->handle, &rtp);\n\t\t}\n\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\tif(sender->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t/* Restore the original payload descriptor as well, as it will be needed by the next viewer */\n\t\t\tmemcpy(payload, vp8pd, sizeof(vp8pd));\n\t\t}\n\t} else {\n\t\t/* Fix sequence number and timestamp (publisher switching may be involved) */\n\t\tjanus_rtp_header_update(packet->data, packet->is_video ? &session->vrtpctx : &session->artpctx, packet->is_video, 0);\n\t\t/* Send the packet */\n\t\tif(duktape_janus_core != NULL) {\n\t\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = packet->is_video,\n\t\t\t\t.buffer = (char *)packet->data, .length = packet->length, .extensions = packet->extensions };\n\t\t\tduktape_janus_core->relay_rtp(session->handle, &rtp);\n\t\t}\n\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t}\n\n\treturn;\n}\n\nstatic void janus_duktape_relay_data_packet(gpointer data, gpointer user_data) {\n\tjanus_duktape_rtp_relay_packet *packet = (janus_duktape_rtp_relay_packet *)user_data;\n\tif(!packet || packet->is_rtp || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_duktape_session *session = (janus_duktape_session *)data;\n\tif(!session || !session->handle || !g_atomic_int_get(&session->started) ||\n\t\t\t!session->accept_data || !g_atomic_int_get(&session->dataready)) {\n\t\treturn;\n\t}\n\tif(duktape_janus_core != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Forwarding %s DataChannel message (%d bytes) to session %\"SCNu32\"\\n\",\n\t\t\tpacket->textdata ? \"text\" : \"binary\", packet->length, session->id);\n\t\tjanus_plugin_data data = {\n\t\t\t.label = NULL,\n\t\t\t.protocol = NULL,\n\t\t\t.binary = !packet->textdata,\n\t\t\t.buffer = (char *)packet->data,\n\t\t\t.length = packet->length\n\t\t};\n\t\tduktape_janus_core->relay_data(session->handle, &data);\n\t}\n\treturn;\n}\n\n/* This is a scheduler thread: if we know there are coroutines to resume in\n * JavaScript (e.g., for asynchronous requests), we do that ourselves here */\nstatic void *janus_duktape_scheduler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining Duktape scheduler thread\\n\");\n\tjanus_duktape_event *event = NULL;\n\t/* Wait until there are events to process */\n\twhile(g_atomic_int_get(&duktape_initialized) && !g_atomic_int_get(&duktape_stopping)) {\n\t\tevent = g_async_queue_pop(events);\n\t\tif(event == GUINT_TO_POINTER(janus_duktape_event_exit))\n\t\t\tbreak;\n\t\tif(event == GUINT_TO_POINTER(janus_duktape_event_resume)) {\n\t\t\t/* There are coroutines to resume */\n\t\t\tjanus_mutex_lock(&duktape_mutex);\n\t\t\tduk_get_global_string(duktape_ctx, \"resumeScheduler\");\n\t\t\tint res = duk_pcall(duktape_ctx, 0);\n\t\t\tif(res != DUK_EXEC_SUCCESS) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(duktape_ctx, -1));\n\t\t\t}\n\t\t\tduk_pop(duktape_ctx);\n\t\t\t/* Print the count of elements into Duktape stack */\n\t\t\tjanus_duktape_stackdump(duktape_ctx);\n\t\t\tjanus_mutex_unlock(&duktape_mutex);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving Duktape scheduler thread\\n\");\n\treturn NULL;\n}\n\n/* This is a loop that can be used for timing callbacks, e.g., whenever\n * the JS script asks for asynchronously invoking one of its methods\n * after some time, rather than immediately (which is what the scheduler\n * would be for instead). Allows for a string parameter to be passed. */\nstatic void *janus_duktape_timer(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining Duktape timer loop\\n\");\n\tGMainLoop *loop = (GMainLoop *)data;\n\t/* Start loop */\n\tg_main_loop_run(loop);\n\t/* Done */\n\tJANUS_LOG(LOG_VERB, \"Leaving Duktape timer loop\\n\");\n\treturn NULL;\n}\n\n/* Callback to trigger timed callbacks */\nstatic gboolean janus_duktape_timer_cb(void *data) {\n\tjanus_duktape_callback *cb = (janus_duktape_callback *)data;\n\tif(cb == NULL)\n\t\treturn FALSE;\n\t/* Invoke the callback with the provided argument, if available */\n\tJANUS_LOG(LOG_VERB, \"Invoking scheduled callback (waited %\"SCNu32\"ms) with ID %u\\n\", cb->ms, cb->id);\n\tjanus_mutex_lock(&duktape_mutex);\n\tduk_idx_t thr_idx = duk_push_thread(duktape_ctx);\n\tduk_context *t = duk_get_context(duktape_ctx, thr_idx);\n\tduk_get_global_string(t, cb->function);\n\tif(cb->argument) {\n\t\tduk_push_string(t, cb->argument);\n\t}\n\tint res = duk_pcall(t, cb->argument ? 1 : 0);\n\tif(res != DUK_EXEC_SUCCESS) {\n\t\tJANUS_LOG(LOG_ERR, \"Duktape error: %s\\n\", duk_safe_to_string(t, -1));\n\t}\n\tduk_pop(t);\n\tduk_pop(duktape_ctx);\n\t/* Done */\n\tg_hash_table_remove(callbacks, cb);\n\tjanus_mutex_unlock(&duktape_mutex);\n\treturn FALSE;\n}\n"
  },
  {
    "path": "src/plugins/janus_duktape_data.h",
    "content": "/*! \\file   janus_duktape_data.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Duktape data/session definition (headers)\n * \\details  The Janus Duktape plugin implements all the mandatory hooks to\n * allow the C code to interact with a custom JavaScript script, and vice-versa.\n * That said, the janus_duktape_extra.c code allows for custom hooks to be\n * added in C, to expose additional JavaScript functions and implement more\n * complex media management than the one provided by the stock plugin.\n * For this to work, though, the janus_duktape_session object and its\n * indexing in the hashtable need to be defined externally, which is\n * what this file is for.\n *\n * Notice that all the management associated to sessions (creating or\n * destroying sessions, locking their global mutex, updating the\n * hashtable) is done in the core of the JavaScript plugin: here we only\n * define them, so that they can be accessed/used by the extra code too.\n *\n * \\ingroup jspapi\n * \\ref jspapi\n */\n\n#ifndef JANUS_DUKTAPE_DATA_H\n#define JANUS_DUKTAPE_DATA_H\n\n#include <duktape.h>\n#include \"duktape-deps/duk_console.h\"\n#include \"duktape-deps/duk_module_duktape.h\"\n\n#include \"plugin.h\"\n\n#include \"debug.h\"\n#include \"apierror.h\"\n#include \"config.h\"\n#include \"mutex.h\"\n#include \"rtp.h\"\n#include \"rtcp.h\"\n#include \"sdp-utils.h\"\n#include \"record.h\"\n#include \"utils.h\"\n\n/* Core pointer and related flags */\nextern volatile gint duktape_initialized, duktape_stopping;\nextern janus_callbacks *duktape_janus_core;\n\n/* Duktape context: we define context and mutex as extern */\nextern duk_context *duktape_ctx;\nextern janus_mutex duktape_mutex;\n\n/* Duktape session: we keep only the barebone stuff here, the rest will be in the JavaScript script */\ntypedef struct janus_duktape_session {\n\tjanus_plugin_session *handle;\t\t/* Pointer to the core-plugin session */\n\tuint32_t id;\t\t\t\t\t\t/* Unique session ID (will be used to correlate with the JavaScript script) */\n\t/* The following are only needed for media manipulation, feedback and routing, and may not all be used */\n\tgboolean accept_audio;\t\t\t\t/* Whether incoming audio can be accepted or must be dropped */\n\tgboolean accept_video;\t\t\t\t/* Whether incoming video can be accepted or must be dropped */\n\tgboolean accept_data;\t\t\t\t/* Whether incoming data can be accepted or must be dropped */\n\tgboolean send_audio;\t\t\t\t/* Whether outgoing audio can be sent or must be dropped */\n\tgboolean send_video;\t\t\t\t/* Whether outgoing video can be sent or must be dropped */\n\tgboolean send_data;\t\t\t\t\t/* Whether outgoing data can be sent or must be dropped */\n\tjanus_rtp_switching_context artpctx, vrtpctx;\n\tjanus_rtp_switching_context rtpctx;\t/* RTP switching context */\n\tjanus_videocodec vcodec;\t\t\t/* Video codec this session is using */\n\tuint32_t ssrc[3];\t\t\t\t\t/* Only needed in case VP8 (or H.264) simulcasting is involved */\n\tchar *rid[3];\t\t\t\t\t\t/* Only needed if simulcasting is rid-based */\n\tint rid_extmap_id;\t\t\t\t\t/* rid extmap ID */\n\tjanus_mutex rid_mutex;\t\t\t\t/* Mutex to protect access to the rid array and the extmap ID */\n\tjanus_rtp_simulcasting_context sim_context;\n\tjanus_vp8_simulcast_context vp8_context;\n\tuint32_t bitrate;\t\t\t\t\t/* Bitrate limit */\n\tuint16_t pli_freq;\t\t\t\t\t/* Regular PLI frequency (0=disabled) */\n\tgint64 pli_latest;\t\t\t\t\t/* Time of latest sent PLI (to avoid flooding) */\n\tGSList *recipients;\t\t\t\t\t/* Sessions that should receive media from this session */\n\tstruct janus_duktape_session *sender;\t/* Other session this session is receiving media from */\n\tjanus_mutex recipients_mutex;\t\t/* Mutex to lock the recipients list */\n\tjanus_recorder *arc;\t\t\t\t/* The Janus recorder instance for audio, if enabled */\n\tjanus_recorder *vrc;\t\t\t\t/* The Janus recorder instance for video, if enabled */\n\tjanus_recorder *drc;\t\t\t\t/* The Janus recorder instance for data, if enabled */\n\tjanus_rtp_switching_context rec_ctx;\n\tjanus_rtp_simulcasting_context rec_simctx;\n\tgboolean e2ee;\t\t\t\t\t\t/* Whether media is encrypted, e.g., using Insertable Streams */\n\tjanus_mutex rec_mutex;\t\t\t\t/* Mutex to protect the recorders from race conditions */\n\tvolatile gint started;\t\t\t\t/* Whether this session's PeerConnection is ready or not */\n\tvolatile gint dataready;\t\t\t/* Whether the data channel was established on this sessions's PeerConnection */\n\tvolatile gint hangingup;\t\t\t/* Whether this session's PeerConnection is hanging up */\n\tvolatile gint destroyed;\t\t\t/* Whether this session's been marked as destroyed */\n\t/* If you need any additional property (e.g., for hooks you added in janus_duktape_extra.c) add them below this line */\n\n\t/* Reference counter */\n\tjanus_refcount ref;\n} janus_duktape_session;\nextern GHashTable *duktape_sessions, *duktape_ids;\nextern janus_mutex duktape_sessions_mutex;\njanus_duktape_session *janus_duktape_lookup_session(janus_plugin_session *handle);\n\n#endif\n"
  },
  {
    "path": "src/plugins/janus_duktape_extra.c",
    "content": "/*! \\file   janus_duktape_extra.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Duktape plugin extra hooks\n * \\details  The Janus Duktape plugin implements all the mandatory hooks to\n * allow the C code to interact with a custom JavaScript script, and vice-versa.\n * Anyway, JavaScript developers may want to have the C code do more than what\n * is provided out of the box, e.g., by exposing additional JavaScript methods\n * from C for further low level processing or native integration. This\n * \"extra\" implementation provides a mechanism to do just that, as\n * developers can just add their own custom hooks in the C extra code,\n * and the Duktape plugin will register the new methods along the stock ones.\n *\n * More specifically, the Janus Duktape plugin will always invoke the\n * janus_duktape_register_extra_functions() method when initializing. This\n * means that all developers will need to do to register a new function\n * is adding new \\c duk_push_c_function calls to register their own functions\n * there, and they'll be added to the stack.\n *\n * \\ingroup jspapi\n * \\ref jspapi\n */\n\n#include \"janus_duktape_data.h\"\n#include \"janus_duktape_extra.h\"\n\n\n/* Sample extra function we can register */\nstatic duk_ret_t janus_duktape_extra_sample(duk_context *ctx) {\n\t/* Let's do nothing, and return 1234 */\n\tduk_push_int(ctx, 1234);\n\treturn 1;\n}\n\n/* This is where you can add your custom extra functions */\n\n\n/* Public method to register all custom extra functions */\nvoid janus_duktape_register_extra_functions(duk_context *ctx) {\n\tif(ctx == NULL)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"Registering extra Duktape functions\\n\");\n\t/* Register all extra functions here */\n\tduk_push_c_function(ctx, janus_duktape_extra_sample, 0);\n\tduk_put_global_string(ctx, \"testExtraFunction\");\n}\n"
  },
  {
    "path": "src/plugins/janus_duktape_extra.h",
    "content": "/*! \\file   janus_duktape_extra.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Duktape plugin extra hooks (headers)\n * \\details  The Janus Duktape plugin implements all the mandatory hooks to\n * allow the C code to interact with a custom JavaScript script, and vice-versa.\n * Anyway, JavaScript developers may want to have the C code do more than what\n * is provided out of the box, e.g., by exposing additional JavaScript methods\n * from C for further low level processing or native integration. This\n * \"extra\" implementation provides a mechanism to do just that, as\n * developers can just add their own custom hooks in the C extra code,\n * and the Duktape plugin will register the new methods along the stock ones.\n *\n * More specifically, the Janus Duktape plugin will always invoke the\n * janus_duktape_register_extra_functions() method when initializing. This\n * means that all developers will need to do to register a new function\n * is adding new \\c duk_push_c_function calls to register their own functions\n * there, and they'll be added to the stack.\n *\n * \\ingroup jspapi\n * \\ref jspapi\n */\n\n#ifndef JANUS_DUKTAPE_EXTRA_H\n#define JANUS_DUKTAPE_EXTRA_H\n\n#include <duktape.h>\n\n/*! \\brief Method to register extra JavaScript functions in the C code\n * @param[in] ctx The Duktape context to register the functions on */\nvoid janus_duktape_register_extra_functions(duk_context *ctx);\n\n#endif\n"
  },
  {
    "path": "src/plugins/janus_echotest.c",
    "content": "/*! \\file   janus_echotest.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus EchoTest plugin\n * \\details Check the \\ref echotest for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page echotest EchoTest plugin documentation\n * This is a trivial EchoTest plugin for Janus, just used to\n * showcase the plugin interface. A peer attaching to this plugin will\n * receive back the same RTP packets and RTCP messages he sends: the\n * RTCP messages, of course, would be modified on the way by the Janus core\n * to make sure they are coherent with the involved SSRCs. In order to\n * demonstrate how peer-provided messages can change the behaviour of a\n * plugin, this plugin implements a simple API based on three messages:\n *\n * 1. a message to enable/disable audio (that is, to tell the plugin\n * whether incoming audio RTP packets need to be sent back or discarded);\n * 2. a message to enable/disable video (that is, to tell the plugin\n * whether incoming video RTP packets need to be sent back or discarded);\n * 3. a message to cap the bitrate (which would modify incoming RTCP\n * REMB messages before sending them back, in order to trick the peer into\n * thinking the available bandwidth is different).\n *\n * \\section echoapi Echo Test API\n *\n * There's a single unnamed request you can send and it's asynchronous,\n * which means all responses (successes and errors) will be delivered\n * as events with the same transaction.\n *\n * The request has to be formatted as follows. All the attributes are\n * optional, so any request can contain a subset of them:\n *\n\\verbatim\n{\n\t\"audio\" : true|false,\n\t\"audiocodec\" : \"<optional codec name; only used when creating a PeerConnection>\",\n\t\"video\" : true|false,\n\t\"videocodec\" : \"<optional codec name; only used when creating a PeerConnection>\",\n\t\"videoprofile\" : \"<optional codec profile to force; only used when creating a PeerConnection, only valid for VP9 (0 or 2) and H.264 (e.g., 42e01f)>\",\n\t\"bitrate\" : <numeric bitrate value>,\n\t\"record\" : true|false,\n\t\"filename\" : <base path/filename to use for the recording>,\n\t\"substream\" : <substream to receive (0-2), in case simulcasting is enabled>,\n\t\"temporal\" : <temporal layers to receive (0-2), in case simulcasting is enabled>,\n\t\"svc\" : true|false,\n\t\"spatial_layer\" : <spatial layer to receive (0-2), in case SVC is enabled>,\n\t\"temporal_layer\" : <temporal layers to receive (0-2), in case SVC is enabled>\n}\n\\endverbatim\n *\n * When negotiating a new PeerConnection, by default the EchoTest tries to\n * use the preferred audio codecs as set by the user; if for any reason you\n * want to override what the browsers offered first and use a different\n * codec instead (e.g., to try VP9 instead of VP8), you can use the\n * \\c audiocodec property for audio, and \\c videocodec for video. For video\n * codecs supporting a specific profile negotiation (VP9 and H.264), you can\n * specify which profile you're interested in using the \\c videoprofile property.\n *\n * All the other settings can be applied dynamically during the session:\n * \\c audio instructs the plugin to do or do not bounce back audio\n * frames; \\c video does the same for video; \\c bitrate caps the\n * bandwidth to force on the browser encoding side (e.g., 128000 for\n * 128kbps); \\c record enables or disables the recording of this peer;\n * in case recording is enabled, \\c filename allows to specify a base\n * path/filename to use for the files (-audio.mjr, -video.mjr and -data.mjr\n * are automatically appended); finally, in case the session uses\n * simulcasting, \\c substream and \\c temporal can be used to manually\n * pick which substream and/or temporal layer should be received back,\n * while \\c spatial_layer and \\c temporal_layer provide the same\n * functionality but within the context of SVC.\n *\n * A JSEP offer can be sent along any request to negotiate a PeerConnection:\n * in that case, a JSEP answer will be provided with the asynchronous\n * response notification. Other requests (e.g., to dynamically manipulate\n * the bitrate while testing) have to be sent without any JSEP payload\n * attached, unless you want to renegotiate a session (e.g., to add/remove\n * a media stream, or force an ICE restart): in case of renegotiations,\n * the same rules as the first JSEP offer apply.\n *\n * A successful request will result in an \\c ok event:\n *\n\\verbatim\n{\n\t\"echotest\" : \"event\",\n\t\"result\": \"ok\"\n}\n\\endverbatim\n *\n * An error instead will provide both an error code and a more verbose\n * description of the cause of the issue:\n *\n\\verbatim\n{\n\t\"echotest\" : \"event\",\n\t\"error_code\" : <numeric ID, check Macros below>,\n\t\"error\" : \"<error description as a string>\"\n}\n\\endverbatim\n *\n * If the plugin detects a loss of the associated PeerConnection, a\n * \"done\" notification is triggered to inform the application the Echo\n * Test session is over:\n *\n\\verbatim\n{\n\t\"echotest\" : \"event\",\n\t\"result\": \"done\"\n}\n\\endverbatim\n */\n\n#include \"plugin.h\"\n\n#include <jansson.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../record.h\"\n#include \"../rtp.h\"\n#include \"../rtcp.h\"\n#include \"../sdp-utils.h\"\n#include \"../utils.h\"\n\n\n/* Plugin information */\n#define JANUS_ECHOTEST_VERSION\t\t\t7\n#define JANUS_ECHOTEST_VERSION_STRING\t\"0.0.7\"\n#define JANUS_ECHOTEST_DESCRIPTION\t\t\"This is a trivial EchoTest plugin for Janus, just used to showcase the plugin interface.\"\n#define JANUS_ECHOTEST_NAME\t\t\t\t\"JANUS EchoTest plugin\"\n#define JANUS_ECHOTEST_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_ECHOTEST_PACKAGE\t\t\t\"janus.plugin.echotest\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_echotest_init(janus_callbacks *callback, const char *config_path);\nvoid janus_echotest_destroy(void);\nint janus_echotest_get_api_compatibility(void);\nint janus_echotest_get_version(void);\nconst char *janus_echotest_get_version_string(void);\nconst char *janus_echotest_get_description(void);\nconst char *janus_echotest_get_name(void);\nconst char *janus_echotest_get_author(void);\nconst char *janus_echotest_get_package(void);\nvoid janus_echotest_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_echotest_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_echotest_handle_admin_message(json_t *message);\nvoid janus_echotest_setup_media(janus_plugin_session *handle);\nvoid janus_echotest_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_echotest_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_echotest_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet);\nvoid janus_echotest_data_ready(janus_plugin_session *handle);\nvoid janus_echotest_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink);\nvoid janus_echotest_hangup_media(janus_plugin_session *handle);\nvoid janus_echotest_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_echotest_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_echotest_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_echotest_init,\n\t\t.destroy = janus_echotest_destroy,\n\n\t\t.get_api_compatibility = janus_echotest_get_api_compatibility,\n\t\t.get_version = janus_echotest_get_version,\n\t\t.get_version_string = janus_echotest_get_version_string,\n\t\t.get_description = janus_echotest_get_description,\n\t\t.get_name = janus_echotest_get_name,\n\t\t.get_author = janus_echotest_get_author,\n\t\t.get_package = janus_echotest_get_package,\n\n\t\t.create_session = janus_echotest_create_session,\n\t\t.handle_message = janus_echotest_handle_message,\n\t\t.handle_admin_message = janus_echotest_handle_admin_message,\n\t\t.setup_media = janus_echotest_setup_media,\n\t\t.incoming_rtp = janus_echotest_incoming_rtp,\n\t\t.incoming_rtcp = janus_echotest_incoming_rtcp,\n\t\t.incoming_data = janus_echotest_incoming_data,\n\t\t.data_ready = janus_echotest_data_ready,\n\t\t.slow_link = janus_echotest_slow_link,\n\t\t.hangup_media = janus_echotest_hangup_media,\n\t\t.destroy_session = janus_echotest_destroy_session,\n\t\t.query_session = janus_echotest_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_ECHOTEST_NAME);\n\treturn &janus_echotest_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"substream\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"fallback\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"svc\", JANUS_JSON_BOOL, 0},\n\t{\"spatial_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"videocodec\", JSON_STRING, 0},\n\t{\"videoprofile\", JSON_STRING, 0},\n\t{\"opusred\", JANUS_JSON_BOOL, 0},\n\t{\"min_delay\", JSON_INTEGER, 0},\n\t{\"max_delay\", JSON_INTEGER, 0},\n};\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic janus_callbacks *gateway = NULL;\nstatic GThread *handler_thread;\nstatic void *janus_echotest_handler(void *data);\nstatic void janus_echotest_hangup_media_internal(janus_plugin_session *handle);\n\ntypedef struct janus_echotest_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_echotest_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_echotest_message exit_message;\n\ntypedef struct janus_echotest_session {\n\tjanus_plugin_session *handle;\n\tgboolean has_audio;\n\tgboolean has_video;\n\tgboolean has_data;\n\tgboolean audio_active;\n\tgboolean video_active;\n\tjanus_audiocodec acodec;/* Codec used for audio, if available */\n\tjanus_videocodec vcodec;/* Codec used for video, if available */\n\tchar *vfmtp;\n\tint opusred_pt;\n\tuint32_t bitrate, peer_bitrate;\n\tjanus_rtp_switching_context context;\n\tuint32_t ssrc[3];\t\t/* Only needed in case VP8 (or H.264) simulcasting is involved */\n\tchar *rid[3];\t\t\t/* Only needed if simulcasting is rid-based */\n\tjanus_mutex rid_mutex;\t/* Mutex to protect access to the rid array */\n\tjanus_rtp_simulcasting_context sim_context;\n\tjanus_vp8_simulcast_context vp8_context;\n\tgboolean svc;\n\tjanus_rtp_svc_context svc_context;\n\tjanus_recorder *arc;\t/* The Janus recorder instance for this user's audio, if enabled */\n\tjanus_recorder *vrc;\t/* The Janus recorder instance for this user's video, if enabled */\n\tjanus_recorder *drc;\t/* The Janus recorder instance for this user's data, if enabled */\n\tgboolean e2ee;\t\t\t/* Whether media is encrypted, e.g., using Insertable Streams */\n\tjanus_mutex rec_mutex;\t/* Mutex to protect the recorders from race conditions */\n\tguint16 slowlink_count;\n\tint16_t min_delay, max_delay;\n\tint8_t spatial_layers, temporal_layers;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_echotest_session;\nstatic GHashTable *sessions;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_echotest_session_destroy(janus_echotest_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_echotest_session_free(const janus_refcount *session_ref) {\n\tjanus_echotest_session *session = janus_refcount_containerof(session_ref, janus_echotest_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tg_free(session->vfmtp);\n\tjanus_mutex_destroy(&session->rid_mutex);\n\tjanus_mutex_destroy(&session->rec_mutex);\n\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\tjanus_rtp_svc_context_reset(&session->svc_context);\n\tg_free(session);\n}\n\nstatic void janus_echotest_message_free(janus_echotest_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_echotest_session *session = (janus_echotest_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\n\n/* Error codes */\n#define JANUS_ECHOTEST_ERROR_NO_MESSAGE\t\t\t411\n#define JANUS_ECHOTEST_ERROR_INVALID_JSON\t\t412\n#define JANUS_ECHOTEST_ERROR_INVALID_ELEMENT\t413\n#define JANUS_ECHOTEST_ERROR_INVALID_SDP\t\t414\n\n\n/* Plugin implementation */\nint janus_echotest_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_ECHOTEST_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_ECHOTEST_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_ECHOTEST_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_ECHOTEST_NAME);\n\t\t}\n\t}\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_echotest_session_destroy);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_echotest_message_free);\n\t/* This is the callback we'll need to invoke to contact the server */\n\tgateway = callback;\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming messages */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"echotest handler\", janus_echotest_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the EchoTest handler thread...\\n\", error->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_ECHOTEST_NAME);\n\treturn 0;\n}\n\nvoid janus_echotest_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_ECHOTEST_NAME);\n}\n\nint janus_echotest_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_echotest_get_version(void) {\n\treturn JANUS_ECHOTEST_VERSION;\n}\n\nconst char *janus_echotest_get_version_string(void) {\n\treturn JANUS_ECHOTEST_VERSION_STRING;\n}\n\nconst char *janus_echotest_get_description(void) {\n\treturn JANUS_ECHOTEST_DESCRIPTION;\n}\n\nconst char *janus_echotest_get_name(void) {\n\treturn JANUS_ECHOTEST_NAME;\n}\n\nconst char *janus_echotest_get_author(void) {\n\treturn JANUS_ECHOTEST_AUTHOR;\n}\n\nconst char *janus_echotest_get_package(void) {\n\treturn JANUS_ECHOTEST_PACKAGE;\n}\n\nstatic janus_echotest_session *janus_echotest_lookup_session(janus_plugin_session *handle) {\n\tjanus_echotest_session *session = NULL;\n\tif (g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_echotest_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_echotest_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_echotest_session *session = g_malloc0(sizeof(janus_echotest_session));\n\tsession->handle = handle;\n\tsession->has_audio = FALSE;\n\tsession->has_video = FALSE;\n\tsession->has_data = FALSE;\n\tsession->audio_active = TRUE;\n\tsession->video_active = TRUE;\n\tjanus_mutex_init(&session->rec_mutex);\n\tsession->bitrate = 0;\t/* No limit */\n\tsession->peer_bitrate = 0;\n\tjanus_rtp_switching_context_reset(&session->context);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tjanus_rtp_svc_context_reset(&session->svc_context);\n\tjanus_mutex_init(&session->rid_mutex);\n\tsession->min_delay = -1;\n\tsession->max_delay = -1;\n\tsession->spatial_layers = -1;\n\tsession->temporal_layers = -1;\n\tsession->destroyed = 0;\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tjanus_refcount_init(&session->ref, janus_echotest_session_free);\n\thandle->plugin_handle = session;\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_echotest_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_echotest_session *session = janus_echotest_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Removing Echo Test session...\\n\");\n\tjanus_echotest_hangup_media_internal(handle);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn;\n}\n\njson_t *janus_echotest_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_echotest_session *session = janus_echotest_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* In the echo test, every session is the same: we just provide some configure info */\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"audio_active\", session->audio_active ? json_true() : json_false());\n\tjson_object_set_new(info, \"video_active\", session->video_active ? json_true() : json_false());\n\tif(session->acodec != JANUS_AUDIOCODEC_NONE) {\n\t\tjson_object_set_new(info, \"audio_codec\", json_string(janus_audiocodec_name(session->acodec)));\n\t\tif(session->opusred_pt)\n\t\t\tjson_object_set_new(info, \"audio_red\", json_true());\n\t}\n\tif(session->vcodec != JANUS_VIDEOCODEC_NONE)\n\t\tjson_object_set_new(info, \"video_codec\", json_string(janus_videocodec_name(session->vcodec)));\n\tjson_object_set_new(info, \"bitrate\", json_integer(session->bitrate));\n\tjson_object_set_new(info, \"peer-bitrate\", json_integer(session->peer_bitrate));\n\tif(session->ssrc[0] != 0 || session->rid[0] != NULL) {\n\t\tjson_object_set_new(info, \"simulcast\", json_true());\n\t\tjson_object_set_new(info, \"substream\", json_integer(session->sim_context.substream));\n\t\tjson_object_set_new(info, \"substream-target\", json_integer(session->sim_context.substream_target));\n\t\tjson_object_set_new(info, \"temporal-layer\", json_integer(session->sim_context.templayer));\n\t\tjson_object_set_new(info, \"temporal-layer-target\", json_integer(session->sim_context.templayer_target));\n\t\tif(session->sim_context.drop_trigger > 0)\n\t\t\tjson_object_set_new(info, \"fallback\", json_integer(session->sim_context.drop_trigger));\n\t}\n\tif(session->svc) {\n\t\tjson_object_set_new(info, \"svc\", json_true());\n\t\tjson_object_set_new(info, \"spatial-layer\", json_integer(session->svc_context.spatial));\n\t\tjson_object_set_new(info, \"spatial-layer-target\", json_integer(session->svc_context.spatial_target));\n\t\tjson_object_set_new(info, \"temporal-layer\", json_integer(session->svc_context.temporal));\n\t\tjson_object_set_new(info, \"temporal-layer-target\", json_integer(session->svc_context.temporal_target));\n\t}\n\tif(session->arc || session->vrc || session->drc) {\n\t\tjson_t *recording = json_object();\n\t\tif(session->arc && session->arc->filename)\n\t\t\tjson_object_set_new(recording, \"audio\", json_string(session->arc->filename));\n\t\tif(session->vrc && session->vrc->filename)\n\t\t\tjson_object_set_new(recording, \"video\", json_string(session->vrc->filename));\n\t\tif(session->drc && session->drc->filename)\n\t\t\tjson_object_set_new(recording, \"data\", json_string(session->drc->filename));\n\t\tjson_object_set_new(info, \"recording\", recording);\n\t}\n\tif(session->e2ee)\n\t\tjson_object_set_new(info, \"e2ee\", json_true());\n\tjson_object_set_new(info, \"slowlink_count\", json_integer(session->slowlink_count));\n\tjson_object_set_new(info, \"hangingup\", json_integer(g_atomic_int_get(&session->hangingup)));\n\tjson_object_set_new(info, \"destroyed\", json_integer(g_atomic_int_get(&session->destroyed)));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstruct janus_plugin_result *janus_echotest_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\tjanus_echotest_session *session = (janus_echotest_session *)handle->plugin_handle;\n\tif(!session)\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\tjanus_echotest_message *msg = g_malloc(sizeof(janus_echotest_message));\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\n\tmsg->handle = handle;\n\tmsg->transaction = transaction;\n\tmsg->message = message;\n\tmsg->jsep = jsep;\n\tg_async_queue_push(messages, msg);\n\n\t/* All the requests to this plugin are handled asynchronously: we add a comment\n\t * (a JSON object with a \"hint\" string in it, that's what the core expects),\n\t * but we don't have to: other plugins don't put anything in there */\n\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, \"I'm taking my time!\", NULL);\n}\n\njson_t *janus_echotest_handle_admin_message(json_t *message) {\n\t/* Just here as a proof of concept: since there's nothing to configure,\n\t * as an EchoTest plugin we echo this Admin request back as well */\n\tjson_t *response = json_deep_copy(message);\n\treturn response;\n}\n\nvoid janus_echotest_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_ECHOTEST_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_echotest_session *session = janus_echotest_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* We really don't care, as we only send RTP/RTCP we get in the first place back anyway */\n}\n\nvoid janus_echotest_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\t/* Simple echo test */\n\tif(gateway) {\n\t\t/* Honour the audio/video active flags */\n\t\tjanus_echotest_session *session = (janus_echotest_session *)handle->plugin_handle;\n\t\tif(!session) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed))\n\t\t\treturn;\n\t\tgboolean video = packet->video;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\tif(session->min_delay > -1 && session->max_delay > -1) {\n\t\t\tpacket->extensions.min_delay = session->min_delay;\n\t\t\tpacket->extensions.max_delay = session->max_delay;\n\t\t}\n\t\tgboolean new_vla = FALSE;\n\t\tif(packet->extensions.spatial_layers > -1 || packet->extensions.temporal_layers > -1) {\n\t\t\t/* We have info from the video-layers-allocation RTP extension */\n\t\t\tif(packet->extensions.spatial_layers != session->spatial_layers ||\n\t\t\t\t\tpacket->extensions.temporal_layers != session->temporal_layers) {\n\t\t\t\t/* It's new information, keep track of it */\n\t\t\t\tnew_vla = TRUE;\n\t\t\t\tsession->spatial_layers = packet->extensions.spatial_layers;\n\t\t\t\tsession->temporal_layers = packet->extensions.temporal_layers;\n\t\t\t}\n\t\t}\n\t\tgboolean simulcast = (session->ssrc[0] != 0 || session->rid[0] != NULL);\n\t\tif(video && session->video_active && (simulcast || session->svc)) {\n\t\t\t/* Handle simulcast or SVC: backup the header information first */\n\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\tuint32_t seq_number = ntohs(header->seq_number);\n\t\t\tuint32_t timestamp = ntohl(header->timestamp);\n\t\t\tuint32_t ssrc = ntohl(header->ssrc);\n\t\t\tgboolean relay = FALSE;\n\t\t\tif(simulcast) {\n\t\t\t\t/* Process this simulcast packet: don't relay if it's not the SSRC/layer we wanted to handle */\n\t\t\t\trelay = janus_rtp_simulcasting_context_process_rtp(&session->sim_context,\n\t\t\t\t\tbuf, len, packet->extensions.dd_content, packet->extensions.dd_len,\n\t\t\t\t\tsession->ssrc, session->rid, session->vcodec, &session->context, &session->rid_mutex);\n\t\t\t} else {\n\t\t\t\t/* Process this SVC packet: don't relay if it's not the layer we wanted to handle */\n\t\t\t\trelay = janus_rtp_svc_context_process_rtp(&session->svc_context,\n\t\t\t\t\tbuf, len, packet->extensions.dd_content, packet->extensions.dd_len, session->vcodec, NULL, &session->context);\n\t\t\t}\n\t\t\tif(session->sim_context.need_pli || session->svc_context.need_pli) {\n\t\t\t\t/* Send a PLI */\n\t\t\t\tgateway->send_pli(handle);\n\t\t\t}\n\t\t\t/* Do we need to drop this? */\n\t\t\tif(!relay)\n\t\t\t\treturn;\n\t\t\t/* Any event we should notify? */\n\t\t\tif(simulcast && (new_vla || session->sim_context.changed_substream || session->sim_context.changed_temporal)) {\n\t\t\t\t/* Notify the user about the substream change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(event, \"substream\", json_integer(session->sim_context.substream));\n\t\t\t\tjson_object_set_new(event, \"temporal\", json_integer(session->sim_context.templayer));\n\t\t\t\tif(session->temporal_layers > -1)\n\t\t\t\t\tjson_object_set_new(event, \"tot_temporal_layers\", json_integer(session->temporal_layers));\n\t\t\t\tgateway->push_event(handle, &janus_echotest_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\tif(session->svc && (new_vla || session->svc_context.changed_spatial || session->svc_context.changed_temporal)) {\n\t\t\t\t/* Notify the user about the spatial layer change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(event, \"spatial_layer\", json_integer(session->svc_context.spatial));\n\t\t\t\tjson_object_set_new(event, \"temporal_layer\", json_integer(session->svc_context.temporal));\n\t\t\t\tif(session->spatial_layers > -1)\n\t\t\t\t\tjson_object_set_new(event, \"tot_spatial_layers\", json_integer(session->spatial_layers));\n\t\t\t\tif(session->temporal_layers > -1)\n\t\t\t\t\tjson_object_set_new(event, \"tot_temporal_layers\", json_integer(session->temporal_layers));\n\t\t\t\tgateway->push_event(handle, &janus_echotest_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\t/* If we got here, update the RTP header and send the packet */\n\t\t\tjanus_rtp_header_update(header, &session->context, TRUE, 0);\n\t\t\tif(session->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t\tint plen = 0;\n\t\t\t\tchar *payload = janus_rtp_payload(buf, len, &plen);\n\t\t\t\tjanus_vp8_simulcast_descriptor_update(payload, plen, &session->vp8_context, session->sim_context.changed_substream);\n\t\t\t}\n\t\t\t/* Save the frame if we're recording (and make sure the SSRC never changes even if the substream does) */\n\t\t\theader->ssrc = htonl(1);\n\t\t\tjanus_recorder_save_frame(session->vrc, buf, len);\n\t\t\t/* Send the frame back */\n\t\t\tgateway->relay_rtp(handle, packet);\n\t\t\t/* Restore header or core statistics will be messed up */\n\t\t\theader->ssrc = htonl(ssrc);\n\t\t\theader->timestamp = htonl(timestamp);\n\t\t\theader->seq_number = htons(seq_number);\n\t\t} else {\n\t\t\tif((!video && session->audio_active) || (video && session->video_active)) {\n\t\t\t\t/* Save the frame if we're recording */\n\t\t\t\tjanus_recorder_save_frame(video ? session->vrc : session->arc, buf, len);\n\t\t\t\t/* Send the frame back */\n\t\t\t\tgateway->relay_rtp(handle, packet);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid janus_echotest_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\t/* Simple echo test */\n\tif(gateway) {\n\t\tjanus_echotest_session *session = (janus_echotest_session *)handle->plugin_handle;\n\t\tif(!session) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed))\n\t\t\treturn;\n\t\tguint32 bitrate = janus_rtcp_get_remb(packet->buffer, packet->length);\n\t\tif(bitrate > 0) {\n\t\t\t/* If a REMB arrived, make sure we cap it to our configuration, and send it as a video RTCP */\n\t\t\tsession->peer_bitrate = bitrate;\n\t\t\t/* No limit ~= 10000000 */\n\t\t\tgateway->send_remb(handle, session->bitrate ? session->bitrate : 10000000);\n\t\t\treturn;\n\t\t}\n\t\tgateway->relay_rtcp(handle, packet);\n\t}\n}\n\nvoid janus_echotest_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\t/* Simple echo test */\n\tif(gateway) {\n\t\tjanus_echotest_session *session = (janus_echotest_session *)handle->plugin_handle;\n\t\tif(!session) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed))\n\t\t\treturn;\n\t\tif(packet->buffer == NULL || packet->length == 0)\n\t\t\treturn;\n\t\tchar *label = packet->label;\n\t\tchar *protocol = packet->protocol;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\tif(packet->binary) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Got a binary DataChannel message (label=%s, protocol=%s, %d bytes) to bounce back\\n\",\n\t\t\t\tlabel, protocol, len);\n\t\t\t/* Save the frame if we're recording */\n\t\t\tjanus_recorder_save_frame(session->drc, buf, len);\n\t\t\t/* Binary data, shoot back as it is */\n\t\t\tgateway->relay_data(handle, packet);\n\t\t\treturn;\n\t\t}\n\t\t/* Text data */\n\t\tchar *text = g_malloc(len+1);\n\t\tmemcpy(text, buf, len);\n\t\t*(text+len) = '\\0';\n\t\tJANUS_LOG(LOG_VERB, \"Got a DataChannel message (label=%s, protocol=%s, %zu bytes) to bounce back: %s\\n\",\n\t\t\tlabel, protocol, strlen(text), text);\n\t\t/* Save the frame if we're recording */\n\t\tjanus_recorder_save_frame(session->drc, text, strlen(text));\n\t\t/* We send back the same text with a custom prefix */\n\t\tconst char *prefix = \"Janus EchoTest here! You wrote: \";\n\t\tchar *reply = g_malloc(strlen(prefix)+len+1);\n\t\tg_snprintf(reply, strlen(prefix)+len+1, \"%s%s\", prefix, text);\n\t\tg_free(text);\n\t\t/* Prepare the packet and send it back */\n\t\tjanus_plugin_data r = {\n\t\t\t.label = label,\n\t\t\t.protocol = protocol,\n\t\t\t.binary = FALSE,\n\t\t\t.buffer = reply,\n\t\t\t.length = strlen(reply)\n\t\t};\n\t\tgateway->relay_data(handle, &r);\n\t\tg_free(reply);\n\t}\n}\n\nvoid janus_echotest_data_ready(janus_plugin_session *handle) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) ||\n\t\t\tg_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized) || !gateway)\n\t\treturn;\n\t/* Data channels are writable */\n}\n\nvoid janus_echotest_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink) {\n\t/* The core is informing us that our peer got or sent too many NACKs, are we pushing media too hard? */\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_echotest_session *session = janus_echotest_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tsession->slowlink_count++;\n\tif(uplink && !video && !session->audio_active) {\n\t\t/* We're not relaying audio and the peer is expecting it, so NACKs are normal */\n\t\tJANUS_LOG(LOG_VERB, \"Getting a lot of NACKs (slow uplink) for audio, but that's expected, a configure disabled the audio forwarding\\n\");\n\t} else if(uplink && video && !session->video_active) {\n\t\t/* We're not relaying video and the peer is expecting it, so NACKs are normal */\n\t\tJANUS_LOG(LOG_VERB, \"Getting a lot of NACKs (slow uplink) for video, but that's expected, a configure disabled the video forwarding\\n\");\n\t} else {\n\t\tJANUS_LOG(LOG_WARN, \"Getting a lot of NACKs (slow %s) for %s\\n\",\n\t\t\tuplink ? \"uplink\" : \"downlink\", video ? \"video\" : \"audio\");\n\t\tif(!uplink) {\n\t\t\t/* Send an event on the handle to notify the application: it's\n\t\t\t * up to the application to then choose a policy and enforce it */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"event\", json_string(\"slow_link\"));\n\t\t\tjson_object_set_new(event, \"media\", json_string(video ? \"video\" : \"audio\"));\n\t\t\tif(video) {\n\t\t\t\t/* Also add info on what the current bitrate cap is */\n\t\t\t\tjson_object_set_new(event, \"current-bitrate\", json_integer(session->bitrate));\n\t\t\t}\n\t\t\tgateway->push_event(session->handle, &janus_echotest_plugin, NULL, event, NULL);\n\t\t\tjson_decref(event);\n\t\t}\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_echotest_recorder_close(janus_echotest_session *session) {\n\tif(session->arc) {\n\t\tjanus_recorder *rc = session->arc;\n\t\tsession->arc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->vrc) {\n\t\tjanus_recorder *rc = session->vrc;\n\t\tsession->vrc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed video recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->drc) {\n\t\tjanus_recorder *rc = session->drc;\n\t\tsession->drc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed data recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n}\n\nvoid janus_echotest_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_ECHOTEST_PACKAGE, handle);\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_echotest_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_echotest_hangup_media_internal(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_echotest_session *session = janus_echotest_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\t/* Send an event to the browser and tell it's over */\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\tjson_object_set_new(event, \"result\", json_string(\"done\"));\n\tint ret = gateway->push_event(handle, &janus_echotest_plugin, NULL, event, NULL);\n\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\tjson_decref(event);\n\t/* Get rid of the recorders, if available */\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_echotest_recorder_close(session);\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Reset controls */\n\tsession->has_audio = FALSE;\n\tsession->has_video = FALSE;\n\tsession->has_data = FALSE;\n\tsession->audio_active = TRUE;\n\tsession->video_active = TRUE;\n\tsession->acodec = JANUS_AUDIOCODEC_NONE;\n\tsession->vcodec = JANUS_VIDEOCODEC_NONE;\n\tg_free(session->vfmtp);\n\tsession->vfmtp = NULL;\n\tsession->opusred_pt = -1;\n\tsession->e2ee = FALSE;\n\tsession->bitrate = 0;\n\tsession->peer_bitrate = 0;\n\tjanus_rtp_simulcasting_cleanup(NULL, session->ssrc, session->rid, &session->rid_mutex);\n\tjanus_rtp_switching_context_reset(&session->context);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tsession->min_delay = -1;\n\tsession->max_delay = -1;\n\tsession->spatial_layers = -1;\n\tsession->temporal_layers = -1;\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_echotest_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining EchoTest handler thread\\n\");\n\tjanus_echotest_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_echotest_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_echotest_session *session = janus_echotest_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_echotest_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_echotest_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = msg->message;\n\t\tif(msg->message == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_ECHOTEST_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(!json_is_object(root)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\t\terror_code = JANUS_ECHOTEST_ERROR_INVALID_JSON;\n\t\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\t\tgoto error;\n\t\t}\n\t\t/* Parse request */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\t0, JANUS_ECHOTEST_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto error;\n\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\tif(msg_simulcast && json_array_size(msg_simulcast) > 0) {\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(msg_simulcast); i++) {\n\t\t\t\tjson_t *s = json_array_get(msg_simulcast, i);\n\t\t\t\tint mindex = json_integer_value(json_object_get(s, \"mindex\"));\n\t\t\t\tJANUS_LOG(LOG_VERB, \"EchoTest client is going to do simulcasting (#%d)\\n\", mindex);\n\t\t\t\tint rid_ext_id = -1;\n\t\t\t\tjanus_mutex_lock(&session->rid_mutex);\n\t\t\t\t/* Clear existing RIDs in case this is a renegotiation */\n\t\t\t\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\t\t\t\tjanus_rtp_simulcasting_prepare(s, &rid_ext_id, session->ssrc, session->rid);\n\t\t\t\tsession->sim_context.rid_ext_id = rid_ext_id;\n\t\t\t\tjanus_mutex_unlock(&session->rid_mutex);\n\t\t\t\tsession->sim_context.substream_target = 2;\t/* Let's aim for the highest quality */\n\t\t\t\tsession->sim_context.templayer_target = 2;\t/* Let's aim for all temporal layers */\n\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tjson_t *msg_svc = json_object_get(msg->jsep, \"svc\");\n\t\tif(msg_svc && json_array_size(msg_svc) > 0) {\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(msg_svc); i++) {\n\t\t\t\tjson_t *s = json_array_get(msg_svc, i);\n\t\t\t\tint mindex = json_integer_value(json_object_get(s, \"mindex\"));\n\t\t\t\tJANUS_LOG(LOG_VERB, \"EchoTest client is going to do SVC (#%d)\\n\", mindex);\n\t\t\t\tif(!session->svc) {\n\t\t\t\t\tjanus_rtp_svc_context_reset(&session->svc_context);\n\t\t\t\t\tsession->svc_context.spatial_target = 2;\t/* FIXME Actually depends on the scalabilityMode */\n\t\t\t\t\tsession->svc_context.temporal_target = 2;\t/* FIXME Actually depends on the scalabilityMode */\n\t\t\t\t\tsession->svc = TRUE;\n\t\t\t\t}\n\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tjson_t *msg_e2ee = json_object_get(msg->jsep, \"e2ee\");\n\t\tif(json_is_true(msg_e2ee))\n\t\t\tsession->e2ee = TRUE;\n\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\tjson_t *video = json_object_get(root, \"video\");\n\t\tjson_t *bitrate = json_object_get(root, \"bitrate\");\n\t\tjson_t *substream = json_object_get(root, \"substream\");\n\t\tif(substream && json_integer_value(substream) > 2) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (substream should be 0, 1 or 2)\\n\");\n\t\t\terror_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Invalid value (substream should be 0, 1 or 2)\");\n\t\t\tgoto error;\n\t\t}\n\t\tjson_t *temporal = json_object_get(root, \"temporal\");\n\t\tif(temporal && json_integer_value(temporal) > 2) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (temporal should be 0, 1 or 2)\\n\");\n\t\t\terror_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Invalid value (temporal should be 0, 1 or 2)\");\n\t\t\tgoto error;\n\t\t}\n\t\tjson_t *spatial_layer = json_object_get(root, \"spatial_layer\");\n\t\tif(spatial_layer && json_integer_value(spatial_layer) > 2) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (spatial_layer should be 0, 1 or 2)\\n\");\n\t\t\terror_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Invalid value (spatial_layer should be 0, 1 or 2)\");\n\t\t\tgoto error;\n\t\t}\n\t\tjson_t *temporal_layer = json_object_get(root, \"temporal_layer\");\n\t\tif(temporal_layer && json_integer_value(temporal_layer) > 2) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (temporal_layer should be 0, 1 or 2)\\n\");\n\t\t\terror_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Invalid value (temporal_layer should be 0, 1 or 2)\");\n\t\t\tgoto error;\n\t\t}\n\t\tjson_t *fallback = json_object_get(root, \"fallback\");\n\t\tjson_t *record = json_object_get(root, \"record\");\n\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\tjson_t *videocodec = json_object_get(root, \"videocodec\");\n\t\tjson_t *videoprofile = json_object_get(root, \"videoprofile\");\n\t\tjson_t *opusred = json_object_get(root, \"opusred\");\n\t\tjson_t *min_delay = json_object_get(root, \"min_delay\");\n\t\tjson_t *max_delay = json_object_get(root, \"max_delay\");\n\t\t/* Enforce request */\n\t\tif(audio) {\n\t\t\tsession->audio_active = json_is_true(audio);\n\t\t\tJANUS_LOG(LOG_VERB, \"Setting audio property: %s\\n\", session->audio_active ? \"true\" : \"false\");\n\t\t}\n\t\tif(video) {\n\t\t\tif(!session->video_active && json_is_true(video)) {\n\t\t\t\t/* Send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Just (re-)enabled video, sending a PLI to recover it\\n\");\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t\tsession->video_active = json_is_true(video);\n\t\t\tJANUS_LOG(LOG_VERB, \"Setting video property: %s\\n\", session->video_active ? \"true\" : \"false\");\n\t\t}\n\t\tif(bitrate) {\n\t\t\tsession->bitrate = json_integer_value(bitrate);\n\t\t\tJANUS_LOG(LOG_VERB, \"Setting video bitrate: %\"SCNu32\"\\n\", session->bitrate);\n\t\t\tgateway->send_remb(session->handle, session->bitrate ? session->bitrate : 10000000);\n\t\t}\n\t\tif(fallback) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Setting fallback timer (simulcast): %lld (was %\"SCNu32\")\\n\",\n\t\t\t\tjson_integer_value(fallback) ? json_integer_value(fallback) : 250000,\n\t\t\t\tsession->sim_context.drop_trigger ? session->sim_context.drop_trigger : 250000);\n\t\t\tsession->sim_context.drop_trigger = json_integer_value(fallback);\n\t\t}\n\t\tif(substream) {\n\t\t\tsession->sim_context.substream_target = json_integer_value(substream);\n\t\t\tif(session->sim_context.substream_target >= 0 && session->sim_context.substream_target <= 2) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video SSRC to let through (simulcast): %\"SCNu32\" (index %d, was %d)\\n\",\n\t\t\t\t\tsession->ssrc[session->sim_context.substream_target], session->sim_context.substream_target, session->sim_context.substream);\n\t\t\t}\n\t\t\tif(session->sim_context.substream_target == session->sim_context.substream) {\n\t\t\t\t/* No need to do anything, we're already getting the right substream, so notify the user */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(event, \"substream\", json_integer(session->sim_context.substream));\n\t\t\t\tgateway->push_event(session->handle, &janus_echotest_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t} else {\n\t\t\t\t/* We need to change substream, send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Simulcasting substream change, sending a PLI to kickstart it\\n\");\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t}\n\t\tif(temporal) {\n\t\t\tsession->sim_context.templayer_target = json_integer_value(temporal);\n\t\t\tJANUS_LOG(LOG_VERB, \"Setting video temporal layer to let through (simulcast): %d (was %d)\\n\",\n\t\t\t\tsession->sim_context.templayer_target, session->sim_context.templayer);\n\t\t\tif(session->sim_context.templayer_target == session->sim_context.templayer) {\n\t\t\t\t/* No need to do anything, we're already getting the right temporal, so notify the user */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(event, \"temporal\", json_integer(session->sim_context.templayer));\n\t\t\t\tgateway->push_event(session->handle, &janus_echotest_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t} else {\n\t\t\t\t/* We need to change temporal, send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Simulcasting temporal layer change, sending a PLI to kickstart it\\n\");\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t}\n\t\tif(spatial_layer) {\n\t\t\tsession->svc_context.spatial_target = json_integer_value(spatial_layer);\n\t\t\tif(session->svc_context.spatial_target == session->svc_context.spatial) {\n\t\t\t\t/* No need to do anything, we're already getting the right spatial layer, so notify the user */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(event, \"spatial_layer\", json_integer(session->svc_context.spatial));\n\t\t\t\tgateway->push_event(session->handle, &janus_echotest_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t} else {\n\t\t\t\t/* We need to change spatial layer, send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"SVC spatial layer change, sending a PLI to kickstart it\\n\");\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t}\n\t\tif(temporal_layer) {\n\t\t\tsession->svc_context.temporal_target = json_integer_value(temporal_layer);\n\t\t\tif(session->svc_context.temporal_target == session->svc_context.temporal) {\n\t\t\t\t/* No need to do anything, we're already getting the right temporal layer, so notify the user */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(event, \"temporal_layer\", json_integer(session->svc_context.temporal));\n\t\t\t\tgateway->push_event(session->handle, &janus_echotest_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t} else {\n\t\t\t\t/* We need to change temporal layer, send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"SVC temporal layer change, sending a PLI to kickstart it\\n\");\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t}\n\t\tif(min_delay) {\n\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\tif(md < 0) {\n\t\t\t\tsession->min_delay = -1;\n\t\t\t\tsession->max_delay = -1;\n\t\t\t} else {\n\t\t\t\tsession->min_delay = md;\n\t\t\t\tif(session->min_delay > session->max_delay)\n\t\t\t\t\tsession->max_delay = session->min_delay;\n\t\t\t}\n\t\t}\n\t\tif(max_delay) {\n\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\tif(md < 0) {\n\t\t\t\tsession->min_delay = -1;\n\t\t\t\tsession->max_delay = -1;\n\t\t\t} else {\n\t\t\t\tsession->max_delay = md;\n\t\t\t\tif(session->max_delay < session->min_delay)\n\t\t\t\t\tsession->min_delay = session->max_delay;\n\t\t\t}\n\t\t}\n\n\t\t/* Any SDP to handle? */\n\t\tif(msg_sdp) {\n\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well:\\n%s\\n\", msg_sdp_type, msg_sdp);\n\t\t\tsession->has_audio = (strstr(msg_sdp, \"m=audio\") != NULL);\n\t\t\tsession->has_video = (strstr(msg_sdp, \"m=video\") != NULL);\n\t\t\tsession->has_data = (strstr(msg_sdp, \"DTLS/SCTP\") != NULL);\n\t\t}\n\n\t\tif(!audio && !video && !videocodec && !videoprofile && !opusred && !bitrate &&\n\t\t\t\t!substream && !temporal && !fallback && !spatial_layer && !temporal_layer &&\n\t\t\t\t!record && !min_delay && !max_delay && !msg_sdp) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No supported attributes found\\n\");\n\t\t\terror_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Message error: no supported attributes found\");\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Prepare JSON event */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\tjson_object_set_new(event, \"result\", json_string(\"ok\"));\n\t\tif(!msg_sdp) {\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_echotest_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t} else {\n\t\t\t/* Answer the offer and pass it to the core, to start the echo test */\n\t\t\tconst char *type = \"answer\";\n\t\t\tchar error_str[512];\n\t\t\tjanus_sdp *offer = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str));\n\t\t\tif(offer == NULL) {\n\t\t\t\tjson_decref(event);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing offer: %s\\n\", error_str);\n\t\t\t\terror_code = JANUS_ECHOTEST_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing offer: %s\", error_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Check if we need to negotiate Opus FEC and/or DTX */\n\t\t\tgboolean opus_fec = FALSE, opus_dtx = FALSE, opus_stereo = FALSE;\n\t\t\tchar custom_fmtp[256];\n\t\t\tcustom_fmtp[0] = '\\0';\n\t\t\tGList *temp = offer->m_lines;\n\t\t\twhile(temp) {\n\t\t\t\t/* Which media are available? */\n\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\tif((m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) && m->port > 0) {\n\t\t\t\t\t/* Are the extmaps we care about there? */\n\t\t\t\t\tGList *ma = m->attributes;\n\t\t\t\t\twhile(ma) {\n\t\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\t\t\tif(a->value) {\n\t\t\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO && !strcasecmp(a->name, \"fmtp\")) {\n\t\t\t\t\t\t\t\tif(strstr(a->value, \"useinbandfec=1\")) {\n\t\t\t\t\t\t\t\t\topus_fec = TRUE;\n\t\t\t\t\t\t\t\t\tif(strlen(custom_fmtp) == 0) {\n\t\t\t\t\t\t\t\t\t\tg_snprintf(custom_fmtp, sizeof(custom_fmtp), \"useinbandfec=1\");\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tjanus_strlcat(custom_fmtp, \";useinbandfec=1\", sizeof(custom_fmtp));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(strstr(a->value, \"usedtx=1\")) {\n\t\t\t\t\t\t\t\t\topus_dtx = TRUE;\n\t\t\t\t\t\t\t\t\tif(strlen(custom_fmtp) == 0) {\n\t\t\t\t\t\t\t\t\t\tg_snprintf(custom_fmtp, sizeof(custom_fmtp), \"usedtx=1\");\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tjanus_strlcat(custom_fmtp, \";usedtx=1\", sizeof(custom_fmtp));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(strstr(a->value, \"stereo=1\")) {\n\t\t\t\t\t\t\t\t\topus_stereo = TRUE;\n\t\t\t\t\t\t\t\t\tif(strlen(custom_fmtp) == 0) {\n\t\t\t\t\t\t\t\t\t\tg_snprintf(custom_fmtp, sizeof(custom_fmtp), \"stereo=1\");\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tg_strlcat(custom_fmtp, \";stereo=1\", sizeof(custom_fmtp));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tma = ma->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tjanus_sdp *answer = janus_sdp_generate_answer(offer);\n\t\t\ttemp = offer->m_lines;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\tjanus_sdp_generate_answer_mline(offer, answer, m,\n\t\t\t\t\tJANUS_SDP_OA_MLINE, m->type,\n\t\t\t\t\tJANUS_SDP_OA_CODEC, (m->type == JANUS_SDP_AUDIO ? json_string_value(audiocodec) :\n\t\t\t\t\t\t(m->type == JANUS_SDP_VIDEO ? json_string_value(videocodec) : NULL)),\n\t\t\t\t\tJANUS_SDP_OA_FMTP, ((m->type == JANUS_SDP_AUDIO && (opus_fec || opus_dtx || opus_stereo)) ? custom_fmtp : NULL),\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_OPUSRED, (m->type == JANUS_SDP_AUDIO && json_is_true(opusred)),\n\t\t\t\t\tJANUS_SDP_OA_VP9_PROFILE, json_string_value(videoprofile),\n\t\t\t\t\tJANUS_SDP_OA_H264_PROFILE, json_string_value(videoprofile),\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_RID,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_REPAIRED_RID,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_AUDIO_LEVEL,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_PLAYOUT_DELAY,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_DEPENDENCY_DESC,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_ABS_SEND_TIME,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME,\n\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_VIDEO_LAYERS,\n\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\t/* If we ended up sendonly, switch to inactive (as we don't really send anything ourselves) */\n\t\t\tjanus_sdp_mline *m = janus_sdp_mline_find(answer, JANUS_SDP_AUDIO);\n\t\t\tif(m && m->direction == JANUS_SDP_SENDONLY)\n\t\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\tm = janus_sdp_mline_find(answer, JANUS_SDP_VIDEO);\n\t\t\tif(m && m->direction == JANUS_SDP_SENDONLY)\n\t\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\t/* Check which codecs we ended up with */\n\t\t\tconst char *acodec = NULL, *vcodec = NULL;\n\t\t\tjanus_sdp_find_first_codec(answer, JANUS_SDP_AUDIO, -1, &acodec);\n\t\t\tif(acodec)\n\t\t\t\tsession->acodec = janus_audiocodec_from_name(acodec);\n\t\t\tjanus_sdp_find_first_codec(answer, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\tif(vcodec)\n\t\t\t\tsession->vcodec = janus_videocodec_from_name(vcodec);\n\t\t\tsession->has_audio = session->acodec != JANUS_AUDIOCODEC_NONE;\n\t\t\tsession->has_video = session->vcodec != JANUS_VIDEOCODEC_NONE;\n\t\t\tg_free(session->vfmtp);\n\t\t\tsession->vfmtp = NULL;\n\t\t\tif(session->has_video) {\n\t\t\t\tconst char *vfmtp = janus_sdp_get_fmtp(answer, -1, janus_sdp_get_codec_pt(answer, -1, vcodec));\n\t\t\t\tif(vfmtp != NULL)\n\t\t\t\t\tsession->vfmtp = g_strdup(vfmtp);\n\t\t\t}\n\t\t\tif(json_is_true(opusred))\n\t\t\t\tsession->opusred_pt = janus_sdp_get_opusred_pt(answer, -1);\n\t\t\t/* Done */\n\t\t\tchar *sdp = janus_sdp_write(answer);\n\t\t\tjanus_sdp_destroy(offer);\n\t\t\tjanus_sdp_destroy(answer);\n\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", type, \"sdp\", sdp);\n\t\t\tif(session->e2ee)\n\t\t\t\tjson_object_set_new(jsep, \"e2ee\", json_true());\n\t\t\t/* How long will the core take to push the event? */\n\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\tint res = gateway->push_event(msg->handle, &janus_echotest_plugin, msg->transaction, event, jsep);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\",\n\t\t\t\tres, janus_get_monotonic_time()-start);\n\t\t\tg_free(sdp);\n\t\t\t/* We don't need the event and jsep anymore */\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(jsep);\n\t\t}\n\t\tif(record) {\n\t\t\tgboolean recording = json_is_true(record);\n\t\t\tconst char *recording_base = json_string_value(recfile);\n\t\t\tJANUS_LOG(LOG_VERB, \"Recording %s (base filename: %s)\\n\", recording ? \"enabled\" : \"disabled\", recording_base ? recording_base : \"not provided\");\n\t\t\tjanus_mutex_lock(&session->rec_mutex);\n\t\t\tif(!recording) {\n\t\t\t\tjanus_echotest_recorder_close(session);\n\t\t\t} else {\n\t\t\t\t/* We've started recording, send a PLI and go on */\n\t\t\t\tchar filename[255];\n\t\t\t\tgint64 now = janus_get_real_time();\n\t\t\t\tif(session->has_audio) {\n\t\t\t\t\t/* Prepare an audio recording */\n\t\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-audio\", recording_base);\n\t\t\t\t\t\trc = janus_recorder_create(NULL, janus_audiocodec_name(session->acodec), filename);\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this EchoTest user!\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\tg_snprintf(filename, 255, \"echotest-%p-%\"SCNi64\"-audio\", session, now);\n\t\t\t\t\t\trc = janus_recorder_create(NULL, janus_audiocodec_name(session->acodec), filename);\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this EchoTest user!\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* If RED is in use, take note of it */\n\t\t\t\t\tif(session->opusred_pt > 0)\n\t\t\t\t\t\tjanus_recorder_opusred(rc, session->opusred_pt);\n\t\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\t\tif(session->e2ee)\n\t\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\t\tsession->arc = rc;\n\t\t\t\t}\n\t\t\t\tif(session->has_video) {\n\t\t\t\t\t/* Prepare a video recording */\n\t\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-video\", recording_base);\n\t\t\t\t\t\trc = janus_recorder_create_full(NULL,\n\t\t\t\t\t\t\tjanus_videocodec_name(session->vcodec), session->vfmtp, filename);\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an video recording file for this EchoTest user!\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\tg_snprintf(filename, 255, \"echotest-%p-%\"SCNi64\"-video\", session, now);\n\t\t\t\t\t\trc = janus_recorder_create_full(NULL,\n\t\t\t\t\t\t\tjanus_videocodec_name(session->vcodec), session->vfmtp, filename);\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an video recording file for this EchoTest user!\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording video, sending a PLI to kickstart it\\n\");\n\t\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\t\tif(session->e2ee)\n\t\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\t\tsession->vrc = rc;\n\t\t\t\t}\n\t\t\t\tif(session->has_data) {\n\t\t\t\t\t/* Prepare a data recording */\n\t\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-data\", recording_base);\n\t\t\t\t\t\trc = janus_recorder_create(NULL, \"text\", filename);\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open a text data recording file for this EchoTest user!\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\tg_snprintf(filename, 255, \"echotest-%p-%\"SCNi64\"-data\", session, now);\n\t\t\t\t\t\trc = janus_recorder_create(NULL, \"text\", filename);\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open a text data recording file for this EchoTest user!\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* Media encryption doesn't apply to data channels */\n\t\t\t\t\tsession->drc = rc;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rec_mutex);\n\t\t}\n\t\tjanus_echotest_message_free(msg);\n\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t/* Just to showcase how you can notify handlers, let's update them on our configuration */\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"audio_active\", session->audio_active ? json_true() : json_false());\n\t\t\tjson_object_set_new(info, \"video_active\", session->video_active ? json_true() : json_false());\n\t\t\tjson_object_set_new(info, \"bitrate\", json_integer(session->bitrate));\n\t\t\tif(session->ssrc[0] || session->rid[0]) {\n\t\t\t\tjson_t *simulcast = json_object();\n\t\t\t\tjson_object_set_new(simulcast, \"substream\", json_integer(session->sim_context.substream));\n\t\t\t\tjson_object_set_new(simulcast, \"temporal-layer\", json_integer(session->sim_context.templayer));\n\t\t\t\tjson_object_set_new(info, \"simulcast\", simulcast);\n\t\t\t}\n\t\t\tif(session->arc || session->vrc || session->drc) {\n\t\t\t\tjson_t *recording = json_object();\n\t\t\t\tif(session->arc && session->arc->filename)\n\t\t\t\t\tjson_object_set_new(recording, \"audio\", json_string(session->arc->filename));\n\t\t\t\tif(session->vrc && session->vrc->filename)\n\t\t\t\t\tjson_object_set_new(recording, \"video\", json_string(session->vrc->filename));\n\t\t\t\tif(session->drc && session->drc->filename)\n\t\t\t\t\tjson_object_set_new(recording, \"data\", json_string(session->drc->filename));\n\t\t\t\tjson_object_set_new(info, \"recording\", recording);\n\t\t\t}\n\t\t\tgateway->notify_event(&janus_echotest_plugin, session->handle, info);\n\t\t}\n\n\t\t/* Done, on to the next request */\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"echotest\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_echotest_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjanus_echotest_message_free(msg);\n\t\t\t/* We don't need the event anymore */\n\t\t\tjson_decref(event);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving EchoTest handler thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/plugins/janus_lua.c",
    "content": "/*! \\file   janus_lua.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Lua plugin\n * \\details Check the \\ref lua for more details.\n *\n * \\ingroup plugins\n * \\ingroup luapapi\n * \\ref plugins\n * \\ref luapapi\n *\n * \\page lua Lua plugin documentation\n * This is a plugin that implements a simple bridge to Lua\n * scripts. While the plugin implements low level stuff like media\n * manipulation, routing, recording, etc., all the logic is demanded\n * to an external Lua script. This means that the C code exposes functions\n * to the Lua script (e.g., to dictate what to do with media, whether\n * recording should be done, sending PLIs, etc.), while Lua exposes\n * functions to be notified by the C code about important events (e.g.,\n * new users, WebRTC state, incoming messages, etc.).\n *\n * Considering the C code and the Lua script will need some sort of\n * \"contract\" in order to be able to properly interact with each other,\n * the interface (as in method names) must be consistent, but the logic\n * in the Lua script can be completely customized, so that it fits\n * whatever requirement one has (e.g., something like the EchoTest, or\n * something like the VideoRoom).\n *\n * \\section luaapi Lua interfaces\n *\n * Every Lua script that wants to implement a Janus plugin must provide\n * the following functions as callbacks:\n *\n * - \\c init(): called when janus_lua.c is initialized;\n * - \\c destroy(): called when janus_lua.c is deinitialized (Janus shutting down);\n * - \\c createSession(): called when a new user attaches to the Janus Lua plugin;\n * - \\c destroySession(): called when an attached user detaches from the Janus Lua plugin;\n * - \\c querySession(): called when an Admin API query for a specific user gets to the Janus Lua plugin;\n * - \\c handleMessage(): called when a user sends a message to the Janus Lua plugin;\n * - \\c setupMedia(): called when a users's WebRTC PeerConnection goes up;\n * - \\c hangupMedia(): called when a users's WebRTC PeerConnection goes down;\n * - \\c resumeScheduler(): called by the C scheduler to resume coroutines.\n *\n * While \\c init() expects a path to a config file (which you can ignore if\n * unneeded), and \\c destroy() and \\c resumeScheduler() don't need any\n * argument, all other functions expect at the very least a numeric session\n * identifier, that will uniquely address a user in the plugin. Such a\n * value is created dynamically by the C code, and so all the Lua script\n * needs to do is track it as a unique session identifier when handling\n * requests and pushing responses/events/actions towards the C code.\n * Refer to the existing examples (e.g., \\c echotest.lua) to see the\n * exact signature for all the above callbacks.\n *\n * \\note Notice that, along the above mentioned callbacks, Lua scripts\n * can also implement functions like \\c incomingRtp() \\c incomingRtcp()\n * \\c incomingTextData() and \\c incomingBinaryData() to handle those packets\n * directly, instead of letting the C code worry about relaying/processing\n * them. While it might make sense to handle incoming data channel messages\n * with \\c incomingTextData() or \\c incomingBinaryData\n * though, the performance impact of directly processing and manipulating\n * RTP an RTCP packets is probably too high, and so their usage is currently\n * discouraged. The \\c dataReady() callback can be used to figure out when\n * data can be sent. As an additional note, Lua scripts can also decide to\n * implement the functions that return information about the plugin itself,\n * namely \\c getVersion() \\c getVersionString() \\c getDescription()\n * \\c getName() \\c getAuthor() and \\c getPackage(). If not implemented,\n * the Lua plugin will return its own info (i.e., \"janus.plugin.lua\", etc.).\n * Most of the times, Lua scripts will not need to override this information,\n * unless they really want to register their own name spaces and versioning.\n * Lua scripts can also receive information on slow links via the\n * \\c slowLink() callback, in order to react accordingly: e.g., reduce\n * the bitrate of a video sender if they, or their viewers, are experiencing\n * issues. Finally, in case simulcast is used, Lia scripts may receive\n * events on substream and/or temporal layer changes happening for\n * receiving sessions via the \\c substreamChanged() and the\n * \\c temporalLayerChanged() callbacks: this may be useful to track\n * which layer is actually being sent, vs. what was requested.\n *\n * \\section capi C interfaces\n *\n * Just as the Lua script needs to expose callbacks that the C code can\n * invoke, the C code exposes methods as Lua functions accessible from\n * the Lua script. This includes means to push events, configure how\n * media should be routed without handling each packet in Lua, sending\n * RTCP feedback, start/stop recording and so on.\n *\n * The following are the functions the C code exposes:\n *\n * - \\c pushEvent(): push an event to the user via Janus API;\n * - \\c eventsIsEnabled(): check if Event Handlers are enabled in the core;\n * - \\c notifyEvent(): send an event to Event Handlers;\n * - \\c closePc(): force the closure of a PeerConnection;\n * - \\c endSession(): force the detach of a plugin handle;\n * - \\c configureMedium(): specify whether audio/video/data can be received/sent;\n * - \\c addRecipient(): specify which user should receive a user's media;\n * - \\c removeRecipient(): specify which user should not receive a user's media anymore;\n * - \\c setBitrate(): specify the bitrate to force on a user via REMB feedback;\n * - \\c setPliFreq(): specify how often the plugin should send a PLI to this user;\n * - \\c setSubstream(): set the target simulcast substream;\n * - \\c setTemporalLayer(): set the target simulcast temporal layer;\n * - \\c sendPli(): send a PLI (keyframe request);\n * - \\c startRecording(): start recording audio, video and or data for a user;\n * - \\c stopRecording(): start recording audio, video and or data for a user;\n * - \\c pokeScheduler(): notify the C code that there's a coroutine to resume;\n * - \\c timeCallback(): trigger the execution of a Lua function after X milliseconds.\n *\n * As anticipated in the previous section, almost all these methods also\n * expect the unique session identifier to address a specific user in the\n * plugin. This is true for all the above methods expect \\c eventsIsEnabled\n * and, more importantly, both \\c timeCallback() and \\c pokeScheduler() which,\n * together with Lua's \\c resumeScheduler(), will be clearer in the next section.\n *\n * \\section coroutines Lua/C coroutines scheduler\n *\n * Lua is a single threaded environment. While it has a concept similar\n * to threads called coroutines, these are not threads as known in C.\n * In order to allow for an easy to implement asynchronous behaviour in\n * Lua scripts, you can leverage a scheduler implemented in the C code.\n *\n * More specifically, when the plugin starts a dedicated thread is devoted\n * to the only purpose of acting as a scheduler for Lua coroutines. This\n * means that, whenever this C scheduler is awaken, it will call the\n * \\c resumeScheduler() function in the Lua script, thus allowing the\n * Lua script to execute one or more pending coroutines. The C scheduler\n * only acts when triggered, which means it's up to the Lua script to\n * tell it when to wake up: this is possible via the \\c pokeScheduler()\n * function, which does nothing more than sending a simple signal to the\n * C scheduler to wake it up. As such, it's easy for the Lua script to\n * implement asynchronous behaviour, e.g.:\n *\n * 1. Lua script needs to do something asynchronously;\n * 2. Lua script creates coroutine, and takes note of it somewhere;\n * 3. Lua script calls \\c pokeScheduler();\n * 4. C code sends signal to the thread acting as a scheduler;\n * 5. when the scheduling thread wakes up, it calls \\c resumeScheduler();\n * 6. Lua script resumes the previously queued coroutine.\n *\n * This simple mechanism is what the sample Lua scripts provided in this\n * repo use, for instance, to handle incoming messages asynchronously,\n * so you can refer to those to have an idea of how it can be used. The\n * next section will address \\ref timers instead.\n *\n * \\note You can implement asynchronous behaviour any way you want, and\n * you're not required to use this C scheduler. Anyway, you must implement\n * a method called \\c resumeScheduler() anyway, as the C code checks for\n * its presence and fails if it's not there. If you don't need it, just\n * create an empty function that does nothing and you'll be fine.\n *\n * \\section timers Lua/C time-based scheduler\n *\n * Another helpful way to implement asynchronous behaviour is with the\n * help of the \\c timeCallback() function. Specifically, this function\n * implements a mechanism to ask for a specific Lua method to be invoked\n * after a provided amount of time. To specify the function to invoke,\n * an optional argument to pass (which MUST be a string) and the time to\n * wait to do that. This is particularly helpful when you're handling\n * asynchronous behaviour that you want to inspect on a regular basis.\n *\n * The \\c timeCallback() function expects three arguments:\n *\n * \\verbatim\ntimeCallback(function, argument, milliseconds)\n\\endverbatim\n *\n * The only mandatory parameter is \\c function: if you set \\c argument\n * to \\c nil no argument will be passed to \\c function when it's executed;\n * it \\c milliseconds is 0, \\c function will be executed as soon as possible.\n *\n * \\verbatim\n-- This will cause an error (timeCallback needs three arguments)\ntimeCallback()\n-- Invoke test() in 500 milliseconds\ntimeCallback(\"test\", nil, 500)\n-- Invoke test(\"ciccio\") in 2 seconds\ntimeCallback(\"test\", \"ciccio\", 2000)\n\\endverbatim\n *\n * Notice that \\c timeCallback() allows you to formally recreate the\n * mechanism \\c pokeScheduler() and \\c resumeScheduler() implement, as\n * the following is pretty much an equivalent of that:\n *\n * \\verbatim\ntimeCallback(\"resumeScheduler\", nil, 0)\n\\endverbatim\n *\n * Anyway, \\c pokeScheduler() and \\c resumeScheduler() is much more\n * compact and less verbose, and as such is preferred in cases where\n * timing and opaque arguments are not needed.\n *\n * Refer to the \\ref luapapi section for more information on how you\n * can register your own C functions.\n */\n\n#include <jansson.h>\n\n/* Session definition and hashtable */\n#include \"janus_lua_data.h\"\n/* Extra/custom C hooks and code */\n#include \"janus_lua_extra.h\"\n\n\n/* Plugin information */\n#define JANUS_LUA_VERSION\t\t\t1\n#define JANUS_LUA_VERSION_STRING\t\"0.0.1\"\n#define JANUS_LUA_DESCRIPTION\t\t\"A custom plugin for the Lua framework.\"\n#define JANUS_LUA_NAME\t\t\t\t\"Janus Lua plugin\"\n#define JANUS_LUA_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_LUA_PACKAGE\t\t\t\"janus.plugin.lua\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_lua_init(janus_callbacks *callback, const char *config_path);\nvoid janus_lua_destroy(void);\nint janus_lua_get_api_compatibility(void);\nint janus_lua_get_version(void);\nconst char *janus_lua_get_version_string(void);\nconst char *janus_lua_get_description(void);\nconst char *janus_lua_get_name(void);\nconst char *janus_lua_get_author(void);\nconst char *janus_lua_get_package(void);\nvoid janus_lua_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_lua_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_lua_handle_admin_message(json_t *message);\nvoid janus_lua_setup_media(janus_plugin_session *handle);\nvoid janus_lua_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_lua_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_lua_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet);\nvoid janus_lua_data_ready(janus_plugin_session *handle);\nvoid janus_lua_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink);\nvoid janus_lua_hangup_media(janus_plugin_session *handle);\nvoid janus_lua_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_lua_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_lua_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_lua_init,\n\t\t.destroy = janus_lua_destroy,\n\n\t\t.get_api_compatibility = janus_lua_get_api_compatibility,\n\t\t.get_version = janus_lua_get_version,\n\t\t.get_version_string = janus_lua_get_version_string,\n\t\t.get_description = janus_lua_get_description,\n\t\t.get_name = janus_lua_get_name,\n\t\t.get_author = janus_lua_get_author,\n\t\t.get_package = janus_lua_get_package,\n\n\t\t.create_session = janus_lua_create_session,\n\t\t.handle_message = janus_lua_handle_message,\n\t\t.handle_admin_message = janus_lua_handle_admin_message,\n\t\t.setup_media = janus_lua_setup_media,\n\t\t.incoming_rtp = janus_lua_incoming_rtp,\n\t\t.incoming_rtcp = janus_lua_incoming_rtcp,\n\t\t.incoming_data = janus_lua_incoming_data,\n\t\t.data_ready = janus_lua_data_ready,\n\t\t.slow_link = janus_lua_slow_link,\n\t\t.hangup_media = janus_lua_hangup_media,\n\t\t.destroy_session = janus_lua_destroy_session,\n\t\t.query_session = janus_lua_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_LUA_NAME);\n\treturn &janus_lua_plugin;\n}\n\n/* Useful stuff */\nvolatile gint lua_initialized = 0, lua_stopping = 0;\njanus_callbacks *lua_janus_core = NULL;\n\n/* Lua stuff */\nlua_State *lua_state = NULL;\njanus_mutex lua_mutex = JANUS_MUTEX_INITIALIZER;\nstatic const char *lua_functions[] = {\n\t\"init\", \"destroy\", \"resumeScheduler\",\n\t\"createSession\", \"destroySession\", \"querySession\",\n\t\"handleMessage\",\n\t\"setupMedia\", \"hangupMedia\"\n};\nstatic uint lua_funcsize = sizeof(lua_functions)/sizeof(*lua_functions);\n/* Some bindings are optional */\nstatic gboolean has_get_version = FALSE;\nstatic int lua_script_version = -1;\nstatic gboolean has_get_version_string = FALSE;\nstatic char *lua_script_version_string = NULL;\nstatic gboolean has_get_description = FALSE;\nstatic char *lua_script_description = NULL;\nstatic gboolean has_get_name = FALSE;\nstatic char *lua_script_name = NULL;\nstatic gboolean has_get_author = FALSE;\nstatic char *lua_script_author = NULL;\nstatic gboolean has_get_package = FALSE;\nstatic char *lua_script_package = NULL;\nstatic gboolean has_handle_admin_message = FALSE;\nstatic gboolean has_incoming_rtp = FALSE;\nstatic gboolean has_incoming_rtcp = FALSE;\nstatic gboolean has_incoming_data_legacy = FALSE,\t/* Legacy callback */\n\thas_incoming_text_data = FALSE,\n\thas_incoming_binary_data = FALSE;\nstatic gboolean has_data_ready = FALSE;\nstatic gboolean has_slow_link = FALSE;\nstatic gboolean has_substream_changed = FALSE;\nstatic gboolean has_temporal_changed = FALSE;\n/* Lua C scheduler (for coroutines) */\nstatic GThread *scheduler_thread = NULL;\nstatic void *janus_lua_scheduler(void *data);\nstatic GAsyncQueue *events = NULL;\ntypedef enum janus_lua_event {\n\tjanus_lua_event_none = 0,\n\tjanus_lua_event_resume,\t\t/* Resume one or more pending coroutines */\n\tjanus_lua_event_exit\t\t/* Break the scheduler loop */\n} janus_lua_event;\n/* Lua timer loop (for scheduled callbacks) */\nstatic GMainContext *timer_context = NULL;\nstatic GMainLoop *timer_loop = NULL;\nstatic GThread *timer_thread = NULL;\nstatic void *janus_lua_timer(void *data);\nstatic gboolean janus_lua_timer_cb(void *data);\ntypedef struct janus_lua_callback {\n\tguint id;\n\tuint32_t ms;\n\tGSource *source;\n\tchar *function;\n\tchar *argument;\n} janus_lua_callback;\nstatic GHashTable *callbacks = NULL;\nstatic void janus_lua_callback_free(janus_lua_callback *cb) {\n\tif(!cb)\n\t\treturn;\n\tg_source_destroy(cb->source);\n\tg_source_unref(cb->source);\n\tg_free(cb->function);\n\tg_free(cb->argument);\n\tg_free(cb);\n}\n\n/* Helper function to sample the number of occupied slots into Lua stack */\nstatic void janus_lua_stackdump(lua_State* l) {\n    int top = lua_gettop(l);\n    JANUS_LOG(LOG_HUGE, \"Total in lua stack %d\\n\", top);\n}\n\n/* janus_lua_session is defined in janus_lua_data.h, but it's managed here */\nGHashTable *lua_sessions, *lua_ids;\njanus_mutex lua_sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_lua_session_destroy(janus_lua_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n}\n\nstatic void janus_lua_session_free(const janus_refcount *session_ref) {\n\tjanus_lua_session *session = janus_refcount_containerof(session_ref, janus_lua_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tg_hash_table_remove(lua_ids, GUINT_TO_POINTER(session->id));\n\tjanus_recorder_destroy(session->arc);\n\tjanus_recorder_destroy(session->vrc);\n\tjanus_recorder_destroy(session->drc);\n\tjanus_mutex_destroy(&session->rec_mutex);\n\tjanus_mutex_destroy(&session->recipients_mutex);\n\tjanus_mutex_destroy(&session->rid_mutex);\n\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\tg_free(session);\n}\n\n/* Packet data and routing */\ntypedef struct janus_lua_rtp_relay_packet {\n\tjanus_lua_session *sender;\n\tjanus_rtp_header *data;\n\tgint length;\n\tjanus_plugin_rtp_extensions extensions;\n\tgboolean is_rtp;\t/* This may be a data packet and not RTP */\n\tgboolean is_video;\n\tuint32_t ssrc[3];\n\tuint32_t timestamp;\n\tuint16_t seq_number;\n\t/* The following is only relevant for datachannels */\n\tgboolean textdata;\n} janus_lua_rtp_relay_packet;\nstatic void janus_lua_relay_rtp_packet(gpointer data, gpointer user_data);\nstatic void janus_lua_relay_data_packet(gpointer data, gpointer user_data);\n\n\n/* Helper struct to address outgoing notifications, e.g., involving PeerConnections */\ntypedef enum janus_lua_async_event_type {\n\tjanus_lua_async_event_type_none = 0,\n\tjanus_lua_async_event_type_pushevent\n} janus_lua_async_event_type;\ntypedef struct janus_lua_async_event {\n\tjanus_lua_session *session;\t\t\t/* Who this event is for */\n\tjanus_lua_async_event_type type;\t/* What this event is about */\n\tchar *transaction;\t\t\t\t\t/* Notification transaction, if any */\n\tjson_t *event;\t\t\t\t\t\t/* Content of the notification, if any */\n\tjson_t *jsep;\t\t\t\t\t\t/* Content of JSEP SDP, if any */\n} janus_lua_async_event;\n/* Helper thread to push events that need to be asynchronous, e.g., for those\n * that would keep the Lua state busy longer than usual and cause delays,\n * or those that might actually result in a deadlock if done synchronously */\nstatic void *janus_lua_async_event_helper(void *data) {\n\tjanus_lua_async_event *asev = (janus_lua_async_event *)data;\n\tif(asev == NULL)\n\t\treturn NULL;\n\tif(asev->type == janus_lua_async_event_type_pushevent) {\n\t\t/* Send the event */\n\t\tlua_janus_core->push_event(asev->session->handle, &janus_lua_plugin, asev->transaction, asev->event, asev->jsep);\n\t}\n\tjson_decref(asev->event);\n\tjson_decref(asev->jsep);\n\tg_free(asev->transaction);\n\tjanus_refcount_decrease(&asev->session->ref);\n\tg_free(asev);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\n\n/* Methods that we expose to the Lua script */\nstatic int janus_lua_method_januslog(lua_State *s) {\n\t/* This method allows the Lua script to use the Janus internal logger */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 3)\\n\", n);\n\t\treturn 0;\n\t}\n\tint level = lua_tonumber(s, 1);\n\tconst char *text = lua_tostring(s, 2);\n\tif(text == NULL) {\n\t\t/* Ignore */\n\t\treturn 0;\n\t}\n\tJANUS_LOG(level, \"%s\\n\", text);\n\treturn 0;\n}\n\nstatic int janus_lua_method_pokescheduler(lua_State *s) {\n\t/* This method allows the Lua script to poke the scheduler and have it wake up ASAP */\n\tg_async_queue_push(events, GUINT_TO_POINTER(janus_lua_event_resume));\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_timecallback(lua_State *s) {\n\t/* This method allows the Lua script to schedule a callback after a specified amount of time */\n\tint n = lua_gettop(s);\n\tif(n != 3) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 3)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tconst char *function = lua_tostring(s, 1);\n\tif(function == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid argument (missing function name)\\n\");\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tconst char *argument = lua_tostring(s, 2);\n\tguint32 ms = lua_tonumber(s, 3);\n\t/* Create a callback instance */\n\tjanus_lua_callback *cb = g_malloc0(sizeof(janus_lua_callback));\n\tcb->function = g_strdup(function);\n\tif(argument != NULL)\n\t\tcb->argument = g_strdup(argument);\n\tcb->ms = ms;\n\tcb->source = g_timeout_source_new(ms);\n\tg_source_set_callback(cb->source, janus_lua_timer_cb, cb, NULL);\n\tg_hash_table_insert(callbacks, cb, cb);\n\tcb->id = g_source_attach(cb->source, timer_context);\n\tJANUS_LOG(LOG_VERB, \"Created scheduled callback (%\"SCNu32\"ms) with ID %u\\n\", cb->ms, cb->id);\n\t/* Done */\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_pushevent(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 4) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 4)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tconst char *transaction = lua_tostring(s, 2);\n\tconst char *event_text = lua_tostring(s, 3);\n\tconst char *jsep_text = lua_tostring(s, 4);\n\t/* Parse the event/jsep strings to Jansson objects */\n\tjson_error_t error;\n\tjson_t *event = json_loads(event_text, 0, &error);\n\tif(!event) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjson_t *jsep = NULL;\n\tif(jsep_text != NULL) {\n\t\tjsep = json_loads(jsep_text, 0, &error);\n\t\tif(!jsep) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\t\tjson_decref(event);\n\t\t\tlua_pushnumber(s, -1);\n\t\t\treturn 1;\n\t\t}\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tjson_decref(event);\n\t\tif(jsep)\n\t\t\tjson_decref(jsep);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* If there's an SDP attached, create a thread to send the event asynchronously:\n\t * sending it here would keep the locked Lua state busy much longer than intended */\n\tif(jsep != NULL) {\n\t\t/* Let's parse the SDP first, though */\n\t\tconst char *sdp = json_string_value(json_object_get(jsep, \"sdp\"));\n\t\tconst char *sdp_type = json_string_value(json_object_get(jsep, \"type\"));\n\t\tchar error_str[512];\n\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(sdp, error_str, sizeof(error_str));\n\t\tif(parsed_sdp == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing answer: %s\\n\", error_str);\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(jsep);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tlua_pushnumber(s, -1);\n\t\t\treturn 1;\n\t\t}\n\t\tjanus_lua_async_event *asev = g_malloc0(sizeof(janus_lua_async_event));\n\t\tasev->session = session;\n\t\tasev->type = janus_lua_async_event_type_pushevent;\n\t\tasev->transaction = transaction ? g_strdup(transaction) : NULL;\n\t\tasev->event = event;\n\t\tasev->jsep = jsep;\n\t\tif(json_is_true(json_object_get(jsep, \"e2ee\")))\n\t\t\tsession->e2ee = TRUE;\n\t\tif(sdp_type && !strcasecmp(sdp_type, \"answer\")) {\n\t\t\t/* Take note of which video codec were negotiated */\n\t\t\tconst char *vcodec = NULL;\n\t\t\tjanus_sdp_find_first_codec(parsed_sdp, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\tif(vcodec)\n\t\t\t\tsession->vcodec = janus_videocodec_from_name(vcodec);\n\t\t}\n\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t/* Send asynchronously */\n\t\tGError *error = NULL;\n\t\tg_thread_try_new(\"lua pushevent\", janus_lua_async_event_helper, asev, &error);\n\t\tif(error != NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Lua pushevent thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(jsep);\n\t\t\tg_free(asev->transaction);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tg_free(asev);\n\t\t}\n\t\t/* Return a success/error right away */\n\t\tlua_pushnumber(s, error ? 1 : 0);\n\t\treturn 1;\n\t}\n\t/* No SDP, send the event now */\n\tint res = lua_janus_core->push_event(session->handle, &janus_lua_plugin, transaction, event, NULL);\n\tjanus_refcount_decrease(&session->ref);\n\tjson_decref(event);\n\tlua_pushnumber(s, res);\n\treturn 1;\n}\n\nstatic int janus_lua_method_notifyevent(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tif(!lua_janus_core->events_is_enabled()) {\n\t\t/* Event handlers are disabled in the core, ignoring */\n\t\tlua_pushnumber(s, 0);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tconst char *event_text = lua_tostring(s, 2);\n\t/* Parse the event/jsep strings to Jansson objects */\n\tjson_error_t error;\n\tjson_t *event = json_loads(event_text, 0, &error);\n\tif(!event) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Find the session (optional) */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session != NULL)\n\t\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Notify the event */\n\tlua_janus_core->notify_event(&janus_lua_plugin, session ? session->handle : NULL, event);\n\tif(session != NULL)\n\t\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_eventsisenabled(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 0)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Event handlers are disabled in the core, ignoring */\n\tlua_pushnumber(s, lua_janus_core->events_is_enabled());\n\treturn 1;\n}\n\nstatic int janus_lua_method_closepc(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 1)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Close the PeerConnection */\n\tlua_janus_core->close_pc(session->handle);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_endsession(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 1)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Close the plugin handle */\n\tlua_janus_core->end_session(session->handle);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_configuremedium(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 4) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 4)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tconst char *medium = lua_tostring(s, 2);\n\tconst char *direction = lua_tostring(s, 3);\n\tint enabled = lua_toboolean(s, 4);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Modify the session media property */\n\tif(medium && direction) {\n\t\tif(!strcasecmp(medium, \"audio\")) {\n\t\t\tif(!strcasecmp(direction, \"in\")) {\n\t\t\t\tsession->accept_audio = enabled ? TRUE : FALSE;\n\t\t\t} else {\n\t\t\t\tsession->send_audio = enabled ? TRUE : FALSE;\n\t\t\t}\n\t\t} else if(!strcasecmp(medium, \"video\")) {\n\t\t\tif(!strcasecmp(direction, \"in\")) {\n\t\t\t\tsession->accept_video = enabled ? TRUE : FALSE;\n\t\t\t} else {\n\t\t\t\tsession->send_video = enabled ? TRUE : FALSE;\n\t\t\t}\n\t\t} else if(!strcasecmp(medium, \"data\")) {\n\t\t\tif(!strcasecmp(direction, \"in\")) {\n\t\t\t\tsession->accept_data = enabled ? TRUE : FALSE;\n\t\t\t} else {\n\t\t\t\tsession->send_data = enabled ? TRUE : FALSE;\n\t\t\t}\n\t\t}\n\t}\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_addrecipient(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tguint32 rid = lua_tonumber(s, 2);\n\t/* Find the sessions */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->recipients_mutex);\n\tjanus_lua_session *recipient = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(rid));\n\tif(recipient == NULL || g_atomic_int_get(&recipient->destroyed)) {\n\t\tjanus_mutex_unlock(&session->recipients_mutex);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&recipient->ref);\n\t/* Add to the list of recipients */\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(g_slist_find(session->recipients, recipient) == NULL) {\n\t\tjanus_refcount_increase(&session->ref);\n\t\tjanus_refcount_increase(&recipient->ref);\n\t\tsession->recipients = g_slist_append(session->recipients, recipient);\n\t\trecipient->sender = session;\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tjanus_refcount_decrease(&recipient->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_removerecipient(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tguint32 rid = lua_tonumber(s, 2);\n\t/* Find the sessions */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->recipients_mutex);\n\tjanus_lua_session *recipient = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(rid));\n\tif(recipient == NULL) {\n\t\tjanus_mutex_unlock(&session->recipients_mutex);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&recipient->ref);\n\t/* Remove from the list of recipients */\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tgboolean unref = FALSE;\n\tif(g_slist_find(session->recipients, recipient) != NULL) {\n\t\tsession->recipients = g_slist_remove(session->recipients, recipient);\n\t\trecipient->sender = NULL;\n\t\tunref = TRUE;\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\tif(unref) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&recipient->ref);\n\t}\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tjanus_refcount_decrease(&recipient->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_setbitrate(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tguint32 bitrate = lua_tonumber(s, 2);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tsession->bitrate = bitrate;\n\t/* Send a REMB right away too, if the PeerConnection is up */\n\tif(g_atomic_int_get(&session->started)) {\n\t\t/* No limit ~= 10000000 */\n\t\tlua_janus_core->send_remb(session->handle, session->bitrate ? session->bitrate : 10000000);\n\t}\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_setplifreq(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tguint16 pli_freq = lua_tonumber(s, 2);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tsession->pli_freq = pli_freq;\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_setsubstream(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tguint16 substream = lua_tonumber(s, 2);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(substream <= 2)\n\t\tsession->sim_context.substream_target = substream;\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_settemporallayer(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tguint16 temporal = lua_tonumber(s, 2);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(temporal <= 2)\n\t\tsession->sim_context.templayer_target = temporal;\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_sendpli(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 1)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Send a PLI */\n\tsession->pli_latest = janus_get_monotonic_time();\n\tJANUS_LOG(LOG_HUGE, \"Sending PLI to session %\"SCNu32\"\\n\", session->id);\n\tlua_janus_core->send_pli(session->handle);\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_relayrtp(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 4) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 4)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tint is_video = lua_toboolean(s, 2);\n\tconst char *payload = lua_tostring(s, 3);\n\tint len = lua_tonumber(s, 4);\n\tif(!payload || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid payload\\n\");\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Send the RTP packet */\n\tjanus_plugin_rtp rtp = { .mindex = -1, .video = is_video, .buffer = (char *)payload, .length = len };\n\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\tlua_janus_core->relay_rtp(session->handle, &rtp);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_relayrtcp(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 4) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 4)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tint is_video = lua_toboolean(s, 2);\n\tconst char *payload = lua_tostring(s, 3);\n\tint len = lua_tonumber(s, 4);\n\tif(!payload || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid payload\\n\");\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Send the RTCP packet */\n\tjanus_plugin_rtcp rtcp = { .video = is_video, .buffer = (char *)payload, .length = len };\n\tlua_janus_core->relay_rtcp(session->handle, &rtcp);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_relaytextdata(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n < 3 || n > 5) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 3-5)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tconst char *payload = lua_tostring(s, 2);\n\tint len = lua_tonumber(s, 3);\n\tif(!payload || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid data\\n\");\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Check if label and/or protocol were provided as well */\n\tconst char *label = NULL, *protocol = NULL;\n\tif(n > 3) {\n\t\tlabel = lua_tostring(s, 4);\n\t\tif(n > 4)\n\t\t\tprotocol = lua_tostring(s, 5);\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(!g_atomic_int_get(&session->dataready)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_WARN, \"Datachannel not ready yet for session %\"SCNu32\", dropping data\\n\", id);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Send the data */\n\tjanus_plugin_data data = {\n\t\t.label = (char *)label,\n\t\t.protocol = (char *)protocol,\n\t\t.binary = FALSE,\n\t\t.buffer = (char *)payload,\n\t\t.length = len\n\t};\n\tlua_janus_core->relay_data(session->handle, &data);\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_relaybinarydata(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n < 3 || n > 5) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 3-5)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\tconst char *payload = lua_tostring(s, 2);\n\tint len = lua_tonumber(s, 3);\n\tif(!payload || len < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid data\\n\");\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Check if label and/or protocol were provided as well */\n\tconst char *label = NULL, *protocol = NULL;\n\tif(n > 3) {\n\t\tlabel = lua_tostring(s, 4);\n\t\tif(n > 4)\n\t\t\tprotocol = lua_tostring(s, 5);\n\t}\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(!g_atomic_int_get(&session->dataready)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_WARN, \"Datachannel not ready yet for session %\"SCNu32\", dropping data\\n\", id);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\t/* Send the data */\n\tjanus_plugin_data data = {\n\t\t.label = (char *)label,\n\t\t.protocol = (char *)protocol,\n\t\t.binary = TRUE,\n\t\t.buffer = (char *)payload,\n\t\t.length = len\n\t};\n\tlua_janus_core->relay_data(session->handle, &data);\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_relaydata(lua_State *s) {\n\tJANUS_LOG(LOG_WARN, \"Deprecated function 'relayData' called, invoking 'relayTextData' instead\\n\");\n\treturn janus_lua_method_relaytextdata(s);\n}\n\nstatic int janus_lua_method_startrecording(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 5 && n != 9 && n != 13) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 5, 9 or 13)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Iterate on all arguments, to see what we're being asked to record */\n\tint recordings = 0;\n\tn--;\n\tint i = 1;\n\tjanus_recorder *arc = NULL, *vrc = NULL, *drc = NULL;\n\twhile(n > 0) {\n\t\ti++; n--;\n\t\tconst char *type = lua_tostring(s, i);\n\t\ti++; n--;\n\t\tconst char *codec = lua_tostring(s, i);\n\t\ti++; n--;\n\t\tconst char *folder = lua_tostring(s, i);\n\t\ti++; n--;\n\t\tconst char *filename = lua_tostring(s, i);\n\t\tif(type == NULL || codec == NULL) {\n\t\t\t/* No type or codec provided, skip this */\n\t\t\tcontinue;\n\t\t}\n\t\t/* Check if the codec contains some fmtp stuff too */\n\t\tconst char *c = codec, *f = NULL;\n\t\tgchar **parts = NULL;\n\t\tif(strstr(codec, \"/fmtp=\") != NULL) {\n\t\t\tparts = g_strsplit(codec, \"/fmtp=\", 2);\n\t\t\tc = parts[0];\n\t\t\tf = parts[1];\n\t\t}\n\t\t/* Create the recorder */\n\t\tjanus_recorder *rc = janus_recorder_create_full(folder, c, f, filename);\n\t\tif(parts != NULL)\n\t\t\tg_strfreev(parts);\n\t\tif(rc == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating '%s' recorder...\\n\", type);\n\t\t\tgoto error;\n\t\t}\n\t\tif(!strcasecmp(type, \"audio\")) {\n\t\t\tif(arc != NULL || session->arc != NULL) {\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicate audio recording, skipping\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\tif(session->e2ee)\n\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\tarc = rc;\n\t\t} else if(!strcasecmp(type, \"video\")) {\n\t\t\tif(vrc != NULL || session->vrc != NULL) {\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicate video recording, skipping\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_rtp_switching_context_reset(&session->rec_ctx);\n\t\t\tjanus_rtp_simulcasting_context_reset(&session->rec_simctx);\n\t\t\tsession->rec_simctx.substream_target = 2;\n\t\t\tsession->rec_simctx.templayer_target = 2;\n\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\tif(session->e2ee)\n\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\tvrc = rc;\n\t\t} else if(!strcasecmp(type, \"data\")) {\n\t\t\tif(drc != NULL || session->drc != NULL) {\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Duplicate data recording\\n\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdrc = rc;\n\t\t}\n\t\trecordings++;\n\t}\n\tif(recordings == 0)\n\t\tgoto error;\n\tif(arc) {\n\t\tsession->arc = arc;\n\t}\n\tif(vrc) {\n\t\tsession->vrc = vrc;\n\t\t/* Also send a keyframe request */\n\t\tsession->pli_latest = janus_get_monotonic_time();\n\t\tJANUS_LOG(LOG_HUGE, \"Sending PLI to session %\"SCNu32\"\\n\", session->id);\n\t\tlua_janus_core->send_pli(session->handle);\n\t}\n\tif(drc) {\n\t\tsession->drc = drc;\n\t}\n\tjanus_refcount_decrease(&session->ref);\n\tgoto done;\n\nerror:\n\tjanus_recorder_destroy(arc);\n\tjanus_recorder_destroy(vrc);\n\tjanus_recorder_destroy(drc);\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Something went wrong */\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, -1);\n\treturn 1;\n\ndone:\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Done */\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\nstatic int janus_lua_method_stoprecording(lua_State *s) {\n\t/* Get the arguments from the provided state */\n\tint n = lua_gettop(s);\n\tif(n != 2 && n != 3 && n != 4) {\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2, 3 or 4)\\n\", n);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tguint32 id = lua_tonumber(s, 1);\n\t/* Find the session */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id));\n\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tlua_pushnumber(s, -1);\n\t\treturn 1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Iterate on all arguments, to see what which recording we're being asked to stop */\n\tn--;\n\tint i = 1;\n\twhile(n > 0) {\n\t\ti++; n--;\n\t\tconst char *type = lua_tostring(s, i);\n\t\tif(!strcasecmp(type, \"audio\")) {\n\t\t\tif(session->arc != NULL) {\n\t\t\t\tjanus_recorder *rc = session->arc;\n\t\t\t\tsession->arc = NULL;\n\t\t\t\tjanus_recorder_close(rc);\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t}\n\t\t} else if(!strcasecmp(type, \"video\")) {\n\t\t\tif(session->vrc != NULL) {\n\t\t\t\tjanus_recorder *rc = session->vrc;\n\t\t\t\tsession->vrc = NULL;\n\t\t\t\tjanus_recorder_close(rc);\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t}\n\t\t} else if(!strcasecmp(type, \"data\")) {\n\t\t\tif(session->drc != NULL) {\n\t\t\t\tjanus_recorder *rc = session->drc;\n\t\t\t\tsession->drc = NULL;\n\t\t\t\tjanus_recorder_close(rc);\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t}\n\t\t}\n\t}\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Done */\n\tjanus_refcount_decrease(&session->ref);\n\tlua_pushnumber(s, 0);\n\treturn 1;\n}\n\n\n/* Plugin implementation */\nint janus_lua_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&lua_stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_LUA_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_LUA_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_LUA_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config == NULL) {\n\t\t/* No config means no Lua script */\n\t\tJANUS_LOG(LOG_ERR, \"Failed to load configuration file for Lua plugin...\\n\");\n\t\treturn -1;\n\t}\n\tjanus_config_print(config);\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\tchar *lua_folder = NULL;\n\tjanus_config_item *folder = janus_config_get(config, config_general, janus_config_type_item, \"path\");\n\tif(folder && folder->value)\n\t\tlua_folder = g_strdup(folder->value);\n\tjanus_config_item *script = janus_config_get(config, config_general, janus_config_type_item, \"script\");\n\tif(script == NULL || script->value == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Missing script path in Lua plugin configuration...\\n\");\n\t\tjanus_config_destroy(config);\n\t\tg_free(lua_folder);\n\t\treturn -1;\n\t}\n\tchar *lua_file = g_strdup(script->value);\n\tchar *lua_config = NULL;\n\tjanus_config_item *conf = janus_config_get(config, config_general, janus_config_type_item, \"config\");\n\tif(conf && conf->value)\n\t\tlua_config = g_strdup(conf->value);\n\tjanus_config_destroy(config);\n\n\t/* Initialize Lua */\n\tlua_state = luaL_newstate();\n\tluaL_openlibs(lua_state);\n\n\tif(lua_folder != NULL) {\n\t\t/* Add the script folder to the path, so that we can load other scripts from there */\n\t\tlua_getglobal(lua_state, \"package\");\n\t\tlua_getfield(lua_state, -1, \"path\");\n\t\tconst char *cur_path = lua_tostring(lua_state, -1);\n\t\tchar new_path[1024];\n\t\tmemset(new_path, 0, sizeof(new_path));\n\t\tg_snprintf(new_path, sizeof(new_path), \"%s;%s/?.lua\", cur_path, lua_folder);\n\t\tlua_pop(lua_state, 1);\n\t\tlua_pushstring(lua_state, new_path);\n\t\tlua_setfield(lua_state, -2, \"path\");\n\t\tlua_pop(lua_state, 1);\n\t}\n\n\t/* Register our functions */\n\tlua_register(lua_state, \"janusLog\", janus_lua_method_januslog);\n\tlua_register(lua_state, \"pokeScheduler\", janus_lua_method_pokescheduler);\n\tlua_register(lua_state, \"timeCallback\", janus_lua_method_timecallback);\n\tlua_register(lua_state, \"pushEvent\", janus_lua_method_pushevent);\n\tlua_register(lua_state, \"notifyEvent\", janus_lua_method_notifyevent);\n\tlua_register(lua_state, \"eventsIsEnabled\", janus_lua_method_eventsisenabled);\n\tlua_register(lua_state, \"closePc\", janus_lua_method_closepc);\n\tlua_register(lua_state, \"endSession\", janus_lua_method_endsession);\n\tlua_register(lua_state, \"configureMedium\", janus_lua_method_configuremedium);\n\tlua_register(lua_state, \"addRecipient\", janus_lua_method_addrecipient);\n\tlua_register(lua_state, \"removeRecipient\", janus_lua_method_removerecipient);\n\tlua_register(lua_state, \"setBitrate\", janus_lua_method_setbitrate);\n\tlua_register(lua_state, \"setPliFreq\", janus_lua_method_setplifreq);\n\tlua_register(lua_state, \"setSubstream\", janus_lua_method_setsubstream);\n\tlua_register(lua_state, \"setTemporalLayer\", janus_lua_method_settemporallayer);\n\tlua_register(lua_state, \"sendPli\", janus_lua_method_sendpli);\n\tlua_register(lua_state, \"relayRtp\", janus_lua_method_relayrtp);\n\tlua_register(lua_state, \"relayRtcp\", janus_lua_method_relayrtcp);\n\tlua_register(lua_state, \"relayData\", janus_lua_method_relaydata);\t/* Legacy function, deprecated */\n\tlua_register(lua_state, \"relayTextData\", janus_lua_method_relaytextdata);\n\tlua_register(lua_state, \"relayBinaryData\", janus_lua_method_relaybinarydata);\n\tlua_register(lua_state, \"startRecording\", janus_lua_method_startrecording);\n\tlua_register(lua_state, \"stopRecording\", janus_lua_method_stoprecording);\n\t/* Register all extra functions, if any were added */\n\tjanus_lua_register_extra_functions(lua_state);\n\n\t/* Now load the script */\n\tint err = luaL_dofile(lua_state, lua_file);\n\tif(err) {\n\t\tJANUS_LOG(LOG_ERR, \"Error loading Lua script %s: %s\\n\", lua_file, lua_tostring(lua_state, -1));\n\t\tlua_close(lua_state);\n\t\tg_free(lua_folder);\n\t\tg_free(lua_file);\n\t\treturn -1;\n\t}\n\t/* Make sure that all the functions we need are there */\n\tuint i=0;\n\tfor(i=0; i<lua_funcsize; i++) {\n\t\tlua_getglobal(lua_state, lua_functions[i]);\n\t\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) == 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Function '%s' is missing in %s\\n\", lua_functions[i], lua_file);\n\t\t\tlua_close(lua_state);\n\t\t\tg_free(lua_folder);\n\t\t\tg_free(lua_file);\n\t\t\treturn -1;\n\t\t}\n\t}\n\t/* Some Lua functions are optional (e.g., those to directly handle RTP, RTCP and\n\t * data, as those will typically be kept at a C level, with Lua only dictating\n\t * the logic, or those overriding the plugin namespace and versioning information */\n\tlua_getglobal(lua_state, \"getVersion\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_get_version = TRUE;\n\tlua_getglobal(lua_state, \"getVersionString\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_get_version_string = TRUE;\n\tlua_getglobal(lua_state, \"getDescription\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_get_description = TRUE;\n\tlua_getglobal(lua_state, \"getName\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_get_name = TRUE;\n\tlua_getglobal(lua_state, \"getAuthor\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_get_author = TRUE;\n\tlua_getglobal(lua_state, \"getPackage\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_get_package = TRUE;\n\tlua_getglobal(lua_state, \"handleAdminMessage\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_handle_admin_message = TRUE;\n\tlua_getglobal(lua_state, \"incomingRtp\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_incoming_rtp = TRUE;\n\tlua_getglobal(lua_state, \"incomingRtcp\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_incoming_rtcp = TRUE;\n\tlua_getglobal(lua_state, \"incomingData\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0) {\n\t\thas_incoming_data_legacy = TRUE;\n\t\tJANUS_LOG(LOG_WARN, \"The Lua script contains the deprecated 'incomingData' callback: update it \"\n\t\t\t\"to use 'incomingTextData' and/or 'incomingBinaryData' in the future (see PR #1878)\\n\");\n\t}\n\tlua_getglobal(lua_state, \"incomingTextData\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_incoming_text_data = TRUE;\n\tlua_getglobal(lua_state, \"incomingBinaryData\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_incoming_binary_data = TRUE;\n\tlua_getglobal(lua_state, \"dataReady\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_data_ready = TRUE;\n\tlua_getglobal(lua_state, \"slowLink\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_slow_link = TRUE;\n\tlua_getglobal(lua_state, \"substreamChanged\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_substream_changed = TRUE;\n\tlua_getglobal(lua_state, \"temporalLayerChanged\");\n\tif(lua_isfunction(lua_state, lua_gettop(lua_state)) != 0)\n\t\thas_temporal_changed = TRUE;\n\n\tlua_sessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_lua_session_destroy);\n\tlua_ids = g_hash_table_new(NULL, NULL);\n\tevents = g_async_queue_new();\n\tcallbacks = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_lua_callback_free);\n\n\tg_atomic_int_set(&lua_initialized, 1);\n\n\t/* Launch the scheduler thread (which will be responsible for resuming asynchronous coroutines) */\n\tGError *error = NULL;\n\tscheduler_thread = g_thread_try_new(\"lua scheduler\", janus_lua_scheduler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&lua_initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Lua scheduler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tlua_close(lua_state);\n\t\tg_free(lua_folder);\n\t\tg_free(lua_file);\n\t\tg_free(lua_config);\n\t\treturn -1;\n\t}\n\t/* Launch the timer loop thread (which will be responsible for scheduling timed callbacks) */\n\ttimer_context = g_main_context_new();\n\ttimer_loop = g_main_loop_new(timer_context, FALSE);\n\ttimer_thread = g_thread_try_new(\"lua timer\", janus_lua_timer, timer_loop, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&lua_initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Lua timer loop thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tif(timer_loop != NULL)\n\t\t\tg_main_loop_unref(timer_loop);\n\t\tif(timer_context != NULL)\n\t\t\tg_main_context_unref(timer_context);\n\t\tlua_close(lua_state);\n\t\tg_free(lua_folder);\n\t\tg_free(lua_file);\n\t\tg_free(lua_config);\n\t\treturn -1;\n\t}\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tlua_janus_core = callback;\n\n\t/* Init the Lua script, in case it's needed */\n\tlua_getglobal(lua_state, \"init\");\n\tlua_pushstring(lua_state, lua_config);\n\tlua_call(lua_state, 1, 0);\n\n\tg_free(lua_folder);\n\tg_free(lua_file);\n\tg_free(lua_config);\n\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_LUA_NAME);\n\treturn 0;\n}\n\nvoid janus_lua_destroy(void) {\n\tif(!g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tg_atomic_int_set(&lua_stopping, 1);\n\n\tg_async_queue_push(events, GUINT_TO_POINTER(janus_lua_event_exit));\n\tif(scheduler_thread != NULL) {\n\t\tg_thread_join(scheduler_thread);\n\t\tscheduler_thread = NULL;\n\t}\n\tif(timer_loop != NULL)\n\t\tg_main_loop_quit(timer_loop);\n\tif(timer_thread != NULL) {\n\t\tg_thread_join(timer_thread);\n\t\ttimer_thread = NULL;\n\t}\n\tif(timer_loop != NULL) {\n\t\tg_main_loop_unref(timer_loop);\n\t\ttimer_loop = NULL;\n\t}\n\tif(timer_context != NULL) {\n\t\tg_main_context_unref(timer_context);\n\t\ttimer_context = NULL;\n\t}\n\n\t/* Deinit the Lua script, in case it's needed */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_getglobal(lua_state, \"destroy\");\n\tlua_call(lua_state, 0, 0);\n\tg_hash_table_destroy(callbacks);\n\tcallbacks = NULL;\n\tjanus_mutex_unlock(&lua_mutex);\n\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tg_hash_table_destroy(lua_sessions);\n\tlua_sessions = NULL;\n\tg_hash_table_destroy(lua_ids);\n\tlua_ids = NULL;\n\tg_async_queue_unref(events);\n\tevents = NULL;\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_close(lua_state);\n\tlua_state = NULL;\n\tjanus_mutex_unlock(&lua_mutex);\n\n\tg_free(lua_script_version_string);\n\tg_free(lua_script_description);\n\tg_free(lua_script_name);\n\tg_free(lua_script_author);\n\tg_free(lua_script_package);\n\n\tg_atomic_int_set(&lua_initialized, 0);\n\tg_atomic_int_set(&lua_stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_LUA_NAME);\n}\n\nint janus_lua_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_lua_get_version(void) {\n\t/* Check if the Lua script wants to override this method and return info itself */\n\tif(has_get_version) {\n\t\t/* Yep, pass the request to the Lua script and return the info */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tif(lua_script_version != -1) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\treturn lua_script_version;\n\t\t}\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"getVersion\");\n\t\tlua_call(t, 0, 1);\n\t\tlua_script_version = (int)lua_tonumber(t, -1);\n\t\tlua_pop(t, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn lua_script_version;\n\t}\n\t/* No override, return the Janus Lua plugin info */\n\treturn JANUS_LUA_VERSION;\n}\n\nconst char *janus_lua_get_version_string(void) {\n\t/* Check if the Lua script wants to override this method and return info itself */\n\tif(has_get_version_string) {\n\t\t/* Yep, pass the request to the Lua script and return the info */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tif(lua_script_version_string != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\treturn lua_script_version_string;\n\t\t}\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"getVersionString\");\n\t\tlua_call(t, 0, 1);\n\t\tconst char *version = lua_tostring(t, -1);\n\t\tif(version != NULL)\n\t\t\tlua_script_version_string = g_strdup(version);\n\t\tlua_pop(t, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn lua_script_version_string;\n\t}\n\t/* No override, return the Janus Lua plugin info */\n\treturn JANUS_LUA_VERSION_STRING;\n}\n\nconst char *janus_lua_get_description(void) {\n\t/* Check if the Lua script wants to override this method and return info itself */\n\tif(has_get_description) {\n\t\t/* Yep, pass the request to the Lua script and return the info */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tif(lua_script_description != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\treturn lua_script_description;\n\t\t}\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"getDescription\");\n\t\tlua_call(t, 0, 1);\n\t\tconst char *description = lua_tostring(t, -1);\n\t\tif(description != NULL)\n\t\t\tlua_script_description = g_strdup(description);\n\t\tlua_pop(t, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn lua_script_description;\n\t}\n\t/* No override, return the Janus Lua plugin info */\n\treturn JANUS_LUA_DESCRIPTION;\n}\n\nconst char *janus_lua_get_name(void) {\n\t/* Check if the Lua script wants to override this method and return info itself */\n\tif(has_get_name) {\n\t\t/* Yep, pass the request to the Lua script and return the info */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tif(lua_script_name != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\treturn lua_script_name;\n\t\t}\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"getName\");\n\t\tlua_call(t, 0, 1);\n\t\tconst char *name = lua_tostring(t, -1);\n\t\tif(name != NULL)\n\t\t\tlua_script_name = g_strdup(name);\n\t\tlua_pop(t, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn lua_script_name;\n\t}\n\t/* No override, return the Janus Lua plugin info */\n\treturn JANUS_LUA_NAME;\n}\n\nconst char *janus_lua_get_author(void) {\n\t/* Check if the Lua script wants to override this method and return info itself */\n\tif(has_get_author) {\n\t\t/* Yep, pass the request to the Lua script and return the info */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tif(lua_script_author != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\treturn lua_script_author;\n\t\t}\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"getAuthor\");\n\t\tlua_call(t, 0, 1);\n\t\tconst char *author = lua_tostring(t, -1);\n\t\tif(author != NULL)\n\t\t\tlua_script_author = g_strdup(author);\n\t\tlua_pop(t, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn lua_script_author;\n\t}\n\t/* No override, return the Janus Lua plugin info */\n\treturn JANUS_LUA_AUTHOR;\n}\n\nconst char *janus_lua_get_package(void) {\n\t/* Check if the Lua script wants to override this method and return info itself */\n\tif(has_get_package) {\n\t\t/* Yep, pass the request to the Lua script and return the info */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tif(lua_script_package != NULL) {\n\t\t\t/* Unless we asked already */\n\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\treturn lua_script_package;\n\t\t}\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"getPackage\");\n\t\tlua_call(t, 0, 1);\n\t\tconst char *package = lua_tostring(t, -1);\n\t\tif(package != NULL)\n\t\t\tlua_script_package = g_strdup(package);\n\t\tlua_pop(t, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn lua_script_package;\n\t}\n\t/* No override, return the Janus Lua plugin info */\n\treturn JANUS_LUA_PACKAGE;\n}\n\njanus_lua_session *janus_lua_lookup_session(janus_plugin_session *handle) {\n\tjanus_lua_session *session = NULL;\n\tif (g_hash_table_contains(lua_sessions, handle)) {\n\t\tsession = (janus_lua_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_lua_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tguint32 id = 0;\n\twhile(id == 0) {\n\t\tid = janus_random_uint32();\n\t\tif(g_hash_table_lookup(lua_ids, GUINT_TO_POINTER(id))) {\n\t\t\tid = 0;\n\t\t\tcontinue;\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Creating new Lua session %\"SCNu32\"...\\n\", id);\n\tjanus_lua_session *session = (janus_lua_session *)g_malloc0(sizeof(janus_lua_session));\n\tsession->handle = handle;\n\tsession->id = id;\n\tjanus_rtp_switching_context_reset(&session->artpctx);\n\tjanus_rtp_switching_context_reset(&session->vrtpctx);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tsession->sim_context.substream_target = 2;\n\tsession->sim_context.templayer_target = 2;\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tsession->rid_extmap_id = -1;\n\tjanus_mutex_init(&session->rid_mutex);\n\tjanus_mutex_init(&session->recipients_mutex);\n\tjanus_mutex_init(&session->rec_mutex);\n\tsession->vcodec = JANUS_VIDEOCODEC_NONE;\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tjanus_refcount_init(&session->ref, janus_lua_session_free);\n\thandle->plugin_handle = session;\n\tg_hash_table_insert(lua_sessions, handle, session);\n\tg_hash_table_insert(lua_ids, GUINT_TO_POINTER(session->id), session);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\n\t/* Notify the Lua script */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, \"createSession\");\n\tlua_pushnumber(t, session->id);\n\tlua_call(t, 1, 0);\n\tlua_pop(lua_state, 1);\n\tjanus_mutex_unlock(&lua_mutex);\n\n\treturn;\n}\n\nvoid janus_lua_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = janus_lua_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tguint32 id = session->id;\n\tJANUS_LOG(LOG_VERB, \"Removing Lua session %\"SCNu32\"...\\n\", id);\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\n\t/* Notify the Lua script */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, \"destroySession\");\n\tlua_pushnumber(t, id);\n\tlua_call(t, 1, 0);\n\tlua_pop(lua_state, 1);\n\tjanus_mutex_unlock(&lua_mutex);\n\n\t/* Get any rid references recipients of this sessions may have */\n\tjanus_mutex_lock(&session->recipients_mutex);\n\twhile(session->recipients != NULL) {\n\t\tjanus_lua_session *recipient = (janus_lua_session *)session->recipients->data;\n\t\tif(recipient != NULL) {\n\t\t\trecipient->sender = NULL;\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tjanus_refcount_decrease(&recipient->ref);\n\t\t}\n\t\tsession->recipients = g_slist_remove(session->recipients, recipient);\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\n\t/* Finally, remove from the hashtable */\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tg_hash_table_remove(lua_sessions, handle);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tjanus_refcount_decrease(&session->ref);\n\n\treturn;\n}\n\njson_t *janus_lua_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = janus_lua_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t/* Ask the Lua script for information on this session */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, \"querySession\");\n\tlua_pushnumber(t, session->id);\n\tlua_call(t, 1, 1);\n\tlua_pop(lua_state, 1);\n\tjanus_refcount_decrease(&session->ref);\n\tconst char *info = lua_tostring(t, -1);\n\tlua_pop(t, 1);\n\t/* We need a Jansson object */\n\tjson_error_t error;\n\tjson_t *json = json_loads(info, 0, &error);\n\tjanus_mutex_unlock(&lua_mutex);\n\tif(!json) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\", error.line, error.text);\n\t\treturn NULL;\n\t}\n\treturn json;\n}\n\nstruct janus_plugin_result *janus_lua_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&lua_stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = janus_lua_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\n\t/* Processing the message is up to the Lua script: serialize the Jansson objects to strings */\n\tchar *message_text = message ? json_dumps(message, JSON_INDENT(0) | JSON_PRESERVE_ORDER) : NULL;\n\tjson_decref(message);\n\tif(message == NULL || message_text == NULL) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_ERR, \"Invalid message..?\\n\");\n\t\tif(jsep != NULL)\n\t\t\tjson_decref(jsep);\n\t\tg_free(transaction);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\t}\n\tchar *jsep_text = jsep ? json_dumps(jsep, JSON_INDENT(0) | JSON_PRESERVE_ORDER) : NULL;\n\tif(jsep != NULL) {\n\t\tjson_t *simulcast = json_object_get(jsep, \"simulcast\");\n\t\tif(simulcast && json_array_size(simulcast) > 0) {\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(simulcast); i++) {\n\t\t\t\tjson_t *s = json_array_get(simulcast, i);\n\t\t\t\t/* Clear existing RIDs in case this is a renegotiation */\n\t\t\t\tjanus_mutex_lock(&session->rid_mutex);\n\t\t\t\tjanus_rtp_simulcasting_cleanup(&session->rid_extmap_id, NULL, session->rid, NULL);\n\t\t\t\tjanus_rtp_simulcasting_prepare(s,\n\t\t\t\t\t&session->rid_extmap_id,\n\t\t\t\t\tsession->ssrc, session->rid);\n\t\t\t\tjanus_mutex_unlock(&session->rid_mutex);\n\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tconst char *sdp_type = json_string_value(json_object_get(jsep, \"type\"));\n\t\tif(sdp_type && !strcasecmp(sdp_type, \"answer\")) {\n\t\t\t/* Take note of which video codec were negotiated */\n\t\t\tchar error_str[512];\n\t\t\tconst char *sdp = json_string_value(json_object_get(jsep, \"sdp\"));\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(sdp, error_str, sizeof(error_str));\n\t\t\tconst char *vcodec = NULL;\n\t\t\tjanus_sdp_find_first_codec(parsed_sdp, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\tif(vcodec)\n\t\t\t\tsession->vcodec = janus_videocodec_from_name(vcodec);\n\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t}\n\t\tif(json_is_true(json_object_get(jsep, \"e2ee\")))\n\t\t\tsession->e2ee = TRUE;\n\t\tjson_decref(jsep);\n\t}\n\t/* Invoke the script function */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, \"handleMessage\");\n\tlua_pushnumber(t, session->id);\n\tlua_pushstring(t, transaction);\n\tlua_pushstring(t, message_text);\n\tlua_pushstring(t, jsep_text);\n\tlua_call(t, 4, 2);\n\tlua_pop(lua_state, 1);\n\tjanus_refcount_decrease(&session->ref);\n\tif(message_text != NULL)\n\t\tfree(message_text);\n\tif(jsep_text != NULL)\n\t\tfree(jsep_text);\n\tg_free(transaction);\n\tint n = lua_gettop(t);\n\tif(n != 2) {\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 2)\\n\", n);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"Lua error\", NULL);\n\t}\n\t/* Check if this is a synchronous or asynchronous response */\n\tint res = (int)lua_tonumber(t, 1);\n\tconst char *response = lua_tostring(t, 2);\n\tlua_pop(t, 2);\n\tif(res < 0) {\n\t\t/* We got an error */\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, response ? response : \"Lua error\", NULL);\n\t} else if(res == 0) {\n\t\t/* Synchronous response: we need a Jansson object */\n\t\tjson_error_t error;\n\t\tjson_t *json = json_loads(response, 0, &error);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\tif(!json) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"Lua error\", NULL);\n\t\t}\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, json);\n\t}\n\tjanus_mutex_unlock(&lua_mutex);\n\t/* If we got here, it's an asynchronous response */\n\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n}\n\njson_t *janus_lua_handle_admin_message(json_t *message) {\n\tif(!has_handle_admin_message || message == NULL)\n\t\treturn NULL;\n\tchar *message_text = json_dumps(message, JSON_INDENT(0) | JSON_PRESERVE_ORDER);\n\tif(message_text == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\treturn NULL;\n\t}\n\t/* Invoke the script function */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, \"handleAdminMessage\");\n\tlua_pushstring(t, message_text);\n\tlua_call(t, 1, 1);\n\tlua_pop(lua_state, 1);\n\tif(message_text != NULL)\n\t\tfree(message_text);\n\tint n = lua_gettop(t);\n\tif(n != 1) {\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"Wrong number of arguments: %d (expected 1)\\n\", n);\n\t\treturn NULL;\n\t}\n\t/* Get the response */\n\tconst char *response = lua_tostring(t, 1);\n\tjson_error_t error;\n\tjson_t *json = json_loads(response, 0, &error);\n\tjanus_mutex_unlock(&lua_mutex);\n\tif(!json) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\treturn NULL;\n\t}\n\treturn json;\n}\n\nvoid janus_lua_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"WebRTC media is now available\\n\");\n\tif(g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = janus_lua_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->started, 1);\n\tsession->pli_latest = janus_get_monotonic_time();\n\n\t/* Notify the Lua script */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, \"setupMedia\");\n\tlua_pushnumber(t, session->id);\n\tlua_call(t, 1, 0);\n\tlua_pop(lua_state, 1);\n\tjanus_mutex_unlock(&lua_mutex);\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_lua_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *rtp_packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tjanus_lua_session *session = (janus_lua_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tgboolean video = rtp_packet->video;\n\tchar *buf = rtp_packet->buffer;\n\tuint16_t len = rtp_packet->length;\n\t/* Check if the Lua script wants to handle/manipulate RTP packets itself */\n\tif(has_incoming_rtp) {\n\t\t/* Yep, pass the data to the Lua script and return */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"incomingRtp\");\n\t\tlua_pushnumber(t, session->id);\n\t\tlua_pushboolean(t, video);\n\t\tlua_pushlstring(t, buf, len);\n\t\tlua_pushnumber(t, len);\n\t\tlua_call(t, 4, 0);\n\t\tlua_pop(lua_state, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn;\n\t}\n\t/* Is this session allowed to send media? */\n\tif((video && !session->send_video) || (!video && !session->send_audio))\n\t\treturn;\n\t/* Handle the packet */\n\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\t/* Check if we're simulcasting, and if so, keep track of the \"layer\" */\n\tint sc = video ? 0 : -1;\n\tif(video && (session->ssrc[0] != 0 || session->rid[0] != NULL)) {\n\t\tuint32_t ssrc = ntohl(rtp->ssrc);\n\t\tif(ssrc == session->ssrc[0])\n\t\t\tsc = 0;\n\t\telse if(ssrc == session->ssrc[1])\n\t\t\tsc = 1;\n\t\telse if(ssrc == session->ssrc[2])\n\t\t\tsc = 2;\n\t\telse if(session->rid_extmap_id > 0) {\n\t\t\tjanus_mutex_lock(&session->rid_mutex);\n\t\t\t/* We may not know the SSRC yet, try the rid RTP extension */\n\t\t\tchar sdes_item[16];\n\t\t\tif(janus_rtp_header_extension_parse_rid(buf, len, session->rid_extmap_id, sdes_item, sizeof(sdes_item)) == 0) {\n\t\t\t\tif(session->rid[0] != NULL && !strcmp(session->rid[0], sdes_item)) {\n\t\t\t\t\tsession->ssrc[0] = ssrc;\n\t\t\t\t\tsc = 0;\n\t\t\t\t} else if(session->rid[1] != NULL && !strcmp(session->rid[1], sdes_item)) {\n\t\t\t\t\tsession->ssrc[1] = ssrc;\n\t\t\t\t\tsc = 1;\n\t\t\t\t} else if(session->rid[2] != NULL && !strcmp(session->rid[2], sdes_item)) {\n\t\t\t\t\tsession->ssrc[2] = ssrc;\n\t\t\t\t\tsc = 2;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rid_mutex);\n\t\t}\n\t}\n\t/* Are we recording? */\n\tif(!video || (session->ssrc[0] == 0 && session->rid[0] == NULL)) {\n\t\tjanus_recorder_save_frame(video ? session->vrc : session->arc, buf, len);\n\t} else {\n\t\t/* We're simulcasting, save the best video quality */\n\t\tgboolean save = janus_rtp_simulcasting_context_process_rtp(&session->rec_simctx,\n\t\t\tbuf, len, NULL, 0, session->ssrc, session->rid, session->vcodec, &session->rec_ctx, &session->rid_mutex);\n\t\tif(save) {\n\t\t\tuint32_t seq_number = ntohs(rtp->seq_number);\n\t\t\tuint32_t timestamp = ntohl(rtp->timestamp);\n\t\t\tuint32_t ssrc = ntohl(rtp->ssrc);\n\t\t\tjanus_rtp_header_update(rtp, &session->rec_ctx, TRUE, 0);\n\t\t\t/* We use a fixed SSRC for the whole recording */\n\t\t\trtp->ssrc = session->ssrc[0];\n\t\t\tjanus_recorder_save_frame(session->vrc, buf, len);\n\t\t\t/* Restore the header, as it will be needed by recipients of this packet */\n\t\t\trtp->ssrc = htonl(ssrc);\n\t\t\trtp->timestamp = htonl(timestamp);\n\t\t\trtp->seq_number = htons(seq_number);\n\t\t}\n\t}\n\tjanus_lua_rtp_relay_packet packet;\n\tpacket.sender = session;\n\tpacket.data = rtp;\n\tpacket.length = len;\n\tpacket.is_rtp = TRUE;\n\tpacket.is_video = video;\n\tpacket.extensions = rtp_packet->extensions;\n\tpacket.ssrc[0] = (sc != -1 ? session->ssrc[0] : 0);\n\tpacket.ssrc[1] = (sc != -1 ? session->ssrc[1] : 0);\n\tpacket.ssrc[2] = (sc != -1 ? session->ssrc[2] : 0);\n\t/* Backup the actual timestamp and sequence number set by the publisher, in case switching is involved */\n\tpacket.timestamp = ntohl(packet.data->timestamp);\n\tpacket.seq_number = ntohs(packet.data->seq_number);\n\t/* Relay to all recipients */\n\tjanus_mutex_lock_nodebug(&session->recipients_mutex);\n\tg_slist_foreach(session->recipients, janus_lua_relay_rtp_packet, &packet);\n\tjanus_mutex_unlock_nodebug(&session->recipients_mutex);\n\n\t/* Check if we need to send any PLI to this media source */\n\tif(video && session->pli_freq > 0) {\n\t\t/* We send a FIR every tot seconds, depending on what the Lua script configured */\n\t\tgint64 now = janus_get_monotonic_time();\n\t\tif((now-session->pli_latest) >= ((gint64)session->pli_freq*G_USEC_PER_SEC)) {\n\t\t\tsession->pli_latest = now;\n\t\t\tJANUS_LOG(LOG_HUGE, \"Sending PLI to session %\"SCNu32\"\\n\", session->id);\n\t\t\tlua_janus_core->send_pli(handle);\n\t\t}\n\t}\n}\n\nvoid janus_lua_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tjanus_lua_session *session = (janus_lua_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tgboolean video = packet->video;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\t/* Check if the Lua script wants to handle/manipulate RTCP packets itself */\n\tif(has_incoming_rtcp) {\n\t\t/* Yep, pass the data to the Lua script and return */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"incomingRtcp\");\n\t\tlua_pushnumber(t, session->id);\n\t\tlua_pushboolean(t, video);\n\t\tlua_pushlstring(t, buf, len);\n\t\tlua_pushnumber(t, len);\n\t\tlua_call(t, 4, 0);\n\t\tlua_pop(lua_state, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn;\n\t}\n\t/* If a REMB arrived, make sure we cap it to our configuration, and send it as a video RTCP */\n\tguint32 bitrate = janus_rtcp_get_remb(buf, len);\n\tif(bitrate > 0) {\n\t\t/* No limit ~= 10000000 */\n\t\tlua_janus_core->send_remb(handle, session->bitrate ? session->bitrate : 10000000);\n\t}\n\t/* If there's an incoming PLI, instead, relay it to the source of the media if any */\n\tif(janus_rtcp_has_pli(buf, len)) {\n\t\tif(session->sender != NULL) {\n\t\t\tjanus_mutex_lock_nodebug(&session->sender->recipients_mutex);\n\t\t\t/* Send a PLI */\n\t\t\tsession->sender->pli_latest = janus_get_monotonic_time();\n\t\t\tJANUS_LOG(LOG_HUGE, \"Sending PLI to session %\"SCNu32\"\\n\", session->sender->id);\n\t\t\tlua_janus_core->send_pli(session->sender->handle);\n\t\t\tjanus_mutex_unlock_nodebug(&session->sender->recipients_mutex);\n\t\t}\n\t}\n}\n\nvoid janus_lua_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tjanus_lua_session *session = (janus_lua_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\tchar *label = packet->label;\n\tchar *protocol = packet->protocol;\n\t/* Are we recording? */\n\tjanus_recorder_save_frame(session->drc, buf, len);\n\t/* Check if the Lua script wants to handle/manipulate data channel packets itself */\n\tif((!packet->binary && (has_incoming_data_legacy || has_incoming_text_data)) || (packet->binary && has_incoming_binary_data)) {\n\t\t/* Yep, pass the data to the Lua script and return */\n\t\tif(!packet->binary && !has_incoming_text_data)\n\t\t\tJANUS_LOG(LOG_WARN, \"Missing 'incomingTextData', invoking deprecated function 'incomingData' instead\\n\");\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, packet->binary ? \"incomingBinaryData\" : (has_incoming_text_data ? \"incomingTextData\" : \"incomingData\"));\n\t\tlua_pushnumber(t, session->id);\n\t\t/* We use a string for both text and binary data */\n\t\tlua_pushlstring(t, buf, len);\n\t\tlua_pushnumber(t, len);\n\t\tlua_pushlstring(t, label, label ? strlen(label) : 0);\n\t\tlua_pushlstring(t, protocol, protocol ? strlen(protocol) : 0);\n\t\tlua_call(t, 5, 0);\n\t\tlua_pop(lua_state, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn;\n\t}\n\t/* Is this session allowed to send data? */\n\tif(!session->send_data)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"Got a %s DataChannel message (%d bytes) to forward\\n\",\n\t\tpacket->binary ? \"binary\" : \"text\", len);\n\t/* Relay to all recipients */\n\tjanus_lua_rtp_relay_packet pkt;\n\tpkt.sender = session;\n\tpkt.data = (janus_rtp_header *)buf;\n\tpkt.length = len;\n\tpkt.is_rtp = FALSE;\n\tpkt.textdata = !packet->binary;\n\tjanus_mutex_lock_nodebug(&session->recipients_mutex);\n\t/* FIXME We should add support for labels, here */\n\tg_slist_foreach(session->recipients, janus_lua_relay_data_packet, &pkt);\n\tjanus_mutex_unlock_nodebug(&session->recipients_mutex);\n}\n\nvoid janus_lua_data_ready(janus_plugin_session *handle) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tjanus_lua_session *session = (janus_lua_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tif(g_atomic_int_compare_and_exchange(&session->dataready, 0, 1)) {\n\t\tJANUS_LOG(LOG_INFO, \"[%s-%p] Data channel available\\n\", JANUS_LUA_PACKAGE, handle);\n\t}\n\t/* Check if the Lua script wants to receive this event */\n\tif(has_data_ready) {\n\t\t/* Yep, pass the event to the Lua script and return */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"dataReady\");\n\t\tlua_pushnumber(t, session->id);\n\t\tlua_call(t, 1, 0);\n\t\tlua_pop(lua_state, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t\treturn;\n\t}\n}\n\nvoid janus_lua_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = janus_lua_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\t/* Check if the Lua script wants to handle such events */\n\tjanus_refcount_increase(&session->ref);\n\tif(has_slow_link) {\n\t\t/* Notify the Lua script */\n\t\tjanus_mutex_lock(&lua_mutex);\n\t\tlua_State *t = lua_newthread(lua_state);\n\t\tlua_getglobal(t, \"slowLink\");\n\t\tlua_pushnumber(t, session->id);\n\t\tlua_pushboolean(t, uplink);\n\t\tlua_pushboolean(t, video);\n\t\tlua_call(t, 3, 0);\n\t\tlua_pop(lua_state, 1);\n\t\tjanus_mutex_unlock(&lua_mutex);\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_lua_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_LUA_PACKAGE, handle);\n\tif(g_atomic_int_get(&lua_stopping) || !g_atomic_int_get(&lua_initialized))\n\t\treturn;\n\tjanus_mutex_lock(&lua_sessions_mutex);\n\tjanus_lua_session *session = janus_lua_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&lua_sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&lua_sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1)) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->started, 0);\n\tg_atomic_int_set(&session->dataready, 0);\n\n\t/* Reset the media properties */\n\tsession->accept_audio = FALSE;\n\tsession->accept_video = FALSE;\n\tsession->accept_data = FALSE;\n\tsession->send_audio = FALSE;\n\tsession->send_video = FALSE;\n\tsession->send_data = FALSE;\n\tsession->bitrate = 0;\n\tsession->pli_freq = 0;\n\tsession->pli_latest = 0;\n\tsession->e2ee = FALSE;\n\tjanus_rtp_switching_context_reset(&session->artpctx);\n\tjanus_rtp_switching_context_reset(&session->vrtpctx);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tsession->sim_context.substream_target = 2;\n\tsession->sim_context.templayer_target = 2;\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tsession->vcodec = JANUS_VIDEOCODEC_NONE;\n\tjanus_rtp_simulcasting_cleanup(&session->rid_extmap_id, session->ssrc, session->rid, &session->rid_mutex);\n\n\t/* Get rid of the recipients */\n\tjanus_mutex_lock(&session->recipients_mutex);\n\twhile(session->recipients) {\n\t\tjanus_lua_session *recipient = (janus_lua_session *)session->recipients->data;\n\t\tsession->recipients = g_slist_remove(session->recipients, recipient);\n\t\trecipient->sender = NULL;\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&recipient->ref);\n\t}\n\tjanus_mutex_unlock(&session->recipients_mutex);\n\n\t/* Notify the Lua script */\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, \"hangupMedia\");\n\tlua_pushnumber(t, session->id);\n\tlua_call(t, 1, 0);\n\tlua_pop(lua_state, 1);\n\tjanus_mutex_unlock(&lua_mutex);\n\tjanus_refcount_decrease(&session->ref);\n}\n\n/* Helpers to quickly relay RTP and data packets to the intended recipients */\nstatic void janus_lua_relay_rtp_packet(gpointer data, gpointer user_data) {\n\tjanus_lua_rtp_relay_packet *packet = (janus_lua_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_lua_session *sender = (janus_lua_session *)packet->sender;\n\tjanus_lua_session *session = (janus_lua_session *)data;\n\tif(!sender || !session || !session->handle || !g_atomic_int_get(&session->started)) {\n\t\treturn;\n\t}\n\n\t/* Check if this recipient is willing/allowed to receive this medium */\n\tif((packet->is_video && !session->accept_video) || (!packet->is_video && !session->accept_audio)) {\n\t\t/* Nope, don't relay */\n\t\treturn;\n\t}\n\tif(packet->ssrc[0] != 0) {\n\t\t/* Handle simulcast: make sure we have a payload to work with */\n\t\tint plen = 0;\n\t\tchar *payload = janus_rtp_payload((char *)packet->data, packet->length, &plen);\n\t\tif(payload == NULL)\n\t\t\treturn;\n\t\t/* Process this packet: don't relay if it's not the SSRC/layer we wanted to handle */\n\t\tgboolean relay = janus_rtp_simulcasting_context_process_rtp(&session->sim_context,\n\t\t\t(char *)packet->data, packet->length, packet->extensions.dd_content, packet->extensions.dd_len,\n\t\t\tpacket->ssrc, NULL, sender->vcodec, &session->vrtpctx, NULL);\n\t\tif(session->sim_context.need_pli && sender->handle) {\n\t\t\t/* Send a PLI */\n\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the simulcast context\\n\");\n\t\t\tlua_janus_core->send_pli(sender->handle);\n\t\t}\n\t\t/* Do we need to drop this? */\n\t\tif(!relay)\n\t\t\treturn;\n\t\t/* Any event we should notify? */\n\t\tif(session->sim_context.changed_substream) {\n\t\t\t/* Notify the script about the substream change */\n\t\t\tif(has_substream_changed) {\n\t\t\t\tjanus_mutex_lock(&lua_mutex);\n\t\t\t\tlua_State *t = lua_newthread(lua_state);\n\t\t\t\tlua_getglobal(t, \"substreamChanged\");\n\t\t\t\tlua_pushnumber(t, session->id);\n\t\t\t\tlua_pushnumber(t, session->sim_context.substream);\n\t\t\t\tlua_call(t, 2, 0);\n\t\t\t\tlua_pop(lua_state, 1);\n\t\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\t}\n\t\t}\n\t\tif(session->sim_context.changed_temporal) {\n\t\t\t/* Notify the user about the temporal layer change */\n\t\t\tif(has_substream_changed) {\n\t\t\t\tjanus_mutex_lock(&lua_mutex);\n\t\t\t\tlua_State *t = lua_newthread(lua_state);\n\t\t\t\tlua_getglobal(t, \"temporalLayerChanged\");\n\t\t\t\tlua_pushnumber(t, session->id);\n\t\t\t\tlua_pushnumber(t, session->sim_context.templayer);\n\t\t\t\tlua_call(t, 2, 0);\n\t\t\t\tlua_pop(lua_state, 1);\n\t\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t\t}\n\t\t}\n\t\t/* If we got here, update the RTP header and send the packet */\n\t\tjanus_rtp_header_update(packet->data, &session->vrtpctx, TRUE, 0);\n\t\tchar vp8pd[6];\n\t\tif(sender->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t/* For VP8, we save the original payload descriptor, to restore it after */\n\t\t\tmemcpy(vp8pd, payload, sizeof(vp8pd));\n\t\t\tjanus_vp8_simulcast_descriptor_update(payload, plen, &session->vp8_context,\n\t\t\t\tsession->sim_context.changed_substream);\n\t\t}\n\t\t/* Send the packet */\n\t\tif(lua_janus_core != NULL) {\n\t\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = packet->is_video,\n\t\t\t\t.buffer = (char *)packet->data, .length = packet->length, .extensions = packet->extensions };\n\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\tlua_janus_core->relay_rtp(session->handle, &rtp);\n\t\t}\n\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\tif(sender->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t/* Restore the original payload descriptor as well, as it will be needed by the next viewer */\n\t\t\tmemcpy(payload, vp8pd, sizeof(vp8pd));\n\t\t}\n\t} else {\n\t\t/* Fix sequence number and timestamp (publisher switching may be involved) */\n\t\tjanus_rtp_header_update(packet->data, packet->is_video ? &session->vrtpctx : &session->artpctx, packet->is_video, 0);\n\t\t/* Send the packet */\n\t\tif(lua_janus_core != NULL) {\n\t\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = packet->is_video,\n\t\t\t\t.buffer = (char *)packet->data, .length = packet->length, .extensions = packet->extensions };\n\t\t\tlua_janus_core->relay_rtp(session->handle, &rtp);\n\t\t}\n\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t}\n\n\treturn;\n}\n\nstatic void janus_lua_relay_data_packet(gpointer data, gpointer user_data) {\n\tjanus_lua_rtp_relay_packet *packet = (janus_lua_rtp_relay_packet *)user_data;\n\tif(!packet || packet->is_rtp || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_lua_session *session = (janus_lua_session *)data;\n\tif(!session || !session->handle || !g_atomic_int_get(&session->started) ||\n\t\t\t!session->accept_data || !g_atomic_int_get(&session->dataready)) {\n\t\treturn;\n\t}\n\tif(lua_janus_core != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Forwarding %s DataChannel message (%d bytes) to session %\"SCNu32\"\\n\",\n\t\t\tpacket->textdata ? \"text\" : \"binary\", packet->length, session->id);\n\t\tjanus_plugin_data data = {\n\t\t\t.label = NULL,\n\t\t\t.protocol = NULL,\n\t\t\t.binary = !packet->textdata,\n\t\t\t.buffer = (char *)packet->data,\n\t\t\t.length = packet->length\n\t\t};\n\t\tlua_janus_core->relay_data(session->handle, &data);\n\t}\n\treturn;\n}\n\n/* This is a scheduler thread: if we know there are coroutines to resume\n * in Lua (e.g., for asynchronous requests), we do that ourselves here */\nstatic void *janus_lua_scheduler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining Lua scheduler thread\\n\");\n\tjanus_lua_event *event = NULL;\n\t/* Wait until there are events to process */\n\twhile(g_atomic_int_get(&lua_initialized) && !g_atomic_int_get(&lua_stopping)) {\n\t\tevent = g_async_queue_pop(events);\n\t\tif(event == GUINT_TO_POINTER(janus_lua_event_exit))\n\t\t\tbreak;\n\t\tif(event == GUINT_TO_POINTER(janus_lua_event_resume)) {\n\t\t\t/* There are coroutines to resume */\n\t\t\tjanus_mutex_lock(&lua_mutex);\n\t\t\tlua_getglobal(lua_state, \"resumeScheduler\");\n\t\t\tlua_call(lua_state, 0, 0);\n\t\t\t/* Print the count of elements into Lua stack */\n\t\t\tjanus_lua_stackdump(lua_state);\n\t\t\tjanus_mutex_unlock(&lua_mutex);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving Lua scheduler thread\\n\");\n\treturn NULL;\n}\n\n/* This is a loop that can be used for timing callbacks, e.g., whenever\n * the Lua script asks for asynchronously invoking one of its methods\n * after some time, rather than immediately (which is what the scheduler\n * would be for instead). Allows for a string parameter to be passed. */\nstatic void *janus_lua_timer(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining Lua timer loop\\n\");\n\tGMainLoop *loop = (GMainLoop *)data;\n\t/* Start loop */\n\tg_main_loop_run(loop);\n\t/* Done */\n\tJANUS_LOG(LOG_VERB, \"Leaving Lua timer loop\\n\");\n\treturn NULL;\n}\n\n/* Callback to trigger timed callbacks */\nstatic gboolean janus_lua_timer_cb(void *data) {\n\tjanus_lua_callback *cb = (janus_lua_callback *)data;\n\tif(cb == NULL)\n\t\treturn FALSE;\n\t/* Invoke the callback with the provided argument, if available */\n\tJANUS_LOG(LOG_VERB, \"Invoking scheduled callback (waited %\"SCNu32\"ms) with ID %u\\n\", cb->ms, cb->id);\n\tjanus_mutex_lock(&lua_mutex);\n\tlua_State *t = lua_newthread(lua_state);\n\tlua_getglobal(t, cb->function);\n\tif(cb->argument == NULL) {\n\t\tlua_call(t, 0, 0);\n\t} else {\n\t\tlua_pushstring(t, cb->argument);\n\t\tlua_call(t, 1, 0);\n\t}\n\tlua_pop(lua_state, 1);\n\t/* Done */\n\tg_hash_table_remove(callbacks, cb);\n\tjanus_mutex_unlock(&lua_mutex);\n\treturn FALSE;\n}\n"
  },
  {
    "path": "src/plugins/janus_lua_data.h",
    "content": "/*! \\file   janus_lua_data.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Lua data/session definition (headers)\n * \\details  The Janus Lua plugin implements all the mandatory hooks to\n * allow the C code to interact with a custom Lua script, and vice-versa.\n * That said, the janus_lua_extra.c code allows for custom hooks to be\n * added in C, to expose additional Lua functions and implement more\n * complex media management than the one provided by the stock plugin.\n * For this to work, though, the janus_lua_session object and its\n * indexing in the hashtable need to be defined externally, which is\n * what this file is for.\n *\n * Notice that all the management associated to sessions (creating or\n * destroying sessions, locking their global mutex, updating the\n * hashtable) is done in the core of the Lua plugin: here we only\n * define them, so that they can be accessed/used by the extra code too.\n *\n * \\ingroup luapapi\n * \\ref luapapi\n */\n\n#ifndef JANUS_LUA_DATA_H\n#define JANUS_LUA_DATA_H\n\n#include <lua.h>\n#include <lualib.h>\n#include <lauxlib.h>\n\n#include \"plugin.h\"\n\n#include \"debug.h\"\n#include \"apierror.h\"\n#include \"config.h\"\n#include \"mutex.h\"\n#include \"rtp.h\"\n#include \"rtcp.h\"\n#include \"sdp-utils.h\"\n#include \"record.h\"\n#include \"utils.h\"\n\n/* Core pointer and related flags */\nextern volatile gint lua_initialized, lua_stopping;\nextern janus_callbacks *lua_janus_core;\n\n/* Lua state: we define state and mutex as extern */\nextern lua_State *lua_state;\nextern janus_mutex lua_mutex;\n\n/* Lua session: we keep only the barebone stuff here, the rest will be in the Lua script */\ntypedef struct janus_lua_session {\n\tjanus_plugin_session *handle;\t\t/* Pointer to the core-plugin session */\n\tuint32_t id;\t\t\t\t\t\t/* Unique session ID (will be used to correlate with the Lua script) */\n\t/* The following are only needed for media manipulation, feedback and routing, and may not all be used */\n\tgboolean accept_audio;\t\t\t\t/* Whether incoming audio can be accepted or must be dropped */\n\tgboolean accept_video;\t\t\t\t/* Whether incoming video can be accepted or must be dropped */\n\tgboolean accept_data;\t\t\t\t/* Whether incoming data can be accepted or must be dropped */\n\tgboolean send_audio;\t\t\t\t/* Whether outgoing audio can be sent or must be dropped */\n\tgboolean send_video;\t\t\t\t/* Whether outgoing video can be sent or must be dropped */\n\tgboolean send_data;\t\t\t\t\t/* Whether outgoing data can be sent or must be dropped */\n\tjanus_rtp_switching_context artpctx, vrtpctx;\n\tjanus_videocodec vcodec;\t\t\t/* Video codec this session is using */\n\tuint32_t ssrc[3];\t\t\t\t\t/* Only needed in case VP8 (or H.264) simulcasting is involved */\n\tchar *rid[3];\t\t\t\t\t\t/* Only needed if simulcasting is rid-based */\n\tint rid_extmap_id;\t\t\t\t\t/* rid extmap ID */\n\tjanus_mutex rid_mutex;\t\t\t\t/* Mutex to protect access to the rid array and the extmap ID */\n\tjanus_rtp_simulcasting_context sim_context;\n\tjanus_vp8_simulcast_context vp8_context;\n\tuint32_t bitrate;\t\t\t\t\t/* Bitrate limit */\n\tuint16_t pli_freq;\t\t\t\t\t/* Regular PLI frequency (0=disabled) */\n\tgint64 pli_latest;\t\t\t\t\t/* Time of latest sent PLI (to avoid flooding) */\n\tGSList *recipients;\t\t\t\t\t/* Sessions that should receive media from this session */\n\tstruct janus_lua_session *sender;\t/* Other session this session is receiving media from */\n\tjanus_mutex recipients_mutex;\t\t/* Mutex to lock the recipients list */\n\tjanus_recorder *arc;\t\t\t\t/* The Janus recorder instance for audio, if enabled */\n\tjanus_recorder *vrc;\t\t\t\t/* The Janus recorder instance for video, if enabled */\n\tjanus_recorder *drc;\t\t\t\t/* The Janus recorder instance for data, if enabled */\n\tjanus_rtp_switching_context rec_ctx;\n\tjanus_rtp_simulcasting_context rec_simctx;\n\tgboolean e2ee;\t\t\t\t\t\t/* Whether media is encrypted, e.g., using Insertable Streams */\n\tjanus_mutex rec_mutex;\t\t\t\t/* Mutex to protect the recorders from race conditions */\n\tvolatile gint started;\t\t\t\t/* Whether this session's PeerConnection is ready or not */\n\tvolatile gint dataready;\t\t\t/* Whether the data channel was established on this sessions's PeerConnection */\n\tvolatile gint hangingup;\t\t\t/* Whether this session's PeerConnection is hanging up */\n\tvolatile gint destroyed;\t\t\t/* Whether this session's been marked as destroyed */\n\t/* If you need any additional property (e.g., for hooks you added in janus_lua_extra.c) add them below this line */\n\n\t/* Reference counter */\n\tjanus_refcount ref;\n} janus_lua_session;\nextern GHashTable *lua_sessions, *lua_ids;\nextern janus_mutex lua_sessions_mutex;\njanus_lua_session *janus_lua_lookup_session(janus_plugin_session *handle);\n\n#endif\n"
  },
  {
    "path": "src/plugins/janus_lua_extra.c",
    "content": "/*! \\file   janus_lua_extra.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Lua plugin extra hooks\n * \\details  The Janus Lua plugin implements all the mandatory hooks to\n * allow the C code to interact with a custom Lua script, and vice-versa.\n * Anyway, Lua developers may want to have the C code do more than what\n * is provided out of the box, e.g., by exposing additional Lua methods\n * from C for further low level processing or native integration. This\n * \"extra\" implementation provides a mechanism to do just that, as\n * developers can just add their own custom hooks in the C extra code,\n * and the Lua plugin will register the new methods along the stock ones.\n *\n * More specifically, the Janus Lua plugin will always invoke the\n * janus_lua_register_extra_functions() method when initializing. This\n * means that all developers will need to do to register a new function\n * is adding new \\c lua_register calls to register their own functions\n * there, and they'll be added to the stack.\n *\n * \\ingroup luapapi\n * \\ref luapapi\n */\n\n#include \"janus_lua_data.h\"\n#include \"janus_lua_extra.h\"\n\n\n/* Sample extra function we can register */\nstatic int janus_lua_extra_sample(lua_State *s) {\n\t/* Let's do nothing, and return 1234 */\n\tlua_pushnumber(s, 1234);\n\treturn 1;\n}\n\n/* This is where you can add your custom extra functions */\n\n\n/* Public method to register all custom extra functions */\nvoid janus_lua_register_extra_functions(lua_State *state) {\n\tif(state == NULL)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"Registering extra Lua functions\\n\");\n\t/* Register all extra functions here */\n\tlua_register(state, \"testExtraFunction\", janus_lua_extra_sample);\n}\n"
  },
  {
    "path": "src/plugins/janus_lua_extra.h",
    "content": "/*! \\file   janus_lua_extra.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Lua plugin extra hooks (headers)\n * \\details  The Janus Lua plugin implements all the mandatory hooks to\n * allow the C code to interact with a custom Lua script, and vice-versa.\n * Anyway, Lua developers may want to have the C code do more than what\n * is provided out of the box, e.g., by exposing additional Lua methods\n * from C for further low level processing or native integration. This\n * \"extra\" implementation provides a mechanism to do just that, as\n * developers can just add their own custom hooks in the C extra code,\n * and the Lua plugin will register the new methods along the stock ones.\n *\n * More specifically, the Janus Lua plugin will always invoke the\n * janus_lua_register_extra_functions() method when initializing. This\n * means that all developers will need to do to register a new function\n * is adding new \\c lua_register calls to register their own functions\n * there, and they'll be added to the stack.\n *\n * \\ingroup luapapi\n * \\ref luapapi\n */\n\n#ifndef JANUS_LUA_EXTRA_H\n#define JANUS_LUA_EXTRA_H\n\n#include <lua.h>\n#include <lualib.h>\n#include <lauxlib.h>\n\n/*! \\brief Method to register extra Lua functions in the C code\n * @param[in] state The Lua state to register the functions on */\nvoid janus_lua_register_extra_functions(lua_State *state);\n\n#endif\n"
  },
  {
    "path": "src/plugins/janus_nosip.c",
    "content": "/*! \\file   janus_nosip.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus NoSIP plugin\n * \\details Check the \\ref nosip for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page nosip NoSIP plugin documentation\n *\n * This is quite a basic plugin, as it only takes care of acting as an\n * RTP bridge. It is named \"NoSIP\" since, as the name suggests, signalling\n * takes no place here, and is entirely up to the application. The typical\n * usage of this application is something like this:\n *\n * 1. a WebRTC application handles signalling on its own (e.g., SIP), but\n * needs to interact with a peer that doesn't support WebRTC (DTLS/ICE);\n * 2. it creates a handle with the NoSIP plugin, creates a JSEP SDP offer,\n * and passes it to the plugin;\n * 3. the plugin creates a barebone SDP that can be used to communicate\n * with the legacy peer, binds to the ports for RTP/RTCP, and sends this\n * plain SDP back to the application;\n * 4. the application uses this barebone SDP in its signalling, and expects\n * an answer from the peer;\n * 5. the SDP answer from the peer will be barebone as well, and so unfit\n * for WebRTC usage; as such, the application passes it to the plugin as\n * the answer to match the offer created before;\n * 6. the plugin matches the answer to the offer, and starts exchanging\n * RTP/RTCP with the legacy peer: media coming from the peer is relayed\n * via WebRTC to the application, and WebRTC stuff coming from the application\n * is relayed via plain RTP/RTCP to the legacy peer.\n *\n * The same behaviour can be followed if the application is the callee\n * instead, with the only difference being that the barebone offer will\n * come from the peer in this case, and the application will ask the\n * NoSIP plugin for a barebone answer instead.\n *\n * As you can see, the behaviour is pretty much the same as the SIP plugin,\n * with the key difference being that in this case there's no SIP stack in\n * the plugin itself. All signalling is left to the application, and Janus\n * (via the NoSIP plugin) is only responsible for bridging the media. This\n * might be more appropriate than the SIP plugin in cases where developers\n * want to keep control on the signalling layer, while still involving a\n * server of sorts. Of course, SIP is just an example here: other signalling\n * protocols may be involved as well (e.g., IAX, XMPP, others). The NoSIP\n * plugin, though, will generate and expect plain SDP, so you'll need to\n * take care of any adaptation that may be needed to make this work with\n * the signalling protocol of your choice.\n *\n * \\section nosipapi NoSIP Plugin API\n *\n * The plugin mainly supports two requests, \\c generate and \\c process,\n * which are both asynchronous. The \\c generate request take a JSEP offer\n * or answer, and generates a barebone SDP the \"legacy\" application can\n * use; the \\c process request, on the other hand, processes a remote\n * barebone SDP, and matches it to the plugin may have generated before,\n * in order to then return a JSEP offer or answer that can be used to\n * setup a PeerConnection.\n *\n * The \\c generate request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"generate\",\n\t\"info\" : \"<opaque string that the user can provide for context; optional>\",\n\t\"srtp\" : \"<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>\",\n\t\"srtp_profile\" : \"<SRTP profile to negotiate, in case SRTP is offered; optional>\"\n}\n\\endverbatim\n *\n * As anticipated, this requires a JSEP offer or answer passed via Janus\n * API as part of a WebRTC PeerConnection negotiation. If the conversion\n * of the WebRTC JSEP SDP to barebone SDP is successful, a \\c generated\n * event is sent back to the user:\n *\n\\verbatim\n{\n\t\"nosip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"generated\",\n\t\t\"type\" : \"<offer|answer, depending on the nature of the provided JSEP>\",\n\t\t\"sdp\" : \"<barebone SDP content>\",\n\t\t\"unique_id\" : \"<unique UUID of this session in this plugin, needed for some requests>\n\t}\n}\n\\endverbatim\n *\n * The \\c process request, instead, must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"process\",\n\t\"type\" : \"<offer|answer, depending on the nature of the provided SDP>\",\n\t\"sdp\" : \"<barebone SDP to convert>\"\n\t\"info\" : \"<opaque string that the user can provide for context; optional>\",\n\t\"srtp\" : \"<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>\",\n\t\"srtp_profile\" : \"<SRTP profile to negotiate, in case SRTP is offered; optional>\"\n}\n\\endverbatim\n *\n * As anticipated, this requires a \"legacy\" SDP offer or answer passed via\n * NoSIP plugin messaging, which is why the caller must specify if it's an\n * offer or answer. If the request is successful, a \\c processed event is\n * sent back to the user, along to the JSEP offer or answer that Janus\n * generated out of the barebone SDP:\n *\n\\verbatim\n{\n\t\"nosip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"processed\",\n\t\t\"srtp\" : \"<whether the barebone SDP mandates (sdes_mandatory) or offers (sdes_optional) SRTP support; optional>\",\n\t\t\"unique_id\" : \"<unique UUID of this session in this plugin, needed for some requests>\n\t}\n}\n\\endverbatim\n *\n * To close a session you can use the \\c hangup request, which needs no\n * additional arguments, as the whole context can be extracted from the\n * current state of the session in the plugin:\n *\n\\verbatim\n{\n\t\"request\" : \"hangup\"\n}\n\\endverbatim\n *\n * An \\c hangingup event will be sent back, as this is an asynchronous request.\n *\n * As to other features, just as in the SIP plugin, the multimedia session\n * can be recorded. Considering the NoSIP plugin also assumes two peers\n * are in a call with each other (although it makes no assumptions on\n * the signalling that ties them together), it works exactly the same\n * way as the SIP plugin dooes when it comes to recording.\n * Specifically, you make use of the \\c recording request to either start\n * or stop a recording, using the following syntax:\n *\n\\verbatim\n{\n\t\"request\" : \"recording\",\n\t\"action\" : \"<start|stop, depending on whether you want to start or stop recording something>\"\n\t\"audio\" : <true|false; whether or not our audio should be recorded>,\n\t\"video\" : <true|false; whether or not our video should be recorded>,\n\t\"peer_audio\" : <true|false; whether or not our peer's audio should be recorded>,\n\t\"peer_video\" : <true|false; whether or not our peer's video should be recorded>,\n\t\"filename\" : \"<base path/filename to use for all the recordings>\"\n}\n\\endverbatim\n *\n * As you can see, this means that the two sides of conversation are recorded\n * separately, and so are the audio and video streams if available. You can\n * choose which ones to record, in case you're interested in just a subset.\n * The \\c filename part is just a prefix, and dictates the actual filenames\n * that will be used for the up-to-four recordings that may need to be enabled.\n *\n * A \\c recordingupdated event is sent back in case the request is successful.\n *\n * To programmatically send a video keyframe request to either the WebRTC user\n * or the SIP peer (or both), the \\c keyframe request can be used. This\n * request is particularly useful when the SIP peer doesn't support RTCP PLI,\n * and so may use other mechanisms (e.g., via signalling) to ask for a keyframe\n * to get video working. By using this request, the WebRTC user can ask Janus\n * to originate a PLI programmatically. The direction of the keyframe request\n * can be provided by using the \\c user and \\c peer properties: if \\c user\n * is \\c TRUE a keyframe request will be sent by Janus to the WebRTC user;\n * if \\c peer is \\c TRUE a keyframe request will be sent by Janus to the\n * SIP peer. In both cases an RTCP PLI message will be sent. The syntax of\n * the message is the following:\n *\n\\verbatim\n{\n\t\"request\" : \"keyframe\",\n\t\"user\" : <true|false; whether or not to send a keyframe request to the WebRTC user>,\n\t\"peer\" : <true|false; whether or not to send a keyframe request to the SIP peer>\n}\n\\endverbatim\n *\n * A \\c keyframesent event is sent back in case the request is successful.\n *\n * \\subsection nosiprtpfwd RTP forwarders in the NoSIP plugin\n *\n * RTP forwarders are a quite useful functionality that a few plugins\n * in Janus can take advantage of. As the name suggests, their main\n * purpose is indeed forwarding/relaying RTP traffic to an external\n * backend, for further processing or management of media packets outside\n * of the context of the plugin that's responsible for it.\n *\n * Within the context of the NoSIP plugin, RTP forwarders allow you to\n * send the RTP traffic belonging to a specific call to an external\n * backend, e.g., to a component implementing transcriptions, a remote\n * recorder, a lawful interception application or whatever else may have\n * a need for the content of a call in real-time. The RTP forwarder\n * functionality is conceived in a way that allows you to separately\n * forward individual RTP streams: this allows you to only forward, e.g.,\n * the audio packets of the local Janus user, only forward the video\n * packets of the peer, forward everything, or any combination in\n * between. For every media stream you're interested in forwarding, a\n * separate RTP forwarder will be needed, which is what enables this\n * flexibility. Multiple forwarders can be created for the same stream,\n * in case the same RTP traffic should be sent to multiple backends at\n * the same time.\n *\n * Notice that no signalling protocol is used for RTP forwarders. RTP traffic\n * is relayed out of context, using a specific API that is part of the\n * NoSIP plugin itself. RTP forwarders will need an ongoing call to\n * operate, though, as they'll be associated to existing media streams\n * in an existing call; more specifically, forwarding requests must\n * be performed on the handle that's handling the call that should be\n * the target of its operations, which is why the related requests don't\n * need any identifier (the scope is automatically obtained from the\n * context of the handle). Forwarders that were created while a call\n * was active will automatically be destroyed when the call is hung up\n *\n * To setup new RTP forwarders, you can use the \\c rtp_forward request,\n * which must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"rtp_forward\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"type\": \"<what we want forward: must be one of audio, video, peer_audio, peer_video>\",\n\t\t\t\"host\" : \"<host address to forward the packets to>\",\n\t\t\t\"host_family\" : \"<optional; ipv4 by default>\",\n\t\t\t\"port\" : <port to forward the packets to>,\n\t\t\t\"ssrc\" : <SSRC to use to use when forwarding; optional>,\n\t\t\t\"pt\" : <payload type to use when forwarding; optional>,\n\t\t\t\"srtp_suite\" : <length of authentication tag (32 or 80); optional>,\n\t\t\t\"srtp_crypto\" : \"<key to use as crypto (base64 encoded key as in SDES); optional>\"\n\t\t},\n\t\t{\n\t\t\t.. other streams, if needed..\n\t\t}\n\t]\n}\n\\endverbatim\n *\n * The syntax of the request makes it easy to set up multiple RTP forwarders\n * for a call at the same time, by adding multiple objects as part of the\n * \\c streams array.\n *\n * A successful request will result in an \\c rtp_forward event, containing\n * the relevant info associated to the new forwarder(s):\n *\n\\verbatim\n{\n\t\"nosip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"rtp_forward\",\n\t\t\"forwarders\" : [\n\t\t\t{\n\t\t\t\t\"stream_id\" : <unique numeric ID assigned to this forwarder, if any>,\n\t\t\t\t\"type\" : \"<type of this forwarder, as configured in the request>\",\n\t\t\t\t\"host\" : \"<host this forwarder is streaming to, same as request if not resolved>\",\n\t\t\t\t\"port\" : <port this forwarder is streaming to, same as request if configured>,\n\t\t\t\t\"media\" : \"<audio or video>\",\n\t\t\t\t\"ssrc\" : <SSRC this forwarder is using, if any>,\n\t\t\t\t\"pt\" : <payload type this forwarder is using, if any>,\n\t\t\t\t\"srtp\" : <true|false, whether the RTP stream is encrypted>\n\t\t\t},\n\t\t\t// Other forwarders, if configured\n\t\t]\n\t}\n}\n\\endverbatim\n *\n * The \\c stream_id property returned for each forwarder is what will\n * need to be used for managing it, i.e., to destroy it once done.\n *\n * To stop a previously created RTP forwarder and stop it, you can use\n * the \\c stop_rtp_forward request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"stop_rtp_forward\",\n\t\"stream_id\" : <unique numeric ID of the RTP forwarder>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c stop_rtp_forward event:\n *\n\\verbatim\n{\n\t\"nosip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"stop_rtp_forward\",\n\t\t\"stream_id\" : <unique numeric ID, same as request>\n\t}\n}\n\\endverbatim\n *\n * To get a list of all the forwarders for an active call, instead, you\n * can make use of the \\c listforwarders request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listforwarders\"\n}\n\\endverbatim\n *\n * A successful request will produce a list of RTP forwarders in a\n * \\c forwarders event:\n *\n\\verbatim\n{\n\t\"nosip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"forwarders\",\n\t\t\"forwarders\" : [\t\t// Array of RTP forwarders\n\t\t\t{\t// RTP forwarder #1\n\t\t\t\t\"stream_id\" : <unique numeric ID assigned to this forwarder, if any>,\n\t\t\t\t\"type\" : \"<type of this forwarder, as configured in the request>\",\n\t\t\t\t\"host\" : \"<host this forwarder is streaming to, same as request if not resolved>\",\n\t\t\t\t\"port\" : <port this forwarder is streaming to, same as request if configured>,\n\t\t\t\t\"media\" : \"<audio or video>\",\n\t\t\t\t\"ssrc\" : <SSRC this forwarder is using, if any>,\n\t\t\t\t\"pt\" : <payload type this forwarder is using, if any>,\n\t\t\t\t\"srtp\" : <true|false, whether the RTP stream is encrypted>\n\t\t\t},\n\t\t\t// Other forwarders for this publisher\n\t\t]\n\t}\n}\n\\endverbatim\n *\n */\n\n#include \"plugin.h\"\n\n#include <arpa/inet.h>\n#include <net/if.h>\n#include <sys/socket.h>\n#include <netdb.h>\n#include <poll.h>\n\n#include <jansson.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../record.h\"\n#include \"../rtp.h\"\n#include \"../rtpsrtp.h\"\n#include \"../rtcp.h\"\n#include \"../rtpfwd.h\"\n#include \"../ip-utils.h\"\n#include \"../sdp-utils.h\"\n#include \"../utils.h\"\n\n\n/* Plugin information */\n#define JANUS_NOSIP_VERSION\t\t\t2\n#define JANUS_NOSIP_VERSION_STRING\t\"0.0.2\"\n#define JANUS_NOSIP_DESCRIPTION\t\t\"This is a simple RTP bridging plugin that leaves signalling details (e.g., SIP) up to the application.\"\n#define JANUS_NOSIP_NAME\t\t\t\"JANUS NoSIP plugin\"\n#define JANUS_NOSIP_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_NOSIP_PACKAGE\t\t\t\"janus.plugin.nosip\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_nosip_init(janus_callbacks *callback, const char *config_path);\nvoid janus_nosip_destroy(void);\nint janus_nosip_get_api_compatibility(void);\nint janus_nosip_get_version(void);\nconst char *janus_nosip_get_version_string(void);\nconst char *janus_nosip_get_description(void);\nconst char *janus_nosip_get_name(void);\nconst char *janus_nosip_get_author(void);\nconst char *janus_nosip_get_package(void);\nvoid janus_nosip_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_nosip_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\nvoid janus_nosip_setup_media(janus_plugin_session *handle);\nvoid janus_nosip_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_nosip_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_nosip_hangup_media(janus_plugin_session *handle);\nvoid janus_nosip_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_nosip_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_nosip_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_nosip_init,\n\t\t.destroy = janus_nosip_destroy,\n\n\t\t.get_api_compatibility = janus_nosip_get_api_compatibility,\n\t\t.get_version = janus_nosip_get_version,\n\t\t.get_version_string = janus_nosip_get_version_string,\n\t\t.get_description = janus_nosip_get_description,\n\t\t.get_name = janus_nosip_get_name,\n\t\t.get_author = janus_nosip_get_author,\n\t\t.get_package = janus_nosip_get_package,\n\n\t\t.create_session = janus_nosip_create_session,\n\t\t.handle_message = janus_nosip_handle_message,\n\t\t.setup_media = janus_nosip_setup_media,\n\t\t.incoming_rtp = janus_nosip_incoming_rtp,\n\t\t.incoming_rtcp = janus_nosip_incoming_rtcp,\n\t\t.hangup_media = janus_nosip_hangup_media,\n\t\t.destroy_session = janus_nosip_destroy_session,\n\t\t.query_session = janus_nosip_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_NOSIP_NAME);\n\treturn &janus_nosip_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter generate_parameters[] = {\n\t{\"info\", JSON_STRING, 0},\n\t{\"srtp\", JSON_STRING, 0},\n\t{\"srtp_profile\", JSON_STRING, 0},\n\t{\"update\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter process_parameters[] = {\n\t{\"type\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"sdp\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"info\", JSON_STRING, 0},\n\t{\"srtp\", JSON_STRING, 0},\n\t{\"srtp_profile\", JSON_STRING, 0},\n\t{\"update\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter recording_parameters[] = {\n\t{\"action\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"peer_audio\", JANUS_JSON_BOOL, 0},\n\t{\"peer_video\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter keyframe_parameters[] = {\n\t{\"user\", JANUS_JSON_BOOL, 0},\n\t{\"peer\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter rtp_forward_parameters[] = {\n\t{\"streams\", JANUS_JSON_ARRAY, JANUS_JSON_PARAM_REQUIRED},\n};\nstatic struct janus_json_parameter rtp_forward_stream_parameters[] = {\n\t{\"type\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host_family\", JSON_STRING, 0},\n\t{\"port\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"ssrc\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"pt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter stop_rtp_forward_parameters[] = {\n\t{\"stream_id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic gboolean ipv6_disabled = FALSE;\nstatic janus_callbacks *gateway = NULL;\n\nstatic char *local_ip = NULL, *sdp_ip = NULL;\nstatic janus_network_address janus_network_local_ip = { 0 };\n#define DEFAULT_RTP_RANGE_MIN 10000\n#define DEFAULT_RTP_RANGE_MAX 60000\nstatic uint16_t rtp_range_min = DEFAULT_RTP_RANGE_MIN;\nstatic uint16_t rtp_range_max = DEFAULT_RTP_RANGE_MAX;\nstatic uint16_t rtp_range_slider = DEFAULT_RTP_RANGE_MIN;\nstatic int dscp_audio_rtp = 0;\nstatic int dscp_video_rtp = 0;\n\nstatic GThread *handler_thread;\nstatic void *janus_nosip_handler(void *data);\nstatic void janus_nosip_hangup_media_internal(janus_plugin_session *handle);\n\ntypedef struct janus_nosip_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_nosip_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_nosip_message exit_message;\n\n\ntypedef struct janus_nosip_media {\n\tchar *remote_audio_ip;\n\tchar *remote_video_ip;\n\tgboolean ready;\n\tgboolean require_srtp, has_srtp_local, has_srtp_remote;\n\tjanus_srtp_profile srtp_profile;\n\tgboolean has_audio;\n\tint audio_rtp_fd, audio_rtcp_fd;\n\tint local_audio_rtp_port, remote_audio_rtp_port;\n\tint local_audio_rtcp_port, remote_audio_rtcp_port;\n\tguint32 audio_ssrc, audio_ssrc_peer;\n\tint audio_pt, opusred_pt;\n\tconst char *audio_pt_name;\n\tgint32 audio_srtp_tag;\n\tsrtp_t audio_srtp_in, audio_srtp_out;\n\tsrtp_policy_t audio_remote_policy, audio_local_policy;\n\tchar *audio_srtp_local_profile, *audio_srtp_local_crypto;\n\tgboolean audio_send;\n\tgboolean has_video;\n\tint video_rtp_fd, video_rtcp_fd;\n\tint local_video_rtp_port, remote_video_rtp_port;\n\tint local_video_rtcp_port, remote_video_rtcp_port;\n\tguint32 video_ssrc, video_ssrc_peer;\n\tguint32 simulcast_ssrc;\n\tint video_pt;\n\tconst char *video_pt_name;\n\tgint32 video_srtp_tag;\n\tsrtp_t video_srtp_in, video_srtp_out;\n\tsrtp_policy_t video_remote_policy, video_local_policy;\n\tchar *video_srtp_local_profile, *video_srtp_local_crypto;\n\tgboolean video_send;\n\tgboolean video_pli_supported;\n\tjanus_rtp_switching_context acontext, vcontext;\n\tint pipefd[2];\n\tgboolean updated;\n\tint video_orientation_extension_id;\n\tint audio_level_extension_id;\n} janus_nosip_media;\n\ntypedef struct janus_nosip_session {\n\tjanus_plugin_session *handle;\n\tchar *unique_id;\n\tgint64 sdp_version;\n\tjanus_nosip_media media;\t/* Media gatewaying stuff (same stuff as the SIP plugin) */\n\tjanus_sdp *sdp;\t\t\t\t/* The SDP this user sent */\n\tjanus_recorder *arc;\t\t/* The Janus recorder instance for this user's audio, if enabled */\n\tjanus_recorder *arc_peer;\t/* The Janus recorder instance for the peer's audio, if enabled */\n\tjanus_recorder *vrc;\t\t/* The Janus recorder instance for this user's video, if enabled */\n\tjanus_recorder *vrc_peer;\t/* The Janus recorder instance for the peer's video, if enabled */\n\tjanus_mutex rec_mutex;\t\t/* Mutex to protect the recorders from race conditions */\n\tGHashTable *audio_forwarders, *video_forwarders,\n\t\t*peer_audio_forwarders, *peer_video_forwarders,\n\t\t*all_forwarders;\t/* RTP forwarders for this call (all streams), if any */\n\tjanus_mutex rtp_forwarders_mutex;\n\tint udp_sock; \t\t\t\t/* The socket on which to forward RTP packets */\n\tGThread *relayer_thread;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n\tjanus_mutex mutex;\n} janus_nosip_session;\nstatic GHashTable *sessions;\nstatic GHashTable *unique_ids;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_nosip_srtp_cleanup(janus_nosip_session *session);\nstatic void janus_nosip_media_reset(janus_nosip_session *session);\nstatic void janus_nosip_rtcp_pli_send(janus_nosip_session *session);\n\nstatic janus_rtp_forwarder *janus_nosip_rtp_forwarder_add_helper(janus_nosip_session *session, const char *type,\n\tconst gchar *host, int port, int pt, uint32_t ssrc, int srtp_suite, const char *srtp_crypto);\nstatic json_t *janus_nosip_rtp_forwarder_summary(janus_rtp_forwarder *f);\n\nstatic void janus_nosip_session_destroy(janus_nosip_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_nosip_session_free(const janus_refcount *session_ref) {\n\tjanus_nosip_session *session = janus_refcount_containerof(session_ref, janus_nosip_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tjanus_sdp_destroy(session->sdp);\n\tsession->sdp = NULL;\n\tg_free(session->unique_id);\n\tsession->unique_id = NULL;\n\tg_free(session->media.remote_audio_ip);\n\tsession->media.remote_audio_ip = NULL;\n\tg_free(session->media.remote_video_ip);\n\tsession->media.remote_video_ip = NULL;\n\tjanus_nosip_srtp_cleanup(session);\n\tsession->handle = NULL;\n\tg_hash_table_destroy(session->audio_forwarders);\n\tsession->audio_forwarders = NULL;\n\tg_hash_table_destroy(session->video_forwarders);\n\tsession->video_forwarders = NULL;\n\tg_hash_table_destroy(session->peer_audio_forwarders);\n\tsession->peer_audio_forwarders = NULL;\n\tg_hash_table_destroy(session->peer_video_forwarders);\n\tsession->peer_video_forwarders = NULL;\n\tg_hash_table_destroy(session->all_forwarders);\n\tsession->all_forwarders = NULL;\n\tjanus_mutex_destroy(&session->rtp_forwarders_mutex);\n\tjanus_mutex_destroy(&session->mutex);\n\tjanus_mutex_destroy(&session->rec_mutex);\n\tg_free(session);\n\tsession = NULL;\n}\n\nstatic void janus_nosip_message_free(janus_nosip_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_nosip_session *session = (janus_nosip_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\n\n/* SRTP stuff (in case we need SDES) */\nstatic int janus_nosip_srtp_set_local(janus_nosip_session *session, gboolean video, char **profile, char **crypto) {\n\tif(session == NULL)\n\t\treturn -1;\n\t/* Which SRTP profile are we going to negotiate? */\n\tint key_length = 0, salt_length = 0, master_length = 0;\n\tif(session->media.srtp_profile == JANUS_SRTP_AES128_CM_SHA1_32) {\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AES_CM_128_HMAC_SHA1_32\");\n\t} else if(session->media.srtp_profile == JANUS_SRTP_AES128_CM_SHA1_80) {\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AES_CM_128_HMAC_SHA1_80\");\n#ifdef HAVE_SRTP_AESGCM\n\t} else if(session->media.srtp_profile == JANUS_SRTP_AEAD_AES_128_GCM) {\n\t\tkey_length = SRTP_AESGCM128_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM128_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM128_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AEAD_AES_128_GCM\");\n\t} else if(session->media.srtp_profile == JANUS_SRTP_AEAD_AES_256_GCM) {\n\t\tkey_length = SRTP_AESGCM256_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM256_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM256_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AEAD_AES_256_GCM\");\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Unsupported SRTP profile\\n\", session);\n\t\treturn -2;\n\t}\n\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] %s\\n\", session, *profile);\n\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] Key/Salt/Master: %d/%d/%d\\n\",\n\t\tsession, master_length, key_length, salt_length);\n\t/* Generate key/salt */\n\tuint8_t *key = g_malloc0(master_length);\n\tsrtp_crypto_get_random(key, master_length);\n\t/* Set SRTP policies */\n\tsrtp_policy_t *policy = video ? &session->media.video_local_policy : &session->media.audio_local_policy;\n\tswitch(session->media.srtp_profile) {\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_32:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_80:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n#ifdef HAVE_SRTP_AESGCM\n\t\tcase JANUS_SRTP_AEAD_AES_128_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AEAD_AES_256_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n#endif\n\t\tdefault:\n\t\t\t/* Will never happen? */\n\t\t\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] Unsupported SRTP profile\\n\", session);\n\t\t\tbreak;\n\t}\n\tpolicy->ssrc.type = ssrc_any_inbound;\n\tpolicy->key = key;\n\tpolicy->next = NULL;\n\t/* Create SRTP context */\n\tsrtp_err_status_t res = srtp_create(video ? &session->media.video_srtp_out : &session->media.audio_srtp_out, policy);\n\tif(res != srtp_err_status_ok) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Oops, error creating outbound SRTP session: %d (%s)\\n\", res, janus_srtp_error_str(res));\n\t\tg_free(*profile);\n\t\t*profile = NULL;\n\t\tg_free(key);\n\t\tpolicy->key = NULL;\n\t\treturn -2;\n\t}\n\t/* Base64 encode the salt */\n\t*crypto = g_base64_encode(key, master_length);\n\tif((video && session->media.video_srtp_out) || (!video && session->media.audio_srtp_out)) {\n\t\tJANUS_LOG(LOG_VERB, \"%s outbound SRTP session created\\n\", video ? \"Video\" : \"Audio\");\n\t}\n\treturn 0;\n}\nstatic int janus_nosip_srtp_set_remote(janus_nosip_session *session, gboolean video, const char *profile, const char *crypto) {\n\tif(session == NULL || profile == NULL || crypto == NULL)\n\t\treturn -1;\n\t/* Which SRTP profile is being negotiated? */\n\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] %s\\n\", session, profile);\n\tgsize key_length = 0, salt_length = 0, master_length = 0;\n\tif(!strcasecmp(profile, \"AES_CM_128_HMAC_SHA1_32\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AES128_CM_SHA1_32;\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n\t} else if(!strcasecmp(profile, \"AES_CM_128_HMAC_SHA1_80\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n#ifdef HAVE_SRTP_AESGCM\n\t} else if(!strcasecmp(profile, \"AEAD_AES_128_GCM\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AEAD_AES_128_GCM;\n\t\tkey_length = SRTP_AESGCM128_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM128_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM128_MASTER_LENGTH;\n\t} else if(!strcasecmp(profile, \"AEAD_AES_256_GCM\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AEAD_AES_256_GCM;\n\t\tkey_length = SRTP_AESGCM256_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM256_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM256_MASTER_LENGTH;\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] Unsupported SRTP profile %s\\n\", session, profile);\n\t\treturn -2;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[NoSIP-%p] Key/Salt/Master: %zu/%zu/%zu\\n\",\n\t\tsession, master_length, key_length, salt_length);\n\t/* Base64 decode the crypto string and set it as the remote SRTP context */\n\tgsize len = 0;\n\tguchar *decoded = g_base64_decode(crypto, &len);\n\tif(len < master_length) {\n\t\t/* FIXME Can this happen? */\n\t\tg_free(decoded);\n\t\treturn -3;\n\t}\n\t/* Set SRTP policies */\n\tsrtp_policy_t *policy = video ? &session->media.video_remote_policy : &session->media.audio_remote_policy;\n\tswitch(session->media.srtp_profile) {\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_32:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_80:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n#ifdef HAVE_SRTP_AESGCM\n\t\tcase JANUS_SRTP_AEAD_AES_128_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AEAD_AES_256_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n#endif\n\t\tdefault:\n\t\t\t/* Will never happen? */\n\t\t\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] Unsupported SRTP profile\\n\", session);\n\t\t\tbreak;\n\t}\n\tpolicy->ssrc.type = ssrc_any_inbound;\n\tpolicy->key = decoded;\n\tpolicy->next = NULL;\n\t/* Create SRTP context */\n\tsrtp_err_status_t res = srtp_create(video ? &session->media.video_srtp_in : &session->media.audio_srtp_in, policy);\n\tif(res != srtp_err_status_ok) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Oops, error creating inbound SRTP session: %d (%s)\\n\", res, janus_srtp_error_str(res));\n\t\tg_free(decoded);\n\t\tpolicy->key = NULL;\n\t\treturn -2;\n\t}\n\tif((video && session->media.video_srtp_in) || (!video && session->media.audio_srtp_in)) {\n\t\tJANUS_LOG(LOG_VERB, \"%s inbound SRTP session created\\n\", video ? \"Video\" : \"Audio\");\n\t}\n\treturn 0;\n}\nstatic void janus_nosip_srtp_cleanup(janus_nosip_session *session) {\n\tif(session == NULL)\n\t\treturn;\n\tsession->media.require_srtp = FALSE;\n\tsession->media.has_srtp_local = FALSE;\n\tsession->media.has_srtp_remote = FALSE;\n\tsession->media.srtp_profile = 0;\n\t/* Audio */\n\tsession->media.audio_srtp_tag = 0;\n\tif(session->media.audio_srtp_out)\n\t\tsrtp_dealloc(session->media.audio_srtp_out);\n\tsession->media.audio_srtp_out = NULL;\n\tg_free(session->media.audio_local_policy.key);\n\tsession->media.audio_local_policy.key = NULL;\n\tif(session->media.audio_srtp_in)\n\t\tsrtp_dealloc(session->media.audio_srtp_in);\n\tsession->media.audio_srtp_in = NULL;\n\tg_free(session->media.audio_remote_policy.key);\n\tsession->media.audio_remote_policy.key = NULL;\n\tif(session->media.audio_srtp_local_profile) {\n\t\tg_free(session->media.audio_srtp_local_profile);\n\t\tsession->media.audio_srtp_local_profile = NULL;\n\t}\n\tif(session->media.audio_srtp_local_crypto) {\n\t\tg_free(session->media.audio_srtp_local_crypto);\n\t\tsession->media.audio_srtp_local_crypto = NULL;\n\t}\n\t/* Video */\n\tsession->media.video_srtp_tag = 0;\n\tif(session->media.video_srtp_out)\n\t\tsrtp_dealloc(session->media.video_srtp_out);\n\tsession->media.video_srtp_out = NULL;\n\tg_free(session->media.video_local_policy.key);\n\tsession->media.video_local_policy.key = NULL;\n\tif(session->media.video_srtp_in)\n\t\tsrtp_dealloc(session->media.video_srtp_in);\n\tsession->media.video_srtp_in = NULL;\n\tg_free(session->media.video_remote_policy.key);\n\tsession->media.video_remote_policy.key = NULL;\n\tif(session->media.video_srtp_local_profile) {\n\t\tg_free(session->media.video_srtp_local_profile);\n\t\tsession->media.video_srtp_local_profile = NULL;\n\t}\n\tif(session->media.video_srtp_local_crypto) {\n\t\tg_free(session->media.video_srtp_local_crypto);\n\t\tsession->media.video_srtp_local_crypto = NULL;\n\t}\n}\n\nvoid janus_nosip_media_reset(janus_nosip_session *session) {\n\tif(session == NULL)\n\t\treturn;\n\tg_free(session->media.remote_audio_ip);\n\tsession->media.remote_audio_ip = NULL;\n\tg_free(session->media.remote_video_ip);\n\tsession->media.remote_video_ip = NULL;\n\tsession->media.updated = FALSE;\n\tsession->media.ready = FALSE;\n\tsession->media.require_srtp = FALSE;\n\tsession->media.has_audio = FALSE;\n\tsession->media.audio_pt = -1;\n\tsession->media.opusred_pt = -1;\n\tsession->media.audio_pt_name = NULL;\t/* Immutable string, no need to free*/\n\tsession->media.audio_send = TRUE;\n\tsession->media.has_video = FALSE;\n\tsession->media.video_pt = -1;\n\tsession->media.video_pt_name = NULL;\t/* Immutable string, no need to free*/\n\tsession->media.video_send = TRUE;\n\tsession->media.video_pli_supported = FALSE;\n\tsession->media.video_orientation_extension_id = -1;\n\tsession->media.audio_level_extension_id = -1;\n\tjanus_rtp_switching_context_reset(&session->media.acontext);\n\tjanus_rtp_switching_context_reset(&session->media.vcontext);\n}\n\n\n/* SDP parsing and manipulation */\nvoid janus_nosip_sdp_process(janus_nosip_session *session, janus_sdp *sdp, gboolean answer, gboolean update, gboolean *changed);\nchar *janus_nosip_sdp_manipulate(janus_nosip_session *session, janus_sdp *sdp, gboolean answer);\n/* Media */\nstatic int janus_nosip_allocate_local_ports(janus_nosip_session *session, gboolean update);\nstatic void *janus_nosip_relay_thread(void *data);\nstatic void janus_nosip_media_cleanup(janus_nosip_session *session);\n\n\n/* Error codes */\n#define JANUS_NOSIP_ERROR_UNKNOWN_ERROR\t\t\t499\n#define JANUS_NOSIP_ERROR_NO_MESSAGE\t\t\t440\n#define JANUS_NOSIP_ERROR_INVALID_JSON\t\t\t441\n#define JANUS_NOSIP_ERROR_INVALID_REQUEST\t\t442\n#define JANUS_NOSIP_ERROR_MISSING_ELEMENT\t\t443\n#define JANUS_NOSIP_ERROR_INVALID_ELEMENT\t\t444\n#define JANUS_NOSIP_ERROR_WRONG_STATE\t\t\t445\n#define JANUS_NOSIP_ERROR_MISSING_SDP\t\t\t446\n#define JANUS_NOSIP_ERROR_INVALID_SDP\t\t\t447\n#define JANUS_NOSIP_ERROR_IO_ERROR\t\t\t\t448\n#define JANUS_NOSIP_ERROR_RECORDING_ERROR\t\t449\n#define JANUS_NOSIP_ERROR_TOO_STRICT\t\t\t450\n#define JANUS_NOSIP_ERROR_NO_SUCH_STREAM\t\t451\n\n\n/* Plugin implementation */\nint janus_nosip_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_NOSIP_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_NOSIP_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_NOSIP_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\tjanus_config_print(config);\n\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"local_ip\");\n\t\tif(item && item->value && strlen(item->value) > 0) {\n\t\t\t/* Verify that the address is valid */\n\t\t\tstruct ifaddrs *ifas = NULL;\n\t\t\tjanus_network_address iface;\n\t\t\tjanus_network_address_string_buffer ibuf;\n\t\t\tif(getifaddrs(&ifas) == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unable to acquire list of network devices/interfaces; some configurations may not work as expected... %d (%s)\\n\",\n\t\t\t\t\terrno, g_strerror(errno));\n\t\t\t} else {\n\t\t\t\tif(janus_network_lookup_interface(ifas, item->value, &iface) != 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting local IP address to %s, falling back to detecting IP address...\\n\", item->value);\n\t\t\t\t} else {\n\t\t\t\t\tif(janus_network_address_to_string_buffer(&iface, &ibuf) != 0 || janus_network_address_string_buffer_is_null(&ibuf)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error getting local IP address from %s, falling back to detecting IP address...\\n\", item->value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlocal_ip = g_strdup(janus_network_address_string_from_buffer(&ibuf));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfreeifaddrs(ifas);\n\t\t\t}\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"sdp_ip\");\n\t\tif(item && item->value && strlen(item->value) > 0) {\n\t\t\tsdp_ip = g_strdup(item->value);\n\t\t\tJANUS_LOG(LOG_VERB, \"IP to advertise in SDP: %s\\n\", sdp_ip);\n\t\t}\n\n\t\t/* Make sure both IPs are valid, if provided */\n\t\tjanus_network_address_nullify(&janus_network_local_ip);\n\t\tif(local_ip) {\n\t\t\tif(janus_network_string_to_address(janus_network_query_options_any_ip, local_ip, &janus_network_local_ip) != 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid local media IP address [%s]...\\n\", local_ip);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif((janus_network_local_ip.family == AF_INET && janus_network_local_ip.ipv4.s_addr == INADDR_ANY) ||\n\t\t\t\t\t(janus_network_local_ip.family == AF_INET6 && IN6_IS_ADDR_UNSPECIFIED(&janus_network_local_ip.ipv6))) {\n\t\t\t\tjanus_network_address_nullify(&janus_network_local_ip);\n\t\t\t}\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Binding media address set to [%s]...\\n\", janus_network_address_is_null(&janus_network_local_ip) ? \"any\" : local_ip);\n\t\tif(!sdp_ip) {\n\t\t\tchar *ip = janus_network_address_is_null(&janus_network_local_ip) ? local_ip : NULL;\n\t\t\tif(ip) {\n\t\t\t\tsdp_ip = g_strdup(ip);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"IP to advertise in SDP: %s\\n\", sdp_ip);\n\t\t\t}\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"rtp_port_range\");\n\t\tif(item && item->value) {\n\t\t\t/* Split in min and max port */\n\t\t\tchar *maxport = strrchr(item->value, '-');\n\t\t\tif(maxport != NULL) {\n\t\t\t\t*maxport = '\\0';\n\t\t\t\tmaxport++;\n\t\t\t\tif(janus_string_to_uint16(item->value, &rtp_range_min) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP min port value: %s (assuming 0)\\n\", item->value);\n\t\t\t\tif(janus_string_to_uint16(maxport, &rtp_range_max) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP max port value: %s (assuming 0)\\n\", maxport);\n\t\t\t\tmaxport--;\n\t\t\t\t*maxport = '-';\n\t\t\t}\n\t\t\tif(rtp_range_min > rtp_range_max) {\n\t\t\t\tuint16_t temp_port = rtp_range_min;\n\t\t\t\trtp_range_min = rtp_range_max;\n\t\t\t\trtp_range_max = temp_port;\n\t\t\t}\n\t\t\tif(rtp_range_min % 2)\n\t\t\t\trtp_range_min++;\t/* Pick an even port for RTP */\n\t\t\tif(rtp_range_min > rtp_range_max) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Incorrect port range (%u -- %u), switching min and max\\n\", rtp_range_min, rtp_range_max);\n\t\t\t\tuint16_t range_temp = rtp_range_max;\n\t\t\t\trtp_range_max = rtp_range_min;\n\t\t\t\trtp_range_min = range_temp;\n\t\t\t}\n\t\t\tif(rtp_range_max == 0)\n\t\t\t\trtp_range_max = 65535;\n\t\t\trtp_range_slider = rtp_range_min;\n\t\t\tJANUS_LOG(LOG_VERB, \"NoSIP RTP/RTCP port range: %u -- %u\\n\", rtp_range_min, rtp_range_max);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(item != NULL && item->value != NULL)\n\t\t\tnotify_events = janus_is_true(item->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_NOSIP_NAME);\n\t\t}\n\n\t\t/* Is there any DSCP TOS to apply? */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"dscp_audio_rtp\");\n\t\tif(item && item->value) {\n\t\t\tint val = atoi(item->value);\n\t\t\tif(val < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring dscp_audio_rtp value as it's not a positive integer\\n\");\n\t\t\t} else {\n\t\t\t\tdscp_audio_rtp = val;\n\t\t\t}\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"dscp_video_rtp\");\n\t\tif(item && item->value) {\n\t\t\tint val = atoi(item->value);\n\t\t\tif(val < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring dscp_video_rtp value as it's not a positive integer\\n\");\n\t\t\t} else {\n\t\t\t\tdscp_video_rtp = val;\n\t\t\t}\n\t\t}\n\n\t\tjanus_config_destroy(config);\n\t}\n\tconfig = NULL;\n\n\tif(local_ip == NULL) {\n\t\tlocal_ip = janus_network_detect_local_ip_as_string(janus_network_query_options_any_ip);\n\t\tif(local_ip == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find any address! using 127.0.0.1 as the local IP... (which is NOT going to work out of your machine)\\n\");\n\t\t\tlocal_ip = g_strdup(\"127.0.0.1\");\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Local IP set to %s\\n\", local_ip);\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_nosip_session_destroy);\n\tunique_ids = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_nosip_message_free);\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\tif(janus_network_address_is_null(&janus_network_local_ip) ||\n\t\t\tjanus_network_local_ip.family == AF_INET6) {\n\t\t/* Finally, let's check if IPv6 is disabled, as we may need to know for RTP/RTCP sockets */\n\t\tint fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\t\tif(fd <= 0) {\n\t\t\tipv6_disabled = TRUE;\n\t\t} else {\n\t\t\tint v6only = 0;\n\t\t\tif(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)\n\t\t\t\tipv6_disabled = TRUE;\n\t\t}\n\t\tif(fd > 0)\n\t\t\tclose(fd);\n\t\tif(ipv6_disabled) {\n\t\t\tif(!janus_network_address_is_null(&janus_network_local_ip)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"IPv6 disabled and local media address is IPv6...\\n\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_WARN, \"IPv6 disabled, will only use IPv4 for RTP/RTCP sockets (SIP)\\n\");\n\t\t}\n\t} else if(janus_network_local_ip.family == AF_INET) {\n\t\t/* Disable if we have a specified IPv4 address for RTP/RTCP sockets */\n\t\tipv6_disabled = TRUE;\n\t}\n\n\tg_atomic_int_set(&initialized, 1);\n\n\tGError *error = NULL;\n\t/* Launch the thread that will handle incoming messages */\n\thandler_thread = g_thread_try_new(\"nosip handler\", janus_nosip_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the NoSIP handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_NOSIP_NAME);\n\treturn 0;\n}\n\nvoid janus_nosip_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tg_hash_table_destroy(unique_ids);\n\tunique_ids = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\n\tg_free(local_ip);\n\tg_free(sdp_ip);\n\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_NOSIP_NAME);\n}\n\nint janus_nosip_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_nosip_get_version(void) {\n\treturn JANUS_NOSIP_VERSION;\n}\n\nconst char *janus_nosip_get_version_string(void) {\n\treturn JANUS_NOSIP_VERSION_STRING;\n}\n\nconst char *janus_nosip_get_description(void) {\n\treturn JANUS_NOSIP_DESCRIPTION;\n}\n\nconst char *janus_nosip_get_name(void) {\n\treturn JANUS_NOSIP_NAME;\n}\n\nconst char *janus_nosip_get_author(void) {\n\treturn JANUS_NOSIP_AUTHOR;\n}\n\nconst char *janus_nosip_get_package(void) {\n\treturn JANUS_NOSIP_PACKAGE;\n}\n\nstatic janus_nosip_session *janus_nosip_lookup_session(janus_plugin_session *handle) {\n\tjanus_nosip_session *session = NULL;\n\tif (g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_nosip_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_nosip_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_nosip_session *session = g_malloc0(sizeof(janus_nosip_session));\n\tsession->handle = handle;\n\tsession->sdp = NULL;\n\tsession->media.remote_audio_ip = NULL;\n\tsession->media.remote_video_ip = NULL;\n\tsession->media.ready = FALSE;\n\tsession->media.require_srtp = FALSE;\n\tsession->media.has_srtp_local = FALSE;\n\tsession->media.has_srtp_remote = FALSE;\n\tsession->media.srtp_profile = 0;\n\tsession->media.audio_srtp_local_profile = NULL;\n\tsession->media.audio_srtp_local_crypto = NULL;\n\tsession->media.video_srtp_local_profile = NULL;\n\tsession->media.video_srtp_local_crypto = NULL;\n\tsession->media.has_audio = FALSE;\n\tsession->media.audio_rtp_fd = -1;\n\tsession->media.audio_rtcp_fd = -1;\n\tsession->media.local_audio_rtp_port = 0;\n\tsession->media.remote_audio_rtp_port = 0;\n\tsession->media.local_audio_rtcp_port = 0;\n\tsession->media.remote_audio_rtcp_port = 0;\n\tsession->media.audio_ssrc = 0;\n\tsession->media.audio_ssrc_peer = 0;\n\tsession->media.audio_pt = -1;\n\tsession->media.opusred_pt = -1;\n\tsession->media.audio_pt_name = NULL;\n\tsession->media.audio_send = TRUE;\n\tsession->media.has_video = FALSE;\n\tsession->media.video_rtp_fd = -1;\n\tsession->media.video_rtcp_fd = -1;\n\tsession->media.local_video_rtp_port = 0;\n\tsession->media.remote_video_rtp_port = 0;\n\tsession->media.local_video_rtcp_port = 0;\n\tsession->media.remote_video_rtcp_port = 0;\n\tsession->media.video_ssrc = 0;\n\tsession->media.video_ssrc_peer = 0;\n\tsession->media.simulcast_ssrc = 0;\n\tsession->media.video_pt = -1;\n\tsession->media.video_pt_name = NULL;\n\tsession->media.video_send = TRUE;\n\tsession->media.video_pli_supported = FALSE;\n\tsession->media.video_orientation_extension_id = -1;\n\tsession->media.audio_level_extension_id = -1;\n\t/* Initialize the RTP context */\n\tjanus_rtp_switching_context_reset(&session->media.acontext);\n\tjanus_rtp_switching_context_reset(&session->media.vcontext);\n\tsession->media.pipefd[0] = -1;\n\tsession->media.pipefd[1] = -1;\n\tsession->media.updated = FALSE;\n\tsession->media.audio_remote_policy.ssrc.type = ssrc_any_inbound;\n\tsession->media.audio_local_policy.ssrc.type = ssrc_any_inbound;\n\tsession->media.video_remote_policy.ssrc.type = ssrc_any_inbound;\n\tsession->media.video_local_policy.ssrc.type = ssrc_any_inbound;\n\tjanus_mutex_init(&session->rec_mutex);\n\tjanus_mutex_init(&session->rtp_forwarders_mutex);\n\tsession->audio_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->video_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->peer_audio_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->peer_video_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->all_forwarders = g_hash_table_new(NULL, NULL);\n\tsession->udp_sock = -1;\n\tg_atomic_int_set(&session->destroyed, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n\tjanus_mutex_init(&session->mutex);\n\thandle->plugin_handle = session;\n\tjanus_refcount_init(&session->ref, janus_nosip_session_free);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\twhile(session->unique_id == NULL) {\n\t\tsession->unique_id = janus_random_uuid();\n\t\tif(g_hash_table_lookup(unique_ids, session->unique_id) != NULL) {\n\t\t\tg_free(session->unique_id);\n\t\t\tsession->unique_id = NULL;\n\t\t}\n\t\tg_hash_table_insert(unique_ids, g_strdup(session->unique_id), session);\n\t}\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_nosip_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_nosip_session *session = janus_nosip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No NoSIP session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Destroying NoSIP session (%p)...\\n\", session);\n\tjanus_nosip_hangup_media_internal(handle);\n\tif(session->unique_id)\n\t\tg_hash_table_remove(sessions, session->unique_id);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn;\n}\n\njson_t *janus_nosip_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_nosip_session *session = janus_nosip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Provide some generic info, e.g., if we're in a call and with whom */\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"unique_id\", json_string(session->unique_id));\n\tif(session->sdp) {\n\t\tjson_object_set_new(info, \"srtp-required\", json_string(session->media.require_srtp ? \"yes\" : \"no\"));\n\t\tjson_object_set_new(info, \"sdes-local\", json_string(session->media.has_srtp_local ? \"yes\" : \"no\"));\n\t\tjson_object_set_new(info, \"sdes-remote\", json_string(session->media.has_srtp_remote ? \"yes\" : \"no\"));\n\t}\n\tif(session->arc || session->vrc || session->arc_peer || session->vrc_peer) {\n\t\tjson_t *recording = json_object();\n\t\tif(session->arc && session->arc->filename)\n\t\t\tjson_object_set_new(recording, \"audio\", json_string(session->arc->filename));\n\t\tif(session->vrc && session->vrc->filename)\n\t\t\tjson_object_set_new(recording, \"video\", json_string(session->vrc->filename));\n\t\tif(session->arc_peer && session->arc_peer->filename)\n\t\t\tjson_object_set_new(recording, \"audio-peer\", json_string(session->arc_peer->filename));\n\t\tif(session->vrc_peer && session->vrc_peer->filename)\n\t\t\tjson_object_set_new(recording, \"video-peer\", json_string(session->vrc_peer->filename));\n\t\tjson_object_set_new(info, \"recording\", recording);\n\t}\n\tjson_object_set_new(info, \"hangingup\", json_integer(g_atomic_int_get(&session->hangingup)));\n\tjson_object_set_new(info, \"destroyed\", json_integer(g_atomic_int_get(&session->destroyed)));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstruct janus_plugin_result *janus_nosip_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_nosip_session *session = janus_nosip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\t}\n\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\tjanus_nosip_message *msg = g_malloc(sizeof(janus_nosip_message));\n\tmsg->handle = handle;\n\tmsg->transaction = transaction;\n\tmsg->message = message;\n\tmsg->jsep = jsep;\n\tg_async_queue_push(messages, msg);\n\n\t/* All the requests to this plugin are handled asynchronously */\n\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n}\n\nvoid janus_nosip_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"WebRTC media is now available\\n\");\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_nosip_session *session = janus_nosip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nvoid janus_nosip_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(gateway) {\n\t\t/* Honour the audio/video active flags */\n\t\tjanus_nosip_session *session = (janus_nosip_session *)handle->plugin_handle;\n\t\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tgboolean video = packet->video;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\t/* Forward to our NoSIP peer */\n\t\tif((video && !session->media.video_send) || (!video && !session->media.audio_send)) {\n\t\t\t/* Dropping packet, peer doesn't want to receive it */\n\t\t\treturn;\n\t\t}\n\t\tif(video && session->media.simulcast_ssrc) {\n\t\t\t/* The user is simulcasting: drop everything except the base layer */\n\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\tuint32_t ssrc = ntohl(header->ssrc);\n\t\t\tif(ssrc != session->media.simulcast_ssrc) {\n\t\t\t\tJANUS_LOG(LOG_DBG, \"Dropping packet (not base simulcast substream)\\n\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif((video && session->media.video_ssrc == 0) || (!video && session->media.audio_ssrc == 0)) {\n\t\t\trtp_header *header = (rtp_header *)buf;\n\t\t\tif(video) {\n\t\t\t\tsession->media.video_ssrc = ntohl(header->ssrc);\n\t\t\t} else {\n\t\t\t\tsession->media.audio_ssrc = ntohl(header->ssrc);\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Got NoSIP %s SSRC: %\"SCNu32\"\\n\",\n\t\t\t\tvideo ? \"video\" : \"audio\",\n\t\t\t\tvideo ? session->media.video_ssrc : session->media.audio_ssrc);\n\t\t}\n\t\tif((video && session->media.has_video && session->media.video_rtp_fd != -1) ||\n\t\t\t\t(!video && session->media.has_audio && session->media.audio_rtp_fd != -1)) {\n\t\t\t/* Check if there are forwarders interested in this traffic */\n\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, video ? session->video_forwarders : session->audio_forwarders);\n\t\t\twhile(session->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\t\t\tif((!video && rtp_forward->is_video) || (video && !rtp_forward->is_video))\n\t\t\t\t\tcontinue;\n\t\t\t\tjanus_rtp_forwarder_send_rtp(rtp_forward, buf, len, 0);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t/* Save the frame if we're recording */\n\t\t\tjanus_recorder_save_frame(video ? session->vrc : session->arc, buf, len);\n\t\t\t/* Is SRTP involved? */\n\t\t\tif(session->media.has_srtp_local) {\n\t\t\t\tchar sbuf[2048];\n\t\t\t\tmemcpy(&sbuf, buf, len);\n\t\t\t\tint protected = len;\n\t\t\t\tint res = srtp_protect(\n\t\t\t\t\t(video ? session->media.video_srtp_out : session->media.audio_srtp_out),\n\t\t\t\t\t&sbuf, &protected);\n\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\trtp_header *header = (rtp_header *)&sbuf;\n\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] %s SRTP protect error... %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\", janus_srtp_error_str(res), len, protected, timestamp, seq);\n\t\t\t\t} else {\n\t\t\t\t\t/* Forward the frame to the peer */\n\t\t\t\t\tif(send((video ? session->media.video_rtp_fd : session->media.audio_rtp_fd), sbuf, protected, 0) < 0) {\n\t\t\t\t\t\trtp_header *header = (rtp_header *)&sbuf;\n\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Error sending %s SRTP packet... %s (len=%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\", g_strerror(errno), protected, timestamp, seq);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Forward the frame to the peer */\n\t\t\t\tif(send((video ? session->media.video_rtp_fd : session->media.audio_rtp_fd), buf, len, 0) < 0) {\n\t\t\t\t\trtp_header *header = (rtp_header *)&buf;\n\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Error sending %s RTP packet... %s (len=%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\", g_strerror(errno), len, timestamp, seq);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid janus_nosip_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(gateway) {\n\t\tjanus_nosip_session *session = (janus_nosip_session *)handle->plugin_handle;\n\t\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tgboolean video = packet->video;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\t/* Forward to our NoSIP peer */\n\t\tif((video && session->media.has_video && session->media.video_rtcp_fd != -1) ||\n\t\t\t\t(!video && session->media.has_audio && session->media.audio_rtcp_fd != -1)) {\n\t\t\t/* Fix SSRCs as the Janus core does */\n\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Fixing %s SSRCs (local %u, peer %u)\\n\",\n\t\t\t\tsession, video ? \"video\" : \"audio\",\n\t\t\t\t(video ? session->media.video_ssrc : session->media.audio_ssrc),\n\t\t\t\t(video ? session->media.video_ssrc_peer : session->media.audio_ssrc_peer));\n\t\t\tjanus_rtcp_fix_ssrc(NULL, (char *)buf, len, video,\n\t\t\t\t(video ? session->media.video_ssrc : session->media.audio_ssrc),\n\t\t\t\t(video ? session->media.video_ssrc_peer : session->media.audio_ssrc_peer));\n\t\t\t/* Is SRTP involved? */\n\t\t\tif(session->media.has_srtp_local) {\n\t\t\t\tchar sbuf[2048];\n\t\t\t\tmemcpy(&sbuf, buf, len);\n\t\t\t\tint protected = len;\n\t\t\t\tint res = srtp_protect_rtcp(\n\t\t\t\t\t(video ? session->media.video_srtp_out : session->media.audio_srtp_out),\n\t\t\t\t\t&sbuf, &protected);\n\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] %s SRTCP protect error... %s (len=%d-->%d)...\\n\",\n\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\",\n\t\t\t\t\t\tjanus_srtp_error_str(res), len, protected);\n\t\t\t\t} else {\n\t\t\t\t\t/* Forward the message to the peer */\n\t\t\t\t\tif(send((video ? session->media.video_rtcp_fd : session->media.audio_rtcp_fd), sbuf, protected, 0) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Error sending SRTCP %s packet... %s (len=%d)...\\n\",\n\t\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\", g_strerror(errno), protected);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Forward the message to the peer */\n\t\t\t\tif(send((video ? session->media.video_rtcp_fd : session->media.audio_rtcp_fd), buf, len, 0) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Error sending RTCP %s packet... %s (len=%d)...\\n\",\n\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\", g_strerror(errno), len);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nstatic void janus_nosip_recorder_close(janus_nosip_session *session,\n\t\tgboolean stop_audio, gboolean stop_audio_peer, gboolean stop_video, gboolean stop_video_peer) {\n\tif(session->arc && stop_audio) {\n\t\tjanus_recorder *rc = session->arc;\n\t\tsession->arc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed user's audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->arc_peer && stop_audio_peer) {\n\t\tjanus_recorder *rc = session->arc_peer;\n\t\tsession->arc_peer = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed peer's audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->vrc && stop_video) {\n\t\tjanus_recorder *rc = session->vrc;\n\t\tsession->vrc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed user's video recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->vrc_peer && stop_video_peer) {\n\t\tjanus_recorder *rc = session->vrc_peer;\n\t\tsession->vrc_peer = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed peer's video recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n}\n\nvoid janus_nosip_hangup_media(janus_plugin_session *handle) {\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_nosip_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_nosip_hangup_media_internal(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"No WebRTC media anymore\\n\");\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_nosip_session *session = janus_nosip_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\tsession->media.simulcast_ssrc = 0;\n\t/* Notify the thread that it's time to go */\n\tif(session->media.pipefd[1] > 0) {\n\t\tint code = 1;\n\t\tssize_t res = 0;\n\t\tdo {\n\t\t\tres = write(session->media.pipefd[1], &code, sizeof(int));\n\t\t} while(res == -1 && errno == EINTR);\n\t}\n\t/* Do cleanup if media thread has not been created */\n\tif(!session->media.ready && !session->relayer_thread) {\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tjanus_nosip_media_cleanup(session);\n\t\tjanus_mutex_unlock(&session->mutex);\n\t}\n\t/* Get rid of the recorders, if available */\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_nosip_recorder_close(session, TRUE, TRUE, TRUE, TRUE);\n\tjanus_mutex_unlock(&session->rec_mutex);\n\tg_atomic_int_set(&session->hangingup, 0);\n\t/* Get rid of RTP forwarders, if any */\n\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\tg_hash_table_remove_all(session->audio_forwarders);\n\tg_hash_table_remove_all(session->video_forwarders);\n\tg_hash_table_remove_all(session->peer_audio_forwarders);\n\tg_hash_table_remove_all(session->peer_video_forwarders);\n\tg_hash_table_remove_all(session->all_forwarders);\n\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_nosip_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining NoSIP handler thread\\n\");\n\tjanus_nosip_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_nosip_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_nosip_session *session = janus_nosip_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_nosip_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_nosip_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = msg->message;\n\t\tif(msg->message == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_NOSIP_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(!json_is_object(root)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_JSON;\n\t\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\t\tgoto error;\n\t\t}\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto error;\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tjson_t *result = NULL, *localjsep = NULL;\n\n\t\tif(!strcasecmp(request_text, \"generate\") || !strcasecmp(request_text, \"process\")) {\n\t\t\t/* Shared code for two different requests:\n\t\t\t * \t\tgenerate: Take a JSEP offer or answer and generate a barebone SDP the application can use\n\t\t\t * \t\tprocess: Process a remote barebone SDP, and match it to the one we may have generated before */\n\t\t\tgboolean generate = !strcasecmp(request_text, \"generate\") ? TRUE : FALSE;\n\t\t\tif(generate) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, generate_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, process_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\t/* Any SDP to handle? if not, something's wrong */\n\t\t\tconst char *msg_sdp_type = json_string_value(json_object_get(generate ? msg->jsep : root, \"type\"));\n\t\t\tconst char *msg_sdp = json_string_value(json_object_get(generate ? msg->jsep : root, \"sdp\"));\n\t\t\tgboolean sdp_update = json_is_true(json_object_get(generate ? msg->jsep : root, \"update\"));\n\t\t\tif(!generate && session->media.ready) {\n\t\t\t\tsdp_update = TRUE;\n\t\t\t}\n\t\t\tif(!msg_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP\\n\");\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!msg_sdp_type || (strcasecmp(msg_sdp_type, \"offer\") && strcasecmp(msg_sdp_type, \"answer\"))) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing or invalid SDP type\\n\");\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing or invalid SDP type\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tgboolean offer = !strcasecmp(msg_sdp_type, \"offer\");\n\t\t\tif(strstr(msg_sdp, \"m=application\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"The NoSIP plugin does not support DataChannels\\n\");\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"The NoSIP plugin does not support DataChannels\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(json_is_true(json_object_get(msg->jsep, \"e2ee\"))) {\n\t\t\t\t/* Media is encrypted, but legacy endpoints will need unencrypted media frames */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Media encryption unsupported by this plugin\\n\");\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Media encryption unsupported by this plugin\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Check if the user provided an info string to provide context */\n\t\t\tconst char *info = json_string_value(json_object_get(root, \"info\"));\n\t\t\t/* SDES-SRTP is disabled by default, let's see if we need to enable it */\n\t\t\tgboolean do_srtp = FALSE, require_srtp = FALSE;\n\t\t\tjson_t *srtp = json_object_get(root, \"srtp\");\n\t\t\tif(srtp) {\n\t\t\t\tconst char *srtp_text = json_string_value(srtp);\n\t\t\t\tif(!strcasecmp(srtp_text, \"sdes_optional\")) {\n\t\t\t\t\t/* Negotiate SDES, but make it optional */\n\t\t\t\t\tdo_srtp = TRUE;\n\t\t\t\t} else if(!strcasecmp(srtp_text, \"sdes_mandatory\")) {\n\t\t\t\t\t/* Negotiate SDES, and require it */\n\t\t\t\t\tdo_srtp = TRUE;\n\t\t\t\t\trequire_srtp = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (srtp can only be sdes_optional or sdes_mandatory)\\n\");\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (srtp can only be sdes_optional or sdes_mandatory)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(offer && !sdp_update) {\n\t\t\t\t/* Clean up SRTP stuff from before first, in case it's still needed */\n\t\t\t\tjanus_nosip_srtp_cleanup(session);\n\t\t\t\tif(do_srtp) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate SDES-SRTP (%s)...\\n\", require_srtp ? \"mandatory\" : \"optional\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tsession->media.require_srtp = require_srtp;\n\t\t\tif(generate) {\n\t\t\t\tif(!offer) {\n\t\t\t\t\tdo_srtp = do_srtp || session->media.has_srtp_remote;\n\t\t\t\t\t/* Make sure the request is consistent with the state (original offer) */\n\t\t\t\t\tif(session->media.require_srtp && !session->media.has_srtp_remote) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't generate answer: SDES-SRTP required, but caller didn't offer it\\n\");\n\t\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_TOO_STRICT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't generate answer: SDES-SRTP required, but caller didn't offer it\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsession->media.has_srtp_local = do_srtp;\n\t\t\t\tif(do_srtp) {\n\t\t\t\t\t/* Any SRTP profile different from the default? */\n\t\t\t\t\tjanus_srtp_profile srtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n\t\t\t\t\tconst char *profile = json_string_value(json_object_get(root, \"srtp_profile\"));\n\t\t\t\t\tif(profile) {\n\t\t\t\t\t\tif(!strcmp(profile, \"AES_CM_128_HMAC_SHA1_32\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_32;\n\t\t\t\t\t\t} else if(!strcmp(profile, \"AES_CM_128_HMAC_SHA1_80\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n#ifdef HAVE_SRTP_AESGCM\n\t\t\t\t\t\t\t} else if(!strcmp(profile, \"AEAD_AES_128_GCM\")) {\n\t\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AEAD_AES_128_GCM;\n\t\t\t\t\t\t\t} else if(!strcmp(profile, \"AEAD_AES_256_GCM\")) {\n\t\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AEAD_AES_256_GCM;\n#endif\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (unsupported SRTP profile)\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (unsupported SRTP profile)\");\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsession->media.srtp_profile = srtp_profile;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Get video-orientation extension id from SDP we got */\n\t\t\tsession->media.video_orientation_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t/* Get audio-level extension id from SDP we got */\n\t\t\tsession->media.audio_level_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t/* Parse the SDP we got, manipulate some things, and generate a new one */\n\t\t\tchar sdperror[100];\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(msg_sdp, sdperror, sizeof(sdperror));\n\t\t\tif(!parsed_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing SDP: %s\\n\", sdperror);\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing SDP: %s\", sdperror);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(generate) {\n\t\t\t\t/* Allocate RTP ports and merge them with the anonymized SDP */\n\t\t\t\tif(strstr(msg_sdp, \"m=audio\") && !strstr(msg_sdp, \"m=audio 0\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate audio...\\n\");\n\t\t\t\t\tsession->media.has_audio = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t\t}\n\t\t\t\tif(strstr(msg_sdp, \"m=video\") && !strstr(msg_sdp, \"m=video 0\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate video...\\n\");\n\t\t\t\t\tsession->media.has_video = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t\t}\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\tif(janus_nosip_allocate_local_ports(session, sdp_update) < 0) {\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not allocate RTP/RTCP ports\\n\");\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_IO_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not allocate RTP/RTCP ports\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\n\t\t\t\tchar *sdp = janus_nosip_sdp_manipulate(session, parsed_sdp, FALSE);\n\t\t\t\tif(sdp == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not allocate RTP/RTCP ports\\n\");\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_IO_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not allocate RTP/RTCP ports\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Take note of the SDP (may be useful for UPDATEs or re-INVITEs) */\n\t\t\t\tjanus_sdp_destroy(session->sdp);\n\t\t\t\tsession->sdp = parsed_sdp;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Prepared SDP %s for (%p)\\n%s\", msg_sdp_type, info, sdp);\n\t\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(!sdp_update && notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"generated\"));\n\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(offer ? \"offer\" : \"answer\"));\n\t\t\t\t\tjson_object_set_new(info, \"sdp\", json_string(sdp));\n\t\t\t\t\tjson_object_set_new(info, \"unique_id\", json_string(session->unique_id));\n\t\t\t\t\tgateway->notify_event(&janus_nosip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\t/* If the user negotiated simulcasting, just stick with the base substream */\n\t\t\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\t\t\tif(msg_simulcast && json_array_size(msg_simulcast) > 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Client negotiated simulcasting which we don't do here, falling back to base substream...\\n\");\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(msg_simulcast); i++) {\n\t\t\t\t\t\tjson_t *sobj = json_array_get(msg_simulcast, i);\n\t\t\t\t\t\tjson_t *s = json_object_get(sobj, \"ssrcs\");\n\t\t\t\t\t\tif(s && json_array_size(s) > 0)\n\t\t\t\t\t\t\tsession->media.simulcast_ssrc = json_integer_value(json_array_get(s, 0));\n\t\t\t\t\t\tsession->media.simulcast_ssrc = json_integer_value(json_object_get(s, \"ssrc-0\"));\n\t\t\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Send the barebone SDP back */\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"generated\"));\n\t\t\t\tjson_object_set_new(result, \"type\", json_string(offer ? \"offer\" : \"answer\"));\n\t\t\t\tjson_object_set_new(result, \"sdp\", json_string(sdp));\n\t\t\t\tif(sdp_update)\n\t\t\t\t\tjson_object_set_new(result, \"update\", json_true());\n\t\t\t\tjson_object_set_new(result, \"unique_id\", json_string(session->unique_id));\n\t\t\t\tg_free(sdp);\n\t\t\t} else {\n\t\t\t\t/* We got a barebone offer or answer from our peer: process it accordingly */\n\t\t\t\tgboolean changed = FALSE;\n\t\t\t\tjanus_nosip_sdp_process(session, parsed_sdp, !offer, sdp_update, &changed);\n\t\t\t\t/* Check if offer has neither audio nor video, fail */\n\t\t\t\tif(!session->media.has_audio && !session->media.has_video) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No audio and no video being negotiated\\n\");\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No audio and no video being negotiated\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Also fail if there's no remote IP address that can be used for RTP */\n\t\t\t\tif(!session->media.remote_audio_ip && !session->media.remote_video_ip) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No remote IP addresses\\n\");\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No remote IP addresses\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(session->media.require_srtp && !session->media.has_srtp_remote) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't process request: SDES-SRTP required, but caller didn't offer it\\n\");\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_TOO_STRICT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't process request: SDES-SRTP required, but caller didn't offer it\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Take note of the SDP (may be useful for UPDATEs or re-INVITEs) */\n\t\t\t\tjanus_sdp_destroy(session->sdp);\n\t\t\t\tsession->sdp = parsed_sdp;\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(!sdp_update && notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"processed\"));\n\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(offer ? \"offer\" : \"answer\"));\n\t\t\t\t\tjson_object_set_new(info, \"sdp\", json_string(msg_sdp));\n\t\t\t\t\tjson_object_set_new(info, \"unique_id\", json_string(session->unique_id));\n\t\t\t\t\tgateway->notify_event(&janus_nosip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\t/* Send SDP to the browser */\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"processed\"));\n\t\t\t\tif(session->media.has_srtp_remote) {\n\t\t\t\t\tjson_object_set_new(result, \"srtp\",\n\t\t\t\t\t\tjson_string(session->media.require_srtp ? \"sdes_mandatory\" : \"sdes_optional\"));\n\t\t\t\t}\n\t\t\t\tif(sdp_update)\n\t\t\t\t\tjson_object_set_new(result, \"update\", json_true());\n\t\t\t\tjson_object_set_new(result, \"unique_id\", json_string(session->unique_id));\n\t\t\t\tlocaljsep = json_pack(\"{ssss}\", \"type\", msg_sdp_type, \"sdp\", msg_sdp);\n\t\t\t}\n\t\t\t/* If this is an answer, start the media */\n\t\t\tif(!sdp_update && !offer) {\n\t\t\t\t/* Start the media */\n\t\t\t\tsession->media.ready = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"nosiprtp %p\", session);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tsession->relayer_thread = g_thread_try_new(tname, janus_nosip_relay_thread, session, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tsession->relayer_thread = NULL;\n\t\t\t\t\tsession->media.ready = FALSE;\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the RTP/RTCP thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"hangup\")) {\n\t\t\t/* Get rid of an ongoing session */\n\t\t\tgateway->close_pc(session->handle);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"hangingup\"));\n\t\t} else if(!strcasecmp(request_text, \"recording\")) {\n\t\t\t/* Start or stop recording */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, recording_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *action = json_object_get(root, \"action\");\n\t\t\tconst char *action_text = json_string_value(action);\n\t\t\tif(strcasecmp(action_text, \"start\") && strcasecmp(action_text, \"stop\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid action (should be start|stop)\\n\");\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid action (should be start|stop)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tgboolean record_audio = FALSE, record_video = FALSE,\t/* No media is recorded by default */\n\t\t\t\trecord_peer_audio = FALSE, record_peer_video = FALSE;\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\trecord_audio = audio ? json_is_true(audio) : FALSE;\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\trecord_video = video ? json_is_true(video) : FALSE;\n\t\t\tjson_t *peer_audio = json_object_get(root, \"peer_audio\");\n\t\t\trecord_peer_audio = peer_audio ? json_is_true(peer_audio) : FALSE;\n\t\t\tjson_t *peer_video = json_object_get(root, \"peer_video\");\n\t\t\trecord_peer_video = peer_video ? json_is_true(peer_video) : FALSE;\n\t\t\tif(!record_audio && !record_video && !record_peer_audio && !record_peer_video) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid request (at least one of audio, video, peer_audio and peer_video should be true)\\n\");\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_RECORDING_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid request (at least one of audio, video, peer_audio and peer_video should be true)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\t\tconst char *recording_base = json_string_value(recfile);\n\t\t\tjanus_mutex_lock(&session->rec_mutex);\n\t\t\tif(!strcasecmp(action_text, \"start\")) {\n\t\t\t\t/* Start recording something */\n\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\tchar filename[255];\n\t\t\t\tgint64 now = janus_get_real_time();\n\t\t\t\tif(record_peer_audio || record_peer_video) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Starting recording of peer's %s\\n\",\n\t\t\t\t\t\t(record_peer_audio && record_peer_video ? \"audio and video\" : (record_peer_audio ? \"audio\" : \"video\")));\n\t\t\t\t\t/* Start recording this peer's audio and/or video */\n\t\t\t\t\tif(record_peer_audio) {\n\t\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-peer-audio\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"nosip-%p-%\"SCNi64\"-peer-audio\", session, now);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this peer!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* If RED is in use, take note of it */\n\t\t\t\t\t\t\tif(session->media.opusred_pt > 0)\n\t\t\t\t\t\t\t\tjanus_recorder_opusred(rc, session->media.opusred_pt);\n\t\t\t\t\t\t\tsession->arc_peer = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(record_peer_video) {\n\t\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-peer-video\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"nosip-%p-%\"SCNi64\"-peer-video\", session, now);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* TODO We should send a FIR/PLI to this peer... */\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an video recording file for this peer!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsession->vrc_peer = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(record_audio || record_video) {\n\t\t\t\t\t/* Start recording the user's audio and/or video */\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Starting recording of user's %s (%p)\\n\",\n\t\t\t\t\t\t(record_audio && record_video ? \"audio and video\" : (record_audio ? \"audio\" : \"video\")), session);\n\t\t\t\t\tif(record_audio) {\n\t\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-user-audio\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"nosip-%p-%\"SCNi64\"-own-audio\", session, now);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this user!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* If RED is in use, take note of it */\n\t\t\t\t\t\t\tif(session->media.opusred_pt > 0)\n\t\t\t\t\t\t\t\tjanus_recorder_opusred(rc, session->media.opusred_pt);\n\t\t\t\t\t\t\tsession->arc = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(record_video) {\n\t\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-user-video\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"nosip-%p-%\"SCNi64\"-own-video\", session, now);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open a video recording file for this user!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsession->vrc = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording video, sending a PLI to kickstart it\\n\");\n\t\t\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Stop recording something: notice that this never returns an error, even when we were not recording anything */\n\t\t\t\tjanus_nosip_recorder_close(session, record_audio, record_peer_audio, record_video, record_peer_video);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rec_mutex);\n\t\t\t/* Notify the result */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"recordingupdated\"));\n\t\t} else if(!strcasecmp(request_text, \"keyframe\")) {\n\t\t\t/* Programmatically send a keyframe request via RTCP PLI to\n\t\t\t * either the WebRTC user, the SIP peer, or both of them */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, keyframe_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tgboolean user = json_is_true(json_object_get(root, \"user\"));\n\t\t\tgboolean peer = json_is_true(json_object_get(root, \"peer\"));\n\t\t\tif(user) {\n\t\t\t\t/* Send a PLI to the WebRTC user */\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t\tif(peer) {\n\t\t\t\t/* Send a PLI to the SIP peer (but only if they negotiated it) */\n\t\t\t\tif(session->media.video_pli_supported)\n\t\t\t\t\tjanus_nosip_rtcp_pli_send(session);\n\t\t\t}\n\t\t\t/* Notify the result */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"keyframesent\"));\n\t\t} else if(!strcasecmp(request_text, \"rtp_forward\")) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_forward_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\t/* Iterate on the provided streams array */\n\t\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\t\tif(streams == NULL || json_array_size(streams) == 0) {\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_MISSING_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Missing mandatory element streams, or empty array\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Iterate on the streams objects and validate them all */\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, rtp_forward_stream_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tgoto error;\n\t\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\t\tif(strcasecmp(type, \"audio\") && strcasecmp(type, \"video\") &&\n\t\t\t\t\t\tstrcasecmp(type, \"peer_audio\") && strcasecmp(type, \"peer_video\")) {\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Invalid element (type)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Make sure we have a host attribute, either global or stream-specific */\n\t\t\t\tjson_t *stream_host = json_object_get(s, \"host\");\n\t\t\t\tconst char *s_host = json_string_value(stream_host), *resolved_host = NULL;\n\t\t\t\tjson_t *stream_host_family = json_object_get(s, \"host_family\");\n\t\t\t\tconst char *s_host_family = json_string_value(stream_host_family);\n\t\t\t\tint s_family = AF_INET;\n\t\t\t\tif(s_host_family) {\n\t\t\t\t\tif(!strcasecmp(s_host_family, \"ipv4\")) {\n\t\t\t\t\t\ts_family = AF_INET;\n\t\t\t\t\t} else if(!strcasecmp(s_host_family, \"ipv6\")) {\n\t\t\t\t\t\ts_family = AF_INET6;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported protocol family (%s)\\n\", s_host_family);\n\t\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported protocol family (%s)\", s_host_family);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstruct addrinfo *res = NULL, *start = NULL;\n\t\t\t\tjanus_network_address addr;\n\t\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\t\tstruct addrinfo hints;\n\t\t\t\tmemset(&hints, 0, sizeof(hints));\n\t\t\t\tif(s_family != 0)\n\t\t\t\t\thints.ai_family = s_family;\n\t\t\t\tif(getaddrinfo(s_host, NULL, s_family != 0 ? &hints : NULL, &res) == 0) {\n\t\t\t\t\tstart = res;\n\t\t\t\t\twhile(res != NULL) {\n\t\t\t\t\t\tif(janus_network_address_from_sockaddr(res->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t\t\t/* Resolved */\n\t\t\t\t\t\t\tresolved_host = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\t\t\tstart = NULL;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tres = res->ai_next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(resolved_host == NULL) {\n\t\t\t\t\tif(start)\n\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s)...\\n\", s_host);\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not resolve address (%s)...\", s_host);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Add the resolved address to the JSON object, so that we can use it later */\n\t\t\t\tjson_object_set_new(s, \"host\", json_string(resolved_host));\n\t\t\t\t/* We may need to SRTP-encrypt this stream */\n\t\t\t\tint srtp_suite = 0;\n\t\t\t\tconst char *srtp_crypto = NULL;\n\t\t\t\tjson_t *s_suite = json_object_get(s, \"srtp_suite\");\n\t\t\t\tjson_t *s_crypto = json_object_get(s, \"srtp_crypto\");\n\t\t\t\tif(s_suite && s_crypto) {\n\t\t\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\t\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP suite (%d)\\n\", srtp_suite);\n\t\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP suite (%d)\", srtp_suite);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t\t\t\tif(srtp_crypto == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP crypto\\n\");\n\t\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP crypto\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_refcount_increase(&session->ref);\t/* This is just to handle the request for now */\n\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\tif(session->udp_sock <= 0) {\n\t\t\t\tsession->udp_sock = socket(!ipv6_disabled ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\t\tint v6only = 0;\n\t\t\t\tif(session->udp_sock <= 0 ||\n\t\t\t\t\t\t(!ipv6_disabled && setsockopt(session->udp_sock, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)) {\n\t\t\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not open UDP socket for RTP forwarder, %d (%s)\\n\",\n\t\t\t\t\t\terrno, g_strerror(errno));\n\t\t\t\t\terror_code = JANUS_NOSIP_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not open UDP socket for RTP forwarder\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Iterate on all objects, and create the related forwarder(s) */\n\t\t\tresult = json_object();\n\t\t\tjson_t *new_forwarders = json_array();\n\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\t\tjson_t *stream_host = json_object_get(s, \"host\");\n\t\t\t\tconst char *host = json_string_value(stream_host);\n\t\t\t\tjson_t *stream_port = json_object_get(s, \"port\");\n\t\t\t\tuint16_t port = json_integer_value(stream_port);\n\t\t\t\tjson_t *stream_pt = json_object_get(s, \"pt\");\n\t\t\t\tjson_t *stream_ssrc = json_object_get(s, \"ssrc\");\n\t\t\t\tint srtp_suite = 0;\n\t\t\t\tconst char *srtp_crypto = NULL;\n\t\t\t\tjson_t *s_suite = json_object_get(s, \"srtp_suite\");\n\t\t\t\tjson_t *s_crypto = json_object_get(s, \"srtp_crypto\");\n\t\t\t\tif(s_suite && s_crypto) {\n\t\t\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t\t\t}\n\t\t\t\t/* Create the forwarder */\n\t\t\t\tjanus_rtp_forwarder *f = janus_nosip_rtp_forwarder_add_helper(session, type,\n\t\t\t\t\thost, port, json_integer_value(stream_pt), json_integer_value(stream_ssrc), srtp_suite, srtp_crypto);\n\t\t\t\tif(f) {\n\t\t\t\t\tjson_t *rtpf = janus_nosip_rtp_forwarder_summary(f);\n\t\t\t\t\tjson_array_append_new(new_forwarders, rtpf);\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = janus_nosip_rtp_forwarder_summary(f);\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(type));\n\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t\t\t\tgateway->notify_event(&janus_nosip_plugin, NULL, info);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tif(new_forwarders != NULL)\n\t\t\t\tjson_object_set_new(result, \"forwarders\", new_forwarders);\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"rtp_forward\"));\n\t\t} else if(!strcasecmp(request_text, \"stop_rtp_forward\")) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, stop_rtp_forward_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_NOSIP_ERROR_MISSING_ELEMENT, JANUS_NOSIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *id = json_object_get(root, \"stream_id\");\n\t\t\tguint32 stream_id = json_integer_value(id);\n\t\t\tjanus_refcount_increase(&session->ref);\t/* Just to handle the message now */\n\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\t/* Find the forwarder by iterating on all the streams */\n\t\t\tgboolean found = g_hash_table_remove(session->audio_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(!found)\n\t\t\t\tfound = g_hash_table_remove(session->video_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(!found)\n\t\t\t\tfound = g_hash_table_remove(session->peer_audio_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(!found)\n\t\t\t\tfound = g_hash_table_remove(session->peer_video_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(found)\n\t\t\t\tg_hash_table_remove(session->all_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tif(!found) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No such stream (%\"SCNu32\")\\n\", stream_id);\n\t\t\t\terror_code = JANUS_NOSIP_ERROR_NO_SUCH_STREAM;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such stream (%\"SCNu32\")\", stream_id);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"stop_rtp_forward\"));\n\t\t\tjson_object_set_new(result, \"stream_id\", json_integer(stream_id));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"stop_rtp_forward\"));\n\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(stream_id));\n\t\t\t\tgateway->notify_event(&janus_nosip_plugin, NULL, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"listforwarders\")) {\n\t\t\t/* Return a list of all forwarders for this call */\n\t\t\tjson_t *list = json_array();\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\tg_hash_table_iter_init(&iter, session->all_forwarders);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_rtp_forwarder *rf = (janus_rtp_forwarder *)value;\n\t\t\t\tjson_t *fl = janus_nosip_rtp_forwarder_summary(rf);\n\t\t\t\tjson_array_append_new(list, fl);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"forwarders\"));\n\t\t\tjson_object_set_new(result, \"rtp_forwarders\", list);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unknown request (%s)\\n\", request_text);\n\t\t\terror_code = JANUS_NOSIP_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown request (%s)\", request_text);\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Prepare JSON event */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"nosip\", json_string(\"event\"));\n\t\tif(result != NULL)\n\t\t\tjson_object_set_new(event, \"result\", result);\n\t\tint ret = gateway->push_event(msg->handle, &janus_nosip_plugin, msg->transaction, event, localjsep);\n\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\tjson_decref(event);\n\t\tif(localjsep)\n\t\t\tjson_decref(localjsep);\n\t\tjanus_nosip_message_free(msg);\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"nosip\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_nosip_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_nosip_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving NoSIP handler thread\\n\");\n\treturn NULL;\n}\n\n\nvoid janus_nosip_sdp_process(janus_nosip_session *session, janus_sdp *sdp, gboolean answer, gboolean update, gboolean *changed) {\n\tif(!session || !sdp)\n\t\treturn;\n\t/* c= */\n\tint opusred_pt = answer ? janus_sdp_get_opusred_pt(sdp, -1) : -1;\n\tif(sdp->c_addr) {\n\t\tif(update) {\n\t\t\tif (changed && (!session->media.remote_audio_ip || strcmp(sdp->c_addr, session->media.remote_audio_ip)))\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t*changed = TRUE;\n\t\t\tif (changed && (!session->media.remote_video_ip || strcmp(sdp->c_addr, session->media.remote_video_ip)))\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t*changed = TRUE;\n\t\t}\n\t\t/* Regardless if we audio and video are being negotiated we set their connection addresses\n\t\t * from session level c= header by default. If media level connection addresses are available\n\t\t * they will be set when processing appropriate media description.*/\n\t\tg_free(session->media.remote_audio_ip);\n\t\tsession->media.remote_audio_ip = g_strdup(sdp->c_addr);\n\t\tg_free(session->media.remote_video_ip);\n\t\tsession->media.remote_video_ip = g_strdup(sdp->c_addr);\n\t}\n\tGList *temp = sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tsession->media.require_srtp = session->media.require_srtp || (m->proto && !strcasecmp(m->proto, \"RTP/SAVP\"));\n\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\tif(m->port) {\n\t\t\t\tif(m->port != session->media.remote_audio_rtp_port) {\n\t\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t\tif(changed)\n\t\t\t\t\t\t*changed = TRUE;\n\t\t\t\t}\n\t\t\t\tsession->media.has_audio = TRUE;\n\t\t\t\tsession->media.remote_audio_rtp_port = m->port;\n\t\t\t\tsession->media.remote_audio_rtcp_port = m->port+1;\t/* FIXME We're assuming RTCP is on the next port */\n\t\t\t\tif(m->direction == JANUS_SDP_SENDONLY || m->direction == JANUS_SDP_INACTIVE)\n\t\t\t\t\tsession->media.audio_send = FALSE;\n\t\t\t\telse\n\t\t\t\t\tsession->media.audio_send = TRUE;\n\t\t\t} else {\n\t\t\t\tsession->media.audio_send = FALSE;\n\t\t\t}\n\t\t} else if(m->type == JANUS_SDP_VIDEO) {\n\t\t\tif(m->port) {\n\t\t\t\tif(m->port != session->media.remote_video_rtp_port) {\n\t\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t\tif(changed)\n\t\t\t\t\t\t*changed = TRUE;\n\t\t\t\t}\n\t\t\t\tsession->media.has_video = TRUE;\n\t\t\t\tsession->media.remote_video_rtp_port = m->port;\n\t\t\t\tsession->media.remote_video_rtcp_port = m->port+1;\t/* FIXME We're assuming RTCP is on the next port */\n\t\t\t\tif(m->direction == JANUS_SDP_SENDONLY || m->direction == JANUS_SDP_INACTIVE)\n\t\t\t\t\tsession->media.video_send = FALSE;\n\t\t\t\telse\n\t\t\t\t\tsession->media.video_send = TRUE;\n\t\t\t} else {\n\t\t\t\tsession->media.video_send = FALSE;\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported media line (not audio/video)\\n\");\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(m->c_addr && m->type == JANUS_SDP_AUDIO) {\n\t\t\tif(update && (!session->media.remote_audio_ip || strcmp(m->c_addr, session->media.remote_audio_ip))) {\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\tif(changed)\n\t\t\t\t\t*changed = TRUE;\n\t\t\t}\n\t\t\tg_free(session->media.remote_audio_ip);\n\t\t\tsession->media.remote_audio_ip = g_strdup(m->c_addr);\n\t\t}\n\t\telse if (m->c_addr && m->type == JANUS_SDP_VIDEO) {\n\t\t\tif(update && (!session->media.remote_video_ip || strcmp(m->c_addr, session->media.remote_video_ip))) {\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\tif(changed)\n\t\t\t\t\t*changed = TRUE;\n\t\t\t}\n\t\t\tg_free(session->media.remote_video_ip);\n\t\t\tsession->media.remote_video_ip = g_strdup(m->c_addr);\n\t\t}\n\t\tGList *tempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name) {\n\t\t\t\tif(!strcasecmp(a->name, \"crypto\")) {\n\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\t\t\t\tif((m->type == JANUS_SDP_AUDIO && session->media.audio_srtp_in != NULL) || (m->type == JANUS_SDP_VIDEO && session->media.video_srtp_in != NULL)) {\n\t\t\t\t\t\t\t/* Remote SRTP is already set */\n\t\t\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgint32 tag = 0;\n\t\t\t\t\t\tchar profile[101], crypto[101];\n\t\t\t\t\t\tint res = a->value ? (sscanf(a->value, \"%\"SCNi32\" %100s inline:%100s\",\n\t\t\t\t\t\t\t&tag, profile, crypto)) : 0;\n\t\t\t\t\t\tif(res != 3) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Failed to parse crypto line, ignoring... %s\\n\", a->value);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tgboolean video = (m->type == JANUS_SDP_VIDEO);\n\t\t\t\t\t\t\tif(answer && ((!video && tag != session->media.audio_srtp_tag) || (video && tag != session->media.video_srtp_tag))) {\n\t\t\t\t\t\t\t\t/* Not the tag for the crypto line we offered */\n\t\t\t\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(janus_nosip_srtp_set_remote(session, video, profile, crypto) < 0) {\n\t\t\t\t\t\t\t\t/* Unsupported profile? */\n\t\t\t\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(!video) {\n\t\t\t\t\t\t\t\tsession->media.audio_srtp_tag = tag;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tsession->media.video_srtp_tag = tag;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsession->media.has_srtp_remote = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if(m->type == JANUS_SDP_VIDEO && !strcasecmp(a->name, \"rtcp-fb\") && a->value) {\n\t\t\t\t\tif(strstr(a->value, \" pli\"))\n\t\t\t\t\t\tsession->media.video_pli_supported = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\tif(answer && (m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO)) {\n\t\t\t/* Check which codec was negotiated eventually */\n\t\t\tint pt = -1;\n\t\t\tif(m->ptypes)\n\t\t\t\tpt = GPOINTER_TO_INT(m->ptypes->data);\n\t\t\tif(pt > -1) {\n\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\tif(pt == opusred_pt) {\n\t\t\t\t\t\tsession->media.opusred_pt = pt;\n\t\t\t\t\t\tsession->media.audio_pt = m->ptypes->next ? GPOINTER_TO_INT(m->ptypes->next->data) : -1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsession->media.audio_pt = pt;\n\t\t\t\t\t}\n\t\t\t\t\tsession->media.audio_pt_name = janus_sdp_get_codec_name(sdp, m->index, session->media.audio_pt);\n\t\t\t\t} else {\n\t\t\t\t\tsession->media.video_pt = pt;\n\t\t\t\t\tsession->media.video_pt_name = janus_sdp_get_codec_name(sdp, m->index, pt);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tif(update && changed && *changed) {\n\t\t/* Something changed: mark this on the session, so that the thread can update the sockets */\n\t\tsession->media.updated = TRUE;\n\t\tif(session->media.pipefd[1] > 0) {\n\t\t\tint code = 1;\n\t\t\tssize_t res = 0;\n\t\t\tdo {\n\t\t\t\tres = write(session->media.pipefd[1], &code, sizeof(int));\n\t\t\t} while(res == -1 && errno == EINTR);\n\t\t}\n\t}\n}\n\nchar *janus_nosip_sdp_manipulate(janus_nosip_session *session, janus_sdp *sdp, gboolean answer) {\n\tif(!session || !sdp)\n\t\treturn NULL;\n\t/* Start replacing stuff */\n\tJANUS_LOG(LOG_VERB, \"Setting protocol to %s\\n\", session->media.require_srtp ? \"RTP/SAVP\" : \"RTP/AVP\");\n\tif(sdp->c_addr) {\n\t\tg_free(sdp->c_addr);\n\t\tsdp->c_addr = g_strdup(sdp_ip);\n\t}\n\tint opusred_pt = answer ? janus_sdp_get_opusred_pt(sdp, -1) : -1;\n\tGList *temp = sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tg_free(m->proto);\n\t\tm->proto = g_strdup(session->media.require_srtp ? \"RTP/SAVP\" : \"RTP/AVP\");\n\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\tm->port = session->media.local_audio_rtp_port;\n\t\t\tif(session->media.has_srtp_local) {\n\t\t\t\tif(!session->media.audio_srtp_local_profile || !session->media.audio_srtp_local_crypto) {\n\t\t\t\t\tjanus_nosip_srtp_set_local(session, FALSE, &session->media.audio_srtp_local_profile, &session->media.audio_srtp_local_crypto);\n\t\t\t\t}\n\t\t\t\tif(session->media.audio_srtp_tag == 0)\n\t\t\t\t\tsession->media.audio_srtp_tag = 1;\n\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"crypto\", \"%\"SCNi32\" %s inline:%s\",\n\t\t\t\t\tsession->media.audio_srtp_tag, session->media.audio_srtp_local_profile, session->media.audio_srtp_local_crypto);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t}\n\t\t} else if(m->type == JANUS_SDP_VIDEO) {\n\t\t\tm->port = session->media.local_video_rtp_port;\n\t\t\tif(session->media.has_srtp_local) {\n\t\t\t\tif(!session->media.video_srtp_local_profile || !session->media.video_srtp_local_crypto) {\n\t\t\t\t\tjanus_nosip_srtp_set_local(session, TRUE, &session->media.video_srtp_local_profile, &session->media.video_srtp_local_crypto);\n\t\t\t\t}\n\t\t\t\tif(session->media.video_srtp_tag == 0)\n\t\t\t\t\tsession->media.video_srtp_tag = 1;\n\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"crypto\", \"%\"SCNi32\" %s inline:%s\",\n\t\t\t\t\tsession->media.video_srtp_tag, session->media.video_srtp_local_profile, session->media.video_srtp_local_crypto);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t}\n\t\t}\n\t\tg_free(m->c_addr);\n\t\tm->c_addr = g_strdup(sdp_ip ? sdp_ip : local_ip);\n\t\t/* Get rid of some extra attributes to try and keep the SDP short enough */\n\t\tGList *tempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\t/* These are attributes we handle ourselves, the plugins don't need them */\n\t\t\tif(!strcasecmp(a->name, \"mid\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"msid\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"bundle-only\")\n\t\t\t\t\t|| (!strcasecmp(a->name, \"rtcp-fb\") && a->value && strstr(a->value, \"nack pli\") == NULL)\n\t\t\t\t\t|| (!strcasecmp(a->name, \"extmap\") && a->value &&\n\t\t\t\t\t\tstrstr(a->value, JANUS_RTP_EXTMAP_AUDIO_LEVEL) == NULL &&\n\t\t\t\t\t\tstrstr(a->value, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION) == NULL)) {\n\t\t\t\tm->attributes = g_list_remove(m->attributes, a);\n\t\t\t\ttempA = m->attributes;\n\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(answer && (m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO)) {\n\t\t\t/* Check which codec was negotiated eventually */\n\t\t\tint pt = -1;\n\t\t\tif(m->ptypes)\n\t\t\t\tpt = GPOINTER_TO_INT(m->ptypes->data);\n\t\t\tif(pt > -1) {\n\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\tif(pt == opusred_pt) {\n\t\t\t\t\t\tsession->media.opusred_pt = pt;\n\t\t\t\t\t\tsession->media.audio_pt = m->ptypes->next ? GPOINTER_TO_INT(m->ptypes->next->data) : -1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsession->media.audio_pt = pt;\n\t\t\t\t\t}\n\t\t\t\t\tsession->media.audio_pt_name = janus_sdp_get_codec_name(sdp, m->index, session->media.audio_pt);\n\t\t\t\t} else {\n\t\t\t\t\tsession->media.video_pt = pt;\n\t\t\t\t\tsession->media.video_pt_name = janus_sdp_get_codec_name(sdp, m->index, pt);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\t/* Generate a SDP string out of our changes */\n\treturn janus_sdp_write(sdp);\n}\n\nstatic int janus_nosip_bind_socket(int fd, int port) {\n\tgboolean use_ipv6_address_family = !ipv6_disabled &&\n\t\t(janus_network_address_is_null(&janus_network_local_ip) || janus_network_local_ip.family == AF_INET6);\n\tsocklen_t addrlen = use_ipv6_address_family? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);\n\tstruct sockaddr_storage rtp_address = { 0 };\n\tif(use_ipv6_address_family) {\n\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)&rtp_address;\n\t\taddr->sin6_family = AF_INET6;\n\t\taddr->sin6_port = htons(port);\n\t\taddr->sin6_addr = janus_network_address_is_null(&janus_network_local_ip) ? in6addr_any : janus_network_local_ip.ipv6;\n\t} else {\n\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&rtp_address;\n\t\taddr->sin_family = AF_INET;\n\t\taddr->sin_port = htons(port);\n\t\taddr->sin_addr.s_addr = janus_network_address_is_null(&janus_network_local_ip) ? INADDR_ANY : janus_network_local_ip.ipv4.s_addr;\n\t}\n\tif(bind(fd, (struct sockaddr *)(&rtp_address), addrlen) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Bind failed (port %d), error (%s)\\n\", port, g_strerror(errno));\n\t\treturn -1;\n\t}\n\treturn 0;\n}\n\n/* Bind RTP/RTCP port pair */\nstatic int janus_nosip_allocate_port_pair(gboolean video, int fds[2], int ports[2]) {\n\tuint16_t rtp_port_next = rtp_range_slider; \t\t\t\t\t/* Read global slider */\n\tuint16_t rtp_port_start = rtp_port_next;\n\tgboolean rtp_port_wrap = FALSE;\n\n\tgboolean use_ipv6_address_family = !ipv6_disabled &&\n\t\t(janus_network_address_is_null(&janus_network_local_ip) || janus_network_local_ip.family == AF_INET6);\n\n\tint rtp_fd = -1, rtcp_fd = -1;\n\twhile(1) {\n\t\tif(rtp_port_wrap && rtp_port_next >= rtp_port_start) {\t/* Full range scanned */\n\t\t\tJANUS_LOG(LOG_ERR, \"No ports available for %s channel in range: %u -- %u\\n\",\n\t\t\t\t  video ? \"video\" : \"audio\", rtp_range_min, rtp_range_max);\n\t\t\tbreak;\n\t\t}\n\t\tif(rtp_fd == -1) {\n\t\t\trtp_fd = socket(use_ipv6_address_family ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);\n\t\t\tint v6only = 0;\n\t\t\tif(use_ipv6_address_family && rtp_fd != -1 && setsockopt(rtp_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting v6only to false on RTP socket (error=%s)\\n\",\n\t\t\t\t\tg_strerror(errno));\n\t\t\t}\n\t\t\t/* Set the DSCP value if set in the config file */\n\t\t\tif(rtp_fd != -1 && !video && dscp_audio_rtp > 0) {\n\t\t\t\tint optval = dscp_audio_rtp << 2;\n\t\t\t\tint ret = setsockopt(rtp_fd, IPPROTO_IP, IP_TOS, &optval, sizeof(optval));\n\t\t\t\tif(ret < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting IP_TOS %d on audio RTP socket (error=%s)\\n\",\n\t\t\t\t\t\toptval, g_strerror(errno));\n\t\t\t\t}\n\t\t\t} else if(rtp_fd != -1 && video && dscp_video_rtp > 0) {\n\t\t\t\tint optval = dscp_video_rtp << 2;\n\t\t\t\tint ret = setsockopt(rtp_fd, IPPROTO_IP, IP_TOS, &optval, sizeof(optval));\n\t\t\t\tif(ret < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting IP_TOS %d on video RTP socket (error=%s)\\n\",\n\t\t\t\t\t\toptval, g_strerror(errno));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(rtcp_fd == -1) {\n\t\t\tint v6only = 0;\n\t\t\trtcp_fd = socket(use_ipv6_address_family ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);\n\t\t\tif(use_ipv6_address_family && rtcp_fd != -1 && setsockopt(rtcp_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting v6only to false on RTP socket (error=%s)\\n\",\n\t\t\t\t\tg_strerror(errno));\n\t\t\t}\n\t\t}\n\t\tif(rtp_fd == -1 || rtcp_fd == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating %s sockets...\\n\", video ? \"video\" : \"audio\");\n\t\t\tbreak;\n\t\t}\n\t \tint rtp_port = rtp_port_next;\n\t\tint rtcp_port = rtp_port+1;\n\t\tif((uint32_t)(rtp_port_next + 2UL) < rtp_range_max) {\n\t\t\t/* Advance to next pair */\n\t\t\trtp_port_next += 2;\n\t\t} else {\n\t\t\trtp_port_next = rtp_range_min;\n\t\t\trtp_port_wrap = TRUE;\n\t\t}\n\t\tif(janus_nosip_bind_socket(rtp_fd, rtp_port)) {\n\t\t\t/* rtp_fd still unbound, reuse it */\n\t\t} else if(janus_nosip_bind_socket(rtcp_fd, rtcp_port)) {\n\t\t\tclose(rtp_fd);\n\t\t\trtp_fd = -1;\n\t\t\t/* rtcp_fd still unbound, reuse it */\n\t\t} else {\n\t\t\tfds[0] = rtp_fd;\n\t\t\tfds[1] = rtcp_fd;\n\t\t\tports[0] = rtp_port;\n\t\t\tports[1] = rtcp_port;\n\t\t\trtp_range_slider = rtp_port_next;\t\t/* Write global slider */\n\t\t\treturn 0;\n\t\t}\n\t}\n\tif(rtp_fd != -1) {\n\t\tclose(rtp_fd);\n\t}\n\tif(rtcp_fd != -1) {\n\t\tclose(rtcp_fd);\n\t}\n\treturn -1;\n}\n/* Bind local RTP/RTCP sockets */\nstatic int janus_nosip_allocate_local_ports(janus_nosip_session *session, gboolean update) {\n\tif(session == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid session\\n\");\n\t\treturn -1;\n\t}\n\t/* Reset status */\n\tif(!update) {\n\t\tif(session->media.audio_rtp_fd != -1) {\n\t\t\tclose(session->media.audio_rtp_fd);\n\t\t\tsession->media.audio_rtp_fd = -1;\n\t\t}\n\t\tif(session->media.audio_rtcp_fd != -1) {\n\t\t\tclose(session->media.audio_rtcp_fd);\n\t\t\tsession->media.audio_rtcp_fd = -1;\n\t\t}\n\t\tsession->media.local_audio_rtp_port = 0;\n\t\tsession->media.local_audio_rtcp_port = 0;\n\t\tsession->media.audio_ssrc = 0;\n\t\tif(session->media.video_rtp_fd != -1) {\n\t\t\tclose(session->media.video_rtp_fd);\n\t\t\tsession->media.video_rtp_fd = -1;\n\t\t}\n\t\tif(session->media.video_rtcp_fd != -1) {\n\t\t\tclose(session->media.video_rtcp_fd);\n\t\t\tsession->media.video_rtcp_fd = -1;\n\t\t}\n\t\tsession->media.local_video_rtp_port = 0;\n\t\tsession->media.local_video_rtcp_port = 0;\n\t\tsession->media.video_ssrc = 0;\n\t\tif(session->media.pipefd[0] > 0) {\n\t\t\tclose(session->media.pipefd[0]);\n\t\t\tsession->media.pipefd[0] = -1;\n\t\t}\n\t\tif(session->media.pipefd[1] > 0) {\n\t\t\tclose(session->media.pipefd[1]);\n\t\t\tsession->media.pipefd[1] = -1;\n\t\t}\n\t}\n\t/* Start */\n\tif(session->media.has_audio &&\n\t\t\t(session->media.local_audio_rtp_port == 0 || session->media.local_audio_rtcp_port == 0)) {\n\t\tif(session->media.audio_rtp_fd != -1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Audio RTP unbound socket detected, closing ...\\n\");\n\t\t\tclose(session->media.audio_rtp_fd);\n\t\t\tsession->media.audio_rtp_fd = -1;\n\t\t}\n\t\tif(session->media.audio_rtcp_fd != -1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Audio RTCP unbound socket detected, closing ...\\n\");\n\t\t\tclose(session->media.audio_rtcp_fd);\n\t\t\tsession->media.audio_rtcp_fd = -1;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Allocating audio ports:\\n\");\n\t\tint fds[2], ports[2];\n\t\tif(janus_nosip_allocate_port_pair(FALSE, fds, ports)) {\n\t\t\treturn -1;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Audio RTP listener bound to port %d\\n\", ports[0]);\n\t\tJANUS_LOG(LOG_VERB, \"Audio RTCP listener bound to port %d\\n\", ports[1]);\n\t\tsession->media.audio_rtp_fd = fds[0];\n\t\tsession->media.audio_rtcp_fd = fds[1];\n\t\tsession->media.local_audio_rtp_port = ports[0];\n\t\tsession->media.local_audio_rtcp_port = ports[1];\n\t}\n\tif(session->media.has_video &&\n\t\t\t(session->media.local_video_rtp_port == 0 || session->media.local_video_rtcp_port == 0)) {\n\t\tif(session->media.video_rtp_fd != -1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Video RTP unbound socket detected, closing ...\\n\");\n\t\t\tclose(session->media.video_rtp_fd);\n\t\t\tsession->media.video_rtp_fd = -1;\n\t\t}\n\t\tif(session->media.video_rtcp_fd != -1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Video RTCP unbound socket detected, closing ...\\n\");\n\t\t\tclose(session->media.video_rtcp_fd);\n\t\t\tsession->media.video_rtcp_fd = -1;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Allocating video ports:\\n\");\n\t\tint fds[2], ports[2];\n\t\tif(janus_nosip_allocate_port_pair(TRUE, fds, ports)) {\n\t\t\treturn -1;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Video RTP listener bound to port %d\\n\", ports[0]);\n\t\tJANUS_LOG(LOG_VERB, \"Video RTCP listener bound to port %d\\n\", ports[1]);\n\t\tsession->media.video_rtp_fd = fds[0];\n\t\tsession->media.video_rtcp_fd = fds[1];\n\t\tsession->media.local_video_rtp_port = ports[0];\n\t\tsession->media.local_video_rtcp_port = ports[1];\n\t}\n\t/* We need this to quickly interrupt the poll when it's time to update a session or wrap up */\n\tif(!update) {\n\t\tpipe(session->media.pipefd);\n\t} else {\n\t\t/* Something changed: mark this on the session, so that the thread can update the sockets */\n\t\tsession->media.updated = TRUE;\n\t\tif(session->media.pipefd[1] > 0) {\n\t\t\tint code = 1;\n\t\t\tssize_t res = 0;\n\t\t\tdo {\n\t\t\t\tres = write(session->media.pipefd[1], &code, sizeof(int));\n\t\t\t} while(res == -1 && errno == EINTR);\n\t\t}\n\t}\n\treturn 0;\n}\n\n/* Helper method to (re)connect RTP/RTCP sockets */\nstatic void janus_nosip_connect_sockets(janus_nosip_session *session, struct sockaddr_storage *audio_server_addr, struct sockaddr_storage *video_server_addr) {\n\tif(!session || (!audio_server_addr && !video_server_addr))\n\t\treturn;\n\n\tif(session->media.updated) {\n\t\tJANUS_LOG(LOG_VERB, \"Updating session sockets\\n\");\n\t}\n\n\t/* Connect peers (FIXME This pretty much sucks right now) */\n\tif(session->media.remote_audio_rtp_port && audio_server_addr && session->media.audio_rtp_fd != -1) {\n\t\tif(audio_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)audio_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_audio_rtp_port);\n\t\t} else if(audio_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)audio_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_audio_rtp_port);\n\t\t}\n\t\tif(connect(session->media.audio_rtp_fd, (struct sockaddr *)audio_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't connect audio RTP? (%s:%d)\\n\", session,\n\t\t\t\tsession->media.remote_audio_ip, session->media.remote_audio_rtp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t}\n\t}\n\tif(session->media.remote_audio_rtcp_port && audio_server_addr && session->media.audio_rtcp_fd != -1) {\n\t\tif(audio_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)audio_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_audio_rtcp_port);\n\t\t} else if(audio_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)audio_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_audio_rtcp_port);\n\t\t}\n\t\tif(connect(session->media.audio_rtcp_fd, (struct sockaddr *)audio_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't connect audio RTCP? (%s:%d)\\n\", session,\n\t\t\t\tsession->media.remote_audio_ip, session->media.remote_audio_rtcp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t}\n\t}\n\tif(session->media.remote_video_rtp_port && video_server_addr && session->media.video_rtp_fd != -1) {\n\t\tif(video_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)video_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_video_rtp_port);\n\t\t} else if(video_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)video_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_video_rtp_port);\n\t\t}\n\t\tif(connect(session->media.video_rtp_fd, (struct sockaddr *)video_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't connect video RTP? (%s:%d)\\n\", session,\n\t\t\t\tsession->media.remote_video_ip, session->media.remote_video_rtp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t}\n\t}\n\tif(session->media.remote_video_rtcp_port && video_server_addr && session->media.video_rtcp_fd != -1) {\n\t\tif(video_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)video_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_video_rtcp_port);\n\t\t} else if(video_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)video_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_video_rtcp_port);\n\t\t}\n\t\tif(connect(session->media.video_rtcp_fd, (struct sockaddr *)video_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't connect video RTCP? (%s:%d)\\n\", session,\n\t\t\t\tsession->media.remote_video_ip, session->media.remote_video_rtcp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t}\n\t}\n\n}\n\nstatic void janus_nosip_media_cleanup(janus_nosip_session *session) {\n\tif(session->media.audio_rtp_fd != -1) {\n\t\tclose(session->media.audio_rtp_fd);\n\t\tsession->media.audio_rtp_fd = -1;\n\t}\n\tif(session->media.audio_rtcp_fd != -1) {\n\t\tclose(session->media.audio_rtcp_fd);\n\t\tsession->media.audio_rtcp_fd = -1;\n\t}\n\tsession->media.local_audio_rtp_port = 0;\n\tsession->media.local_audio_rtcp_port = 0;\n\tsession->media.remote_audio_rtp_port = 0;\n\tsession->media.remote_audio_rtcp_port = 0;\n\tsession->media.audio_ssrc = 0;\n\tsession->media.audio_ssrc_peer = 0;\n\tif(session->media.video_rtp_fd != -1) {\n\t\tclose(session->media.video_rtp_fd);\n\t\tsession->media.video_rtp_fd = -1;\n\t}\n\tif(session->media.video_rtcp_fd != -1) {\n\t\tclose(session->media.video_rtcp_fd);\n\t\tsession->media.video_rtcp_fd = -1;\n\t}\n\tsession->media.local_video_rtp_port = 0;\n\tsession->media.local_video_rtcp_port = 0;\n\tsession->media.remote_video_rtp_port = 0;\n\tsession->media.remote_video_rtcp_port = 0;\n\tsession->media.video_ssrc = 0;\n\tsession->media.video_ssrc_peer = 0;\n\tsession->media.simulcast_ssrc = 0;\n\tif(session->media.pipefd[0] > 0) {\n\t\tclose(session->media.pipefd[0]);\n\t\tsession->media.pipefd[0] = -1;\n\t}\n\tif(session->media.pipefd[1] > 0) {\n\t\tclose(session->media.pipefd[1]);\n\t\tsession->media.pipefd[1] = -1;\n\t}\n\t/* Clean up SRTP stuff, if needed */\n\tjanus_nosip_srtp_cleanup(session);\n\n\t/* Media fields not cleaned up elsewhere */\n\tjanus_nosip_media_reset(session);\n}\n\n/* Thread to relay RTP/RTCP frames coming from the peer */\nstatic void *janus_nosip_relay_thread(void *data) {\n\tjanus_nosip_session *session = (janus_nosip_session *)data;\n\tif(!session) {\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_INFO, \"[NoSIP-%p] Starting relay thread\\n\", session);\n\n\t/* File descriptors */\n\tsocklen_t addrlen;\n\tstruct sockaddr_in remote = { 0 };\n\tint resfd = 0, bytes = 0, pollerrs = 0;\n\tstruct pollfd fds[5];\n\tint pipe_fd = session->media.pipefd[0];\n\tchar buffer[1500];\n\tmemset(buffer, 0, 1500);\n\tif(pipe_fd == -1) {\n\t\t/* If the pipe file descriptor doesn't exist, it means we're done already,\n\t\t * and/or we may never be notified about sessions being closed, so give up */\n\t\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] Leaving thread, no pipe file descriptor...\\n\", session);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\t/* Loop */\n\tint num = 0;\n\tgboolean goon = TRUE;\n\n\tsession->media.updated = TRUE; /* Connect UDP sockets upon loop entry */\n\tgboolean have_audio_server_ip = TRUE;\n\tgboolean have_video_server_ip = TRUE;\n\n\twhile(goon && session != NULL &&\n\t\t\t!g_atomic_int_get(&session->destroyed) && !g_atomic_int_get(&session->hangingup)) {\n\n\t\tif(session->media.updated) {\n\t\t\t/* Apparently there was a session update, or the loop has just been entered */\n\t\t\tsession->media.updated = FALSE;\n\n\t\t\t/* Resolve the addresses, if needed */\n\t\t\thave_audio_server_ip = FALSE;\n\t\t\thave_video_server_ip = FALSE;\n\t\t\tstruct sockaddr_storage audio_server_addr = { 0 }, video_server_addr = { 0 };\n\t\t\tif(session->media.remote_audio_ip && strcmp(session->media.remote_audio_ip, \"0.0.0.0\")) {\n\t\t\t\tif(janus_network_resolve_address(session->media.remote_audio_ip, &audio_server_addr) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't resolve audio address '%s'\\n\",\n\t\t\t\t\t\tsession, session->media.remote_audio_ip);\n\t\t\t\t} else {\n\t\t\t\t\t/* Address resolved */\n\t\t\t\t\thave_audio_server_ip = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->media.remote_video_ip && strcmp(session->media.remote_video_ip, \"0.0.0.0\")) {\n\t\t\t\tif(janus_network_resolve_address(session->media.remote_video_ip, &video_server_addr) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't resolve video address '%s'\\n\",\n\t\t\t\t\t\tsession, session->media.remote_video_ip);\n\t\t\t\t} else {\n\t\t\t\t\t/* Address resolved */\n\t\t\t\t\thave_video_server_ip = TRUE;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif(have_audio_server_ip || have_video_server_ip) {\n\t\t\t\tjanus_nosip_connect_sockets(session, have_audio_server_ip ? &audio_server_addr : NULL,\n\t\t\t\t\thave_video_server_ip ? &video_server_addr : NULL);\n\t\t\t} else if (session->media.remote_audio_ip == NULL && session->media.remote_video_ip == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't update session details: both audio and video remote IP addresses are NULL\\n\", session);\n\t\t\t} else {\n\t\t\t\tif(session->media.remote_audio_ip)\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't update session details: audio remote IP address (%s) is invalid\\n\",\n\t\t\t\t\t\tsession, session->media.remote_audio_ip);\n\t\t\t\tif(session->media.remote_video_ip)\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Couldn't update session details: video remote IP address (%s) is invalid\\n\",\n\t\t\t\t\t\tsession, session->media.remote_video_ip);\n\t\t\t}\n\t\t}\n\n\t\t/* Prepare poll */\n\t\tnum = 0;\n\t\tif(session->media.audio_rtp_fd != -1) {\n\t\t\tfds[num].fd = session->media.audio_rtp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(session->media.audio_rtcp_fd != -1) {\n\t\t\tfds[num].fd = session->media.audio_rtcp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(session->media.video_rtp_fd != -1) {\n\t\t\tfds[num].fd = session->media.video_rtp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(session->media.video_rtcp_fd != -1) {\n\t\t\tfds[num].fd = session->media.video_rtcp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\t/* Finally, let's add the pipe */\n\t\tpipe_fd = session->media.pipefd[0];\n\t\tif(pipe_fd == -1) {\n\t\t\t/* Pipe was closed? Means the call is over */\n\t\t\tbreak;\n\t\t}\n\t\tfds[num].fd = pipe_fd;\n\t\tfds[num].events = POLLIN;\n\t\tfds[num].revents = 0;\n\t\tnum++;\n\t\t/* Wait for some data */\n\t\tresfd = poll(fds, num, 1000);\n\t\tif(resfd < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Got an EINTR (%s), ignoring...\\n\", session, g_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Error polling...\\n\", session);\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p]   -- %d (%s)\\n\", session, errno, g_strerror(errno));\n\t\t\tbreak;\n\t\t} else if(resfd == 0) {\n\t\t\t/* No data, keep going */\n\t\t\tcontinue;\n\t\t}\n\t\tif(session == NULL || g_atomic_int_get(&session->destroyed))\n\t\t\tbreak;\n\t\tint i = 0;\n\t\tfor(i=0; i<num; i++) {\n\t\t\tif(fds[i].revents & (POLLERR | POLLHUP)) {\n\t\t\t\t/* If we just updated the session, let's wait until things have calmed down */\n\t\t\t\tif(session->media.updated)\n\t\t\t\t\tbreak;\n\t\t\t\t/* Check the socket error */\n\t\t\t\tint error = 0;\n\t\t\t\tsocklen_t errlen = sizeof(error);\n\t\t\t\tgetsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen);\n\t\t\t\tif(error == 0) {\n\t\t\t\t\t/* Maybe not a breaking error after all? */\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(error == 111) {\n\t\t\t\t\t/* ICMP error? If it's related to RTCP, let's just close the RTCP socket and move on */\n\t\t\t\t\tif(fds[i].fd == session->media.audio_rtcp_fd) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] Got a '%s' on the audio RTCP socket, closing it\\n\",\n\t\t\t\t\t\t\tsession, g_strerror(error));\n\t\t\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t\t\tclose(session->media.audio_rtcp_fd);\n\t\t\t\t\t\tsession->media.audio_rtcp_fd = -1;\n\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t} else if(fds[i].fd == session->media.video_rtcp_fd) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[NoSIP-%p] Got a '%s' on the video RTCP socket, closing it\\n\",\n\t\t\t\t\t\t\tsession, g_strerror(error));\n\t\t\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t\t\tclose(session->media.video_rtcp_fd);\n\t\t\t\t\t\tsession->media.video_rtcp_fd = -1;\n\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* FIXME Should we be more tolerant of ICMP errors on RTP sockets as well? */\n\t\t\t\tpollerrs++;\n\t\t\t\tif(pollerrs < 100)\n\t\t\t\t\tcontinue;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Too many errors polling %d (socket #%d): %s...\\n\", session,\n\t\t\t\t\tfds[i].fd, i, fds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\");\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p]   -- %d (%s)\\n\", session, error, g_strerror(error));\n\t\t\t\t/* Can we assume it's pretty much over, after a POLLERR? */\n\t\t\t\tgoon = FALSE;\n\t\t\t\t/* FIXME Close the PeerConnection */\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\tbreak;\n\t\t\t} else if(fds[i].revents & POLLIN) {\n\t\t\t\tif(pipe_fd != -1 && fds[i].fd == pipe_fd) {\n\t\t\t\t\t/* Poll interrupted for a reason, go on */\n\t\t\t\t\tint code = 0;\n\t\t\t\t\t(void)read(pipe_fd, &code, sizeof(int));\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* Got an RTP/RTCP packet */\n\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr*)&remote, &addrlen);\n\t\t\t\tif(bytes < 0) {\n\t\t\t\t\t/* Failed to read? */\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Let's check what this is */\n\t\t\t\tgboolean video = fds[i].fd == session->media.video_rtp_fd || fds[i].fd == session->media.video_rtcp_fd;\n\t\t\t\tgboolean rtcp = fds[i].fd == session->media.audio_rtcp_fd || fds[i].fd == session->media.video_rtcp_fd;\n\t\t\t\tif(!rtcp) {\n\t\t\t\t\t/* Audio or Video RTP */\n\t\t\t\t\tif(!janus_is_rtp(buffer, bytes)) {\n\t\t\t\t\t\t/* Not an RTP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpollerrs = 0;\n\t\t\t\t\trtp_header *header = (rtp_header *)buffer;\n\t\t\t\t\tif((video && session->media.video_ssrc_peer != ntohl(header->ssrc)) ||\n\t\t\t\t\t\t\t(!video && session->media.audio_ssrc_peer != ntohl(header->ssrc))) {\n\t\t\t\t\t\tif(video && session->media.video_ssrc_peer == 0) {\n\t\t\t\t\t\t\tsession->media.video_ssrc_peer = ntohl(header->ssrc);\n\t\t\t\t\t\t} else if(!video && session->media.audio_ssrc_peer == 0) {\n\t\t\t\t\t\t\tsession->media.audio_ssrc_peer = ntohl(header->ssrc);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[NoSIP-%p] Got SIP peer %s SSRC: %\"SCNu32\"\\n\",\n\t\t\t\t\t\t\tsession, video ? \"video\" : \"audio\",\n\t\t\t\t\t\t\tvideo ? session->media.video_ssrc_peer : session->media.audio_ssrc_peer);\n\t\t\t\t\t}\n\t\t\t\t\t/* Is this SRTP? */\n\t\t\t\t\tif(session->media.has_srtp_remote) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect(\n\t\t\t\t\t\t\t(video ? session->media.video_srtp_in : session->media.audio_srtp_in),\n\t\t\t\t\t\t\tbuffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok && res != srtp_err_status_replay_fail && res != srtp_err_status_replay_old) {\n\t\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] %s SRTP unprotect error: %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")\\n\",\n\t\t\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\", janus_srtp_error_str(res), bytes, buflen, timestamp, seq);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* Check if the SSRC changed (e.g., after a re-INVITE or UPDATE) */\n\t\t\t\t\tjanus_rtp_header_update(header, video ? &session->media.vcontext : &session->media.acontext, video, 0);\n\t\t\t\t\t/* Check if there are forwarders interested in this traffic */\n\t\t\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\t\t\tGHashTableIter iter;\n\t\t\t\t\tgpointer value;\n\t\t\t\t\tg_hash_table_iter_init(&iter, video ? session->peer_video_forwarders : session->peer_audio_forwarders);\n\t\t\t\t\twhile(session->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\t\t\t\t\tif((!video && rtp_forward->is_video) || (video && !rtp_forward->is_video))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\tjanus_rtp_forwarder_send_rtp(rtp_forward, buffer, bytes, 0);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t\t\t/* Save the frame if we're recording */\n\t\t\t\t\theader->ssrc = htonl(video ? session->media.video_ssrc_peer : session->media.audio_ssrc_peer);\n\t\t\t\t\tjanus_recorder_save_frame(video ? session->vrc_peer : session->arc_peer, buffer, bytes);\n\t\t\t\t\t/* Relay to browser */\n\t\t\t\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = video, .buffer = buffer, .length = bytes };\n\t\t\t\t\t/* Add audio-level extension, if present */\n\t\t\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\t\t\tif(!video && session->media.audio_level_extension_id != -1) {\n\t\t\t\t\t\tgboolean vad = FALSE;\n\t\t\t\t\t\tint level = -1;\n\t\t\t\t\t\tif(janus_rtp_header_extension_parse_audio_level(buffer, bytes,\n\t\t\t\t\t\t\t\tsession->media.audio_level_extension_id, &vad, &level) == 0) {\n\t\t\t\t\t\t\trtp.extensions.audio_level = level;\n\t\t\t\t\t\t\trtp.extensions.audio_level_vad = vad;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(video && session->media.video_orientation_extension_id > 0) {\n\t\t\t\t\t\tgboolean c = FALSE, f = FALSE, r1 = FALSE, r0 = FALSE;\n\t\t\t\t\t\tif(janus_rtp_header_extension_parse_video_orientation(buffer, bytes,\n\t\t\t\t\t\t\t\tsession->media.video_orientation_extension_id, &c, &f, &r1, &r0) == 0) {\n\t\t\t\t\t\t\trtp.extensions.video_rotation = 0;\n\t\t\t\t\t\t\tif(r1 && r0)\n\t\t\t\t\t\t\t\trtp.extensions.video_rotation = 270;\n\t\t\t\t\t\t\telse if(r1)\n\t\t\t\t\t\t\t\trtp.extensions.video_rotation = 180;\n\t\t\t\t\t\t\telse if(r0)\n\t\t\t\t\t\t\t\trtp.extensions.video_rotation = 90;\n\t\t\t\t\t\t\trtp.extensions.video_back_camera = c;\n\t\t\t\t\t\t\trtp.extensions.video_flipped = f;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\t/* Audio or Video RTCP */\n\t\t\t\t\tif(!janus_is_rtcp(buffer, bytes)) {\n\t\t\t\t\t\t/* Not an RTCP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(session->media.has_srtp_remote) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect_rtcp(\n\t\t\t\t\t\t\t(video ? session->media.video_srtp_in : session->media.audio_srtp_in),\n\t\t\t\t\t\t\tbuffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok && res != srtp_err_status_replay_fail && res != srtp_err_status_replay_old) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] %s SRTCP unprotect error: %s (len=%d-->%d)\\n\",\n\t\t\t\t\t\t\t\tsession, video ? \"Video\" : \"Audio\", janus_srtp_error_str(res), bytes, buflen);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* Relay to browser */\n\t\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = -1, .video = video, .buffer = buffer, bytes };\n\t\t\t\t\tgateway->relay_rtcp(session->handle, &rtcp);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* Cleanup the media session */\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_nosip_media_cleanup(session);\n\tjanus_mutex_unlock(&session->mutex);\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"Leaving NoSIP relay thread\\n\");\n\tsession->relayer_thread = NULL;\n\tjanus_refcount_decrease(&session->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\n/* Helper method to send an RTCP PLI to the peer */\nstatic void janus_nosip_rtcp_pli_send(janus_nosip_session *session) {\n\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(!session->media.has_video || session->media.video_rtcp_fd == -1)\n\t\treturn;\n\t/* Generate a PLI */\n\tchar rtcp_buf[12];\n\tint rtcp_len = 12;\n\tjanus_rtcp_pli((char *)&rtcp_buf, rtcp_len);\n\t/* Fix SSRCs as the Janus core does */\n\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Fixing SSRCs (local %u, peer %u)\\n\",\n\t\tsession, session->media.video_ssrc, session->media.video_ssrc_peer);\n\tjanus_rtcp_fix_ssrc(NULL, (char *)rtcp_buf, rtcp_len, 1, session->media.video_ssrc, session->media.video_ssrc_peer);\n\t/* Is SRTP involved? */\n\tif(session->media.has_srtp_local) {\n\t\tchar sbuf[50];\n\t\tmemcpy(&sbuf, rtcp_buf, rtcp_len);\n\t\tint protected = rtcp_len;\n\t\tint res = srtp_protect_rtcp(session->media.video_srtp_out, &sbuf, &protected);\n\t\tif(res != srtp_err_status_ok) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[NoSIP-%p] Video SRTCP protect error... %s (len=%d-->%d)...\\n\",\n\t\t\t\tsession, janus_srtp_error_str(res), rtcp_len, protected);\n\t\t} else {\n\t\t\t/* Forward the message to the peer */\n\t\t\tif(send(session->media.video_rtcp_fd, sbuf, protected, 0) < 0) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Error sending SRTCP video packet... %s (len=%d)...\\n\",\n\t\t\t\t\tsession, g_strerror(errno), protected);\n\t\t\t}\n\t\t}\n\t} else {\n\t\t/* Forward the message to the peer */\n\t\tif(send(session->media.video_rtcp_fd, rtcp_buf, rtcp_len, 0) < 0) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"[NoSIP-%p] Error sending RTCP video packet... %s (len=%d)...\\n\",\n\t\t\t\tsession, g_strerror(errno), rtcp_len);\n\t\t}\n\t}\n}\n\n/* RTP forwarder helpers */\nstatic janus_rtp_forwarder *janus_nosip_rtp_forwarder_add_helper(janus_nosip_session *session, const char *type,\n\t\tconst gchar *host, int port, int pt, uint32_t ssrc, int srtp_suite, const char *srtp_crypto) {\n\tif(!session || !type || !host)\n\t\treturn NULL;\n\tgboolean is_video = !strcasecmp(type, \"video\") || !strcasecmp(type, \"peer_video\");\n\tgboolean is_peer = !strcasecmp(type, \"peer_audio\") || !strcasecmp(type, \"peer_video\");\n\t/* Create a new RTP forwarder */\n\tjanus_rtp_forwarder *rf = janus_rtp_forwarder_create(JANUS_NOSIP_NAME, 0,\n\t\tsession->udp_sock, host, port, ssrc, pt, srtp_suite, srtp_crypto, FALSE, 0, is_video, FALSE);\n\tif(rf == NULL)\n\t\treturn NULL;\n\trf->metadata = g_strdup(type);\n\t/* Add the forwarder to the ones we have for the publisher stream */\n\tg_hash_table_insert(session->all_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\tif(!is_video && !is_peer) {\n\t\tg_hash_table_insert(session->audio_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t} else if(is_video && !is_peer) {\n\t\tg_hash_table_insert(session->video_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t\tgateway->send_pli(session->handle);\n\t} else if(!is_video && is_peer) {\n\t\tg_hash_table_insert(session->peer_audio_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t} else if(is_video && is_peer) {\n\t\tg_hash_table_insert(session->peer_video_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t\tjanus_nosip_rtcp_pli_send(session);\n\t}\n\t/* Done */\n\tJANUS_LOG(LOG_VERB, \"[NoSIP-%p] Added %s' %s rtp_forward: %s:%d stream_id: %\"SCNu32\"\\n\",\n\t\tsession, is_peer ? \"peer\" : \"user\", is_video ? \"video\" : \"audio\", host, port, rf->stream_id);\n\treturn rf;\n}\n\nstatic json_t *janus_nosip_rtp_forwarder_summary(janus_rtp_forwarder *f) {\n\tif(f == NULL)\n\t\treturn NULL;\n\tjson_t *json = json_object();\n\tjson_object_set_new(json, \"stream_id\", json_integer(f->stream_id));\n\tif(f->metadata)\n\t\tjson_object_set_new(json, \"type\", json_string((const char *)f->metadata));\n\tchar address[100];\n\tif(f->serv_addr.sin_family == AF_INET) {\n\t\tjson_object_set_new(json, \"host\", json_string(\n\t\t\tinet_ntop(AF_INET, &f->serv_addr.sin_addr, address, sizeof(address))));\n\t} else {\n\t\tjson_object_set_new(json, \"host\", json_string(\n\t\t\tinet_ntop(AF_INET6, &f->serv_addr6.sin6_addr, address, sizeof(address))));\n\t}\n\tjson_object_set_new(json, \"port\", json_integer(ntohs(f->serv_addr.sin_port)));\n\tjson_object_set_new(json, \"media\", json_string(f->is_video ? \"video\" : \"audio\"));\n\tif(f->payload_type > 0)\n\t\tjson_object_set_new(json, \"pt\", json_integer(f->payload_type));\n\tif(f->ssrc)\n\t\tjson_object_set_new(json, \"ssrc\", json_integer(f->ssrc));\n\tif(f->is_srtp)\n\t\tjson_object_set_new(json, \"srtp\", json_true());\n\treturn json;\n}\n"
  },
  {
    "path": "src/plugins/janus_recordplay.c",
    "content": "/*! \\file   janus_recordplay.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Record&Play plugin\n * \\details Check the \\ref recordplay for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page recordplay Record&Play plugin documentation\n * This is a simple application that implements two different\n * features: it allows you to record a message you send with WebRTC in\n * the format defined in recorded.c (MJR recording) and subsequently\n * replay this recording (or other previously recorded) through WebRTC\n * as well. For more information on how Janus implements recordings\n * natively and the MJR format, refer to the \\ref recordings documentation.\n *\n * This application aims at showing how easy recording frames sent by\n * a peer is, and how this recording can be re-used directly, without\n * necessarily involving a post-processing process (e.g., through the\n * tool we provide in janus-pp-rec.c). Notice that only audio and video\n * can be recorded and replayed in this plugin: if you're interested in\n * recording data channel messages (which Janus and the .mjr format do\n * support), you should use a different plugin instead.\n *\n * The configuration process is relatively straightforward: in the simplest\n * configuration, you just choose where the recordings should be saved.\n * The same folder will also be used to list the available recordings that\n * can be replayed.\n *\n * Notice that, by default, all recordings are public, which means any\n * user connecting to the plugin and asking for a list of recordings will\n * be able to obtain their IDs. Recordings can be marked as private to\n * avoid that, meaning that users will only be able to consume such a\n * recording if they're aware of its ID via out of band mechanisms.\n * Marking a recording as private can be done in two different ways:\n *\n * -# via the API, that is when the recording is created;\n * -# by setting the \\c private property to \\c true in the plugin\n * configuration file, which will automatically mark all new recordings\n * as private by default unless otherwise specified in the API request.\n *\n * \\note The application creates a special file in INI format with\n * <tt>.nfo</tt> extension for each recording that is saved. This is necessary\n * to map a specific audio .mjr file to a different video .mjr one, as\n * they always get saved in different files. If you want to replay\n * recordings you took in a different application (e.g., the streaming\n * or videoroom plugins) just copy the related files in the folder you\n * configured this plugin to use and create a .nfo file in the same\n * folder to create a mapping, e.g.:\n *\n * \t\t[12345678]\n * \t\tname = My videoroom recording\n * \t\tdate = 2014-10-14 17:11:26\n * \t\tprivate = false\n * \t\taudio = videoroom-audio.mjr\n * \t\tvideo = videoroom-video.mjr\n *\n * Data channel recordings are supported via a \\c data attribute as well.\n *\n * \\section recplayapi Record&Play API\n *\n * The Record&Play API supports several requests, some of which are\n * synchronous and some asynchronous. There are some situations, though,\n * (invalid JSON, invalid request) which will always result in a\n * synchronous error response even for asynchronous requests.\n *\n * \\c list , \\c update and \\c configure are synchronous requests, which means you'll\n * get a response directly within the context of the transaction. \\c list\n * lists all the available recordings, while \\c update forces the plugin\n * to scan the folder of recordings again in case some were added manually\n * and not indexed in the meanwhile. The \\c configure request can be used\n * to tweak some settings while recording a session.\n *\n * The \\c record , \\c play , \\c start , \\c pause , \\c resume and \\c stop requests instead are\n * all asynchronous, which means you'll get a notification about their\n * success or failure in an event. \\c record asks the plugin to start\n * recording a session; \\c play asks the plugin to prepare the playout\n * of one of the previously recorded sessions; \\c start starts the\n * actual playout, and \\c stop stops whatever the session was for, i.e.,\n * recording or replaying. Recording sessions can also be dynamically\n * paused and resumed using \\c pause and \\c resume : the pauses will be\n * omitted from the recording, meaning recordings will not have holes\n * in them, and will move from one section to the other with no pause.\n *\n * The \\c list request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"list\",\n\t\"admin_key\" : \"<plugin administrator key; optional>\"\n}\n\\endverbatim\n *\n * A successful request will result in an array of recordings:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"list\",\n\t\"list\": [\t// Array of recording objects\n\t\t{\t\t\t// Recording #1\n\t\t\t\"id\": <numeric ID>,\n\t\t\t\"name\": \"<Name of the recording>\",\n\t\t\t\"date\": \"<Date of the recording>\",\n\t\t\t\"audio\": \"<Audio rec file, if any; optional>\",\n\t\t\t\"video\": \"<Video rec file, if any; optional>\",\n\t\t\t\"data\": \"<Data rec file, if any; optional>\",\n\t\t\t\"audio_codec\": \"<Audio codec, if any; optional>\",\n\t\t\t\"video_codec\": \"<Video codec, if any; optional>\"\n\t\t},\n\t\t<other recordings>\n\t]\n}\n\\endverbatim\n *\n * An error instead (and the same applies to all other requests, so this\n * won't be repeated) would provide both an error code and a more verbose\n * description of the cause of the issue:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"error_code\" : <numeric ID, check Macros below>,\n\t\"error\" : \"<error description as a string>\"\n}\n\\endverbatim\n *\n * Notice that, as explained previously, the \\c list request will only\n * return the list of all recordings who were \\b not marked as private.\n * To return the list of private recordings as well, the right \\c admin_key\n * must be provided as well. In case no \\c admin_key was configured,\n * then the list of private recordings will never be returned.\n *\n * The \\c update request instead has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"update\",\n\t\"admin_key\" : \"<plugin administrator key; mandatory, if configured>\"\n}\n\\endverbatim\n *\n * which will always result in an immediate ack ( \\c ok ):\n *\n\\verbatim\n{\n\t\"recordplay\" : \"ok\",\n}\n\\endverbatim\n *\n * Notice that, if an \\c admin_key was configured in the configuration\n * file, it is a mandatory property to pass in an \\c update request as\n * well.\n *\n * Coming to the asynchronous requests, \\c record has to be attached to\n * a JSEP offer (failure to do so will result in an error) and has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"record\",\n\t\"id\" : <unique numeric ID for the recording; optional, will be chosen by the server if missing>\n\t\"name\" : \"<Pretty name for the recording>\",\n\t\"is_private\" : <true|false, whether the recording should be listable; the default is what was configured in the plugin config file>,\n\t\"filename\" : \"<Base path/name for the file (media type and extension added by the plugin); optional>\",\n\t\"audiocodec\" : \"<name of the audio codec we prefer for the recording; optional>\",\n\t\"videocodec\" : \"<name of the video codec we prefer for the recording; optional>\",\n\t\"videoprofile\" : \"<in case the video codec supports, profile to use (e.g., \"2\" for VP9, or \"42e01f\" for H.264); optional>\",\n\t\"opusred\" : <true|false, whether RED should be negotiated for audio, if offered; optional (default=false)>,\n\t\"textdata\" : \"<in case data channels have to be recorded, whether the data will be text (default) or binary; optional>\"\n}\n\\endverbatim\n *\n * A successful management of this request will result in a \\c recording\n * event which will include the unique ID of the recording and a JSEP\n * answer to complete the setup of the associated PeerConnection to record:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"recording\",\n\t\t\"id\" : <unique numeric ID>,\n\t\t\"is_private\" : <true|false, same as the request>\n\t}\n}\n\\endverbatim\n *\n * Some properties of the recording session can be tweaked dynamically\n * via the \\c configure request, and has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"configure\",\n\t\"video-bitrate-max\", <bitrate cap that should be forced (via REMB) on the recorder>,\n\t\"video-keyframe-interval\", <how often, in seconds, the plugin should send a PLI to the recorder to request a keyframe>\n}\n\\endverbatim\n *\n * A successful request will result in a confirmation of :\n *\n\\verbatim\n{\n\t\"recordplay\" : \"configure\",\n\t\"status\" : \"ok\",\n\t\"settings\" : {\n\t\t\"video-bitrate-max\" : <current value of the property>,\n\t\t\"video-keyframe-interval\" : <current value of the property>\n\t}\n}\n\\endverbatim\n *\n * You can pause the recording process (meaning that RTP packets will keep\n * on flowing but will not be recorded to file) using the \\c pause request:\n *\n\\verbatim\n{\n\t\"request\" : \"pause\",\n}\n\\endverbatim\n *\n * This will result in a \\c paused status:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"paused\",\n\t\t\"id\" : <unique numeric ID of the paused recording>\n\t}\n}\n\\endverbatim\n *\n * To resume the recording process you can use the \\c resume request:\n *\n\\verbatim\n{\n\t\"request\" : \"resume\",\n}\n\\endverbatim\n *\n * This will result in a \\c resumed status:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"resumed\",\n\t\t\"id\" : <unique numeric ID of the paused recording>\n\t}\n}\n\\endverbatim\n *\n * A \\c stop request can interrupt the recording process and tear the\n * associated PeerConnection down:\n *\n\\verbatim\n{\n\t\"request\" : \"stop\",\n}\n\\endverbatim\n *\n * This will result in a \\c stopped status:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"stopped\",\n\t\t\"id\" : <unique numeric ID of the interrupted recording>,\n\t\t\"is_private\" : <whether the interrupted recording is private>\n\t}\n}\n\\endverbatim\n *\n * For what concerns the playout, instead, the process is slightly\n * different: you first choose a recording to replay, using \\c play ,\n * and then start its playout using a \\c start request. Just as before,\n * a \\c stop request will interrupt the playout and tear the PeerConnection\n * down. It's very important to point out that no JSEP offer must be\n * sent for replaying a recording: in this case, it will always be the\n * plugin to generate a JSON offer (in response to a \\c play request),\n * which means you'll then have to provide a JSEP answer within the\n * context of the following \\c start request which will close the circle.\n * Notice that \\c play can be used to replay private recordings as well:\n * a recording marked as private is simply not returned when retrieving\n * the list of available recordings, but if the user is aware of a\n * recording ID through other means, then that recording can be replayed\n * as all other non-private recordings.\n *\n * A \\c play request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"play\",\n\t\"id\" : <unique numeric ID of the recording to replay>\n}\n\\endverbatim\n *\n * This will result in a \\c preparing status notification which will be\n * attached to the JSEP offer originated by the plugin in order to\n * match the media available in the recording:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"preparing\",\n\t\t\"id\" : <unique numeric ID of the recording>\n\t}\n}\n\\endverbatim\n *\n * A \\c start request, which as anticipated must be attached to the JSEP\n * answer to the previous offer sent by the plugin, has to be formatted\n * as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"start\",\n}\n\\endverbatim\n *\n * This will result in a \\c playing status notification:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"playing\"\n\t}\n}\n\\endverbatim\n *\n * Just as before, a \\c stop request can interrupt the playout process at\n * any time, and tear the associated PeerConnection down:\n *\n\\verbatim\n{\n\t\"request\" : \"stop\",\n}\n\\endverbatim\n *\n * This will result in a \\c stopped status:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"stopped\"\n\t}\n}\n\\endverbatim\n *\n * If the plugin detects a loss of the associated PeerConnection, whether\n * as a result of a \\c stop request or because the connection was closed,\n * a \\c done result notification is triggered to inform the application\n * the recording/playout session is over:\n *\n\\verbatim\n{\n\t\"recordplay\" : \"event\",\n\t\"result\": {\n\t\t\"status\" : \"done\",\n\t\t\"id\" : <unique numeric ID of the completed recording>,\n\t\t\"is_private\" : <whether the completed recording is private>\n\t}\n}\n\\endverbatim\n */\n\n#include \"plugin.h\"\n\n#include <dirent.h>\n#include <arpa/inet.h>\n#include <sys/stat.h>\n#include <sys/time.h>\n#include <jansson.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../record.h\"\n#include \"../sdp-utils.h\"\n#include \"../rtp.h\"\n#include \"../rtcp.h\"\n#include \"../utils.h\"\n\n\n/* Plugin information */\n#define JANUS_RECORDPLAY_VERSION\t\t\t5\n#define JANUS_RECORDPLAY_VERSION_STRING\t\t\"0.0.5\"\n#define JANUS_RECORDPLAY_DESCRIPTION\t\t\"This is a trivial Record&Play plugin for Janus, to record WebRTC sessions and replay them.\"\n#define JANUS_RECORDPLAY_NAME\t\t\t\t\"JANUS Record&Play plugin\"\n#define JANUS_RECORDPLAY_AUTHOR\t\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_RECORDPLAY_PACKAGE\t\t\t\"janus.plugin.recordplay\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_recordplay_init(janus_callbacks *callback, const char *onfig_path);\nvoid janus_recordplay_destroy(void);\nint janus_recordplay_get_api_compatibility(void);\nint janus_recordplay_get_version(void);\nconst char *janus_recordplay_get_version_string(void);\nconst char *janus_recordplay_get_description(void);\nconst char *janus_recordplay_get_name(void);\nconst char *janus_recordplay_get_author(void);\nconst char *janus_recordplay_get_package(void);\nvoid janus_recordplay_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_recordplay_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_recordplay_handle_admin_message(json_t *message);\nvoid janus_recordplay_setup_media(janus_plugin_session *handle);\nvoid janus_recordplay_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_recordplay_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_recordplay_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet);\nvoid janus_recordplay_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink);\nvoid janus_recordplay_hangup_media(janus_plugin_session *handle);\nvoid janus_recordplay_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_recordplay_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_recordplay_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_recordplay_init,\n\t\t.destroy = janus_recordplay_destroy,\n\n\t\t.get_api_compatibility = janus_recordplay_get_api_compatibility,\n\t\t.get_version = janus_recordplay_get_version,\n\t\t.get_version_string = janus_recordplay_get_version_string,\n\t\t.get_description = janus_recordplay_get_description,\n\t\t.get_name = janus_recordplay_get_name,\n\t\t.get_author = janus_recordplay_get_author,\n\t\t.get_package = janus_recordplay_get_package,\n\n\t\t.create_session = janus_recordplay_create_session,\n\t\t.handle_message = janus_recordplay_handle_message,\n\t\t.handle_admin_message = janus_recordplay_handle_admin_message,\n\t\t.setup_media = janus_recordplay_setup_media,\n\t\t.incoming_rtp = janus_recordplay_incoming_rtp,\n\t\t.incoming_rtcp = janus_recordplay_incoming_rtcp,\n\t\t.incoming_data = janus_recordplay_incoming_data,\n\t\t.slow_link = janus_recordplay_slow_link,\n\t\t.hangup_media = janus_recordplay_hangup_media,\n\t\t.destroy_session = janus_recordplay_destroy_session,\n\t\t.query_session = janus_recordplay_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_RECORDPLAY_NAME);\n\treturn &janus_recordplay_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter list_parameters[] = {\n\t{\"admin_key\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter adminkey_parameters[] = {\n\t{\"admin_key\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"video-bitrate-max\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video-keyframe-interval\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter record_parameters[] = {\n\t{\"name\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_NONEMPTY},\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"is_private\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"videocodec\", JSON_STRING, 0},\n\t{\"videoprofile\", JSON_STRING, 0},\n\t{\"opusred\", JANUS_JSON_BOOL, 0},\n\t{\"textdata\", JANUS_JSON_BOOL, 0},\n\t{\"update\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter play_parameters[] = {\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"restart\", JANUS_JSON_BOOL, 0}\n};\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean private_recordings = FALSE;\nstatic char *admin_key = NULL;\nstatic gboolean notify_events = TRUE;\nstatic janus_callbacks *gateway = NULL;\nstatic GThread *handler_thread;\nstatic void *janus_recordplay_handler(void *data);\nstatic void janus_recordplay_hangup_media_internal(janus_plugin_session *handle);\n\ntypedef struct janus_recordplay_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_recordplay_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_recordplay_message exit_message;\n\ntypedef struct janus_recordplay_rtp_header_extension {\n\tuint16_t type;\n\tuint16_t length;\n} janus_recordplay_rtp_header_extension;\n\ntypedef struct janus_recordplay_frame_packet {\n\tuint16_t seq;\t/* RTP Sequence number */\n\tuint64_t ts;\t/* RTP Timestamp */\n\tint len;\t\t/* Length of the data */\n\tlong offset;\t/* Offset of the data in the file */\n\tstruct janus_recordplay_frame_packet *next;\n\tstruct janus_recordplay_frame_packet *prev;\n} janus_recordplay_frame_packet;\njanus_recordplay_frame_packet *janus_recordplay_get_frames(const char *dir, const char *filename);\n\ntypedef struct janus_recordplay_recording {\n\tguint64 id;\t\t\t\t\t/* Recording unique ID */\n\tgboolean is_private;\t\t/* Whether the recording is private */\n\tchar *name;\t\t\t\t\t/* Name of the recording */\n\tchar *date;\t\t\t\t\t/* Time of the recording */\n\tchar *arc_file;\t\t\t\t/* Audio file name */\n\tjanus_audiocodec acodec;\t/* Codec used for audio, if available */\n\tchar *afmtp;\t\t\t\t/* Audio fmtp, if any */\n\tint audio_pt;\t\t\t\t/* Payload type to use for audio when playing recordings */\n\tint opusred_pt;\t\t\t\t/* In case RED is used for audio, payload type to use in playback */\n\tchar *vrc_file;\t\t\t\t/* Video file name */\n\tjanus_videocodec vcodec;\t/* Codec used for video, if available */\n\tchar *vfmtp;\t\t\t\t/* Video fmtp, if any */\n\tint video_pt;\t\t\t\t/* Payload type to use for video when playing recordings */\n\tguint8 audiolevel_ext_id;\t/* Audio level extmap ID */\n\tguint8 videoorient_ext_id;\t/* Video orientation extmap ID */\n\tchar *drc_file;\t\t\t\t/* Data file name */\n\tgboolean textdata;\t\t\t/* Whether data format is text */\n\tchar *offer;\t\t\t\t/* The SDP offer that will be sent to watchers */\n\tgboolean e2ee;\t\t\t\t/* Whether media in the recording is encrypted, e.g., using Insertable Streams */\n\tGList *viewers;\t\t\t\t/* List of users watching this recording */\n\tvolatile gint paused;\t\t/* Whether this recording is paused */\n\tvolatile gint completed;\t/* Whether this recording was completed or still going on */\n\tvolatile gint destroyed;\t/* Whether this recording has been marked as destroyed */\n\tjanus_refcount ref;\t\t\t/* Reference counter */\n\tjanus_mutex mutex;\t\t\t/* Mutex for this recording */\n} janus_recordplay_recording;\nstatic GHashTable *recordings = NULL;\nstatic janus_mutex recordings_mutex = JANUS_MUTEX_INITIALIZER;\n\ntypedef struct janus_recordplay_session {\n\tjanus_plugin_session *handle;\n\tgint64 sdp_sessid;\n\tgint64 sdp_version;\n\tgboolean active;\n\tgboolean recorder;\t\t/* Whether this session is used to record or to replay a WebRTC session */\n\tgboolean firefox;\t\t/* We send Firefox users a different kind of FIR */\n\tchar *video_profile;\t/* Codec-specific video profile to use, if any */\n\tjanus_recordplay_recording *recording;\n\tjanus_recorder *arc;\t/* Audio recorder */\n\tjanus_recorder *vrc;\t/* Video recorder */\n\tjanus_recorder *drc;\t/* Data recorder */\n\tjanus_mutex rec_mutex;\t/* Mutex to protect the recorders from race conditions */\n\tjanus_recordplay_frame_packet *aframes;\t/* Audio frames (for playout) */\n\tjanus_recordplay_frame_packet *vframes;\t/* Video frames (for playout) */\n\tjanus_recordplay_frame_packet *dframes;\t/* Data packets (for playout) */\n\tgboolean opusred;\t\t/* Whether this user supports RED for audio (for playout) */\n\tgboolean textdata;\t\t/* Whether data format is text */\n\tguint video_remb_startup;\n\tgint64 video_remb_last;\n\tguint32 video_bitrate;\n\tguint video_keyframe_interval;\t\t\t/* Keyframe request interval (ms) */\n\tguint64 video_keyframe_request_last;\t/* Timestamp of last keyframe request sent */\n\tgint video_fir_seq;\n\tjanus_rtp_switching_context context;\n\tuint32_t ssrc[3];\t\t/* Only needed in case VP8 (or H.264) simulcasting is involved */\n\tchar *rid[3];\t\t\t/* Only needed if simulcasting is rid-based */\n\tjanus_mutex rid_mutex;\t/* Mutex to protect access to the rid array */\n\tuint32_t rec_vssrc;\t\t/* SSRC we'll put in the recording for video, in case simulcasting is involved) */\n\tjanus_rtp_simulcasting_context sim_context;\n\tjanus_vp8_simulcast_context vp8_context;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_recordplay_session;\nstatic GHashTable *sessions;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_recordplay_session_destroy(janus_recordplay_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_recordplay_session_free(const janus_refcount *session_ref) {\n\tjanus_recordplay_session *session = janus_refcount_containerof(session_ref, janus_recordplay_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tg_free(session->video_profile);\n\tjanus_mutex_destroy(&session->rid_mutex);\n\tjanus_mutex_destroy(&session->rec_mutex);\n\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\tg_free(session);\n}\n\n\nstatic void janus_recordplay_recording_destroy(janus_recordplay_recording *recording) {\n\tif(recording && g_atomic_int_compare_and_exchange(&recording->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&recording->ref);\n}\n\nstatic void janus_recordplay_recording_free(const janus_refcount *recording_ref) {\n\tjanus_recordplay_recording *recording = janus_refcount_containerof(recording_ref, janus_recordplay_recording, ref);\n\t/* This recording can be destroyed, free all the resources */\n\tg_free(recording->name);\n\tg_free(recording->date);\n\tg_free(recording->arc_file);\n\tg_free(recording->vrc_file);\n\tg_free(recording->drc_file);\n\tg_free(recording->afmtp);\n\tg_free(recording->vfmtp);\n\tg_free(recording->offer);\n\tg_free(recording);\n}\n\n\nstatic char *recordings_path = NULL;\nvoid janus_recordplay_update_recordings_list(void);\nstatic void *janus_recordplay_playout_thread(void *data);\n\n/* Helper to send RTCP feedback back to recorders, if needed */\nvoid janus_recordplay_send_rtcp_feedback(janus_plugin_session *handle, int video, char *buf, int len);\n\n/* To make things easier, we use static payload types for viewers (unless it's for G.711 or G.722) */\n#define AUDIO_PT\t\t111\n#define VIDEO_PT\t\t100\n\n/* Helper method to check which codec was used in a specific recording (and if it's end-to-end encrypted) */\nstatic const char *janus_recordplay_parse_codec(const char *dir, const char *filename, char *fmtp, size_t fmtplen,\n\t\tuint8_t *audiolevel_ext_id, uint8_t *videoorient_ext_id, int *opusred_pt, gboolean *e2ee) {\n\tif(dir == NULL || filename == NULL)\n\t\treturn NULL;\n\tif(e2ee)\n\t\t*e2ee = FALSE;\n\tchar source[1024];\n\tif(strstr(filename, \".mjr\"))\n\t\tg_snprintf(source, 1024, \"%s/%s\", dir, filename);\n\telse\n\t\tg_snprintf(source, 1024, \"%s/%s.mjr\", dir, filename);\n\tFILE *file = fopen(source, \"rb\");\n\tif(file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not open file %s\\n\", source);\n\t\treturn NULL;\n\t}\n\tfseek(file, 0L, SEEK_END);\n\tlong fsize = ftell(file);\n\tfseek(file, 0L, SEEK_SET);\n\n\t/* Pre-parse */\n\tJANUS_LOG(LOG_VERB, \"Pre-parsing file %s to generate ordered index...\\n\", source);\n\tgboolean parsed_header = FALSE;\n\tint bytes = 0;\n\tlong offset = 0;\n\tuint16_t len = 0;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\t/* Let's look for timestamp resets first */\n\twhile(offset < fsize) {\n\t\t/* Read frame header */\n\t\tfseek(file, offset, SEEK_SET);\n\t\tbytes = fread(prebuffer, sizeof(char), 8, file);\n\t\tif(bytes != 8 || prebuffer[0] != 'M') {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid header...\\n\");\n\t\t\tfclose(file);\n\t\t\treturn NULL;\n\t\t}\n\t\tif(prebuffer[1] == 'E') {\n\t\t\t/* Either the old .mjr format header ('MEETECHO' header followed by 'audio' or 'video'), or a frame */\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len == 5 && !parsed_header) {\n\t\t\t\t/* This is the main header */\n\t\t\t\tparsed_header = TRUE;\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), 5, file);\n\t\t\t\tif(prebuffer[0] == 'v') {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"This is an old video recording, assuming VP8\\n\");\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn \"vp8\";\n\t\t\t\t} else if(prebuffer[0] == 'a') {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"This is an old audio recording, assuming Opus\\n\");\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn \"opus\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported recording media type...\\n\");\n\t\t\tfclose(file);\n\t\t\treturn NULL;\n\t\t} else if(prebuffer[1] == 'J') {\n\t\t\t/* New .mjr format */\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len > 0 && !parsed_header) {\n\t\t\t\t/* This is the info header */\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\t\t\tif(bytes < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error reading from file... %s\\n\", g_strerror(errno));\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tparsed_header = TRUE;\n\t\t\t\tprebuffer[len] = '\\0';\n\t\t\t\tjson_error_t error;\n\t\t\t\tjson_t *info = json_loads(prebuffer, 0, &error);\n\t\t\t\tif(!info) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error parsing info header...\\n\");\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\t/* Is it audio or video? */\n\t\t\t\tjson_t *type = json_object_get(info, \"t\");\n\t\t\t\tif(!type || !json_is_string(type)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing/invalid recording type in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tconst char *t = json_string_value(type);\n\t\t\t\tgboolean video = FALSE, data = FALSE;\n\t\t\t\tif(!strcasecmp(t, \"v\")) {\n\t\t\t\t\tvideo = TRUE;\n\t\t\t\t} else if(!strcasecmp(t, \"a\")) {\n\t\t\t\t\tvideo = FALSE;\n\t\t\t\t} else if(!strcasecmp(t, \"d\")) {\n\t\t\t\t\tdata = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported recording type '%s' in info header...\\n\", t);\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\t/* Check if the recording is end-to-end encrypted */\n\t\t\t\tjson_t *e = json_object_get(info, \"e\");\n\t\t\t\tif(e2ee)\n\t\t\t\t\t*e2ee = json_is_true(e);\n\t\t\t\t/* Any fmtp? */\n\t\t\t\tjson_t *f = json_object_get(info, \"f\");\n\t\t\t\tif(f && json_is_string(f) && fmtp && fmtplen > 0)\n\t\t\t\t\tg_snprintf(fmtp, fmtplen, \"%s\", json_string_value(f));\n\t\t\t\t/* What codec was used? */\n\t\t\t\tjson_t *codec = json_object_get(info, \"c\");\n\t\t\t\tif(!codec || !json_is_string(codec)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing recording codec in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\t/* Is RED in use for audio? */\n\t\t\t\tif(!video && !data)\n\t\t\t\t\t*opusred_pt = json_integer_value(json_object_get(info, \"or\"));\n\t\t\t\t/* Any RTP extension we care about? */\n\t\t\t\tjson_t *exts = json_object_get(info, \"x\");\n\t\t\t\tif(exts && !data) {\n\t\t\t\t\tint extid = 0;\n\t\t\t\t\tconst char *key = NULL, *extmap = NULL;\n\t\t\t\t\tjson_t *value = NULL;\n\t\t\t\t\tjson_object_foreach(exts, key, value) {\n\t\t\t\t\t\tif(key == NULL || value == NULL || !json_is_string(value))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\textid = atoi(key);\n\t\t\t\t\t\textmap = json_string_value(value);\n\t\t\t\t\t\tif(!video && !strcasecmp(extmap, JANUS_RTP_EXTMAP_AUDIO_LEVEL) && audiolevel_ext_id != NULL)\n\t\t\t\t\t\t\t*audiolevel_ext_id = extid;\n\t\t\t\t\t\telse if(video && !strcasecmp(extmap, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION) && videoorient_ext_id != NULL)\n\t\t\t\t\t\t\t*videoorient_ext_id = extid;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst char *c = json_string_value(codec);\n\t\t\t\tif(data) {\n\t\t\t\t\tconst char *dtype = NULL;\n\t\t\t\t\tif(c && !strcasecmp(c, \"text\")) {\n\t\t\t\t\t\tdtype = \"text\";\n\t\t\t\t\t} else if(c && !strcasecmp(c, \"binary\")) {\n\t\t\t\t\t\tdtype = \"binary\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported data channel format...\\n\");\n\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\tfclose(file);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t\t/* Found! */\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn dtype;\n\t\t\t\t}\n\t\t\t\tconst char *mcodec = janus_sdp_match_preferred_codec(video ? JANUS_SDP_VIDEO : JANUS_SDP_AUDIO, (char *)c);\n\t\t\t\tif(mcodec != NULL) {\n\t\t\t\t\t/* Found! */\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn mcodec;\n\t\t\t\t}\n\t\t\t\tjson_decref(info);\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_WARN, \"No codec found...\\n\");\n\t\t\tfclose(file);\n\t\t\treturn NULL;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid header...\\n\");\n\t\t\tfclose(file);\n\t\t\treturn NULL;\n\t\t}\n\t}\n\tfclose(file);\n\treturn NULL;\n}\n\n/* Helper method to prepare an SDP offer when a recording is available */\nstatic int janus_recordplay_generate_offer(janus_recordplay_recording *rec) {\n\tif(rec == NULL)\n\t\treturn -1;\n\t/* Prepare an SDP offer we'll send to playout viewers */\n\tgboolean offer_audio = (rec->arc_file != NULL && rec->acodec != JANUS_AUDIOCODEC_NONE),\n\t\toffer_video = (rec->vrc_file != NULL && rec->vcodec != JANUS_VIDEOCODEC_NONE),\n\t\toffer_data = (rec->drc_file != NULL);\n\tchar s_name[100];\n\tg_snprintf(s_name, sizeof(s_name), \"Recording %\"SCNu64, rec->id);\n\tguint8 mid_ext_id = 1;\n\twhile(mid_ext_id == rec->audiolevel_ext_id || mid_ext_id == rec->videoorient_ext_id)\n\t\tmid_ext_id++;\n\tjanus_sdp *offer = janus_sdp_generate_offer(\n\t\ts_name, \"1.1.1.1\",\n\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\tJANUS_SDP_OA_ENABLED, offer_audio,\n\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(rec->acodec),\n\t\t\tJANUS_SDP_OA_PT, rec->audio_pt,\n\t\t\tJANUS_SDP_OA_OPUSRED_PT, rec->opusred_pt > 0 ? rec->opusred_pt : 0,\n\t\t\tJANUS_SDP_OA_FMTP, rec->afmtp,\n\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_MID, mid_ext_id,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_AUDIO_LEVEL, rec->audiolevel_ext_id,\n\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_VIDEO,\n\t\t\tJANUS_SDP_OA_ENABLED, offer_video,\n\t\t\tJANUS_SDP_OA_CODEC, janus_videocodec_name(rec->vcodec),\n\t\t\tJANUS_SDP_OA_PT, rec->video_pt,\n\t\t\tJANUS_SDP_OA_FMTP, rec->vfmtp,\n\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_MID, mid_ext_id,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_AUDIO_LEVEL, rec->videoorient_ext_id,\n\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_APPLICATION,\n\t\t\tJANUS_SDP_OA_ENABLED, offer_data,\n\t\tJANUS_SDP_OA_DONE);\n\tg_free(rec->offer);\n\trec->offer = janus_sdp_write(offer);\n\tjanus_sdp_destroy(offer);\n\treturn 0;\n}\n\nstatic void janus_recordplay_message_free(janus_recordplay_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_recordplay_session *session = (janus_recordplay_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\n\n/* Error codes */\n#define JANUS_RECORDPLAY_ERROR_NO_MESSAGE\t\t\t411\n#define JANUS_RECORDPLAY_ERROR_INVALID_JSON\t\t\t412\n#define JANUS_RECORDPLAY_ERROR_INVALID_REQUEST\t\t413\n#define JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT\t\t414\n#define JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT\t\t415\n#define JANUS_RECORDPLAY_ERROR_NOT_FOUND\t\t\t416\n#define JANUS_RECORDPLAY_ERROR_INVALID_RECORDING\t417\n#define JANUS_RECORDPLAY_ERROR_INVALID_STATE\t\t418\n#define JANUS_RECORDPLAY_ERROR_INVALID_SDP\t\t\t419\n#define JANUS_RECORDPLAY_ERROR_RECORDING_EXISTS\t\t420\n#define JANUS_RECORDPLAY_ERROR_UNAUTHORIZED\t\t\t440\n#define JANUS_RECORDPLAY_ERROR_UNKNOWN_ERROR\t\t499\n\n/* Plugin implementation */\nint janus_recordplay_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_RECORDPLAY_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_RECORDPLAY_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_RECORDPLAY_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\t/* Parse configuration */\n\tif(config != NULL) {\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_item *path = janus_config_get(config, config_general, janus_config_type_item, \"path\");\n\t\tif(path && path->value)\n\t\t\trecordings_path = g_strdup(path->value);\n\t\tjanus_config_item *key = janus_config_get(config, config_general, janus_config_type_item, \"admin_key\");\n\t\tif(key != NULL && key->value != NULL)\n\t\t\tadmin_key = g_strdup(key->value);\n\t\tjanus_config_item *pvt = janus_config_get(config, config_general, janus_config_type_item, \"private\");\n\t\tif(pvt != NULL && pvt->value != NULL)\n\t\t\tprivate_recordings = janus_is_true(pvt->value);\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_RECORDPLAY_NAME);\n\t\t}\n\t\t/* Done */\n\t\tjanus_config_destroy(config);\n\t\tconfig = NULL;\n\t}\n\tif(recordings_path == NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"No recordings path specified, giving up...\\n\");\n\t\tg_free(admin_key);\n\t\tadmin_key = NULL;\n\t\treturn -1;\n\t}\n\t/* Create the folder, if needed */\n\tstruct stat st = {0};\n\tif(stat(recordings_path, &st) == -1) {\n\t\tint res = janus_mkdir(recordings_path, 0755);\n\t\tJANUS_LOG(LOG_VERB, \"Creating folder: %d\\n\", res);\n\t\tif(res != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"%s\\n\", g_strerror(errno));\n\t\t\tg_free(recordings_path);\n\t\t\trecordings_path = NULL;\n\t\t\tg_free(admin_key);\n\t\t\tadmin_key = NULL;\n\t\t\treturn -1;\t/* No point going on... */\n\t\t}\n\t}\n\trecordings = g_hash_table_new_full(g_int64_hash, g_int64_equal, (GDestroyNotify)g_free, (GDestroyNotify)janus_recordplay_recording_destroy);\n\tjanus_recordplay_update_recordings_list();\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_recordplay_session_destroy);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_recordplay_message_free);\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming messages */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"recordplay handler\", janus_recordplay_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Record&Play handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tg_free(recordings_path);\n\t\trecordings_path = NULL;\n\t\tg_free(admin_key);\n\t\tadmin_key = NULL;\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_RECORDPLAY_NAME);\n\treturn 0;\n}\n\nvoid janus_recordplay_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tg_hash_table_destroy(recordings);\n\trecordings = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\tg_free(recordings_path);\n\trecordings_path = NULL;\n\tg_free(admin_key);\n\tadmin_key = NULL;\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_RECORDPLAY_NAME);\n}\n\nint janus_recordplay_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_recordplay_get_version(void) {\n\treturn JANUS_RECORDPLAY_VERSION;\n}\n\nconst char *janus_recordplay_get_version_string(void) {\n\treturn JANUS_RECORDPLAY_VERSION_STRING;\n}\n\nconst char *janus_recordplay_get_description(void) {\n\treturn JANUS_RECORDPLAY_DESCRIPTION;\n}\n\nconst char *janus_recordplay_get_name(void) {\n\treturn JANUS_RECORDPLAY_NAME;\n}\n\nconst char *janus_recordplay_get_author(void) {\n\treturn JANUS_RECORDPLAY_AUTHOR;\n}\n\nconst char *janus_recordplay_get_package(void) {\n\treturn JANUS_RECORDPLAY_PACKAGE;\n}\n\nstatic janus_recordplay_session *janus_recordplay_lookup_session(janus_plugin_session *handle) {\n\tjanus_recordplay_session *session = NULL;\n\tif (g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_recordplay_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_recordplay_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_recordplay_session *session = g_malloc0(sizeof(janus_recordplay_session));\n\tsession->handle = handle;\n\tsession->active = FALSE;\n\tsession->recorder = FALSE;\n\tsession->firefox = FALSE;\n\tsession->arc = NULL;\n\tsession->vrc = NULL;\n\tsession->drc = NULL;\n\tjanus_mutex_init(&session->rec_mutex);\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tsession->video_remb_startup = 4;\n\tsession->video_remb_last = janus_get_monotonic_time();\n\tsession->video_bitrate = 1024 * 1024; \t\t/* This is 1mbps by default */\n\tsession->video_keyframe_request_last = 0;\n\tsession->video_keyframe_interval = 15000; \t/* 15 seconds by default */\n\tsession->video_fir_seq = 0;\n\tjanus_rtp_switching_context_reset(&session->context);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tjanus_mutex_init(&session->rid_mutex);\n\tjanus_refcount_init(&session->ref, janus_recordplay_session_free);\n\thandle->plugin_handle = session;\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_recordplay_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_recordplay_session *session = janus_recordplay_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No Record&Play session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Removing Record&Play session...\\n\");\n\tjanus_recordplay_hangup_media_internal(handle);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn;\n}\n\njson_t *janus_recordplay_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_recordplay_session *session = janus_recordplay_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* In the echo test, every session is the same: we just provide some configure info */\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"type\", json_string(session->recorder ? \"recorder\" : (session->recording ? \"player\" : \"none\")));\n\tif(session->recording) {\n\t\tjanus_refcount_increase(&session->recording->ref);\n\t\tjson_object_set_new(info, \"recording_id\", json_integer(session->recording->id));\n\t\tjson_object_set_new(info, \"recording_name\", json_string(session->recording->name));\n\t\tif(session->recording->e2ee)\n\t\t\tjson_object_set_new(info, \"e2ee\", json_true());\n\t\tjanus_refcount_decrease(&session->recording->ref);\n\t}\n\tjson_object_set_new(info, \"hangingup\", json_integer(g_atomic_int_get(&session->hangingup)));\n\tjson_object_set_new(info, \"destroyed\", json_integer(g_atomic_int_get(&session->destroyed)));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstruct janus_plugin_result *janus_recordplay_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\t/* Pre-parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_recordplay_session *session = janus_recordplay_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\terror_code = JANUS_RECORDPLAY_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No session associated with this handle...\");\n\t\tgoto plugin_response;\n\t}\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tJANUS_LOG(LOG_ERR, \"Session has already been destroyed...\\n\");\n\t\terror_code = JANUS_RECORDPLAY_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"Session has already been destroyed...\");\n\t\tgoto plugin_response;\n\t}\n\n\tif(message == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\terror_code = JANUS_RECORDPLAY_ERROR_NO_MESSAGE;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\tgoto plugin_response;\n\t}\n\tif(!json_is_object(root)) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_JSON;\n\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\tgoto plugin_response;\n\t}\n\t/* Get the request first */\n\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\tjson_t *request = json_object_get(root, \"request\");\n\t/* Some requests ('create' and 'destroy') can be handled synchronously */\n\tconst char *request_text = json_string_value(request);\n\tif(!strcasecmp(request_text, \"update\")) {\n\t\tif(admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto plugin_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT, JANUS_RECORDPLAY_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto plugin_response;\n\t\t}\n\t\t/* Update list of available recordings, scanning the folder again */\n\t\tjanus_recordplay_update_recordings_list();\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"recordplay\", json_string(\"ok\"));\n\t\tgoto plugin_response;\n\t} else if(!strcasecmp(request_text, \"list\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, list_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\tgboolean lock_recs_list = TRUE;\n\t\tif(admin_key != NULL) {\n\t\t\tjson_t *admin_key_json = json_object_get(root, \"admin_key\");\n\t\t\t/* Verify admin_key if it was provided */\n\t\t\tif(admin_key_json != NULL && json_is_string(admin_key_json) && strlen(json_string_value(admin_key_json)) > 0) {\n\t\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT, JANUS_RECORDPLAY_ERROR_UNAUTHORIZED);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tgoto plugin_response;\n\t\t\t\t} else {\n\t\t\t\t\tlock_recs_list = FALSE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjson_t *list = json_array();\n\t\tJANUS_LOG(LOG_VERB, \"Request for the list of recordings\\n\");\n\t\t/* Return a list of all available recordings */\n\t\tjanus_mutex_lock(&recordings_mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, recordings);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_recordplay_recording *rec = value;\n\t\t\tif(rec->is_private && lock_recs_list) {\n\t\t\t\t/* Skip private recording if no valid admin_key was provided */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping private recording '%\"SCNu64\"'\\n\", rec->id);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(!g_atomic_int_get(&rec->completed))\t/* Ongoing recording, skip */\n\t\t\t\tcontinue;\n\t\t\tjanus_refcount_increase(&rec->ref);\n\t\t\tjson_t *ml = json_object();\n\t\t\tjson_object_set_new(ml, \"id\", json_integer(rec->id));\n\t\t\tjson_object_set_new(ml, \"is_private\", rec->is_private ? json_true() : json_false());\n\t\t\tjson_object_set_new(ml, \"name\", json_string(rec->name));\n\t\t\tjson_object_set_new(ml, \"date\", json_string(rec->date));\n\t\t\tjson_object_set_new(ml, \"audio\", rec->arc_file ? json_true() : json_false());\n\t\t\tif(rec->acodec != JANUS_AUDIOCODEC_NONE)\n\t\t\t\tjson_object_set_new(ml, \"audio_codec\", json_string(janus_audiocodec_name(rec->acodec)));\n\t\t\tif(rec->opusred_pt > 0)\n\t\t\t\tjson_object_set_new(ml, \"audio_red\", json_true());\n\t\t\tjson_object_set_new(ml, \"video\", rec->vrc_file ? json_true() : json_false());\n\t\t\tif(rec->vcodec != JANUS_VIDEOCODEC_NONE)\n\t\t\t\tjson_object_set_new(ml, \"video_codec\", json_string(janus_videocodec_name(rec->vcodec)));\n\t\t\tjson_object_set_new(ml, \"data\", rec->drc_file ? json_true() : json_false());\n\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\tjson_array_append_new(list, ml);\n\t\t}\n\t\tjanus_mutex_unlock(&recordings_mutex);\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"recordplay\", json_string(\"list\"));\n\t\tjson_object_set_new(response, \"list\", list);\n\t\tgoto plugin_response;\n\t} else if(!strcasecmp(request_text, \"configure\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, configure_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto plugin_response;\n\t\tjson_t *video_bitrate_max = json_object_get(root, \"video-bitrate-max\");\n\t\tif(video_bitrate_max) {\n\t\t\tsession->video_bitrate = json_integer_value(video_bitrate_max);\n\t\t\tJANUS_LOG(LOG_VERB, \"Video bitrate has been set to %\"SCNu32\"\\n\", session->video_bitrate);\n\t\t}\n\t\tjson_t *video_keyframe_interval= json_object_get(root, \"video-keyframe-interval\");\n\t\tif(video_keyframe_interval) {\n\t\t\tsession->video_keyframe_interval = json_integer_value(video_keyframe_interval);\n\t\t\tJANUS_LOG(LOG_VERB, \"Video keyframe interval has been set to %u\\n\", session->video_keyframe_interval);\n\t\t}\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"recordplay\", json_string(\"configure\"));\n\t\tjson_object_set_new(response, \"status\", json_string(\"ok\"));\n\t\t/* Return a success, and also let the client be aware of what changed, to allow crosschecks */\n\t\tjson_t *settings = json_object();\n\t\tjson_object_set_new(settings, \"video-keyframe-interval\", json_integer(session->video_keyframe_interval));\n\t\tjson_object_set_new(settings, \"video-bitrate-max\", json_integer(session->video_bitrate));\n\t\tjson_object_set_new(response, \"settings\", settings);\n\t\tgoto plugin_response;\n\t} else if(!strcasecmp(request_text, \"record\") || !strcasecmp(request_text, \"play\")\n\t\t\t|| !strcasecmp(request_text, \"start\") || !strcasecmp(request_text, \"stop\")\n\t\t\t|| !strcasecmp(request_text, \"pause\") || !strcasecmp(request_text, \"resume\")) {\n\t\t/* These messages are handled asynchronously */\n\t\tjanus_recordplay_message *msg = g_malloc(sizeof(janus_recordplay_message));\n\t\tmsg->handle = handle;\n\t\tmsg->transaction = transaction;\n\t\tmsg->message = root;\n\t\tmsg->jsep = jsep;\n\n\t\tg_async_queue_push(messages, msg);\n\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code == 0 && !response) {\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"recordplay\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\t\tresponse = event;\n\t\t\t}\n\t\t\tif(root != NULL)\n\t\t\t\tjson_decref(root);\n\t\t\tif(jsep != NULL)\n\t\t\t\tjson_decref(jsep);\n\t\t\tg_free(transaction);\n\n\t\t\tif(session != NULL)\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, response);\n\t\t}\n\n}\n\njson_t *janus_recordplay_handle_admin_message(json_t *message) {\n\t/* Some requests (e.g., 'update') can be handled via Admin API */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *response = NULL;\n\n\tJANUS_VALIDATE_JSON_OBJECT(message, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto admin_response;\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\tif(!strcasecmp(request_text, \"update\")) {\n\t\t/* Update list of available recordings, scanning the folder again */\n\t\tif(admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(message, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto admin_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, message, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT, JANUS_RECORDPLAY_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto admin_response;\n\t\t}\n\t\tjanus_recordplay_update_recordings_list();\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"recordplay\", json_string(\"ok\"));\n\t\tgoto admin_response;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nadmin_response:\n\t\t{\n\t\t\tif(!response) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"recordplay\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nvoid janus_recordplay_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_RECORDPLAY_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_recordplay_session *session = janus_recordplay_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_atomic_int_set(&session->hangingup, 0);\n\t/* Take note of the fact that the session is now active */\n\tsession->active = TRUE;\n\tif(!session->recorder) {\n\t\tGError *error = NULL;\n\t\tjanus_refcount_increase(&session->ref);\n\t\tg_thread_try_new(\"recordplay playout thread\", &janus_recordplay_playout_thread, session, &error);\n\t\tif(error != NULL) {\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t/* FIXME Should we notify this back to the user somehow? */\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Record&Play playout thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tgateway->close_pc(session->handle);\n\t\t}\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_recordplay_send_rtcp_feedback(janus_plugin_session *handle, int video, char *buf, int len) {\n\tif(video != 1)\n\t\treturn;\t/* We just do this for video, for now */\n\n\tjanus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;\n\n\t/* Send a RR+SDES+REMB every five seconds, or ASAP while we are still\n\t * ramping up (first 4 RTP packets) */\n\tgint64 now = janus_get_monotonic_time();\n\tgint64 elapsed = now - session->video_remb_last;\n\tgboolean remb_rampup = session->video_remb_startup > 0;\n\n\tif(remb_rampup || (elapsed >= 5*G_USEC_PER_SEC)) {\n\t\tguint32 bitrate = session->video_bitrate;\n\n\t\tif(remb_rampup) {\n\t\t\tbitrate = bitrate / session->video_remb_startup;\n\t\t\tsession->video_remb_startup--;\n\t\t}\n\n\t\t/* Send a new REMB back */\n\t\tgateway->send_remb(handle, bitrate);\n\n\t\tsession->video_remb_last = now;\n\t}\n\n\t/* Request a keyframe on a regular basis (every session->video_keyframe_interval ms) */\n\telapsed = now - session->video_keyframe_request_last;\n\tgint64 interval = (gint64)(session->video_keyframe_interval / 1000) * G_USEC_PER_SEC;\n\n\tif(elapsed >= interval) {\n\t\t/* Send a PLI */\n\t\tgateway->send_pli(handle);\n\t\tsession->video_keyframe_request_last = now;\n\t}\n}\n\nvoid janus_recordplay_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!session->recorder || !session->recording)\n\t\treturn;\n\tgboolean video = packet->video;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\tif(video && (session->ssrc[0] != 0 || session->rid[0] != NULL)) {\n\t\t/* Handle simulcast: backup the header information first */\n\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\tuint32_t seq_number = ntohs(header->seq_number);\n\t\tuint32_t timestamp = ntohl(header->timestamp);\n\t\tuint32_t ssrc = ntohl(header->ssrc);\n\t\t/* Process this packet: don't save if it's not the SSRC/layer we wanted to handle */\n\t\tgboolean save = janus_rtp_simulcasting_context_process_rtp(&session->sim_context,\n\t\t\tbuf, len, NULL, 0, session->ssrc, session->rid, session->recording->vcodec,\n\t\t\t&session->context, &session->rid_mutex);\n\t\tif(session->sim_context.need_pli) {\n\t\t\t/* Send a PLI */\n\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the simulcast context\\n\");\n\t\t\tgateway->send_pli(handle);\n\t\t}\n\t\t/* Do we need to drop this? */\n\t\tif(!save)\n\t\t\treturn;\n\t\t/* If we got here, update the RTP header and save the packet */\n\t\tjanus_rtp_header_update(header, &session->context, TRUE, 0);\n\t\tif(session->recording->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\tint plen = 0;\n\t\t\tchar *payload = janus_rtp_payload(buf, len, &plen);\n\t\t\tjanus_vp8_simulcast_descriptor_update(payload, plen, &session->vp8_context, session->sim_context.changed_substream);\n\t\t}\n\t\t/* Save the frame if we're recording (and make sure the SSRC never changes even if the substream does) */\n\t\tif(session->rec_vssrc == 0)\n\t\t\tsession->rec_vssrc = g_random_int();\n\t\theader->ssrc = htonl(session->rec_vssrc);\n\t\tjanus_recorder_save_frame(session->vrc, buf, len);\n\t\t/* Restore header or core statistics will be messed up */\n\t\theader->ssrc = htonl(ssrc);\n\t\theader->timestamp = htonl(timestamp);\n\t\theader->seq_number = htons(seq_number);\n\t} else {\n\t\t/* Save the frame if we're recording */\n\t\tjanus_recorder_save_frame(video ? session->vrc : session->arc, buf, len);\n\t}\n\n\tjanus_recordplay_send_rtcp_feedback(handle, video, buf, len);\n}\n\nvoid janus_recordplay_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n}\n\nvoid janus_recordplay_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!session->recorder || !session->recording)\n\t\treturn;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\t/* Save the frame if we're recording */\n\tjanus_recorder_save_frame(session->drc, buf, len);\n}\n\nvoid janus_recordplay_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_recordplay_session *session = janus_recordplay_lookup_session(handle);\n\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"recordplay\", json_string(\"event\"));\n\tjson_t *result = json_object();\n\tjson_object_set_new(result, \"status\", json_string(\"slow_link\"));\n\tjson_object_set_new(result, \"media\", json_string(video ? \"video\" : \"audio\"));\n\tif(video)\n\t\tjson_object_set_new(result, \"current-bitrate\", json_integer(session->video_bitrate));\n\t/* What is uplink for the server is downlink for the client, so turn the tables */\n\tjson_object_set_new(result, \"uplink\", json_integer(uplink ? 0 : 1));\n\tjson_object_set_new(event, \"result\", result);\n\tgateway->push_event(session->handle, &janus_recordplay_plugin, NULL, event, NULL);\n\tjson_decref(event);\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_recordplay_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_RECORDPLAY_PACKAGE, handle);\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_recordplay_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_recordplay_hangup_media_internal(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_recordplay_session *session = janus_recordplay_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tsession->active = FALSE;\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\tjanus_rtp_switching_context_reset(&session->context);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\n\t/* Send an event to the browser and tell it's over */\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"recordplay\", json_string(\"event\"));\n\tjson_t *result = json_object();\n\tjson_object_set_new(result, \"status\", json_string(\"done\"));\n\tif(session->recording) {\n\t\tjson_object_set_new(result, \"id\", json_integer(session->recording->id));\n\t\tjson_object_set_new(result, \"is_private\", json_integer(session->recording->is_private));\n\t}\n\tjson_object_set_new(event, \"result\", result);\n\tint ret = gateway->push_event(handle, &janus_recordplay_plugin, NULL, event, NULL);\n\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\tjson_decref(event);\n\n\tsession->active = FALSE;\n\tjanus_mutex_lock(&session->rec_mutex);\n\tif(session->arc) {\n\t\tjanus_recorder *rc = session->arc;\n\t\tsession->arc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->vrc) {\n\t\tjanus_recorder *rc = session->vrc;\n\t\tsession->vrc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed video recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->drc) {\n\t\tjanus_recorder *rc = session->drc;\n\t\tsession->drc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed data recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tjanus_mutex_unlock(&session->rec_mutex);\n\tif(session->recorder) {\n\t\tif(session->recording) {\n\t\t\t/* Create a .nfo file for this recording */\n\t\t\tchar nfofile[1024], nfo[1024];\n\t\t\tnfofile[0] = '\\0';\n\t\t\tnfo[0] = '\\0';\n\t\t\tg_snprintf(nfofile, 1024, \"%s/%\"SCNu64\".nfo\", recordings_path, session->recording->id);\n\t\t\tFILE *file = fopen(nfofile, \"wt\");\n\t\t\tif(file == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating file %s...\\n\", nfofile);\n\t\t\t} else {\n\t\t\t\t/* Write the first part */\n\t\t\t\tg_snprintf(nfo, 1024,\n\t\t\t\t\t\"[%\"SCNu64\"]\\r\\n\"\n\t\t\t\t\t\"name = %s\\r\\n\"\n\t\t\t\t\t\"private = %s\\r\\n\"\n\t\t\t\t\t\"date = %s\\r\\n\",\n\t\t\t\t\t\tsession->recording->id,\n\t\t\t\t\t\tsession->recording->name,\n\t\t\t\t\t\tsession->recording->is_private ? \"true\" : \"false\",\n\t\t\t\t\t\tsession->recording->date);\n\t\t\t\tfwrite(nfo, sizeof(char), strlen(nfo), file);\n\t\t\t\t/* Add lines for each recorded medium */\n\t\t\t\tif(session->recording->arc_file) {\n\t\t\t\t\tg_snprintf(nfo, 1024, \"audio = %s.mjr\\r\\n\", session->recording->arc_file);\n\t\t\t\t\tfwrite(nfo, sizeof(char), strlen(nfo), file);\n\t\t\t\t}\n\t\t\t\tif(session->recording->vrc_file) {\n\t\t\t\t\tg_snprintf(nfo, 1024, \"video = %s.mjr\\r\\n\", session->recording->vrc_file);\n\t\t\t\t\tfwrite(nfo, sizeof(char), strlen(nfo), file);\n\t\t\t\t}\n\t\t\t\tif(session->recording->drc_file) {\n\t\t\t\t\tg_snprintf(nfo, 1024, \"data = %s.mjr\\r\\n\", session->recording->drc_file);\n\t\t\t\t\tfwrite(nfo, sizeof(char), strlen(nfo), file);\n\t\t\t\t}\n\t\t\t\tfclose(file);\n\t\t\t\tg_atomic_int_set(&session->recording->completed, 1);\n\t\t\t\t/* Generate the offer */\n\t\t\t\tif(janus_recordplay_generate_offer(session->recording) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Could not generate offer for recording %\"SCNu64\"...\\n\", session->recording->id);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Got a stop but missing recorder/recording! .nfo file may not have been generated...\\n\");\n\t\t}\n\t}\n\tif(session->recording) {\n\t\tjanus_refcount_decrease(&session->recording->ref);\n\t\tsession->recording = NULL;\n\t}\n\tjanus_rtp_simulcasting_cleanup(NULL, session->ssrc, session->rid, &session->rid_mutex);\n\tg_free(session->video_profile);\n\tsession->video_profile = NULL;\n\tsession->opusred = FALSE;\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_recordplay_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining Record&Play handler thread\\n\");\n\tjanus_recordplay_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_recordplay_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_recordplay_session *session = janus_recordplay_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_recordplay_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_recordplay_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = NULL;\n\t\tif(msg->message == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_RECORDPLAY_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\troot = msg->message;\n\t\t/* Get the request first */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto error;\n\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tjson_t *event = NULL;\n\t\tjson_t *result = NULL;\n\t\tchar *sdp = NULL;\n\t\tgboolean sdp_update = FALSE;\n\t\tif(json_object_get(msg->jsep, \"update\") != NULL)\n\t\t\tsdp_update = json_is_true(json_object_get(msg->jsep, \"update\"));\n\t\tgboolean e2ee = json_is_true(json_object_get(msg->jsep, \"e2ee\"));\n\t\tconst char *filename_text = NULL;\n\t\tif(!strcasecmp(request_text, \"record\")) {\n\t\t\tif(!msg_sdp || !msg_sdp_type || strcasecmp(msg_sdp_type, \"offer\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP offer\\n\");\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP offer\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, record_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tchar error_str[512];\n\t\t\tjanus_sdp *offer = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str)), *answer = NULL;\n\t\t\tif(offer == NULL) {\n\t\t\t\tjson_decref(event);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing offer: %s\\n\", error_str);\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing offer: %s\", error_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *name = json_object_get(root, \"name\");\n\t\t\tconst char *name_text = json_string_value(name);\n\t\t\tgboolean is_private = private_recordings;\n\t\t\tjson_t *pvt = json_object_get(root, \"is_private\");\n\t\t\tif(pvt)\n\t\t\t\tis_private = json_is_true(pvt);\n\t\t\tjson_t *filename = json_object_get(root, \"filename\");\n\t\t\tif(filename) {\n\t\t\t\tfilename_text = json_string_value(filename);\n\t\t\t}\n\t\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\t\tjson_t *videocodec = json_object_get(root, \"videocodec\");\n\t\t\tjson_t *videoprofile = json_object_get(root, \"videoprofile\");\n\t\t\tjson_t *dotextdata = json_object_get(root, \"textdata\");\n\t\t\tjson_t *opusred = json_object_get(root, \"opusred\");\n\t\t\tjson_t *update = json_object_get(root, \"update\");\n\t\t\tgboolean do_update = update ? json_is_true(update) : FALSE;\n\t\t\tif(do_update && !sdp_update) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Got a 'update' request, but no SDP update? Ignoring...\\n\");\n\t\t\t}\n\t\t\t/* Check if this is a new recorder, or if an update is taking place (i.e., ICE restart) */\n\t\t\tguint64 id = 0;\n\t\t\tjanus_recordplay_recording *rec = NULL;\n\t\t\tgboolean audio = FALSE, video = FALSE, data = FALSE, textdata = TRUE;\n\t\t\tif(sdp_update) {\n\t\t\t\t/* Renegotiation: make sure the user provided an offer, and send answer */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Request to update existing recorder\\n\");\n\t\t\t\tif(!session->recorder || !session->recording) {\n\t\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Not a recording session, can't update\\n\");\n\t\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Not a recording session, can't update\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tid = session->recording->id;\n\t\t\t\trec = session->recording;\n\t\t\t\tsession->sdp_version++;\t\t/* This needs to be increased when it changes */\n\t\t\t\taudio = (session->arc != NULL);\n\t\t\t\tvideo = (session->vrc != NULL);\n\t\t\t\tdata = (session->drc != NULL);\n\t\t\t\ttextdata = rec->textdata;\n\t\t\t\tsdp_update = do_update;\n\t\t\t\te2ee = rec->e2ee;\n\t\t\t\tgoto recdone;\n\t\t\t}\n\t\t\t/* If we're here, we're doing a new recording */\n\t\t\tjanus_mutex_lock(&recordings_mutex);\n\t\t\tjson_t *rec_id = json_object_get(root, \"id\");\n\t\t\tif(rec_id) {\n\t\t\t\tid = json_integer_value(rec_id);\n\t\t\t\tif(id > 0) {\n\t\t\t\t\t/* Let's make sure the ID doesn't exist already */\n\t\t\t\t\tif(g_hash_table_lookup(recordings, &id) != NULL) {\n\t\t\t\t\t\t/* It does... */\n\t\t\t\t\t\tjanus_mutex_unlock(&recordings_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Recording %\"SCNu64\" already exists!\\n\", id);\n\t\t\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_RECORDING_EXISTS;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Recording %\"SCNu64\" already exists\", id);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(id == 0) {\n\t\t\t\twhile(id == 0) {\n\t\t\t\t\tid = janus_random_uint64();\n\t\t\t\t\tif(g_hash_table_lookup(recordings, &id) != NULL) {\n\t\t\t\t\t\t/* Recording ID already taken, try another one */\n\t\t\t\t\t\tid = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Starting new recording with ID %\"SCNu64\"\\n\", id);\n\t\t\trec = g_malloc0(sizeof(janus_recordplay_recording));\n\t\t\trec->id = id;\n\t\t\trec->is_private = is_private;\n\t\t\trec->name = g_strdup(name_text);\n\t\t\trec->viewers = NULL;\n\t\t\trec->offer = NULL;\n\t\t\trec->acodec = JANUS_AUDIOCODEC_NONE;\n\t\t\trec->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\t\trec->e2ee = e2ee;\n\t\t\tg_atomic_int_set(&rec->paused, 0);\n\t\t\tg_atomic_int_set(&rec->destroyed, 0);\n\t\t\tg_atomic_int_set(&rec->completed, 0);\n\t\t\tjanus_refcount_init(&rec->ref, janus_recordplay_recording_free);\n\t\t\tjanus_refcount_increase(&rec->ref);\t/* This is for the user writing the recording */\n\t\t\tjanus_mutex_init(&rec->mutex);\n\t\t\t/* Check which codec we should record for audio and/or video */\n\t\t\tconst char *acodec = NULL, *vcodec = NULL;\n\t\t\tif(audiocodec != NULL)\n\t\t\t\tacodec = json_string_value(audiocodec);\n\t\t\telse\n\t\t\t\tjanus_sdp_find_preferred_codec(offer, JANUS_SDP_AUDIO, -1, &acodec);\n\t\t\trec->acodec = janus_audiocodec_from_name(acodec);\n\t\t\tif(videocodec != NULL)\n\t\t\t\tvcodec = json_string_value(videocodec);\n\t\t\telse\n\t\t\t\tjanus_sdp_find_preferred_codec(offer, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\trec->vcodec = janus_videocodec_from_name(vcodec);\n\t\t\t/* We found preferred codecs: let's just make sure the direction is what we need */\n\t\t\tjanus_sdp_mline *m = janus_sdp_mline_find(offer, JANUS_SDP_AUDIO);\n\t\t\tif(m != NULL && m->direction == JANUS_SDP_RECVONLY)\n\t\t\t\trec->acodec = JANUS_AUDIOCODEC_NONE;\n\t\t\taudio = (rec->acodec != JANUS_AUDIOCODEC_NONE);\n\t\t\tif(audio) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Audio codec: %s\\n\", janus_audiocodec_name(rec->acodec));\n\t\t\t}\n\t\t\tm = janus_sdp_mline_find(offer, JANUS_SDP_VIDEO);\n\t\t\tif(m != NULL && m->direction == JANUS_SDP_RECVONLY)\n\t\t\t\trec->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\t\tvideo = (rec->vcodec != JANUS_VIDEOCODEC_NONE);\n\t\t\tif(video) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Video codec: %s\\n\", janus_videocodec_name(rec->vcodec));\n\t\t\t}\n\t\t\t/* Check which video profiles are available, to see if we can force some */\n\t\t\tconst char *video_profile = json_string_value(videoprofile);\n\t\t\tint video_pt = -1;\n\t\t\tif(video_profile != NULL) {\n\t\t\t\t/* Check if the provided profile is supported supported */\n\t\t\t\tvideo_pt = janus_sdp_get_codec_pt_full(offer, -1, janus_videocodec_name(rec->vcodec), video_profile);\n\t\t\t\tif(video_pt == -1) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No such video codec with profile %s, falling back to plain %s\\n\",\n\t\t\t\t\t\tvideo_profile, janus_videocodec_name(rec->vcodec));\n\t\t\t\t\tvideo_profile = NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(video && video_pt == -1)\n\t\t\t\tvideo_pt = janus_sdp_get_codec_pt(offer, -1, janus_videocodec_name(rec->vcodec));\n\t\t\tg_free(session->video_profile);\n\t\t\tsession->video_profile = NULL;\n\t\t\tif(video_profile != NULL)\n\t\t\t\tsession->video_profile = g_strdup(video_profile);\n\t\t\tif(video && video_pt != -1) {\n\t\t\t\tconst char *video_fmtp = janus_sdp_get_fmtp(offer, -1, video_pt);\n\t\t\t\tif(video_fmtp != NULL)\n\t\t\t\t\trec->vfmtp = g_strdup(video_fmtp);\n\t\t\t}\n\t\t\t/* Set payload types */\n\t\t\trec->audio_pt = AUDIO_PT;\n\t\t\tif(rec->acodec != JANUS_AUDIOCODEC_NONE) {\n\t\t\t\t/* Some audio codecs have a fixed payload type that we can't mess with */\n\t\t\t\tif(rec->acodec == JANUS_AUDIOCODEC_PCMU)\n\t\t\t\t\trec->audio_pt = 0;\n\t\t\t\telse if(rec->acodec == JANUS_AUDIOCODEC_PCMA)\n\t\t\t\t\trec->audio_pt = 8;\n\t\t\t\telse if(rec->acodec == JANUS_AUDIOCODEC_G722)\n\t\t\t\t\trec->audio_pt = 9;\n\t\t\t\t/* Check if RED was offered */\n\t\t\t\tif(json_is_true(opusred) && rec->acodec == JANUS_AUDIOCODEC_OPUS)\n\t\t\t\t\trec->opusred_pt = janus_sdp_get_opusred_pt(offer, -1);\n\t\t\t}\n\t\t\trec->video_pt = VIDEO_PT;\n\t\t\t/* Check if relevant extensions are negotiated */\n\t\t\tGList *temp = offer->m_lines;\n\t\t\twhile(temp) {\n\t\t\t\t/* Which media are available? */\n\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\t\t\t/* Are the extmaps we care about there? */\n\t\t\t\t\tGList *ma = m->attributes;\n\t\t\t\t\twhile(ma) {\n\t\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\t\t\tif(a->name && a->value) {\n\t\t\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO && strstr(a->value, JANUS_RTP_EXTMAP_AUDIO_LEVEL)) {\n\t\t\t\t\t\t\t\tif(janus_string_to_uint8(a->value, &rec->audiolevel_ext_id) < 0)\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio-level extension ID: %s\\n\", a->value);\n\t\t\t\t\t\t\t} else if(m->type == JANUS_SDP_VIDEO && strstr(a->value, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION)) {\n\t\t\t\t\t\t\t\tif(janus_string_to_uint8(a->value, &rec->videoorient_ext_id) < 0)\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid video-orientation extension ID: %s\\n\", a->value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tma = ma->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\t/* Create a date string */\n\t\t\ttime_t t = time(NULL);\n\t\t\tstruct tm *tmv = localtime(&t);\n\t\t\tchar outstr[200];\n\t\t\tstrftime(outstr, sizeof(outstr), \"%Y-%m-%d %H:%M:%S\", tmv);\n\t\t\trec->date = g_strdup(outstr);\n\t\t\t/* Create the recordings */\n\t\t\tif(audio) {\n\t\t\t\tchar filename[256];\n\t\t\t\tif(filename_text != NULL) {\n\t\t\t\t\tg_snprintf(filename, 256, \"%s-audio\", filename_text);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(filename, 256, \"rec-%\"SCNu64\"-audio\", id);\n\t\t\t\t}\n\t\t\t\trec->arc_file = g_strdup(filename);\n\t\t\t\tjanus_recorder *rc = janus_recorder_create(recordings_path, janus_audiocodec_name(rec->acodec), rec->arc_file);\n\t\t\t\t/* If the audio-level extension has been negotiated, mark it in the recording */\n\t\t\t\tif(rec->audiolevel_ext_id > 0)\n\t\t\t\t\tjanus_recorder_add_extmap(rc, rec->audiolevel_ext_id, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t\t/* If RED is in use, take note of it */\n\t\t\t\tif(rec->opusred_pt > 0)\n\t\t\t\t\tjanus_recorder_opusred(rc, rec->opusred_pt);\n\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\tif(e2ee)\n\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\tsession->arc = rc;\n\t\t\t}\n\t\t\tif(video) {\n\t\t\t\tchar filename[256];\n\t\t\t\tif(filename_text != NULL) {\n\t\t\t\t\tg_snprintf(filename, 256, \"%s-video\", filename_text);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(filename, 256, \"rec-%\"SCNu64\"-video\", id);\n\t\t\t\t}\n\t\t\t\trec->vrc_file = g_strdup(filename);\n\t\t\t\tjanus_recorder *rc = janus_recorder_create_full(recordings_path,\n\t\t\t\t\tjanus_videocodec_name(rec->vcodec), rec->vfmtp, rec->vrc_file);\n\t\t\t\t/* If the video-orientation extension has been negotiated, mark it in the recording */\n\t\t\t\tif(rec->videoorient_ext_id > 0)\n\t\t\t\t\tjanus_recorder_add_extmap(rc, rec->videoorient_ext_id, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\tif(e2ee)\n\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\tsession->vrc = rc;\n\t\t\t}\n\t\t\t/* Check if we should record data too */\n\t\t\tdata = (janus_sdp_mline_find(offer, JANUS_SDP_APPLICATION) != NULL);\n\t\t\tif(data) {\n\t\t\t\ttextdata = dotextdata ? json_is_true(dotextdata) : TRUE;\n\t\t\t\tchar filename[256];\n\t\t\t\tif(filename_text != NULL) {\n\t\t\t\t\tg_snprintf(filename, 256, \"%s-data\", filename_text);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(filename, 256, \"rec-%\"SCNu64\"-data\", id);\n\t\t\t\t}\n\t\t\t\trec->drc_file = g_strdup(filename);\n\t\t\t\trec->textdata = textdata;\n\t\t\t\tjanus_recorder *rc = janus_recorder_create_full(recordings_path,\n\t\t\t\t\ttextdata ? \"text\" : \"binary\", NULL, rec->drc_file);\n\t\t\t\tsession->drc = rc;\n\t\t\t}\n\t\t\tsession->recorder = TRUE;\n\t\t\tsession->recording = rec;\n\t\t\tsession->sdp_version = 1;\t/* This needs to be increased when it changes */\n\t\t\tsession->sdp_sessid = janus_get_real_time();\n\t\t\tg_hash_table_insert(recordings, janus_uint64_dup(rec->id), rec);\n\t\t\tjanus_mutex_unlock(&recordings_mutex);\n\t\t\t/* We need to prepare an answer */\nrecdone:\n\t\t\tanswer = janus_sdp_generate_answer(offer);\n\t\t\tgboolean audio_accepted = FALSE, video_accepted = FALSE;\n\t\t\ttemp = offer->m_lines;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\tif(m->type == JANUS_SDP_AUDIO && audio && !audio_accepted) {\n\t\t\t\t\taudio_accepted = TRUE;\n\t\t\t\t\tjanus_sdp_generate_answer_mline(offer, answer, m,\n\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_RECVONLY,\n\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(rec->acodec),\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_OPUSRED, rec->opusred_pt > 0,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_RID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_REPAIRED_RID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_AUDIO_LEVEL,\n\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t} else if(m->type == JANUS_SDP_VIDEO && video && !video_accepted) {\n\t\t\t\t\tvideo_accepted = TRUE;\n\t\t\t\t\tjanus_sdp_generate_answer_mline(offer, answer, m,\n\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_VIDEO,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_RECVONLY,\n\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_videocodec_name(rec->vcodec),\n\t\t\t\t\t\t\tJANUS_SDP_OA_VP9_PROFILE, session->video_profile,\n\t\t\t\t\t\t\tJANUS_SDP_OA_H264_PROFILE, session->video_profile,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_RID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_REPAIRED_RID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION,\n\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t}\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tg_free(answer->s_name);\n\t\t\tchar s_name[100];\n\t\t\tg_snprintf(s_name, sizeof(s_name), \"Recording %\"SCNu64, rec->id);\n\t\t\tanswer->s_name = g_strdup(s_name);\n\t\t\t/* Let's overwrite a couple o= fields, in case this is a renegotiation */\n\t\t\tanswer->o_sessid = session->sdp_sessid;\n\t\t\tanswer->o_version = session->sdp_version;\n\t\t\t/* Generate the SDP string */\n\t\t\tsdp = janus_sdp_write(answer);\n\t\t\tjanus_sdp_destroy(offer);\n\t\t\tjanus_sdp_destroy(answer);\n\t\t\tJANUS_LOG(LOG_VERB, \"Going to answer this SDP:\\n%s\\n\", sdp);\n\t\t\t/* If the user negotiated simulcasting, prepare it accordingly */\n\t\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\t\tif(msg_simulcast && json_array_size(msg_simulcast) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(msg_simulcast); i++) {\n\t\t\t\t\tjson_t *s = json_array_get(msg_simulcast, i);\n\t\t\t\t\tint mindex = json_integer_value(json_object_get(s, \"mindex\"));\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording client is going to do simulcasting (#%d)\\n\", mindex);\n\t\t\t\t\tint rid_ext_id = -1;\n\t\t\t\t\tjanus_mutex_lock(&session->rid_mutex);\n\t\t\t\t\t/* Clear existing RIDs in case this is a renegotiation */\n\t\t\t\t\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\t\t\t\t\tjanus_rtp_simulcasting_prepare(s, &rid_ext_id, session->ssrc, session->rid);\n\t\t\t\t\tsession->sim_context.rid_ext_id = rid_ext_id;\n\t\t\t\t\tjanus_mutex_unlock(&session->rid_mutex);\n\t\t\t\t\tsession->sim_context.substream_target = 2;\t/* Let's aim for the highest quality */\n\t\t\t\t\tsession->sim_context.templayer_target = 2;\t/* Let's aim for all temporal layers */\n\t\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Done! */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(\"recording\"));\n\t\t\tjson_object_set_new(result, \"id\", json_integer(id));\n\t\t\tjson_object_set_new(result, \"is_private\", is_private ? json_true() : json_false());\n\t\t\t/* Also notify event handlers */\n\t\t\tif(!sdp_update && notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"recording\"));\n\t\t\t\tjson_object_set_new(info, \"id\", json_integer(id));\n\t\t\t\tjson_object_set_new(info, \"is_private\", is_private ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"audio\", session->arc ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"video\", session->vrc ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"data\", session->drc ? json_true() : json_false());\n\t\t\t\tgateway->notify_event(&janus_recordplay_plugin, session->handle, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"play\")) {\n\t\t\tif(msg_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"A play request can't contain an SDP\\n\");\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"A play request can't contain an SDP\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Replaying a recording\\n\");\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, play_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_RECORDPLAY_ERROR_MISSING_ELEMENT, JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *restart = json_object_get(root, \"restart\");\n\t\t\tgboolean do_restart = restart ? json_is_true(restart) : FALSE;\n\t\t\t/* Check if this is a new playout, or if an update is taking place (i.e., ICE restart) */\n\t\t\tguint64 id_value = 0;\n\t\t\tjanus_recordplay_recording *rec = NULL;\n\t\t\tconst char *warning = NULL;\n\t\t\tif(sdp_update || do_restart) {\n\t\t\t\t/* Renegotiation: make sure the user provided an offer, and send answer */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Request to perform an ICE restart on existing playout\\n\");\n\t\t\t\tif(session->recorder || session->recording == NULL || session->recording->offer == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Not a playout session, can't restart\\n\");\n\t\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Not a playout session, can't restart\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\trec = session->recording;\n\t\t\t\tid_value = rec->id;\n\t\t\t\tsession->sdp_version++;\t\t/* This needs to be increased when it changes */\n\t\t\t\tsdp_update = TRUE;\n\t\t\t\te2ee = rec->e2ee;\n\t\t\t\t/* Let's overwrite a couple o= fields, in case this is a renegotiation */\n\t\t\t\tchar error_str[512];\n\t\t\t\tjanus_sdp *offer = janus_sdp_parse(rec->offer, error_str, sizeof(error_str));\n\t\t\t\tif(offer == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid offer, can't restart\\n\");\n\t\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid, can't restart\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\toffer->o_sessid = session->sdp_sessid;\n\t\t\t\toffer->o_version = session->sdp_version;\n\t\t\t\tsdp = janus_sdp_write(offer);\n\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\tgoto playdone;\n\t\t\t}\n\t\t\t/* If we got here, it's a new playout */\n\t\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t\tid_value = json_integer_value(id);\n\t\t\t/* Look for this recording */\n\t\t\tjanus_mutex_lock(&recordings_mutex);\n\t\t\trec = g_hash_table_lookup(recordings, &id_value);\n\t\t\tif(rec != NULL)\n\t\t\t\tjanus_refcount_increase(&rec->ref);\n\t\t\tjanus_mutex_unlock(&recordings_mutex);\n\t\t\tif(rec == NULL || rec->offer == NULL || g_atomic_int_get(&rec->destroyed)) {\n\t\t\t\tif(rec != NULL)\n\t\t\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No such recording\\n\");\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_NOT_FOUND;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such recording\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Access the frames */\n\t\t\tif(rec->arc_file) {\n\t\t\t\tsession->aframes = janus_recordplay_get_frames(recordings_path, rec->arc_file);\n\t\t\t\tif(session->aframes == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error opening audio recording, trying to go on anyway\\n\");\n\t\t\t\t\twarning = \"Broken audio file, playing video only\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(rec->vrc_file) {\n\t\t\t\tsession->vframes = janus_recordplay_get_frames(recordings_path, rec->vrc_file);\n\t\t\t\tif(session->vframes == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error opening video recording, trying to go on anyway\\n\");\n\t\t\t\t\twarning = \"Broken video file, playing audio only\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(rec->drc_file) {\n\t\t\t\tsession->dframes = janus_recordplay_get_frames(recordings_path, rec->drc_file);\n\t\t\t\tif(session->dframes == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error opening data recording, trying to go on anyway\\n\");\n\t\t\t\t\twarning = \"Broken data file, playing audio/video only\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->aframes == NULL && session->vframes == NULL && session->dframes == NULL) {\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_RECORDING;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error opening recording files\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(rec->opusred_pt > 0)\n\t\t\t\tsession->opusred = TRUE;\t/* Assume the user does support RED, if it's in the recording */\n\t\t\tsession->recording = rec;\n\t\t\tsession->recorder = FALSE;\n\t\t\trec->viewers = g_list_append(rec->viewers, session);\n\t\t\te2ee = rec->e2ee;\n\t\t\t/* Send this viewer the prepared offer  */\n\t\t\tsdp = g_strdup(rec->offer);\nplaydone:\n\t\t\tJANUS_LOG(LOG_VERB, \"Going to offer this SDP:\\n%s\\n\", sdp);\n\t\t\t/* Done! */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(sdp_update ? \"restarting\" : \"preparing\"));\n\t\t\tjson_object_set_new(result, \"id\", json_integer(id_value));\n\t\t\tif(warning)\n\t\t\t\tjson_object_set_new(result, \"warning\", json_string(warning));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(!sdp_update && notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"playout\"));\n\t\t\t\tjson_object_set_new(info, \"id\", json_integer(id_value));\n\t\t\t\tjson_object_set_new(info, \"audio\", session->aframes ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"video\", session->vframes ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"data\", session->dframes ? json_true() : json_false());\n\t\t\t\tgateway->notify_event(&janus_recordplay_plugin, session->handle, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"start\")) {\n\t\t\tif(!session->aframes && !session->vframes) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Not a playout session, can't start\\n\");\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Not a playout session, can't start\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Just a final message we make use of, e.g., to receive an ANSWER to our OFFER for a playout */\n\t\t\tif(!msg_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP answer\\n\");\n\t\t\t\terror_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP answer\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->opusred && strstr(msg_sdp, \"red/48000/2\") == NULL)\n\t\t\t\tsession->opusred = FALSE;\n\t\t\t/* Done! */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(\"playing\"));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"playing\"));\n\t\t\t\tjson_object_set_new(info, \"id\", json_integer(session->recording->id));\n\t\t\t\tgateway->notify_event(&janus_recordplay_plugin, session->handle, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"stop\")) {\n\t\t\t/* Done! */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(\"stopped\"));\n\t\t\tif(session->recording) {\n\t\t\t\tjson_object_set_new(result, \"id\", json_integer(session->recording->id));\n\t\t\t\tjson_object_set_new(result, \"is_private\", json_integer(session->recording->is_private));\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"stopped\"));\n\t\t\t\t\tif(session->recording)\n\t\t\t\t\t\tjson_object_set_new(info, \"id\", json_integer(session->recording->id));\n\t\t\t\t\tgateway->notify_event(&janus_recordplay_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\t\tgateway->close_pc(session->handle);\n\t\t} else if (!strcasecmp(request_text, \"pause\") || !strcasecmp(request_text, \"resume\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Record&Play: Got pause/resume request\\n\");\n\t\t\tif(session->recording) {\n\t\t\t\tgboolean pause = !strcasecmp(request_text, \"pause\");\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"status\", json_string(pause ? \"paused\" : \"resumed\"));\n\t\t\t\tjson_object_set_new(result, \"id\", json_integer(session->recording->id));\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(pause ? \"paused\" : \"resumed\"));\n\t\t\t\t\tjson_object_set_new(info, \"id\", json_integer(session->recording->id));\n\t\t\t\t\tgateway->notify_event(&janus_recordplay_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\tif (g_atomic_int_compare_and_exchange(&session->recording->paused, !pause, pause)) {\n\t\t\t\t\tif(pause) {\n\t\t\t\t\t\tjanus_recorder_pause(session->arc);\n\t\t\t\t\t\tjanus_recorder_pause(session->vrc);\n\t\t\t\t\t\tjanus_recorder_pause(session->drc);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_recorder_resume(session->arc);\n\t\t\t\t\t\tjanus_recorder_resume(session->vrc);\n\t\t\t\t\t\tjanus_recorder_resume(session->drc);\n\t\t\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Record&Play: Not recording, ignoring pause/resume request\\n\");\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unknown request '%s'\\n\", request_text);\n\t\t\terror_code = JANUS_RECORDPLAY_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Prepare JSON event */\n\t\tevent = json_object();\n\t\tjson_object_set_new(event, \"recordplay\", json_string(\"event\"));\n\t\tif(result != NULL)\n\t\t\tjson_object_set_new(event, \"result\", result);\n\t\tif(!sdp) {\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_recordplay_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t} else {\n\t\t\tconst char *type = session->recorder ? \"answer\" : \"offer\";\n\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", type, \"sdp\", sdp);\n\t\t\tif(sdp_update)\n\t\t\t\tjson_object_set_new(jsep, \"restart\", json_true());\n\t\t\tif(e2ee)\n\t\t\t\tjson_object_set_new(jsep, \"e2ee\", json_true());\n\t\t\t/* How long will the gateway take to push the event? */\n\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\tint res = gateway->push_event(msg->handle, &janus_recordplay_plugin, msg->transaction, event, jsep);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\",\n\t\t\t\tres, janus_get_monotonic_time()-start);\n\t\t\tg_free(sdp);\n\t\t\tjson_decref(event);\n\t\t\tjson_decref(jsep);\n\t\t}\n\t\tjanus_recordplay_message_free(msg);\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"recordplay\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_recordplay_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_recordplay_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"LeavingRecord&Play handler thread\\n\");\n\treturn NULL;\n}\n\nvoid janus_recordplay_update_recordings_list(void) {\n\tif(recordings_path == NULL)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"Updating recordings list in %s\\n\", recordings_path);\n\tjanus_mutex_lock(&recordings_mutex);\n\t/* First of all, let's keep track of which recordings are currently available */\n\tGList *old_recordings = NULL;\n\tif(recordings != NULL && g_hash_table_size(recordings) > 0) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, recordings);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_recordplay_recording *rec = value;\n\t\t\tif(rec) {\n\t\t\t\tjanus_refcount_increase(&rec->ref);\n\t\t\t\told_recordings = g_list_append(old_recordings, &rec->id);\n\t\t\t}\n\t\t}\n\t}\n\t/* Open dir */\n\tDIR *dir = opendir(recordings_path);\n\tif(!dir) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open folder...\\n\");\n\t\tg_list_free(old_recordings);\n\t\tjanus_mutex_unlock(&recordings_mutex);\n\t\treturn;\n\t}\n\tstruct dirent *recent = NULL;\n\tchar recpath[1024];\n\twhile((recent = readdir(dir))) {\n\t\tint len = strlen(recent->d_name);\n\t\tif(len < 4)\n\t\t\tcontinue;\n\t\tif(strcasecmp(recent->d_name+len-4, \".nfo\"))\n\t\t\tcontinue;\n\t\tJANUS_LOG(LOG_VERB, \"Importing recording '%s'...\\n\", recent->d_name);\n\t\tmemset(recpath, 0, 1024);\n\t\tg_snprintf(recpath, 1024, \"%s/%s\", recordings_path, recent->d_name);\n\t\tjanus_config *nfo = janus_config_parse(recpath);\n\t\tif(nfo == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid recording '%s'...\\n\", recent->d_name);\n\t\t\tcontinue;\n\t\t}\n\t\tGList *cl = janus_config_get_categories(nfo, NULL);\n\t\tif(cl == NULL || cl->data == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"No recording info in '%s', skipping...\\n\", recent->d_name);\n\t\t\tjanus_config_destroy(nfo);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_config_category *cat = (janus_config_category *)cl->data;\n\t\tguint64 id = g_ascii_strtoull(cat->name, NULL, 0);\n\t\tif(id == 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Invalid ID, skipping...\\n\");\n\t\t\tg_list_free(cl);\n\t\t\tjanus_config_destroy(nfo);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_recordplay_recording *rec = g_hash_table_lookup(recordings, &id);\n\t\tif(rec != NULL) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Skipping recording with ID %\"SCNu64\", it's already in the list...\\n\", id);\n\t\t\tg_list_free(cl);\n\t\t\tjanus_config_destroy(nfo);\n\t\t\t/* Mark that we updated this recording */\n\t\t\told_recordings = g_list_remove(old_recordings, &rec->id);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_config_item *pvt = janus_config_get(nfo, cat, janus_config_type_item, \"private\");\n\t\tjanus_config_item *name = janus_config_get(nfo, cat, janus_config_type_item, \"name\");\n\t\tjanus_config_item *date = janus_config_get(nfo, cat, janus_config_type_item, \"date\");\n\t\tjanus_config_item *audio = janus_config_get(nfo, cat, janus_config_type_item, \"audio\");\n\t\tjanus_config_item *video = janus_config_get(nfo, cat, janus_config_type_item, \"video\");\n\t\tjanus_config_item *data = janus_config_get(nfo, cat, janus_config_type_item, \"data\");\n\t\tif(!name || !name->value || strlen(name->value) == 0 || !date || !date->value || strlen(date->value) == 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Invalid info for recording %\"SCNu64\", skipping...\\n\", id);\n\t\t\tg_list_free(cl);\n\t\t\tjanus_config_destroy(nfo);\n\t\t\tcontinue;\n\t\t}\n\t\tif((!audio || !audio->value) && (!video || !video->value)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"No audio and no video in recording %\"SCNu64\", skipping...\\n\", id);\n\t\t\tjanus_config_destroy(nfo);\n\t\t\tcontinue;\n\t\t}\n\t\trec = g_malloc0(sizeof(janus_recordplay_recording));\n\t\trec->id = id;\n\t\trec->is_private = (pvt == NULL) ? TRUE : private_recordings;\n\t\tif(pvt && pvt->value)\n\t\t\trec->is_private = janus_is_true(pvt->value);\n\t\trec->name = g_strdup(name->value);\n\t\trec->date = g_strdup(date->value);\n\t\tif(audio && audio->value) {\n\t\t\trec->arc_file = g_strdup(audio->value);\n\t\t\tchar *ext = strstr(rec->arc_file, \".mjr\");\n\t\t\tif(ext != NULL)\n\t\t\t\t*ext = '\\0';\n\t\t\t/* Check which codec is in this recording (and if it's end-to-end encrypted) */\n\t\t\tgboolean e2ee = FALSE;\n\t\t\tchar fmtp[256];\n\t\t\tfmtp[0] = '\\0';\n\t\t\trec->acodec = janus_audiocodec_from_name(janus_recordplay_parse_codec(recordings_path,\n\t\t\t\trec->arc_file, fmtp, sizeof(fmtp), &rec->audiolevel_ext_id, NULL, &rec->opusred_pt, &e2ee));\n\t\t\tif(strlen(fmtp) > 0)\n\t\t\t\trec->afmtp = g_strdup(fmtp);\n\t\t\tif(e2ee)\n\t\t\t\trec->e2ee = TRUE;\n\t\t}\n\t\tif(video && video->value) {\n\t\t\trec->vrc_file = g_strdup(video->value);\n\t\t\tchar *ext = strstr(rec->vrc_file, \".mjr\");\n\t\t\tif(ext != NULL)\n\t\t\t\t*ext = '\\0';\n\t\t\t/* Check which codec is in this recording (and if it's end-to-end encrypted) */\n\t\t\tgboolean e2ee = FALSE;\n\t\t\tchar fmtp[256];\n\t\t\tfmtp[0] = '\\0';\n\t\t\trec->vcodec = janus_videocodec_from_name(janus_recordplay_parse_codec(recordings_path,\n\t\t\t\trec->vrc_file, fmtp, sizeof(fmtp), NULL, &rec->videoorient_ext_id, NULL, &e2ee));\n\t\t\tif(strlen(fmtp) > 0)\n\t\t\t\trec->vfmtp = g_strdup(fmtp);\n\t\t\tif(e2ee)\n\t\t\t\trec->e2ee = TRUE;\n\t\t}\n\t\tif(data && data->value) {\n\t\t\trec->drc_file = g_strdup(data->value);\n\t\t\tchar *ext = strstr(rec->drc_file, \".mjr\");\n\t\t\tif(ext != NULL)\n\t\t\t\t*ext = '\\0';\n\t\t\tconst char *textcodec = janus_recordplay_parse_codec(recordings_path,\n\t\t\t\trec->drc_file, NULL, sizeof(NULL), NULL, NULL, NULL, NULL);\n\t\t\trec->textdata = textcodec && (!strcasecmp(textcodec, \"text\"));\n\t\t}\n\t\trec->audio_pt = AUDIO_PT;\n\t\tif(rec->opusred_pt > 0 && rec->audio_pt == rec->opusred_pt)\n\t\t\trec->audio_pt++;\n\t\tif(rec->acodec != JANUS_AUDIOCODEC_NONE) {\n\t\t\t/* Some audio codecs have a fixed payload type that we can't mess with */\n\t\t\tif(rec->acodec == JANUS_AUDIOCODEC_PCMU)\n\t\t\t\trec->audio_pt = 0;\n\t\t\telse if(rec->acodec == JANUS_AUDIOCODEC_PCMA)\n\t\t\t\trec->audio_pt = 8;\n\t\t\telse if(rec->acodec == JANUS_AUDIOCODEC_G722)\n\t\t\t\trec->audio_pt = 9;\n\t\t}\n\t\trec->video_pt = VIDEO_PT;\n\t\trec->viewers = NULL;\n\t\tif(janus_recordplay_generate_offer(rec) < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Could not generate offer for recording %\"SCNu64\"...\\n\", rec->id);\n\t\t}\n\t\tg_atomic_int_set(&rec->paused, 0);\n\t\tg_atomic_int_set(&rec->destroyed, 0);\n\t\tg_atomic_int_set(&rec->completed, 1);\n\t\tjanus_refcount_init(&rec->ref, janus_recordplay_recording_free);\n\t\tjanus_mutex_init(&rec->mutex);\n\n\t\tg_list_free(cl);\n\t\tjanus_config_destroy(nfo);\n\n\t\t/* Add to the list of recordings */\n\t\tg_hash_table_insert(recordings, janus_uint64_dup(rec->id), rec);\n\t}\n\tclosedir(dir);\n\t/* Now let's check if any of the previously existing recordings was removed */\n\tif(old_recordings != NULL) {\n\t\twhile(old_recordings != NULL) {\n\t\t\tguint64 id = *((guint64 *)old_recordings->data);\n\t\t\tJANUS_LOG(LOG_VERB, \"Recording %\"SCNu64\" is not available anymore, removing...\\n\", id);\n\t\t\tjanus_recordplay_recording *old_rec = g_hash_table_lookup(recordings, &id);\n\t\t\tif(old_rec != NULL) {\n\t\t\t\t/* Remove it */\n\t\t\t\tg_hash_table_remove(recordings, &id);\n\t\t\t\tjanus_refcount_decrease(&old_rec->ref);\n\t\t\t}\n\t\t\told_recordings = old_recordings->next;\n\t\t}\n\t\tg_list_free(old_recordings);\n\t}\n\tjanus_mutex_unlock(&recordings_mutex);\n}\n\njanus_recordplay_frame_packet *janus_recordplay_get_frames(const char *dir, const char *filename) {\n\tif(!dir || !filename)\n\t\treturn NULL;\n\t/* Open the file */\n\tchar source[1024];\n\tif(strstr(filename, \".mjr\"))\n\t\tg_snprintf(source, 1024, \"%s/%s\", dir, filename);\n\telse\n\t\tg_snprintf(source, 1024, \"%s/%s.mjr\", dir, filename);\n\tFILE *file = fopen(source, \"rb\");\n\tif(file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not open file %s\\n\", source);\n\t\treturn NULL;\n\t}\n\tfseek(file, 0L, SEEK_END);\n\tlong fsize = ftell(file);\n\tfseek(file, 0L, SEEK_SET);\n\tJANUS_LOG(LOG_VERB, \"File is %zu bytes\\n\", fsize);\n\n\t/* Pre-parse */\n\tJANUS_LOG(LOG_VERB, \"Pre-parsing file %s to generate ordered index...\\n\", source);\n\tgboolean parsed_header = FALSE;\n\tint bytes = 0;\n\tlong offset = 0;\n\tuint16_t len = 0, count = 0;\n\tuint32_t first_ts = 0, last_ts = 0, reset = 0;\t/* To handle whether there's a timestamp reset in the recording */\n\tint video = 0, audio = 0, data = 0;\n\tgint64 c_time = 0, w_time = 0;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\t/* Let's look for timestamp resets first */\n\twhile(offset < fsize) {\n\t\t/* Read frame header */\n\t\tfseek(file, offset, SEEK_SET);\n\t\tbytes = fread(prebuffer, sizeof(char), 8, file);\n\t\tif(bytes != 8 || prebuffer[0] != 'M') {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid header...\\n\");\n\t\t\tfclose(file);\n\t\t\treturn NULL;\n\t\t}\n\t\tif(prebuffer[1] == 'E') {\n\t\t\t/* Either the old .mjr format header ('MEETECHO' header followed by 'audio' or 'video'), or a frame */\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len == 5 && !parsed_header) {\n\t\t\t\t/* This is the main header */\n\t\t\t\tparsed_header = TRUE;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Old .mjr header format\\n\");\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), 5, file);\n\t\t\t\tif(prebuffer[0] == 'v') {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"This is an old video recording, assuming VP8\\n\");\n\t\t\t\t\tvideo = 1;\n\t\t\t\t} else if(prebuffer[0] == 'a') {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"This is an old audio recording, assuming Opus\\n\");\n\t\t\t\t\taudio = 1;\n\t\t\t\t} else if(prebuffer[0] == 'd') {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"This is an old data recording, assuming Text\\n\");\n\t\t\t\t\tdata = 1;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported recording media type...\\n\");\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\toffset += len;\n\t\t\t\tcontinue;\n\t\t\t} else if(!data && len < 12) {\n\t\t\t\t/* Not RTP, skip */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping packet (not RTP?)\\n\");\n\t\t\t\toffset += len;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} else if(prebuffer[1] == 'J') {\n\t\t\t/* New .mjr format, the header may contain useful info */\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len > 0 && !parsed_header) {\n\t\t\t\t/* This is the info header */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"New .mjr header format\\n\");\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\t\t\tif(bytes < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error reading from file... %s\\n\", g_strerror(errno));\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tparsed_header = TRUE;\n\t\t\t\tprebuffer[len] = '\\0';\n\t\t\t\tjson_error_t error;\n\t\t\t\tjson_t *info = json_loads(prebuffer, 0, &error);\n\t\t\t\tif(!info) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error parsing info header...\\n\");\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\t/* Is it audio or video? */\n\t\t\t\tjson_t *type = json_object_get(info, \"t\");\n\t\t\t\tif(!type || !json_is_string(type)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing/invalid recording type in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tconst char *t = json_string_value(type);\n\t\t\t\tif(!strcasecmp(t, \"v\")) {\n\t\t\t\t\tvideo = 1;\n\t\t\t\t} else if(!strcasecmp(t, \"a\")) {\n\t\t\t\t\taudio = 1;\n\t\t\t\t} else if(!strcasecmp(t, \"d\")) {\n\t\t\t\t\tdata = 1;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported recording type '%s' in info header...\\n\", t);\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\t/* What codec was used? */\n\t\t\t\tjson_t *codec = json_object_get(info, \"c\");\n\t\t\t\tif(!codec || !json_is_string(codec)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing recording codec in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tconst char *c = json_string_value(codec);\n\t\t\t\t/* When was the file created? */\n\t\t\t\tjson_t *created = json_object_get(info, \"s\");\n\t\t\t\tif(!created || !json_is_integer(created)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing recording created time in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tc_time = json_integer_value(created);\n\t\t\t\t/* When was the first frame written? */\n\t\t\t\tjson_t *written = json_object_get(info, \"u\");\n\t\t\t\tif(!written || !json_is_integer(written)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing recording written time in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tw_time = json_integer_value(created);\n\t\t\t\t/* Summary */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"This is %s recording:\\n\", video ? \"a video\" : (audio ? \"an audio\" : \"a data\"));\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Codec:   %s\\n\", c);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Created: %\"SCNi64\"\\n\", c_time);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Written: %\"SCNi64\"\\n\", w_time);\n\t\t\t\tjson_decref(info);\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid header...\\n\");\n\t\t\tfclose(file);\n\t\t\treturn NULL;\n\t\t}\n\t\t/* Only read RTP header */\n\t\tif(audio || video) {\n\t\t\tbytes = fread(prebuffer, sizeof(char), 16, file);\n\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)prebuffer;\n\t\t\tif(last_ts == 0) {\n\t\t\t\tfirst_ts = ntohl(rtp->timestamp);\n\t\t\t\tif(first_ts > 1000*1000)\t/* Just used to check whether a packet is pre- or post-reset */\n\t\t\t\t\tfirst_ts -= 1000*1000;\n\t\t\t} else {\n\t\t\t\tif(ntohl(rtp->timestamp) < last_ts) {\n\t\t\t\t\t/* The new timestamp is smaller than the next one, is it a timestamp reset or simply out of order? */\n\t\t\t\t\tif(last_ts-ntohl(rtp->timestamp) > 2*1000*1000*1000) {\n\t\t\t\t\t\treset = ntohl(rtp->timestamp);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Timestamp reset: %\"SCNu32\"\\n\", reset);\n\t\t\t\t\t}\n\t\t\t\t} else if(ntohl(rtp->timestamp) < reset) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Updating timestamp reset: %\"SCNu32\" (was %\"SCNu32\")\\n\", ntohl(rtp->timestamp), reset);\n\t\t\t\t\treset = ntohl(rtp->timestamp);\n\t\t\t\t}\n\t\t\t}\n\t\t\tlast_ts = ntohl(rtp->timestamp);\n\t\t}\n\t\t/* Skip data for now */\n\t\toffset += len;\n\t}\n\t/* Now let's parse the frames and order them */\n\toffset = 0;\n\tjanus_recordplay_frame_packet *list = NULL, *last = NULL;\n\twhile(offset < fsize) {\n\t\t/* Read frame header */\n\t\tfseek(file, offset, SEEK_SET);\n\t\tbytes = fread(prebuffer, sizeof(char), 8, file);\n\t\tprebuffer[8] = '\\0';\n\t\tJANUS_LOG(LOG_HUGE, \"Header: %s\\n\", prebuffer);\n\t\toffset += 8;\n\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\tlen = ntohs(len);\n\t\tJANUS_LOG(LOG_HUGE, \"  -- Length: %\"SCNu16\"\\n\", len);\n\t\toffset += 2;\n\t\tif(prebuffer[1] == 'J' || (!data && len < 12)) {\n\t\t\t/* Not RTP, skip */\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Not RTP, skipping\\n\");\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif(data) {\n\t\t\t/* Things are simpler for data, no reordering is needed: start by the data time */\n\t\t\tgint64 when = 0;\n\t\t\tbytes = fread(&when, 1, sizeof(gint64), file);\n\t\t\tif(bytes < (int)sizeof(gint64)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing data timestamp header\\n\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\twhen = ntohll((uint64_t)when);\n\t\t\toffset += sizeof(gint64);\n\t\t\tlen -= sizeof(gint64);\n\t\t\t/* Generate frame packet and insert in the ordered list */\n\t\t\tjanus_recordplay_frame_packet *p = g_malloc(sizeof(janus_recordplay_frame_packet));\n\t\t\tp->seq = 0;\n\t\t\t/* We \"abuse\" the timestamp field for the timing info */\n\t\t\tp->ts = when-c_time;\n\t\t\tp->len = len;\n\t\t\tp->offset = offset;\n\t\t\tp->next = NULL;\n\t\t\tp->prev = last;\n\t\t\tif(list == NULL) {\n\t\t\t\tlist = p;\n\t\t\t} else {\n\t\t\t\tlast->next = p;\n\t\t\t}\n\t\t\tlast = p;\n\t\t\t/* Done */\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Only read RTP header */\n\t\tbytes = fread(prebuffer, sizeof(char), 16, file);\n\t\tif(bytes < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Error reading RTP header, stopping here...\\n\");\n\t\t\tbreak;\n\t\t}\n\t\tjanus_rtp_header *rtp = (janus_rtp_header *)prebuffer;\n\t\tJANUS_LOG(LOG_HUGE, \"  -- RTP packet (ssrc=%\"SCNu32\", pt=%\"SCNu16\", ext=%\"SCNu16\", seq=%\"SCNu16\", ts=%\"SCNu32\")\\n\",\n\t\t\t\tntohl(rtp->ssrc), rtp->type, rtp->extension, ntohs(rtp->seq_number), ntohl(rtp->timestamp));\n\t\t/* Generate frame packet and insert in the ordered list */\n\t\tjanus_recordplay_frame_packet *p = g_malloc(sizeof(janus_recordplay_frame_packet));\n\t\tp->seq = ntohs(rtp->seq_number);\n\t\tif(reset == 0) {\n\t\t\t/* Simple enough... */\n\t\t\tp->ts = ntohl(rtp->timestamp);\n\t\t} else {\n\t\t\t/* Is this packet pre- or post-reset? */\n\t\t\tif(ntohl(rtp->timestamp) > first_ts) {\n\t\t\t\t/* Pre-reset... */\n\t\t\t\tp->ts = ntohl(rtp->timestamp);\n\t\t\t} else {\n\t\t\t\t/* Post-reset... */\n\t\t\t\tuint64_t max32 = UINT32_MAX;\n\t\t\t\tmax32++;\n\t\t\t\tp->ts = max32+ntohl(rtp->timestamp);\n\t\t\t}\n\t\t}\n\t\tp->len = len;\n\t\tp->offset = offset;\n\t\tp->next = NULL;\n\t\tp->prev = NULL;\n\t\tif(list == NULL) {\n\t\t\t/* First element becomes the list itself (and the last item), at least for now */\n\t\t\tlist = p;\n\t\t\tlast = p;\n\t\t} else {\n\t\t\t/* Check where we should insert this, starting from the end */\n\t\t\tint added = 0;\n\t\t\tjanus_recordplay_frame_packet *tmp = last;\n\t\t\twhile(tmp) {\n\t\t\t\tif(tmp->ts < p->ts) {\n\t\t\t\t\t/* The new timestamp is greater than the last one we have, append */\n\t\t\t\t\tadded = 1;\n\t\t\t\t\tif(tmp->next != NULL) {\n\t\t\t\t\t\t/* We're inserting */\n\t\t\t\t\t\ttmp->next->prev = p;\n\t\t\t\t\t\tp->next = tmp->next;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Update the last packet */\n\t\t\t\t\t\tlast = p;\n\t\t\t\t\t}\n\t\t\t\t\ttmp->next = p;\n\t\t\t\t\tp->prev = tmp;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if(tmp->ts == p->ts) {\n\t\t\t\t\t/* Same timestamp, check the sequence number */\n\t\t\t\t\tif(tmp->seq < p->seq && (abs(tmp->seq - p->seq) < 10000)) {\n\t\t\t\t\t\t/* The new sequence number is greater than the last one we have, append */\n\t\t\t\t\t\tadded = 1;\n\t\t\t\t\t\tif(tmp->next != NULL) {\n\t\t\t\t\t\t\t/* We're inserting */\n\t\t\t\t\t\t\ttmp->next->prev = p;\n\t\t\t\t\t\t\tp->next = tmp->next;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Update the last packet */\n\t\t\t\t\t\t\tlast = p;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttmp->next = p;\n\t\t\t\t\t\tp->prev = tmp;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(tmp->seq > p->seq && (abs(tmp->seq - p->seq) > 10000)) {\n\t\t\t\t\t\t/* The new sequence number (reset) is greater than the last one we have, append */\n\t\t\t\t\t\tadded = 1;\n\t\t\t\t\t\tif(tmp->next != NULL) {\n\t\t\t\t\t\t\t/* We're inserting */\n\t\t\t\t\t\t\ttmp->next->prev = p;\n\t\t\t\t\t\t\tp->next = tmp->next;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Update the last packet */\n\t\t\t\t\t\t\tlast = p;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttmp->next = p;\n\t\t\t\t\t\tp->prev = tmp;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* If either the timestamp or the sequence number we just got is smaller, keep going back */\n\t\t\t\ttmp = tmp->prev;\n\t\t\t}\n\t\t\tif(!added) {\n\t\t\t\t/* We reached the start */\n\t\t\t\tp->next = list;\n\t\t\t\tlist->prev = p;\n\t\t\t\tlist = p;\n\t\t\t}\n\t\t}\n\t\t/* Skip data for now */\n\t\toffset += len;\n\t\tcount++;\n\t}\n\n\tJANUS_LOG(LOG_VERB, \"Counted %\"SCNu16\" RTP packets\\n\", count);\n\tjanus_recordplay_frame_packet *tmp = list;\n\tcount = 0;\n\twhile(tmp) {\n\t\tcount++;\n\t\tJANUS_LOG(LOG_HUGE, \"[%10lu][%4d] seq=%\"SCNu16\", ts=%\"SCNu64\"\\n\", tmp->offset, tmp->len, tmp->seq, tmp->ts);\n\t\ttmp = tmp->next;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Counted %\"SCNu16\" frame packets\\n\", count);\n\n\t/* Done! */\n\tfclose(file);\n\treturn list;\n}\n\nstatic void *janus_recordplay_playout_thread(void *sessiondata) {\n\tjanus_recordplay_session *session = (janus_recordplay_session *)sessiondata;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid session, can't start playout thread...\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tif(!session->recording) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_ERR, \"No recording object, can't start playout thread...\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->recording->ref);\n\tjanus_recordplay_recording *rec = session->recording;\n\tif(session->recorder) {\n\t\tjanus_refcount_decrease(&rec->ref);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_ERR, \"This is a recorder, can't start playout thread...\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tif(!session->aframes && !session->vframes) {\n\t\tjanus_refcount_decrease(&rec->ref);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_ERR, \"No audio and no video frames, can't start playout thread...\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Joining playout thread\\n\");\n\t/* Open the files */\n\tFILE *afile = NULL, *vfile = NULL, *dfile = NULL;\n\tif(session->aframes) {\n\t\tif(rec->arc_file == NULL) {\n\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"The recording session contains some audio packets but seems to lack a recording file name\\n\");\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t\tchar source[1024];\n\t\tif(strstr(rec->arc_file, \".mjr\"))\n\t\t\tg_snprintf(source, 1024, \"%s/%s\", recordings_path, rec->arc_file);\n\t\telse\n\t\t\tg_snprintf(source, 1024, \"%s/%s.mjr\", recordings_path, rec->arc_file);\n\t\tafile = fopen(source, \"rb\");\n\t\tif(afile == NULL) {\n\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not open audio file %s, can't start playout thread...\\n\", source);\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t}\n\tif(session->vframes) {\n\t\tif(rec->vrc_file == NULL) {\n\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"The recording session contains some video packets but seems to lack a recording file name\\n\");\n\t\t\tif(afile)\n\t\t\t\tfclose(afile);\n\t\t\tafile = NULL;\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t\tchar source[1024];\n\t\tif(strstr(rec->vrc_file, \".mjr\"))\n\t\t\tg_snprintf(source, 1024, \"%s/%s\", recordings_path, rec->vrc_file);\n\t\telse\n\t\t\tg_snprintf(source, 1024, \"%s/%s.mjr\", recordings_path, rec->vrc_file);\n\t\tvfile = fopen(source, \"rb\");\n\t\tif(vfile == NULL) {\n\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not open video file %s, can't start playout thread...\\n\", source);\n\t\t\tif(afile)\n\t\t\t\tfclose(afile);\n\t\t\tafile = NULL;\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t}\n\n\tif(session->dframes) {\n\t\tif(rec->drc_file == NULL) {\n\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"The recording session contains some data packets but seems to lack a recording file name\\n\");\n\t\t\tif(afile)\n\t\t\t\tfclose(afile);\n\t\t\tafile = NULL;\n\t\t\tif(vfile)\n\t\t\t\tfclose(vfile);\n\t\t\tvfile = NULL;\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t\tchar source[1024];\n\t\tif(strstr(rec->drc_file, \".mjr\"))\n\t\t\tg_snprintf(source, 1024, \"%s/%s\", recordings_path, rec->drc_file);\n\t\telse\n\t\t\tg_snprintf(source, 1024, \"%s/%s.mjr\", recordings_path, rec->drc_file);\n\t\tdfile = fopen(source, \"rb\");\n\t\tif(dfile == NULL) {\n\t\t\tjanus_refcount_decrease(&rec->ref);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not open data file %s, can't start playout thread...\\n\", source);\n\t\t\tif(afile)\n\t\t\t\tfclose(afile);\n\t\t\tafile = NULL;\n\t\t\tif(vfile)\n\t\t\t\tfclose(vfile);\n\t\t\tvfile = NULL;\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t}\n\t/* Timer */\n\tgboolean asent = FALSE, vsent = FALSE, dsent = FALSE;\n\tstruct timeval now, abefore, vbefore, dbefore;\n\ttime_t d_s, d_us;\n\tgettimeofday(&now, NULL);\n\tgettimeofday(&abefore, NULL);\n\tgettimeofday(&vbefore, NULL);\n\tgettimeofday(&dbefore, NULL);\n\n\tjanus_recordplay_frame_packet *audio = session->aframes, *video = session->vframes, *data = session->dframes;\n\tchar *buffer = g_malloc0(1500);\n\tint bytes = 0;\n\tint64_t ts_diff = 0, passed = 0;\n\n\tint audio_pt = session->recording->audio_pt;\n\tint video_pt = session->recording->video_pt;\n\n\tint akhz = 48;\n\tif(audio_pt == 0 || audio_pt == 8 || audio_pt == 9)\n\t\takhz = 8;\n\tint vkhz = 90;\n\n\twhile(!g_atomic_int_get(&session->destroyed) && session->active\n\t\t\t&& !g_atomic_int_get(&rec->destroyed) && (audio || video)) {\n\t\tif(!asent && !vsent && !dsent) {\n\t\t\t/* We skipped the last round, so sleep a bit (5ms) */\n\t\t\tg_usleep(5000);\n\t\t}\n\t\tasent = FALSE;\n\t\tvsent = FALSE;\n\t\tdsent = FALSE;\n\t\tif(audio) {\n\t\t\tif(audio == session->aframes) {\n\t\t\t\t/* First packet, send now */\n\t\t\t\tfseek(afile, audio->offset, SEEK_SET);\n\t\t\t\tbytes = fread(buffer, sizeof(char), audio->len, afile);\n\t\t\t\tif(bytes != audio->len)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, audio->len);\n\t\t\t\t/* Update payload type */\n\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t\t\t\tif(rec->opusred_pt == 0 || rtp->type != rec->opusred_pt)\n\t\t\t\t\trtp->type = audio_pt;\n\t\t\t\t/* If the recording contains RED but the user doesn't support it, only use the primary data */\n\t\t\t\tif(rec->opusred_pt > 0 && rtp->type == rec->opusred_pt && !session->opusred) {\n\t\t\t\t\tint plen = 0;\n\t\t\t\t\tchar *payload = janus_rtp_payload(buffer, bytes, &plen);\n\t\t\t\t\tif(payload && plen > 0) {\n\t\t\t\t\t\tGList *blocks = janus_red_parse_blocks(payload, plen);\n\t\t\t\t\t\tif(blocks != NULL) {\n\t\t\t\t\t\t\t/* Copy the last block (primary data) to the RTP payload */\n\t\t\t\t\t\t\tGList *last = g_list_last(blocks);\n\t\t\t\t\t\t\tjanus_red_block *rb = (janus_red_block *)(last ? last->data : NULL);\n\t\t\t\t\t\t\tif(rb && rb->data && rb->length > 0) {\n\t\t\t\t\t\t\t\trtp->type = audio_pt;\n\t\t\t\t\t\t\t\tbytes -= (plen - rb->length);\n\t\t\t\t\t\t\t\tmemmove(payload, rb->data, rb->length);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tg_list_free_full(blocks, (GDestroyNotify)g_free);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_plugin_rtp prtp = { .mindex = -1, .video = FALSE, .buffer = (char *)buffer, .length = bytes };\n\t\t\t\tjanus_plugin_rtp_extensions_reset(&prtp.extensions);\n\t\t\t\tgateway->relay_rtp(session->handle, &prtp);\n\t\t\t\tgettimeofday(&now, NULL);\n\t\t\t\tabefore.tv_sec = now.tv_sec;\n\t\t\t\tabefore.tv_usec = now.tv_usec;\n\t\t\t\tasent = TRUE;\n\t\t\t\taudio = audio->next;\n\t\t\t} else {\n\t\t\t\t/* What's the timestamp skip from the previous packet? */\n\t\t\t\tts_diff = audio->ts - audio->prev->ts;\n\t\t\t\tts_diff = (ts_diff*1000)/akhz;\n\t\t\t\t/* Check if it's time to send */\n\t\t\t\tgettimeofday(&now, NULL);\n\t\t\t\td_s = now.tv_sec - abefore.tv_sec;\n\t\t\t\td_us = now.tv_usec - abefore.tv_usec;\n\t\t\t\tif(d_us < 0) {\n\t\t\t\t\td_us += 1000000;\n\t\t\t\t\t--d_s;\n\t\t\t\t}\n\t\t\t\tpassed = d_s*1000000 + d_us;\n\t\t\t\tif(passed < (ts_diff-5000)) {\n\t\t\t\t\tasent = FALSE;\n\t\t\t\t} else {\n\t\t\t\t\t/* Update the reference time */\n\t\t\t\t\tabefore.tv_usec += ts_diff%1000000;\n\t\t\t\t\tif(abefore.tv_usec > 1000000) {\n\t\t\t\t\t\tabefore.tv_sec++;\n\t\t\t\t\t\tabefore.tv_usec -= 1000000;\n\t\t\t\t\t}\n\t\t\t\t\tif(ts_diff/1000000 > 0) {\n\t\t\t\t\t\tabefore.tv_sec += ts_diff/1000000;\n\t\t\t\t\t\tabefore.tv_usec -= ts_diff/1000000;\n\t\t\t\t\t}\n\t\t\t\t\t/* Send now */\n\t\t\t\t\tfseek(afile, audio->offset, SEEK_SET);\n\t\t\t\t\tbytes = fread(buffer, sizeof(char), audio->len, afile);\n\t\t\t\t\tif(bytes != audio->len)\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, audio->len);\n\t\t\t\t\t/* Update payload type */\n\t\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t\t\t\t\tif(rec->opusred_pt == 0 || rtp->type != rec->opusred_pt)\n\t\t\t\t\t\trtp->type = audio_pt;\n\t\t\t\t\t/* If the recording contains RED but the user doesn't support it, only use the primary data */\n\t\t\t\t\tif(rec->opusred_pt > 0 && rtp->type == rec->opusred_pt && !session->opusred) {\n\t\t\t\t\t\tint plen = 0;\n\t\t\t\t\t\tchar *payload = janus_rtp_payload(buffer, bytes, &plen);\n\t\t\t\t\t\tif(payload && plen > 0) {\n\t\t\t\t\t\t\tGList *blocks = janus_red_parse_blocks(payload, plen);\n\t\t\t\t\t\t\tif(blocks != NULL) {\n\t\t\t\t\t\t\t\t/* Copy the last block (primary data) to the RTP payload */\n\t\t\t\t\t\t\t\tGList *last = g_list_last(blocks);\n\t\t\t\t\t\t\t\tjanus_red_block *rb = (janus_red_block *)(last ? last->data : NULL);\n\t\t\t\t\t\t\t\tif(rb && rb->data && rb->length > 0) {\n\t\t\t\t\t\t\t\t\trtp->type = audio_pt;\n\t\t\t\t\t\t\t\t\tbytes -= (plen - rb->length);\n\t\t\t\t\t\t\t\t\tmemmove(payload, rb->data, rb->length);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tg_list_free_full(blocks, (GDestroyNotify)g_free);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjanus_plugin_rtp prtp = { .mindex = -1, .video = FALSE, .buffer = (char *)buffer, .length = bytes };\n\t\t\t\t\tjanus_plugin_rtp_extensions_reset(&prtp.extensions);\n\t\t\t\t\tgateway->relay_rtp(session->handle, &prtp);\n\t\t\t\t\tasent = TRUE;\n\t\t\t\t\taudio = audio->next;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(video) {\n\t\t\tif(video == session->vframes) {\n\t\t\t\t/* First packets: there may be many of them with the same timestamp, send them all */\n\t\t\t\tuint64_t ts = video->ts;\n\t\t\t\twhile(video && video->ts == ts) {\n\t\t\t\t\tfseek(vfile, video->offset, SEEK_SET);\n\t\t\t\t\tbytes = fread(buffer, sizeof(char), video->len, vfile);\n\t\t\t\t\tif(bytes != video->len)\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, video->len);\n\t\t\t\t\t/* Update payload type */\n\t\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t\t\t\t\trtp->type = video_pt;\n\t\t\t\t\tjanus_plugin_rtp prtp = { .mindex = -1, .video = TRUE, .buffer = (char *)buffer, .length = bytes };\n\t\t\t\t\tjanus_plugin_rtp_extensions_reset(&prtp.extensions);\n\t\t\t\t\tgateway->relay_rtp(session->handle, &prtp);\n\t\t\t\t\tvideo = video->next;\n\t\t\t\t}\n\t\t\t\tvsent = TRUE;\n\t\t\t\tgettimeofday(&now, NULL);\n\t\t\t\tvbefore.tv_sec = now.tv_sec;\n\t\t\t\tvbefore.tv_usec = now.tv_usec;\n\t\t\t} else {\n\t\t\t\t/* What's the timestamp skip from the previous packet? */\n\t\t\t\tts_diff = video->ts - video->prev->ts;\n\t\t\t\tts_diff = (ts_diff*1000)/vkhz;\n\t\t\t\t/* Check if it's time to send */\n\t\t\t\tgettimeofday(&now, NULL);\n\t\t\t\td_s = now.tv_sec - vbefore.tv_sec;\n\t\t\t\td_us = now.tv_usec - vbefore.tv_usec;\n\t\t\t\tif(d_us < 0) {\n\t\t\t\t\td_us += 1000000;\n\t\t\t\t\t--d_s;\n\t\t\t\t}\n\t\t\t\tpassed = d_s*1000000 + d_us;\n\t\t\t\tif(passed < (ts_diff-5000)) {\n\t\t\t\t\tvsent = FALSE;\n\t\t\t\t} else {\n\t\t\t\t\t/* Update the reference time */\n\t\t\t\t\tvbefore.tv_usec += ts_diff%1000000;\n\t\t\t\t\tif(vbefore.tv_usec > 1000000) {\n\t\t\t\t\t\tvbefore.tv_sec++;\n\t\t\t\t\t\tvbefore.tv_usec -= 1000000;\n\t\t\t\t\t}\n\t\t\t\t\tif(ts_diff/1000000 > 0) {\n\t\t\t\t\t\tvbefore.tv_sec += ts_diff/1000000;\n\t\t\t\t\t\tvbefore.tv_usec -= ts_diff/1000000;\n\t\t\t\t\t}\n\t\t\t\t\t/* There may be multiple packets with the same timestamp, send them all */\n\t\t\t\t\tuint64_t ts = video->ts;\n\t\t\t\t\twhile(video && video->ts == ts) {\n\t\t\t\t\t\t/* Send now */\n\t\t\t\t\t\tfseek(vfile, video->offset, SEEK_SET);\n\t\t\t\t\t\tbytes = fread(buffer, sizeof(char), video->len, vfile);\n\t\t\t\t\t\tif(bytes != video->len)\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, video->len);\n\t\t\t\t\t\t/* Update payload type */\n\t\t\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t\t\t\t\t\trtp->type = video_pt;\n\t\t\t\t\t\tjanus_plugin_rtp prtp = { .mindex = -1, .video = TRUE, .buffer = (char *)buffer, .length = bytes };\n\t\t\t\t\t\tjanus_plugin_rtp_extensions_reset(&prtp.extensions);\n\t\t\t\t\t\tgateway->relay_rtp(session->handle, &prtp);\n\t\t\t\t\t\tvideo = video->next;\n\t\t\t\t\t}\n\t\t\t\t\tvsent = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(data) {\n\t\t\tu_int64_t prev_ts = 0; /* All timestamps for data are indexed to 0, since when parsing ts = when - c_time */\n\t\t\tif(data->prev)\n\t\t\t\tprev_ts = data->prev->ts;\n\t\t\tts_diff = data->ts - prev_ts;\n\n\t\t\t/* Check if it's time to send */\n\t\t\tgettimeofday(&now, NULL);\n\t\t\td_s = now.tv_sec - dbefore.tv_sec;\n\t\t\td_us = now.tv_usec - dbefore.tv_usec;\n\t\t\tif(d_us < 0) {\n\t\t\t\td_us += 1000000;\n\t\t\t\t--d_s;\n\t\t\t}\n\t\t\tpassed = d_s*1000000 + d_us;\n\t\t\tif(passed < (ts_diff-5000)) {\n\t\t\t\tdsent = FALSE;\n\t\t\t} else {\n\t\t\t\t/* Update the reference time */\n\t\t\t\tdbefore.tv_usec += ts_diff%1000000;\n\t\t\t\tif(dbefore.tv_usec > 1000000) {\n\t\t\t\t\tdbefore.tv_sec++;\n\t\t\t\t\tdbefore.tv_usec -= 1000000;\n\t\t\t\t}\n\t\t\t\tif(ts_diff/1000000 > 0) {\n\t\t\t\t\tdbefore.tv_sec += ts_diff/1000000;\n\t\t\t\t\tdbefore.tv_usec -= ts_diff/1000000;\n\t\t\t\t}\n\t\t\t\t/* Read data packet */\n\t\t\t\tfseek(dfile, data->offset, SEEK_SET);\n\t\t\t\tbytes = fread(buffer, sizeof(char), data->len, dfile);\n\t\t\t\tif(bytes != data->len)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, data->len);\n\t\t\t\t/* Update payload type */\n\t\t\t\tjanus_plugin_data datapacket = {\n\t\t\t\t\t.label = NULL,\n\t\t\t\t\t.protocol = NULL,\n\t\t\t\t\t.binary = rec->textdata ? FALSE : TRUE,\n\t\t\t\t\t.buffer = (char *)buffer,\n\t\t\t\t\t.length = bytes\n\t\t\t\t};\n\t\t\t\tgateway->relay_data(session->handle, &datapacket);\n\t\t\t\tdsent = TRUE;\n\t\t\t\tdata = data->next;\n\t\t\t}\n\t\t}\n\t}\n\n\tg_free(buffer);\n\n\t/* Get rid of the indexes */\n\tjanus_recordplay_frame_packet *tmp = NULL;\n\taudio = session->aframes;\n\twhile(audio) {\n\t\ttmp = audio->next;\n\t\tg_free(audio);\n\t\taudio = tmp;\n\t}\n\tsession->aframes = NULL;\n\tvideo = session->vframes;\n\twhile(video) {\n\t\ttmp = video->next;\n\t\tg_free(video);\n\t\tvideo = tmp;\n\t}\n\tsession->vframes = NULL;\n\tdata = session->dframes;\n\twhile(data) {\n\t\ttmp = data->next;\n\t\tg_free(data);\n\t\tdata = tmp;\n\t}\n\tsession->dframes = NULL;\n\n\tif(afile)\n\t\tfclose(afile);\n\tafile = NULL;\n\tif(vfile)\n\t\tfclose(vfile);\n\tvfile = NULL;\n\tif(dfile)\n\t\tfclose(dfile);\n\tdfile = NULL;\n\n\t/* Remove from the list of viewers */\n\tjanus_mutex_lock(&rec->mutex);\n\trec->viewers = g_list_remove(rec->viewers, session);\n\tjanus_mutex_unlock(&rec->mutex);\n\n\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\tgateway->close_pc(session->handle);\n\n\tjanus_refcount_decrease(&rec->ref);\n\tjanus_refcount_decrease(&session->ref);\n\n\tJANUS_LOG(LOG_VERB, \"Leaving playout thread\\n\");\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/plugins/janus_sip.c",
    "content": "/*! \\file   janus_sip.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus SIP plugin\n * \\details Check the \\ref sip for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page sip SIP plugin documentation\n * This is a simple SIP plugin for Janus, allowing WebRTC peers\n * to register at a SIP server (e.g., Asterisk) and call SIP user agents\n * through a Janus instance. Specifically, when attaching to the plugin peers\n * are requested to provide their SIP server credentials, i.e., the address\n * of the SIP server and their username/secret. This results in the plugin\n * registering at the SIP server and acting as a SIP client on behalf of\n * the web peer. Most of the SIP states and lifetime are masked by the plugin,\n * and only the relevant events (e.g., INVITEs and BYEs) and functionality\n * (call, hangup) are made available to the web peer: peers can call\n * extensions at the SIP server or wait for incoming INVITEs, and during\n * a call they can send DTMF tones. Calls can do plain RTP or SDES-SRTP.\n *\n * The concept behind this plugin is to allow different web pages associated\n * to the same peer, and hence the same SIP user, to attach to the plugin\n * at the same time and yet just do a SIP REGISTER once. The same should\n * apply for calls: while an incoming call would be notified to all the\n * web UIs associated to the peer, only one would be able to pick up and\n * answer, in pretty much the same way as SIP forking works but without the\n * need to fork in the same place. This specific functionality, though, has\n * not been implemented as of yet.\n *\n * \\section sipapi SIP Plugin API\n *\n * All requests you can send in the SIP Plugin API are asynchronous,\n * which means all responses (successes and errors) will be delivered\n * as events with the same transaction.\n *\n * The supported requests are \\c register , \\c unregister , \\c call ,\n * \\c progress , \\c accept , \\c decline , \\c info , \\c message , \\c dtmf_info ,\n * \\c subscribe , \\c unsubscribe , \\c transfer , \\c recording , \\c keyframe ,\n * \\c hold , \\c unhold , \\c update , \\c rtp_forward , \\c stop_rtp_forward ,\n * \\c listforwarders and \\c hangup . \\c register can be used,\n * as the name suggests, to register a username at a SIP registrar to\n * call and be called, while \\c unregister unregisters it; \\c call is used\n * to send an INVITE to a different SIP URI through the plugin; in case one\n * is invited instead of inviting, \\c progress, \\c accept and \\c decline\n * requests may be used. \\c progress request is optional, and it is used to\n * send 183 Session Progress response back to the caller, while\n * \\c accept and \\c decline are used to accept or reject the call respectively;\n * \\c transfer takes care of attended and blind transfers (see \\ref siptr for\n * more details); \\c hold and \\c unhold can be used respectively to put a\n * call on-hold and to resume it; \\c info allows you to send a generic\n * SIP INFO request, while \\c dtmf_info is focused on using INFO for DTMF\n * instead; \\c message is the method you use to send a SIP message\n * to the other peer; \\c subscribe and \\c unsubscribe are used to deal\n * with SIP events, i.e., to send SUBSCRIBE requests that will result in\n * NOTIFY asynchronous events; \\c recording is used, instead, to record the\n * conversation to one or more .mjr files (depending on the direction you\n * want to record); \\c update allows you to update an existing session\n * (e.g., to do a renegotiation or force an ICE restart); \\c rtp_forward ,\n * \\c stop_rtp_forward and \\c listforwarders are related to the\n * \\ref siprtpfwd functionality; finally, \\c hangup\n * can be used to terminate the communication at any time, either to\n * hangup (BYE) an ongoing call or to cancel/decline (CANCEL/BYE) a call\n * that hasn't started yet.\n *\n * No matter the request, an error response or event is always formatted\n * like this:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"error_code\" : <numeric ID, check Macros below>,\n\t\"error\" : \"<error description as a string>\"\n}\n\\endverbatim\n *\n * Notice that the error syntax above refers to the plugin API messaging,\n * and not SIP error codes obtained in response to SIP requests, which\n * are notified using a different syntax:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"<name of the error event>\",\n\t\t\"code\" : <SIP error code>,\n\t\t\"reason\" : \"<SIP error reason>\",\n\t\t\"reason_header\" : \"<Reason header text; optional>\",\n\t\t\"reason_header_protocol\" : \"<Reason header protocol; optional>\",\n\t\t\"reason_header_cause\" : \"<Reason header cause code; optional>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * Coming to the available requests, you send a SIP REGISTER using the\n * \\c register request. To be more precise, a \\c register request MAY result\n * in a SIP REGISTER, as this method actually provides ways to start using\n * a SIP account with no need for a registration. It is the case, for instance,\n * of the so-called \\c guest registrations: if you register as a \\c guest ,\n * it means you'll use the provided SIP URI in your \\c From headers for calls,\n * but you will actually not send a SIP REGISTER; this is especially useful\n * for outgoing calls to services that don't require registration (e.g., IVR\n * systems, or conference bridges), but also means you won't be able to\n * receive calls unless peers know what your private SIP address is. A SIP\n * REGISTER isn't sent also when registering as a \\c helper : as we'll\n * explain later, \\c helper sessions are sessions only meant to facilitate\n * the setup of \\ref sipmc.\n *\n * That said, a \\c register request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"register\",\n\t\"type\" : \"<if guest or helper, no SIP REGISTER is actually sent; optional>\",\n\t\"send_register\" : <true|false; if false, no SIP REGISTER is actually sent; optional>,\n\t\"force_udp\" : <true|false; if true, forces UDP for the SIP messaging; optional>,\n\t\"force_tcp\" : <true|false; if true, forces TCP for the SIP messaging; optional>,\n\t\"sips\" : <true|false; if true, configures a SIPS URI too when registering; optional>,\n\t\"rfc2543_cancel\" : <true|false; if true, configures sip client to CANCEL pending INVITEs without having received a provisional response first; optional>,\n\t\"automatic_ringing\" : <true|false; if false, don't generate ringing automatically as soon as an INVITE; optional>,\n\t\"username\" : \"<SIP URI to register; mandatory>\",\n\t\"secret\" : \"<password to use to register; optional>\",\n\t\"ha1_secret\" : \"<prehashed password to use to register; optional>\",\n\t\"authuser\" : \"<username to use to authenticate (overrides the one in the SIP URI); optional>\",\n\t\"display_name\" : \"<display name to use when sending SIP REGISTER; optional>\",\n\t\"user_agent\" : \"<user agent to use when sending SIP REGISTER; optional>\",\n\t\"proxy\" : \"<server to register at; optional, as won't be needed in case the REGISTER is not goint to be sent (e.g., guests)>\",\n\t\"outbound_proxy\" : \"<outbound proxy to use, if any; optional>\",\n\t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP REGISTER; optional>\",\n\t\"contact_params\" : \"<array of key/value objects, to specify custom Contact URI params to add to the SIP REGISTER; optional>\",\n\t\"incoming_header_prefixes\" : \"<array of strings, to specify custom (non-standard) headers to read on incoming SIP events; optional>\",\n\t\"refresh\" : \"<true|false; if true, only uses the SIP REGISTER as an update and not a new registration; optional>\",\n\t\"master_id\" : \"<ID of an already registered account, if this is an helper for multiple calls (more on that later); optional>\",\n \t\"register_ttl\" : \"<integer; number of seconds after which the registration should expire; optional>\"\n}\n\\endverbatim\n *\n * A \\c registering event will be sent back, as this is an asynchronous request.\n *\n * In case it is required to, this request will originate a SIP REGISTER to the\n * specified server with the right credentials. 401 and 407 responses will be\n * handled automatically, and so errors will not be notified back to the caller\n * unless they're definitive (e.g., wrong credentials). A failure to register\n * will return an error with name \\c registration_failed. A successful registration,\n * instead, is notified in a \\c registered event formatted like this:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"registered\",\n\t\t\"username\" : <SIP URI username>,\n\t\t\"register_sent\" : <true|false, depending on whether a REGISTER was sent or not>,\n\t\t\"master_id\" : <unique numeric ID of this registered session in the plugin, if a potential master>,\n\t\t\"unique_id\" : \"<unique UUID of this session in this plugin, needed for some requests>\n\t}\n}\n\\endverbatim\n *\n * To unregister, just send an \\c unregister request with no other arguments:\n *\n\\verbatim\n{\n\t\"request\" : \"unregister\"\n}\n\\endverbatim\n *\n * As before, an \\c unregistering event will be sent back. Just as before,\n * this will also send a SIP REGISTER in case it had been sent originally.\n * A successful unregistration is notified in an \\c unregistered event:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"unregistered\",\n\t\t\"username\" : <SIP URI username>,\n\t\t\"register_sent\" : <true|false, depending on whether a REGISTER was sent or not>\n\t}\n}\n\\endverbatim\n *\n * Once registered, you can call or wait to be called: notice that you won't\n * be able to get incoming calls if you chose never to send a REGISTER at\n * all, though.\n *\n * To send a SIP INVITE, you can use the \\c call request, which has to\n * be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"call\",\n\t\"call_id\" : \"<user-defined value of Call-ID SIP header used in all SIP requests throughout the call; optional>\",\n\t\"uri\" : \"<SIP URI to call; mandatory>\",\n\t\"refer_id\" : <in case this is the result of a REFER, the unique identifier that addresses it; optional>,\n\t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP INVITE; optional>\",\n\t\"srtp\" : \"<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>\",\n\t\"srtp_profile\" : \"<SRTP profile to negotiate, in case SRTP is offered; optional>\",\n\t\"secret\" : \"<password to use to call, only needed in case authentication is needed and no REGISTER was sent; optional>\",\n\t\"ha1_secret\" : \"<prehashed password to use to call, only needed in case authentication is needed and no REGISTER was sent; optional>\",\n\t\"authuser\" : \"<username to use to authenticate as to call, only needed in case authentication is needed and no REGISTER was sent; optional>\",\n\t\"autoaccept_reinvites\" : <true|false, whether we should blindly accept re-INVITEs with a 200 OK instead of relaying the SDP to the application; optional, TRUE by default>\n}\n\\endverbatim\n *\n * A \\c calling event will be sent back, as this is an asynchronous request.\n *\n * Notice that this request MUST be associated to a JSEP offer: there's no\n * way to send an offerless INVITE via the SIP plugin. This will generate\n * a SIP INVITE and send it according to the instructions. While a\n * <code>100 Trying</code> will not be notified back to the user, a\n * <code>180 Ringing</code> will, in a \\c ringing event:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"call_id\" : \"<value of SIP Call-ID header for related call>\",\n\t\"result\" : {\n\t\t\"event\" : \"ringing\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * If the call is declined, or any other error occurs, a \\c hangup error\n * event will be sent back. If the call is accepted, instead, an \\c accepted\n * event will be sent back to the user, along with the JSEP answer originated\n * by the callee:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"call_id\" : \"<value of SIP Call-ID header for related call>\",\n\t\"result\" : {\n\t\t\"event\" : \"accepted\",\n\t\t\"username\" : \"<SIP URI of the callee>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * At this point, PeerConnection-related considerations aside, the call\n * can be considered established. A SIP ACK is sent automatically by the\n * SIP plugin, so there's no action required of the application to do\n * that manually.\n *\n * Notice that the SIP plugin supports early-media via \\c 183 responses\n * responses. In case a \\c 183 response is received, it's sent back to\n * the user, along with the JSEP answer originated by the callee, in\n * a \\c progress event:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"call_id\" : \"<value of SIP Call-ID header for related call>\",\n\t\"result\" : {\n\t\t\"event\" : \"progress\",\n\t\t\"username\" : \"<SIP URI of the callee>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * In case the caller received a \\c progress event, the following\n * \\c accepted event will NOT contain a JSEP answer, as the one received\n * in the \"Session Progress\" event will act as the SDP answer for the session.\n *\n * Notice that you only use \\c call to start a conversation, that is for\n * the first INVITE. To update a session via a re-INVITE, e.g., to renegotiate\n * a session to add/remove streams or force an ICE restart, you do NOT\n * use \\c call, but another request called \\c update instead. This request\n * needs no arguments, as the whole context is derived from the current\n * state of the session. It does need the new JSEP offer to provide, though,\n * as part of the renegotiation.\n *\n\\verbatim\n{\n\t\"request\" : \"update\"\n}\n\\endverbatim\n *\n * An \\c updating event will be sent back, as this is an asynchronous request.\n *\n * While the \\c call request allows you to send a SIP INVITE (and the\n * \\c update request allows you to update an existing session), there is\n * a way to react to SIP INVITEs as well, that is to handle incoming calls.\n * Incoming calls are notified to the application via \\c incomingcall\n * events:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"call_id\" : \"<value of SIP Call-ID header for related call>\",\n\t\"result\" : {\n\t\t\"event\" : \"incomingcall\",\n\t\t\"username\" : \"<SIP URI of the caller>\",\n\t\t\"displayname\" : \"<display name of the caller, if available; optional>\",\n\t\t\"callee\" : \"<SIP URI that was called (in case the user is associated with multiple public URIs)>\",\n\t\t\"referred_by\" : \"<SIP URI header conveying the identity of the transferor, if this is a transfer; optional>\",\n\t\t\"replaces\" : \"<call-ID of the call that this is supposed to replace, if this is an attended transfer; optional>\",\n\t\t\"srtp\" : \"<whether the caller mandates (sdes_mandatory) or offers (sdes_optional) SRTP support; optional>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * The \\c incomingcall may or may not be accompanied by a JSEP offer, depending\n * on whether the caller sent an offerless INVITE or a regular one. Optionally,\n * you can progress the incoming call with the \\c progress request:\n *\n\\verbatim\n{\n\t\"request\" : \"progress\",\n\t\"srtp\" : \"<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>\",\n\t\"srtp_profile\" : \"<SRTP profile to negotiate, in case SRTP is offered; optional, and only needed in response to offerless-INVITEs>\",\n\t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP OK; optional>\"\n\t\"autoaccept_reinvites\" : <true|false, whether we should blindly accept re-INVITEs with a 200 OK instead of relaying the SDP to the browser; optional, TRUE by default>\n}\n\\endverbatim\n *\n * A \\c progressing event will be sent back, as this is an asynchronous request.\n *\n * This will result in a <code>183 Session Progress</code> to be sent back to the caller.\n * A \\c progress request must always be accompanied by a JSEP answer (if the\n * \\c incomingcall event contained an offer) or offer (in case it was an\n * offerless INVITE). This request can be used to inform the caller that the early\n * media is available, such as ringback audio, announcements or other audio streams,\n * without the call being fully established.\n *\n * Furthermore, you can accept the incoming call with the \\c accept request:\n *\n\\verbatim\n{\n\t\"request\" : \"accept\",\n\t\"srtp\" : \"<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>\",\n\t\"srtp_profile\" : \"<SRTP profile to negotiate, in case SRTP is offered; optional, and only needed in response to offerless-INVITEs>\",\n\t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP OK; optional>\"\n\t\"autoaccept_reinvites\" : <true|false, whether we should blindly accept re-INVITEs with a 200 OK instead of relaying the SDP to the browser; optional, TRUE by default>\n}\n\\endverbatim\n *\n * An \\c accepting event will be sent back, as this is an asynchronous request.\n *\n * This will result in a <code>200 OK</code> to be sent back to the caller.\n * As was the case for \\c progress request, an \\c accept request must always\n * be accompanied by a JSEP answer (if the \\c incomingcall event contained an\n * offer) or offer (in case it was an offerless INVITE). In the former case,\n * an \\c accepted event will be sent back just to confirm the call can be\n * considered established; in the latter case, instead, an \\c accepting event\n * will be sent back instead, and an \\c accepted event will only follow later,\n * as soon as a JSEP answer is available in the SIP ACK the caller sent back.\n *\n * Notice that in case you get an incoming call while you're in another\n * call, you will NOT get an \\c incomingcall event, but a \\c missed_call\n * event instead, and just as a notification as there's no way to have\n * two calls at the same time on the same handle in the SIP plugin:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"call_id\" : \"<value of SIP Call-ID header for related call>\",\n\t\"result\" : {\n\t\t\"event\" : \"missed_call\",\n\t\t\"caller\" : \"<SIP URI of the caller>\",\n\t\t\"displayname\" : \"<display name of the caller, if available; optional>\",\n\t\t\"callee\" : \"<SIP URI that was called (in case the user is associated with multiple public URIs)>\"\n\t}\n}\n\\endverbatim\n *\n * Besides, you only use \\c accept to answer the first INVITE. To accept a\n * re-INVITE instead, which would be notified via an \\c updatingcall event,\n * you do NOT use \\c accept, but the previously introduced \\c update instead.\n * This request needs no arguments, as the whole context is derived from the current\n * state of the session. It does need the new JSEP answer to provide, though,\n * as part of the renegotiation. As before, an \\c updated event will be\n * sent back, as this is an asynchronous request.\n *\n * Closing a session depends on the call state. If you have an incoming\n * call that you don't want to accept, use the \\c decline request; in all\n * other cases, use the \\c hangup request instead. Both requests need no\n * additional arguments, as the whole context can be extracted from the\n * current state of the session in the plugin:\n *\n\\verbatim\n{\n\t\"request\" : \"decline\",\n\t\"code\" : <SIP code to be sent, if not set, 486 is used; optional>\",\n \t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP request; optional>\"\n}\n\\endverbatim\n *\n\\verbatim\n{\n\t\"request\" : \"hangup\",\n\t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP BYE; optional>\"\n}\n\\endverbatim\n *\n * Since these are asynchronous requests, you'll get an event in response:\n * \\c declining if you used \\c decline and \\c hangingup if you used \\c hangup.\n *\n * As anticipated before, when a call is declined or being hung up, a\n * \\c hangup event is sent instead, which is basically a SIP error event\n * notification as it includes the \\c code and \\c reason . A regular BYE,\n * for instance, would be notified with \\c 200 and <code>SIP BYE</code>,\n * although a more verbose description may be provided as well.\n *\n * When a session has been established, there are different requests that\n * you can use to interact with the session.\n *\n * First of all, you can put a call on-hold with the \\c hold request.\n * By default, this request will send a new INVITE to the peer with a\n * \\c sendonly direction for media, but in case you want to set a\n * different direction (\\c recvonly or \\c inactive ) you can do that by\n * passing a \\c direction attribute as well:\n *\n\\verbatim\n{\n\t\"request\" : \"hold\",\n\t\"direction\" : \"<sendonly, recvonly or inactive>\"\n}\n\\endverbatim\n *\n * No WebRTC renegotiation will be involved here on the holder side, as\n * this will only trigger a re-INVITE on the SIP side. To remove the\n * call from on-hold, just send a \\c unhold request to the plugin,\n * which requires no additional attributes:\n *\n\\verbatim\n{\n\t\"request\" : \"unhold\"\n}\n\\endverbatim\n *\n * and will restore the media direction that was set in the SDP before\n * putting the call on-hold.\n *\n * The \\c message request allows you to send a SIP MESSAGE to the peer.\n * By default, it is sent in dialog, during active call.\n * But, if the user is registered, it might be sent out of dialog also. In that case the uri parameter is required.\n *\n\\verbatim\n{\n\t\"request\" : \"message\",\n\t\"call_id\" : \"<user-defined value of Call-ID SIP header used to send the message; optional>\",\n\t\"content_type\" : \"<content type; optional>\"\n\t\"content\" : \"<text to send>\",\n \t\"uri\" : \"<SIP URI of the peer; optional; if set, the message will be sent out of dialog>\",\n \t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP MESSAGE; optional>\"\n}\n\\endverbatim\n *\n * A \\c messagesent event will be sent back. Incoming SIP MESSAGEs, instead,\n * are notified in \\c message events:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"message\",\n\t\t\"sender\" : \"<SIP URI of the message sender>\",\n\t\t\"displayname\" : \"<display name of the sender, if available; optional>\",\n\t\t\"content_type\" : \"<content type of the message>\",\n\t\t\"content\" : \"<content of the message>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * After delivery a \\c messagedelivery event will be sent back with the SIP server response.\n * Used to track the delivery status of the message.\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"call_id\" : \"<value of SIP Call-ID header for related message>\",\n\t\"result\" : {\n\t\t\"event\" : \"messagedelivery\",\n\t\t\"code\" : \"<SIP error code>\",\n\t\t\"reason\" : \"<SIP error reason>\",\n\t}\n}\n\\endverbatim\n *\n * SIP INFO works pretty much the same way, except that you use an \\c info\n * request to one to the peer:\n *\n\\verbatim\n{\n\t\"request\" : \"info\",\n\t\"type\" : \"<content type>\"\n\t\"content\" : \"<message to send>\",\n  \t\"headers\" : \"<object with key/value mappings (header name/value), to specify custom headers to add to the SIP INFO; optional>\"\n}\n\\endverbatim\n *\n * A \\c infosent event will be sent back. Incoming SIP INFOs, instead,\n * are notified in \\c info events:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"info\",\n\t\t\"sender\" : \"<SIP URI of the message sender>\",\n\t\t\"displayname\" : \"<display name of the sender, if available; optional>\",\n\t\t\"type\" : \"<content type of the message>\",\n\t\t\"content\" : \"<content of the message>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * As anticipated, SIP events are supported as well, using the SUBSCRIBE\n * and NOTIFY mechanism. To do that, you need to use the \\c subscribe\n * request, which has to be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"subscribe\",\n\t\"call_id\" : \"<user-defined value of Call-ID SIP header used in all SIP requests throughout the subscription; optional>\",\n\t\"event\" : \"<the event to subscribe to, e.g., 'message-summary'; mandatory>\",\n\t\"accept\" : \"<what should be put in the Accept header; optional>\",\n\t\"to\" : \"<who should be the SUBSCRIBE addressed to; optional, will use the user's identity if missing>\",\n\t\"subscribe_ttl\" : \"<integer; number of seconds after which the subscription should expire; optional>\",\n\t\"content\" : \"<content to put in the body of the SUBSCRIBE; optional>\",\n\t\"content_type\" : \"<content-type of the body; optional>\",\n\t\"headers\" : \"<array of key/value objects, to specify custom headers to add to the SIP SUBSCRIBE; optional>\"\n}\n\\endverbatim\n *\n * A \\c subscribing event will be sent back, followed by a \\c subscribe_succeeded if\n * the SUBSCRIBE request was accepted, and a \\c subscribe_failed if the transaction\n * failed instead. Incoming SIP NOTIFY events, instead, are notified in \\c notify events:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"call_id\" : \"<value of SIP Call-ID header for related subscription>\",\n\t\"result\" : {\n\t\t\"event\" : \"notify\",\n\t\t\"notify\" : \"<name of the event that the user is subscribed to, e.g., 'message-summary'>\",\n\t\t\"substate\" : \"<substate of the subscription, e.g., 'active'>\",\n\t\t\"content-type\" : \"<content-type of the message>\"\n\t\t\"content\" : \"<content of the message>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * You can also record a SIP call, and it works pretty much the same the\n * VideoCall plugin does. Specifically, you make use of the \\c recording\n * request to either start or stop a recording, using the following syntax:\n *\n\\verbatim\n{\n\t\"request\" : \"recording\",\n\t\"action\" : \"<start|stop, depending on whether you want to start or stop recording something>\"\n\t\"audio\" : <true|false; whether or not our audio should be recorded>,\n\t\"video\" : <true|false; whether or not our video should be recorded>,\n\t\"peer_audio\" : <true|false; whether or not our peer's audio should be recorded>,\n\t\"peer_video\" : <true|false; whether or not our peer's video should be recorded>,\n\t\"send_peer_pli\" : <true|false; whether or not send PLI to request keyframe from peer>,\n\t\"filename\" : \"<base path/filename to use for all the recordings>\"\n}\n\\endverbatim\n *\n * As you can see, this means that the two sides of conversation are recorded\n * separately, and so are the audio and video streams if available. You can\n * choose which ones to record, in case you're interested in just a subset.\n * The \\c filename part is just a prefix, and dictates the actual filenames\n * that will be used for the up-to-four recordings that may need to be enabled.\n *\n * A \\c recordingupdated event is sent back in case the request is successful.\n *\n * To programmatically send a video keyframe request to either the WebRTC user\n * or the SIP peer (or both), the \\c keyframe request can be used. This\n * request is particularly useful when the SIP peer doesn't support RTCP PLI,\n * and so may use other mechanisms (e.g., via signalling) to ask for a keyframe\n * to get video working. By using this request, the WebRTC user can ask Janus\n * to originate a PLI programmatically. The direction of the keyframe request\n * can be provided by using the \\c user and \\c peer properties: if \\c user\n * is \\c TRUE a keyframe request will be sent by Janus to the WebRTC user;\n * if \\c peer is \\c TRUE a keyframe request will be sent by Janus to the\n * SIP peer. In both cases an RTCP PLI message will be sent. The syntax of\n * the message is the following:\n *\n\\verbatim\n{\n\t\"request\" : \"keyframe\",\n\t\"user\" : <true|false; whether or not to send a keyframe request to the WebRTC user>,\n\t\"peer\" : <true|false; whether or not to send a keyframe request to the SIP peer>\n}\n\\endverbatim\n *\n * A \\c keyframesent event is sent back in case the request is successful.\n *\n * \\section sipmc Simultaneous SIP calls using the same account\n *\n * As anticipated in the previous sections, attaching to the SIP plugin\n * with a Janus handle means creating a SIP stack on behalf of a user\n * or application: this typically means registering an account, and being\n * able to start or receive calls, handle subscriptions, and so on. This\n * also means that, since in Janus each core handle can only be associated\n * with a single PeerConnection, each SIP account is limited to a single\n * call per time: if a user is in a SIP session already, and another call\n * comes in, it's automatically rejected with a \\c 486 \\c Busy .\n *\n * While usually not a big deal, there are use cases where it might make\n * sense to be able to support multiple concurrent calls, and maybe switch\n * from one to the other seamlessly. This is possible in the SIP plugin\n * using the so-called \\c helper sessions. Specifically, \\c helper sessions\n * work under the assumption that there's a \\c master session that is\n * registered normally (the \"regular\" SIP plugin handle, that is), and\n * that these \\c helper sessions can simply be associated to that: any time\n * another concurrent call is needed, if the \\c master session is busy\n * one of the \\c helpers can be used; the more \\c helper sessions are\n * available, the more simultaneous calls can be established.\n *\n * The way this works is simple:\n *\n * 1. you create a SIP session the usual way, and send a regular \\c register\n * there; this will be the \\c master session, and will return a \\c master_id\n * when successfully registered;\n * 2. for each \\c helper you want to add, you attach a new Janus handle\n * to the SIP plugin, and send a \\c register with \\c type: \\c \"helper\" and\n * providing the same \\c username as the master, plus a \\c master_id attribute\n * referencing the main session;\n * 3. at this point, the new \\c helper is associated to the \\c master ,\n * meaning it can be used to start new calls or receive calls exactly\n * as the main session, and using the same account information, credentials,\n * etc.\n *\n * Notice that, as soon as the \\c master unregisters, or the Janus handle\n * it's on is detached, all the \\c helper sessions associated to it are\n * automatically torn down as well. Specifically, the plugin will forcibly\n * detach the related handles. Should you need to register again, and want\n * some helpers there too, you'll have to add them again.\n *\n * If you want to see this in practice, the SIP plugin demo has a \"hidden\"\n * function you can invoke from the JavaScript console to play with helpers:\n * calling the \\c addHelper() function will add a new helper, and show additional\n * controls. You can add as many helpers as you want.\n *\n * \\section siptr Attended and blind transfers\n *\n * The Janus SIP plugin supports both attended and blind transfers, and to\n * do so mostly relies on the multiple calls functionality: as such, make\n * sure you've read and are familiar with the section on \\ref sipmc .\n *\n * Most of the transfer-related functionality are based on existing messages\n * and events already documented in the previous section, but there are a\n * few aspects you need to be aware of. First of all, if you're the transferor,\n * you need to use a new request called \\c transfer , that allows you to\n * send a SIP REFER to the transferee so to reach a different target. The\n * \\c transfer request must be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"transfer\",\n\t\"uri\" : \"<SIP URI to send the transferee too>\",\n\t\"replace\" : \"<call-ID of the call this attended transfer is supposed to replace; default is none, which means blind/unattended transfer>\"\n}\n\\endverbatim\n *\n * Whether this is a blind (no call to replace) or attended transfer,\n * a \\c transferring event will be sent back, as this is an asynchronous\n * request. Further updates will come in the form of NOTIFY-related events,\n * as a REFER implicitly creates a subscription.\n *\n * The recipient of a REFER, instead, will receive an asynchronous event\n * called \\c transfer as well, with info it needs to be aware of. In fact,\n * the SIP plugin doesn't do anything automatically: an incoming REFER is\n * notified to the application, so that it can decide whether to follow\n * up on the transfer or not. The syntax of the event is the following:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"transfer\",\n\t\t\"refer_id\" : <unique ID, internal to Janus, of this referral>,\n\t\t\"refer_to\" : \"<SIP URI to call>\",\n\t\t\"referred_by\" : \"<SIP URI header conveying the identity of the transferor; optional>\",\n\t\t\"replaces\" : \"<call-ID of the call this transfer is supposed to replace; optional, and only present for attended transfers>\",\n\t\t\"headers\" : \"<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>\"\n\t}\n}\n\\endverbatim\n *\n * The most important property in that list is \\c refer_id as that value\n * must be included in the \\c call request to call the target, if the\n * transfer is accepted: in fact, that's the only way the SIP plugin has\n * to correlate the new outgoing call to the previous transfer request,\n * and thus be able to notify the transferor about how the call is\n * proceeding by means of NOTIFY events. Notice that, if the transferee\n * decides to follow up on the transfer request, and they're already in\n * a call (e.g., with the transferor), then they must use a different\n * handle for the purpose, e.g., via a helper as described in the\n * \\ref sipmc section.\n *\n * The transfer target will receive the call exactly as previously discussed,\n * with the difference that it may or may not include a \\c referred_by\n * property for information purposes. Just as the transferee, if they're\n * already in a call, it's up to the application to create a helper to\n * setup a new Janus handle to accept the transfer.\n *\n * Notice that the plugin will NOT put the involved calls on-hold, or\n * automatically close calls that are meant to be replaced by a transfer.\n * All this is the application responsibility, and as such it's up to\n * the developer to react to events accordingly.\n *\n * \\subsection siprtpfwd RTP forwarders in the SIP plugin\n *\n * RTP forwarders are a quite useful functionality that a few plugins\n * in Janus can take advantage of. As the name suggests, their main\n * purpose is indeed forwarding/relaying RTP traffic to an external\n * backend, for further processing or management of media packets outside\n * of the context of the plugin that's responsible for it.\n *\n * Within the context of the SIP plugin, RTP forwarders allow you to\n * send the RTP traffic belonging to a specific SIP call to an external\n * backend, e.g., to a component implementing transcriptions, a remote\n * recorder, a lawful interception application or whatever else may have\n * a need for the content of a call in real-time. The RTP forwarder\n * functionality is conceived in a way that allows you to separately\n * forward individual RTP streams: this allows you to only forward, e.g.,\n * the audio packets of the local Janus user, only forward the video\n * packets of the SIP peer, forward everything, or any combination in\n * between. For every media stream you're interested in forwarding, a\n * separate RTP forwarder will be needed, which is what enables this\n * flexibility. Multiple forwarders can be created for the same stream,\n * in case the same RTP traffic should be sent to multiple backends at\n * the same time.\n *\n * Notice that SIP is not involved for RTP forwarders. RTP traffic is\n * relayed out of context, using a specific API that is part of the\n * SIP plugin itself. RTP forwarders will need an ongoing SIP call to\n * operate, though, as they'll be associated to existing media streams\n * in an existing SIP call; more specifically, forwarding requests must\n * be performed on the handle that's handling the call that should be\n * the target of its operations, which is why the related requests don't\n * need any identifier (the scope is automatically obtained from the\n * context of the handle). Forwarders that were created while a call\n * was active will automatically be destroyed when the call is hung up\n *\n * To setup new RTP forwarders, you can use the \\c rtp_forward request,\n * which must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"rtp_forward\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"type\": \"<what we want forward: must be one of audio, video, peer_audio, peer_video>\",\n\t\t\t\"host\" : \"<host address to forward the packets to>\",\n\t\t\t\"host_family\" : \"<optional; ipv4 by default>\",\n\t\t\t\"port\" : <port to forward the packets to>,\n\t\t\t\"ssrc\" : <SSRC to use to use when forwarding; optional>,\n\t\t\t\"pt\" : <payload type to use when forwarding; optional>,\n\t\t\t\"srtp_suite\" : <length of authentication tag (32 or 80); optional>,\n\t\t\t\"srtp_crypto\" : \"<key to use as crypto (base64 encoded key as in SDES); optional>\"\n\t\t},\n\t\t{\n\t\t\t.. other streams, if needed..\n\t\t}\n\t]\n}\n\\endverbatim\n *\n * The syntax of the request makes it easy to set up multiple RTP forwarders\n * for a call at the same time, by adding multiple objects as part of the\n * \\c streams array.\n *\n * A successful request will result in an \\c rtp_forward event, containing\n * the relevant info associated to the new forwarder(s):\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"rtp_forward\",\n\t\t\"forwarders\" : [\n\t\t\t{\n\t\t\t\t\"stream_id\" : <unique numeric ID assigned to this forwarder, if any>,\n\t\t\t\t\"type\" : \"<type of this forwarder, as configured in the request>\",\n\t\t\t\t\"host\" : \"<host this forwarder is streaming to, same as request if not resolved>\",\n\t\t\t\t\"port\" : <port this forwarder is streaming to, same as request if configured>,\n\t\t\t\t\"media\" : \"<audio or video>\",\n\t\t\t\t\"ssrc\" : <SSRC this forwarder is using, if any>,\n\t\t\t\t\"pt\" : <payload type this forwarder is using, if any>,\n\t\t\t\t\"srtp\" : <true|false, whether the RTP stream is encrypted>\n\t\t\t},\n\t\t\t// Other forwarders, if configured\n\t\t]\n\t},\n\t\"call_id\": \"<call-ID of the call>\"\n}\n\\endverbatim\n *\n * The \\c stream_id property returned for each forwarder is what will\n * need to be used for managing it, i.e., to destroy it once done.\n *\n * To stop a previously created RTP forwarder and stop it, you can use\n * the \\c stop_rtp_forward request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"stop_rtp_forward\",\n\t\"stream_id\" : <unique numeric ID of the RTP forwarder>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c stop_rtp_forward event:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"stop_rtp_forward\",\n\t\t\"stream_id\" : <unique numeric ID, same as request>\n\t},\n\t\"call_id\" : \"<call-ID of the call>\"\n}\n\\endverbatim\n *\n * To get a list of all the forwarders for an active call, instead, you\n * can make use of the \\c listforwarders request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listforwarders\"\n}\n\\endverbatim\n *\n * A successful request will produce a list of RTP forwarders in a\n * \\c forwarders event:\n *\n\\verbatim\n{\n\t\"sip\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"forwarders\",\n\t\t\"rtp_forwarders\" : [\t\t// Array of RTP forwarders\n\t\t\t{\t// RTP forwarder #1\n\t\t\t\t\"stream_id\" : <unique numeric ID assigned to this forwarder, if any>,\n\t\t\t\t\"type\" : \"<type of this forwarder, as configured in the request>\",\n\t\t\t\t\"host\" : \"<host this forwarder is streaming to, same as request if not resolved>\",\n\t\t\t\t\"port\" : <port this forwarder is streaming to, same as request if configured>,\n\t\t\t\t\"media\" : \"<audio or video>\",\n\t\t\t\t\"ssrc\" : <SSRC this forwarder is using, if any>,\n\t\t\t\t\"pt\" : <payload type this forwarder is using, if any>,\n\t\t\t\t\"srtp\" : <true|false, whether the RTP stream is encrypted>\n\t\t\t},\n\t\t\t// Other forwarders for this publisher\n\t\t]\n\t},\n\t\"call_id\" : \"<call-ID of the call>\"\n}\n\\endverbatim\n *\n */\n\n#include \"plugin.h\"\n\n#include <arpa/inet.h>\n#include <net/if.h>\n\n#include <jansson.h>\n\n#include <sofia-sip/msg_header.h>\n#include <sofia-sip/nua.h>\n#include <sofia-sip/nua_tag.h>\n#include <sofia-sip/sdp.h>\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wstrict-prototypes\"\n#include <sofia-sip/sip_header.h>\n#pragma GCC diagnostic pop\n#include <sofia-sip/sip_status.h>\n#include <sofia-sip/url.h>\n#include <sofia-sip/tport_tag.h>\n#include <sofia-sip/su_log.h>\n#include <sofia-sip/sofia_features.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../record.h\"\n#include \"../rtp.h\"\n#include \"../rtpsrtp.h\"\n#include \"../rtcp.h\"\n#include \"../rtpfwd.h\"\n#include \"../sdp-utils.h\"\n#include \"../utils.h\"\n#include \"../ip-utils.h\"\n\n\n/* Plugin information */\n#define JANUS_SIP_VERSION\t\t\t10\n#define JANUS_SIP_VERSION_STRING\t\"0.0.10\"\n#define JANUS_SIP_DESCRIPTION\t\t\"This is a simple SIP plugin for Janus, allowing WebRTC peers to register at a SIP server and call SIP user agents through a Janus instance.\"\n#define JANUS_SIP_NAME\t\t\t\t\"JANUS SIP plugin\"\n#define JANUS_SIP_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_SIP_PACKAGE\t\t\t\"janus.plugin.sip\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_sip_init(janus_callbacks *callback, const char *config_path);\nvoid janus_sip_destroy(void);\nint janus_sip_get_api_compatibility(void);\nint janus_sip_get_version(void);\nconst char *janus_sip_get_version_string(void);\nconst char *janus_sip_get_description(void);\nconst char *janus_sip_get_name(void);\nconst char *janus_sip_get_author(void);\nconst char *janus_sip_get_package(void);\nvoid janus_sip_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_sip_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\nvoid janus_sip_setup_media(janus_plugin_session *handle);\nvoid janus_sip_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_sip_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_sip_hangup_media(janus_plugin_session *handle);\nvoid janus_sip_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_sip_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_sip_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_sip_init,\n\t\t.destroy = janus_sip_destroy,\n\n\t\t.get_api_compatibility = janus_sip_get_api_compatibility,\n\t\t.get_version = janus_sip_get_version,\n\t\t.get_version_string = janus_sip_get_version_string,\n\t\t.get_description = janus_sip_get_description,\n\t\t.get_name = janus_sip_get_name,\n\t\t.get_author = janus_sip_get_author,\n\t\t.get_package = janus_sip_get_package,\n\n\t\t.create_session = janus_sip_create_session,\n\t\t.handle_message = janus_sip_handle_message,\n\t\t.setup_media = janus_sip_setup_media,\n\t\t.incoming_rtp = janus_sip_incoming_rtp,\n\t\t.incoming_rtcp = janus_sip_incoming_rtcp,\n\t\t.hangup_media = janus_sip_hangup_media,\n\t\t.destroy_session = janus_sip_destroy_session,\n\t\t.query_session = janus_sip_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_SIP_NAME);\n\treturn &janus_sip_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter register_parameters[] = {\n\t{\"type\", JSON_STRING, 0},\n\t{\"send_register\", JANUS_JSON_BOOL, 0},\n\t{\"force_udp\", JANUS_JSON_BOOL, 0},\n\t{\"force_tcp\", JANUS_JSON_BOOL, 0},\n\t{\"sips\", JANUS_JSON_BOOL, 0},\n\t{\"rfc2543_cancel\", JANUS_JSON_BOOL, 0},\n\t{\"automatic_ringing\", JANUS_JSON_BOOL, 0},\n\t{\"username\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"secret\", JSON_STRING, 0},\n\t{\"ha1_secret\", JSON_STRING, 0},\n\t{\"authuser\", JSON_STRING, 0},\n\t{\"display_name\", JSON_STRING, 0},\n\t{\"user_agent\", JSON_STRING, 0},\n\t{\"headers\", JSON_OBJECT, 0},\n\t{\"contact_params\", JSON_OBJECT, 0},\n\t{\"master_id\", JANUS_JSON_INTEGER, 0},\n\t{\"refresh\", JANUS_JSON_BOOL, 0},\n\t{\"incoming_header_prefixes\", JSON_ARRAY, 0},\n\t{\"register_ttl\", JANUS_JSON_INTEGER, 0}\n};\nstatic struct janus_json_parameter subscribe_parameters[] = {\n\t{\"to\", JSON_STRING, 0},\n\t{\"event\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"accept\", JSON_STRING, 0},\n\t{\"subscribe_ttl\", JANUS_JSON_INTEGER, 0},\n\t{\"call_id\", JANUS_JSON_STRING, 0},\n\t{\"headers\", JSON_OBJECT, 0},\n\t{\"content\", JSON_STRING, 0},\n\t{\"content_type\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter proxy_parameters[] = {\n\t{\"proxy\", JSON_STRING, 0},\n\t{\"outbound_proxy\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter call_parameters[] = {\n\t{\"uri\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"headers\", JSON_OBJECT, 0},\n\t{\"call_id\", JANUS_JSON_STRING, 0},\n\t{\"srtp\", JSON_STRING, 0},\n\t{\"srtp_profile\", JSON_STRING, 0},\n\t{\"autoaccept_reinvites\", JANUS_JSON_BOOL, 0},\n\t{\"refer_id\", JANUS_JSON_INTEGER, 0},\n\t/* The following are only needed in case \"guest\" registrations\n\t * still need an authenticated INVITE for some reason */\n\t{\"secret\", JSON_STRING, 0},\n\t{\"ha1_secret\", JSON_STRING, 0},\n\t{\"authuser\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter accept_parameters[] = {\n\t{\"srtp\", JSON_STRING, 0},\n\t{\"srtp_profile\", JSON_STRING, 0},\n\t{\"headers\", JSON_OBJECT, 0},\n\t{\"autoaccept_reinvites\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter progress_parameters[] = {\n\t{\"srtp\", JSON_STRING, 0},\n\t{\"srtp_profile\", JSON_STRING, 0},\n\t{\"headers\", JSON_OBJECT, 0},\n\t{\"autoaccept_reinvites\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter decline_parameters[] = {\n\t{\"code\", JANUS_JSON_INTEGER, 0},\n\t{\"headers\", JSON_OBJECT, 0},\n\t{\"refer_id\", JANUS_JSON_INTEGER, 0}\n};\nstatic struct janus_json_parameter transfer_parameters[] = {\n\t{\"uri\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"replace\", JANUS_JSON_STRING, 0}\n};\nstatic struct janus_json_parameter hold_parameters[] = {\n\t{\"direction\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter recording_parameters[] = {\n\t{\"action\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"peer_audio\", JANUS_JSON_BOOL, 0},\n\t{\"peer_video\", JANUS_JSON_BOOL, 0},\n\t{\"send_peer_pli\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter dtmf_info_parameters[] = {\n\t{\"digit\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"duration\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"headers\", JSON_OBJECT, 0}\n};\nstatic struct janus_json_parameter info_parameters[] = {\n\t{\"type\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"content\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"headers\", JSON_OBJECT, 0}\n};\nstatic struct janus_json_parameter sipmessage_parameters[] = {\n\t{\"content_type\", JSON_STRING, 0},\n\t{\"content\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"uri\", JSON_STRING, 0},\n\t{\"headers\", JSON_OBJECT, 0},\n\t{\"call_id\", JANUS_JSON_STRING, 0}\n};\nstatic struct janus_json_parameter keyframe_parameters[] = {\n\t{\"user\", JANUS_JSON_BOOL, 0},\n\t{\"peer\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter rtp_forward_parameters[] = {\n\t{\"streams\", JANUS_JSON_ARRAY, JANUS_JSON_PARAM_REQUIRED},\n};\nstatic struct janus_json_parameter rtp_forward_stream_parameters[] = {\n\t{\"type\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host_family\", JSON_STRING, 0},\n\t{\"port\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"ssrc\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"pt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter stop_rtp_forward_parameters[] = {\n\t{\"stream_id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic gboolean ipv6_disabled = FALSE;\nstatic janus_callbacks *gateway = NULL;\n\nstatic char *local_ip = NULL, *sdp_ip = NULL, *local_media_ip = NULL;\nstatic janus_network_address janus_network_local_media_ip = { 0 };\nstatic int keepalive_interval = 120;\nstatic gboolean behind_nat = FALSE;\nstatic char *user_agent;\n#define JANUS_DEFAULT_REGISTER_TTL\t3600\nstatic int register_ttl = JANUS_DEFAULT_REGISTER_TTL;\n#define JANUS_DEFAULT_SUBSCRIBE_TTL 3600\nstatic int subscribe_ttl = JANUS_DEFAULT_SUBSCRIBE_TTL;\nstatic uint16_t rtp_range_min = 10000;\nstatic uint16_t rtp_range_max = 60000;\nstatic int dscp_audio_rtp = 0;\nstatic int dscp_video_rtp = 0;\nstatic char *sips_certs_dir = NULL;\n#define JANUS_DEFAULT_SIP_TIMER_T1X64 32000\nstatic int sip_timer_t1x64 = JANUS_DEFAULT_SIP_TIMER_T1X64;\nstatic uint16_t dtmf_keys[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', 'A', 'B', 'C', 'D'};\n\nstatic gboolean query_contact_header = FALSE;\n\nstatic GThread *handler_thread;\nstatic void *janus_sip_handler(void *data);\nstatic void janus_sip_hangup_media_internal(janus_plugin_session *handle);\n\ntypedef struct janus_sip_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_sip_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_sip_message exit_message;\n\n\ntypedef enum {\n\tjanus_sip_registration_status_disabled = -2,\n\tjanus_sip_registration_status_failed = -1,\n\tjanus_sip_registration_status_unregistered = 0,\n\tjanus_sip_registration_status_registering,\n\tjanus_sip_registration_status_registered,\n\tjanus_sip_registration_status_unregistering,\n} janus_sip_registration_status;\n\nstatic const char *janus_sip_registration_status_string(janus_sip_registration_status status) {\n\tswitch(status) {\n\t\tcase janus_sip_registration_status_disabled:\n\t\t\treturn \"disabled\";\n\t\tcase janus_sip_registration_status_failed:\n\t\t\treturn \"failed\";\n\t\tcase janus_sip_registration_status_unregistered:\n\t\t\treturn \"unregistered\";\n\t\tcase janus_sip_registration_status_registering:\n\t\t\treturn \"registering\";\n\t\tcase janus_sip_registration_status_registered:\n\t\t\treturn \"registered\";\n\t\tcase janus_sip_registration_status_unregistering:\n\t\t\treturn \"unregistering\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n\ntypedef enum {\n\tjanus_sip_call_status_idle = 0,\n\tjanus_sip_call_status_inviting,\n\tjanus_sip_call_status_invited,\n\tjanus_sip_call_status_progress,\n\tjanus_sip_call_status_incall,\n\tjanus_sip_call_status_incall_reinviting,\n\tjanus_sip_call_status_incall_reinvited,\n\tjanus_sip_call_status_closing,\n} janus_sip_call_status;\n\nstatic const char *janus_sip_call_status_string(janus_sip_call_status status) {\n\tswitch(status) {\n\t\tcase janus_sip_call_status_idle:\n\t\t\treturn \"idle\";\n\t\tcase janus_sip_call_status_inviting:\n\t\t\treturn \"inviting\";\n\t\tcase janus_sip_call_status_invited:\n\t\t\treturn \"invited\";\n\t\tcase janus_sip_call_status_progress:\n\t\t\treturn \"progress\";\n\t\tcase janus_sip_call_status_incall:\n\t\t\treturn \"incall\";\n\t\tcase janus_sip_call_status_incall_reinviting:\n\t\t\treturn \"incall_reinviting\";\n\t\tcase janus_sip_call_status_incall_reinvited:\n\t\t\treturn \"incall_reinvited\";\n\t\tcase janus_sip_call_status_closing:\n\t\t\treturn \"closing\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n\n/* Sofia stuff */\ntypedef struct ssip_s ssip_t;\ntypedef struct ssip_oper_s ssip_oper_t;\n\n#undef SU_ROOT_MAGIC_T\n#define SU_ROOT_MAGIC_T\tssip_t\n#undef NUA_MAGIC_T\n#define NUA_MAGIC_T\t\tssip_t\n#undef NUA_HMAGIC_T\n#define NUA_HMAGIC_T\tssip_oper_t\n\nstruct ssip_s {\n\tsu_home_t s_home[1];\n\tsu_root_t *s_root;\n\tnua_t *s_nua;\n\tnua_handle_t *s_nh_r, *s_nh_i, *s_nh_m;\n\tchar *contact_header;\t/* Only needed for Sofia SIP >= 1.13 */\n\tGHashTable *subscriptions;\n\tjanus_mutex smutex;\n\tstruct janus_sip_session *session;\n};\n\ntypedef struct janus_sip_transfer {\n\tstruct janus_sip_session *session;\n\tchar *referred_by;\n\tchar *custom_headers;\n\tnua_handle_t *nh_s;\n\tnua_saved_event_t saved[1];\n} janus_sip_transfer;\n\ntypedef enum {\n\tjanus_sip_secret_type_plaintext = 1,\n\tjanus_sip_secret_type_hashed = 2,\n\tjanus_sip_secret_type_unknown\n} janus_sip_secret_type;\n\ntypedef struct janus_sip_account {\n\tchar *identity;\n\tchar *user_agent;\t\t/* Used to override the general UA string */\n\tgboolean force_udp;\n\tgboolean force_tcp;\n\tgboolean sips;\n\tgboolean rfc2543_cancel;\n\tgboolean automatic_ringing;\n\tchar *username;\n\tchar *display_name;\t\t/* Used for outgoing calls in the From header */\n\tchar *authuser;\t\t\t/**< username to use for authentication */\n\tchar *secret;\n\tjanus_sip_secret_type secret_type;\n\tint sip_port;\n\tchar *proxy;\n\tchar *outbound_proxy;\n\tjanus_sip_registration_status registration_status;\n} janus_sip_account;\n\ntypedef struct janus_sip_media {\n\tchar *remote_audio_ip;\t\t\t/* Peer audio media IP address */\n\tchar *remote_video_ip;\t\t\t/* Peer video media IP address */\n\tgboolean earlymedia;\n\tgboolean update;\n\tgboolean autoaccept_reinvites;\n\tgboolean ready;\n\tgboolean require_srtp,\n\t\thas_srtp_local_audio, has_srtp_local_video,\n\t\thas_srtp_remote_audio, has_srtp_remote_video;\n\tjanus_srtp_profile srtp_profile;\n\tgboolean on_hold;\n\tgboolean has_audio;\n\tint audio_rtp_fd, audio_rtcp_fd;\n\tint local_audio_rtp_port, remote_audio_rtp_port;\n\tint local_audio_rtcp_port, remote_audio_rtcp_port;\n\tguint32 audio_ssrc, audio_ssrc_peer;\n\tint audio_pt, opusred_pt;\n\tconst char *audio_pt_name;\n\tgint32 audio_srtp_tag;\n\tsrtp_t audio_srtp_in, audio_srtp_out;\n\tsrtp_policy_t audio_remote_policy, audio_local_policy;\n\tchar *audio_srtp_local_profile, *audio_srtp_local_crypto;\n\tgboolean audio_send, audio_recv;\n\tjanus_sdp_mdirection hold_audio_dir, pre_hold_audio_dir;\n\tgboolean has_video;\n\tint video_rtp_fd, video_rtcp_fd;\n\tint local_video_rtp_port, remote_video_rtp_port;\n\tint local_video_rtcp_port, remote_video_rtcp_port;\n\tguint32 video_ssrc, video_ssrc_peer;\n\tguint32 simulcast_ssrc;\n\tint video_pt;\n\tconst char *video_pt_name;\n\tgint32 video_srtp_tag;\n\tsrtp_t video_srtp_in, video_srtp_out;\n\tsrtp_policy_t video_remote_policy, video_local_policy;\n\tchar *video_srtp_local_profile, *video_srtp_local_crypto;\n\tgboolean video_send, video_recv;\n\tgboolean video_pli_supported;\n\tjanus_sdp_mdirection hold_video_dir, pre_hold_video_dir;\n\tjanus_rtp_switching_context acontext, vcontext;\n\tint pipefd[2];\n\tgboolean updated;\n\tint video_orientation_extension_id;\n\tint audio_level_extension_id;\n\tint dtmf_pt;\n} janus_sip_media;\n\ntypedef struct janus_sip_dtmf {\n\tuint16_t dtmf_event_id;\n\tuint32_t timestamp;\n} janus_sip_dtmf;\n\ntypedef struct janus_sip_session {\n\tjanus_plugin_session *handle;\n\tchar *unique_id;\n\tssip_t *stack;\n\tjanus_sip_account account;\n\tjanus_sip_call_status status;\n\tjanus_sip_media media;\n\tchar *transaction;\n\tchar *callee;\n\tchar *callid, *local_tag;\n\tguint32 refer_id;\t\t\t/* In case we were asked to transfer, keep track of the ID */\n\tjanus_sdp *sdp;\t\t\t\t/* The SDP this user sent */\n\tjanus_recorder *arc;\t\t/* The Janus recorder instance for this user's audio, if enabled */\n\tjanus_recorder *arc_peer;\t/* The Janus recorder instance for the peer's audio, if enabled */\n\tjanus_recorder *vrc;\t\t/* The Janus recorder instance for this user's video, if enabled */\n\tjanus_recorder *vrc_peer;\t/* The Janus recorder instance for the peer's video, if enabled */\n\tjanus_mutex rec_mutex;\t\t/* Mutex to protect the recorders from race conditions */\n\tGHashTable *audio_forwarders, *video_forwarders,\n\t\t*peer_audio_forwarders, *peer_video_forwarders,\n\t\t*all_forwarders;\t/* RTP forwarders for this call (all streams), if any */\n\tjanus_mutex rtp_forwarders_mutex;\n\tint udp_sock; \t\t\t\t/* The socket on which to forward RTP packets */\n\tGThread *relayer_thread;\n\tvolatile gint establishing, established;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\t/* Sessions may be helpers under a \"master\" (e.g., for multiple calls from/to the same account) */\n\tguint32 master_id;\t\t/* Master ID the helpers refer to */\n\tstruct janus_sip_session *master;\n\tgboolean helper;\t\t/* Whether this session is a helper or not */\n\tGList *helpers;\t\t\t/* The helper sessions, if this is the \"master\" */\n\tjanus_mutex mutex;\n\tchar *hangup_reason_header;\n\tchar *hangup_reason_header_protocol;\n\tchar *hangup_reason_header_cause;\n\tGList *incoming_header_prefixes;\n\tjson_t *hangup_custom_headers;\n\tGList *active_calls;\n\tjanus_refcount ref;\n\tjanus_sip_dtmf latest_dtmf;\n} janus_sip_session;\n\ntypedef struct janus_sip_call {\n\tjanus_sip_session *caller;\n\tjanus_sip_session *callee;\n} janus_sip_call;\n\nstatic GHashTable *sessions;\nstatic GHashTable *identities;\nstatic GHashTable *callids;\nstatic GHashTable *messageids;\nstatic GHashTable *masters;\nstatic GHashTable *transfers;\nstatic GHashTable *unique_ids;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_sip_srtp_cleanup(janus_sip_session *session);\nstatic void janus_sip_media_reset(janus_sip_session *session);\nstatic void janus_sip_rtcp_pli_send(janus_sip_session *session);\n\nstatic janus_rtp_forwarder *janus_sip_rtp_forwarder_add_helper(janus_sip_session *session, const char *type,\n\tconst gchar *host, int port, int pt, uint32_t ssrc, int srtp_suite, const char *srtp_crypto);\nstatic json_t *janus_sip_rtp_forwarder_summary(janus_rtp_forwarder *f);\n\nstatic void janus_sip_call_update_status(janus_sip_session *session, janus_sip_call_status new_status) {\n\tif(session->status != new_status) {\n\t\tJANUS_LOG(LOG_VERB, \"[%s] Call status change: [%s]-->[%s]\\n\", session->account.username == NULL ? \"null\" : session->account.username, janus_sip_call_status_string(session->status), janus_sip_call_status_string(new_status));\n\t\tsession->status = new_status;\n\t}\n}\n\nstatic gboolean janus_sip_call_is_established(janus_sip_session *session) {\n\treturn (session->status == janus_sip_call_status_incall ||\n\t\tsession->status == janus_sip_call_status_incall_reinviting ||\n\t\tsession->status == janus_sip_call_status_incall_reinvited) ? TRUE : FALSE;\n}\n\nstatic void janus_sip_media_reset(janus_sip_session *session);\n\nstatic void janus_sip_session_destroy(janus_sip_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1)) {\n\t\tif(session->master == NULL && session->account.identity)\n\t\t\tg_hash_table_remove(identities, session->account.identity);\n\t\tif(session->callid) {\n\t\t\tjanus_sip_call *call = g_hash_table_lookup(callids, session->callid);\n\t\t\tif(call) {\n\t\t\t\tif(call->caller == session)\n\t\t\t\t\tcall->caller = NULL;\n\t\t\t\telse if(call->callee == session)\n\t\t\t\t\tcall->callee = NULL;\n\t\t\t\tif(call->caller == NULL && call->callee == NULL)\n\t\t\t\t\tg_hash_table_remove(callids, session->callid);\n\t\t\t}\n\t\t}\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n}\n\nstatic void janus_sip_session_dereference(janus_sip_session *session) {\n\t/* This is used to decrease the reference when removing to the messageids hashtable. janus_refcount_increase(&session->ref) must be called before inserting into messageids hashtable  */\n\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic char *janus_sip_session_contact_header_retrieve(janus_sip_session *session) {\n\tif(session->helper && session->master)\n\t\treturn session->master->stack->contact_header;\n\telse\n\t\treturn session->stack->contact_header;\n}\n\nstatic void janus_sip_session_free(const janus_refcount *session_ref) {\n\tjanus_sip_session *session = janus_refcount_containerof(session_ref, janus_sip_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tif(session->master == NULL && session->account.identity) {\n\t\tg_free(session->account.identity);\n\t\tsession->account.identity = NULL;\n\t}\n\tif(session->stack != NULL) {\n\t\tsu_home_deinit(session->stack->s_home);\n\t\tsu_home_unref(session->stack->s_home);\n\t\tg_free(session->stack->contact_header);\n\t\tjanus_mutex_destroy(&session->stack->smutex);\n\t\tg_free(session->stack);\n\t\tsession->stack = NULL;\n\t}\n\tif(session->account.proxy) {\n\t\tg_free(session->account.proxy);\n\t\tsession->account.proxy = NULL;\n\t}\n\tif(session->account.outbound_proxy) {\n\t\tg_free(session->account.outbound_proxy);\n\t\tsession->account.outbound_proxy = NULL;\n\t}\n\tif(session->account.secret) {\n\t\tg_free(session->account.secret);\n\t\tsession->account.secret = NULL;\n\t}\n\tif(session->account.username) {\n\t\tg_free(session->account.username);\n\t\tsession->account.username = NULL;\n\t}\n\tif(session->account.display_name) {\n\t\tg_free(session->account.display_name);\n\t\tsession->account.display_name = NULL;\n\t}\n\tif(session->account.user_agent) {\n\t\tg_free(session->account.user_agent);\n\t\tsession->account.user_agent = NULL;\n\t}\n\tif(session->account.authuser) {\n\t\tg_free(session->account.authuser);\n\t\tsession->account.authuser = NULL;\n\t}\n\tif(session->callee) {\n\t\tg_free(session->callee);\n\t\tsession->callee = NULL;\n\t}\n\tif(session->callid) {\n\t\tg_free(session->callid);\n\t\tsession->callid = NULL;\n\t}\n\tif(session->local_tag) {\n\t\tg_free(session->local_tag);\n\t\tsession->local_tag = NULL;\n\t}\n\tif(session->sdp) {\n\t\tjanus_sdp_destroy(session->sdp);\n\t\tsession->sdp = NULL;\n\t}\n\tif(session->unique_id) {\n\t\tg_free(session->unique_id);\n\t\tsession->unique_id = NULL;\n\t}\n\tif(session->transaction) {\n\t\tg_free(session->transaction);\n\t\tsession->transaction = NULL;\n\t}\n\tif(session->media.remote_audio_ip) {\n\t\tg_free(session->media.remote_audio_ip);\n\t\tsession->media.remote_audio_ip = NULL;\n\t}\n\tif(session->media.remote_video_ip) {\n\t\tg_free(session->media.remote_video_ip);\n\t\tsession->media.remote_video_ip = NULL;\n\t}\n\tif(session->hangup_reason_header) {\n\t\tg_free(session->hangup_reason_header);\n\t\tsession->hangup_reason_header = NULL;\n\t}\n\tif(session->hangup_reason_header_protocol) {\n\t\tg_free(session->hangup_reason_header_protocol);\n\t\tsession->hangup_reason_header_protocol = NULL;\n\t}\n\tif(session->hangup_reason_header_cause) {\n\t\tg_free(session->hangup_reason_header_cause);\n\t\tsession->hangup_reason_header_cause = NULL;\n\t}\n\tif(session->incoming_header_prefixes) {\n\t\tg_list_free_full(session->incoming_header_prefixes, g_free);\n\t\tsession->incoming_header_prefixes = NULL;\n\t}\n\tif(session->hangup_custom_headers) {\n\t\tjson_decref(session->hangup_custom_headers);\n\t\tsession->hangup_custom_headers = NULL;\n\t}\n\tjanus_sip_srtp_cleanup(session);\n\tg_hash_table_destroy(session->audio_forwarders);\n\tsession->audio_forwarders = NULL;\n\tg_hash_table_destroy(session->video_forwarders);\n\tsession->video_forwarders = NULL;\n\tg_hash_table_destroy(session->peer_audio_forwarders);\n\tsession->peer_audio_forwarders = NULL;\n\tg_hash_table_destroy(session->peer_video_forwarders);\n\tsession->peer_video_forwarders = NULL;\n\tg_hash_table_destroy(session->all_forwarders);\n\tsession->all_forwarders = NULL;\n\tjanus_mutex_destroy(&session->rtp_forwarders_mutex);\n\tjanus_mutex_destroy(&session->mutex);\n\tjanus_mutex_destroy(&session->rec_mutex);\n\tg_free(session);\n}\n\nstatic void janus_sip_message_free(janus_sip_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_sip_session *session = (janus_sip_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\nstatic void janus_sip_transfer_destroy(janus_sip_transfer *t) {\n\tif(t == NULL)\n\t\treturn;\n\tg_free(t->referred_by);\n\tg_free(t->custom_headers);\n\tif(t->session != NULL)\n\t\tjanus_refcount_decrease(&t->session->ref);\n\tg_free(t);\n}\n\n/* SRTP stuff (in case we need SDES) */\nstatic int janus_sip_srtp_set_local(janus_sip_session *session, gboolean video, char **profile, char **crypto) {\n\tif(session == NULL)\n\t\treturn -1;\n\t/* Which SRTP profile are we going to negotiate? */\n\tint key_length = 0, salt_length = 0, master_length = 0;\n\tif(session->media.srtp_profile == JANUS_SRTP_AES128_CM_SHA1_32) {\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AES_CM_128_HMAC_SHA1_32\");\n\t} else if(session->media.srtp_profile == JANUS_SRTP_AES128_CM_SHA1_80) {\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AES_CM_128_HMAC_SHA1_80\");\n#ifdef HAVE_SRTP_AESGCM\n\t} else if(session->media.srtp_profile == JANUS_SRTP_AEAD_AES_128_GCM) {\n\t\tkey_length = SRTP_AESGCM128_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM128_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM128_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AEAD_AES_128_GCM\");\n\t} else if(session->media.srtp_profile == JANUS_SRTP_AEAD_AES_256_GCM) {\n\t\tkey_length = SRTP_AESGCM256_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM256_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM256_MASTER_LENGTH;\n\t\t*profile = g_strdup(\"AEAD_AES_256_GCM\");\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Unsupported SRTP profile\\n\", session->account.username);\n\t\treturn -2;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[SIP-%s] %s\\n\", session->account.username, *profile);\n\tJANUS_LOG(LOG_VERB, \"[SIP-%s] Key/Salt/Master: %d/%d/%d\\n\",\n\t\tsession->account.username, master_length, key_length, salt_length);\n\t/* Generate key/salt */\n\tuint8_t *key = g_malloc0(master_length);\n\tsrtp_crypto_get_random(key, master_length);\n\t/* Set SRTP policies */\n\tsrtp_policy_t *policy = video ? &session->media.video_local_policy : &session->media.audio_local_policy;\n\tswitch(session->media.srtp_profile) {\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_32:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_80:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n#ifdef HAVE_SRTP_AESGCM\n\t\tcase JANUS_SRTP_AEAD_AES_128_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AEAD_AES_256_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n#endif\n\t\tdefault:\n\t\t\t/* Will never happen? */\n\t\t\tJANUS_LOG(LOG_WARN, \"[SIP-%s] Unsupported SRTP profile\\n\", session->account.username);\n\t\t\tbreak;\n\t}\n\tpolicy->ssrc.type = ssrc_any_inbound;\n\tpolicy->key = key;\n\tpolicy->next = NULL;\n\t/* Create SRTP context */\n\tsrtp_err_status_t res = srtp_create(video ? &session->media.video_srtp_out : &session->media.audio_srtp_out, policy);\n\tif(res != srtp_err_status_ok) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Oops, error creating outbound SRTP session: %d (%s)\\n\", res, janus_srtp_error_str(res));\n\t\tg_free(*profile);\n\t\t*profile = NULL;\n\t\tg_free(key);\n\t\tpolicy->key = NULL;\n\t\treturn -2;\n\t}\n\t/* Base64 encode the salt */\n\t*crypto = g_base64_encode(key, master_length);\n\tif((video && session->media.video_srtp_out) || (!video && session->media.audio_srtp_out)) {\n\t\tJANUS_LOG(LOG_VERB, \"%s outbound SRTP session created\\n\", video ? \"Video\" : \"Audio\");\n\t}\n\treturn 0;\n}\nstatic int janus_sip_srtp_set_remote(janus_sip_session *session, gboolean video, const char *profile, const char *crypto) {\n\tif(session == NULL || profile == NULL || crypto == NULL)\n\t\treturn -1;\n\t/* Which SRTP profile is being negotiated? */\n\tJANUS_LOG(LOG_VERB, \"[SIP-%s] %s\\n\", session->account.username, profile);\n\tgsize key_length = 0, salt_length = 0, master_length = 0;\n\tif(!strcasecmp(profile, \"AES_CM_128_HMAC_SHA1_32\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AES128_CM_SHA1_32;\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n\t} else if(!strcasecmp(profile, \"AES_CM_128_HMAC_SHA1_80\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n\t\tkey_length = SRTP_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_MASTER_LENGTH;\n#ifdef HAVE_SRTP_AESGCM\n\t} else if(!strcasecmp(profile, \"AEAD_AES_128_GCM\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AEAD_AES_128_GCM;\n\t\tkey_length = SRTP_AESGCM128_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM128_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM128_MASTER_LENGTH;\n\t} else if(!strcasecmp(profile, \"AEAD_AES_256_GCM\")) {\n\t\tsession->media.srtp_profile = JANUS_SRTP_AEAD_AES_256_GCM;\n\t\tkey_length = SRTP_AESGCM256_MASTER_KEY_LENGTH;\n\t\tsalt_length = SRTP_AESGCM256_MASTER_SALT_LENGTH;\n\t\tmaster_length = SRTP_AESGCM256_MASTER_LENGTH;\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_WARN, \"[SIP-%s] Unsupported SRTP profile %s\\n\", session->account.username, profile);\n\t\treturn -2;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[SIP-%s] Key/Salt/Master: %zu/%zu/%zu\\n\",\n\t\tsession->account.username, master_length, key_length, salt_length);\n\t/* Base64 decode the crypto string and set it as the remote SRTP context */\n\tgsize len = 0;\n\tguchar *decoded = g_base64_decode(crypto, &len);\n\tif(len < master_length) {\n\t\t/* FIXME Can this happen? */\n\t\tg_free(decoded);\n\t\treturn -3;\n\t}\n\t/* Set SRTP policies */\n\tsrtp_policy_t *policy = video ? &session->media.video_remote_policy : &session->media.audio_remote_policy;\n\tswitch(session->media.srtp_profile) {\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_32:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AES128_CM_SHA1_80:\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtcp));\n\t\t\tbreak;\n#ifdef HAVE_SRTP_AESGCM\n\t\tcase JANUS_SRTP_AEAD_AES_128_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_128_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n\t\tcase JANUS_SRTP_AEAD_AES_256_GCM:\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtp));\n\t\t\tsrtp_crypto_policy_set_aes_gcm_256_16_auth(&(policy->rtcp));\n\t\t\tbreak;\n#endif\n\t\tdefault:\n\t\t\t/* Will never happen? */\n\t\t\tJANUS_LOG(LOG_WARN, \"[SIP-%s] Unsupported SRTP profile\\n\", session->account.username);\n\t\t\tbreak;\n\t}\n\tpolicy->ssrc.type = ssrc_any_inbound;\n\tpolicy->key = decoded;\n\tpolicy->next = NULL;\n\t/* Create SRTP context */\n\tsrtp_err_status_t res = srtp_create(video ? &session->media.video_srtp_in : &session->media.audio_srtp_in, policy);\n\tif(res != srtp_err_status_ok) {\n\t\t/* Something went wrong... */\n\t\tJANUS_LOG(LOG_ERR, \"Oops, error creating inbound SRTP session: %d (%s)\\n\", res, janus_srtp_error_str(res));\n\t\tg_free(decoded);\n\t\tpolicy->key = NULL;\n\t\treturn -2;\n\t}\n\tif((video && session->media.video_srtp_in) || (!video && session->media.audio_srtp_in)) {\n\t\tJANUS_LOG(LOG_VERB, \"%s inbound SRTP session created\\n\", video ? \"Video\" : \"Audio\");\n\t}\n\treturn 0;\n}\nstatic void janus_sip_srtp_cleanup(janus_sip_session *session) {\n\tif(session == NULL)\n\t\treturn;\n\tsession->media.require_srtp = FALSE;\n\tsession->media.has_srtp_local_audio = FALSE;\n\tsession->media.has_srtp_local_video = FALSE;\n\tsession->media.has_srtp_remote_audio = FALSE;\n\tsession->media.has_srtp_remote_video = FALSE;\n\tsession->media.srtp_profile = 0;\n\t/* Audio */\n\tsession->media.audio_srtp_tag = 0;\n\tif(session->media.audio_srtp_out)\n\t\tsrtp_dealloc(session->media.audio_srtp_out);\n\tsession->media.audio_srtp_out = NULL;\n\tg_free(session->media.audio_local_policy.key);\n\tsession->media.audio_local_policy.key = NULL;\n\tif(session->media.audio_srtp_in)\n\t\tsrtp_dealloc(session->media.audio_srtp_in);\n\tsession->media.audio_srtp_in = NULL;\n\tg_free(session->media.audio_remote_policy.key);\n\tsession->media.audio_remote_policy.key = NULL;\n\tif(session->media.audio_srtp_local_profile) {\n\t\tg_free(session->media.audio_srtp_local_profile);\n\t\tsession->media.audio_srtp_local_profile = NULL;\n\t}\n\tif(session->media.audio_srtp_local_crypto) {\n\t\tg_free(session->media.audio_srtp_local_crypto);\n\t\tsession->media.audio_srtp_local_crypto = NULL;\n\t}\n\t/* Video */\n\tsession->media.video_srtp_tag = 0;\n\tif(session->media.video_srtp_out)\n\t\tsrtp_dealloc(session->media.video_srtp_out);\n\tsession->media.video_srtp_out = NULL;\n\tg_free(session->media.video_local_policy.key);\n\tsession->media.video_local_policy.key = NULL;\n\tif(session->media.video_srtp_in)\n\t\tsrtp_dealloc(session->media.video_srtp_in);\n\tsession->media.video_srtp_in = NULL;\n\tg_free(session->media.video_remote_policy.key);\n\tsession->media.video_remote_policy.key = NULL;\n\tif(session->media.video_srtp_local_profile) {\n\t\tg_free(session->media.video_srtp_local_profile);\n\t\tsession->media.video_srtp_local_profile = NULL;\n\t}\n\tif(session->media.video_srtp_local_crypto) {\n\t\tg_free(session->media.video_srtp_local_crypto);\n\t\tsession->media.video_srtp_local_crypto = NULL;\n\t}\n}\n\nstatic void janus_sip_media_reset(janus_sip_session *session) {\n\tif(session == NULL)\n\t\treturn;\n\tg_free(session->media.remote_audio_ip);\n\tsession->media.remote_audio_ip = NULL;\n\tg_free(session->media.remote_video_ip);\n\tsession->media.remote_video_ip = NULL;\n\tsession->media.earlymedia = FALSE;\n\tsession->media.update = FALSE;\n\tsession->media.updated = FALSE;\n\tsession->media.autoaccept_reinvites = TRUE;\n\tsession->media.ready = FALSE;\n\tsession->media.require_srtp = FALSE;\n\tsession->media.on_hold = FALSE;\n\tsession->media.has_audio = FALSE;\n\tsession->media.audio_pt = -1;\n\tsession->media.opusred_pt = -1;\n\tsession->media.audio_pt_name = NULL;\t/* Immutable string, no need to free*/\n\tsession->media.audio_send = TRUE;\n\tsession->media.audio_recv = TRUE;\n\tsession->media.hold_audio_dir = JANUS_SDP_SENDONLY;\n\tsession->media.pre_hold_audio_dir = JANUS_SDP_DEFAULT;\n\tsession->media.has_video = FALSE;\n\tsession->media.video_pt = -1;\n\tsession->media.video_pt_name = NULL;\t/* Immutable string, no need to free*/\n\tsession->media.video_send = TRUE;\n\tsession->media.video_recv = TRUE;\n\tsession->media.video_pli_supported = FALSE;\n\tsession->media.hold_video_dir = JANUS_SDP_SENDONLY;\n\tsession->media.pre_hold_video_dir = JANUS_SDP_DEFAULT;\n\tsession->media.video_orientation_extension_id = -1;\n\tsession->media.audio_level_extension_id = -1;\n\tsession->media.dtmf_pt = -1;\n\tjanus_rtp_switching_context_reset(&session->media.acontext);\n\tjanus_rtp_switching_context_reset(&session->media.vcontext);\n}\n\n\n/* Sofia Event thread */\ngpointer janus_sip_sofia_thread(gpointer user_data);\n/* Sofia callbacks */\nvoid janus_sip_sofia_callback(nua_event_t event, int status, char const *phrase, nua_t *nua, nua_magic_t *magic, nua_handle_t *nh, nua_hmagic_t *hmagic, sip_t const *sip, tagi_t tags[]);\nvoid janus_sip_save_reason(sip_t const *sip, janus_sip_session *session);\n/* SDP parsing and manipulation */\nvoid janus_sip_sdp_process(janus_sip_session *session, janus_sdp *sdp, gboolean answer, gboolean update, gboolean *changed);\nchar *janus_sip_sdp_manipulate(janus_sip_session *session, janus_sdp *sdp, gboolean answer);\n/* Media */\nstatic int janus_sip_allocate_local_ports(janus_sip_session *session, gboolean update);\nstatic void *janus_sip_relay_thread(void *data);\nstatic void janus_sip_media_cleanup(janus_sip_session *session);\nstatic void janus_sip_check_rfc2833(janus_sip_session *session, char *buffer, int len);\n\n/* URI parsing utilities */\n\n#define JANUS_SIP_URI_MAXLEN\t1024\ntypedef struct {\n\tchar data[JANUS_SIP_URI_MAXLEN];\n\turl_t url[1];\n} janus_sip_uri_t;\n\n/* Parses a SIP URI (SIPS is not supported), returns 0 on success, -1 otherwise */\nstatic int janus_sip_parse_uri(janus_sip_uri_t *sip_uri, const char *data) {\n\tg_strlcpy(sip_uri->data, data, JANUS_SIP_URI_MAXLEN);\n\tif(url_d(sip_uri->url, sip_uri->data) < 0 || sip_uri->url->url_type != url_sip)\n\t\treturn -1;\n\treturn 0;\n}\n\n/* Similar to the above function, but it also accepts SIPS URIs */\nstatic int janus_sip_parse_proxy_uri(janus_sip_uri_t *sip_uri, const char *data) {\n\tg_strlcpy(sip_uri->data, data, JANUS_SIP_URI_MAXLEN);\n\tif(url_d(sip_uri->url, sip_uri->data) < 0 || (sip_uri->url->url_type != url_sip && sip_uri->url->url_type != url_sips))\n\t\treturn -1;\n\treturn 0;\n}\n\n/* Helper to strip quotes from a SIP Reason Header */\nstatic void janus_sip_remove_quotes(char *str) {\n\tsize_t len = strlen(str);\n\tif(len > 2 && str[0] == '\"' && str[len-1] == '\"') {\n\t\tmemmove(str, str+1, len-2);\n\t\tstr[len-2] = 0;\n\t}\n}\n\nstatic json_t *janus_sip_get_incoming_headers(const sip_t *sip, const janus_sip_session *session) {\n\tjson_t *headers = json_object();\n\tif(!sip)\n\t\treturn headers;\n\tsip_unknown_t *unknown_header = sip->sip_unknown;\n\twhile(unknown_header != NULL) {\n\t\tGList *temp = session->incoming_header_prefixes;\n\t\twhile(temp != NULL) {\n\t\t\tchar *header_prefix = (char *)temp->data;\n\t\t\tif(header_prefix != NULL && unknown_header->un_name != NULL) {\n\t\t\t\tif(strncasecmp(unknown_header->un_name, header_prefix, strlen(header_prefix)) == 0) {\n\t\t\t\t\tjson_object_set(headers, unknown_header->un_name, json_string(unknown_header->un_value));\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tunknown_header = unknown_header->un_next;\n\t}\n\treturn headers;\n}\n\n/* Error codes */\n#define JANUS_SIP_ERROR_UNKNOWN_ERROR\t\t499\n#define JANUS_SIP_ERROR_NO_MESSAGE\t\t\t440\n#define JANUS_SIP_ERROR_INVALID_JSON\t\t441\n#define JANUS_SIP_ERROR_INVALID_REQUEST\t\t442\n#define JANUS_SIP_ERROR_MISSING_ELEMENT\t\t443\n#define JANUS_SIP_ERROR_INVALID_ELEMENT\t\t444\n#define JANUS_SIP_ERROR_ALREADY_REGISTERED\t445\n#define JANUS_SIP_ERROR_INVALID_ADDRESS\t\t446\n#define JANUS_SIP_ERROR_WRONG_STATE\t\t\t447\n#define JANUS_SIP_ERROR_MISSING_SDP\t\t\t448\n#define JANUS_SIP_ERROR_LIBSOFIA_ERROR\t\t449\n#define JANUS_SIP_ERROR_IO_ERROR\t\t\t450\n#define JANUS_SIP_ERROR_RECORDING_ERROR\t\t451\n#define JANUS_SIP_ERROR_TOO_STRICT\t\t\t452\n#define JANUS_SIP_ERROR_HELPER_ERROR\t\t453\n#define JANUS_SIP_ERROR_NO_SUCH_CALLID\t\t454\n#define JANUS_SIP_ERROR_NO_SUCH_STREAM\t\t455\n\n\n/* Random string helper (for call-ids) */\nstatic char charset[] = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\nstatic void janus_sip_random_string(int length, char *buffer) {\n\tif(length > 0 && buffer) {\n\t\tint l = (int)(sizeof(charset)-1);\n\t\tint i=0;\n\t\tfor(i=0; i<length; i++) {\n\t\t\tint key = janus_random_uint32() % l;\n\t\t\tbuffer[i] = charset[key];\n\t\t}\n\t\tbuffer[length-1] = '\\0';\n\t}\n}\n\nstatic void janus_sip_parse_custom_headers(json_t *root, char *custom_headers, size_t size) {\n\tcustom_headers[0] = '\\0';\n\tjson_t *headers = json_object_get(root, \"headers\");\n\tif(headers) {\n\t\tif(json_object_size(headers) > 0) {\n\t\t\t/* Parse custom headers */\n\t\t\tconst char *key = NULL;\n\t\t\tjson_t *value = NULL;\n\t\t\tvoid *iter = json_object_iter(headers);\n\t\t\twhile(iter != NULL) {\n\t\t\t\tkey = json_object_iter_key(iter);\n\t\t\t\tvalue = json_object_get(headers, key);\n\t\t\t\tif(value == NULL || !json_is_string(value)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping header '%s': value is not a string\\n\", key);\n\t\t\t\t\titer = json_object_iter_next(headers, iter);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tchar h[2048];\n\t\t\t\tg_snprintf(h, sizeof(h), \"%s: %s\", key, json_string_value(value));\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Adding custom header, %s\\n\", h);\n\t\t\t\tjanus_strlcat(custom_headers, h, size - 2);\n\t\t\t\tjanus_strlcat(custom_headers, \"\\r\\n\", size);\n\t\t\t\titer = json_object_iter_next(headers, iter);\n\t\t\t}\n\t\t}\n\t}\n}\n\nstatic void janus_sip_parse_custom_contact_params(json_t *root, char *custom_params, size_t size) {\n\tcustom_params[0] = '\\0';\n\tjson_t *params = json_object_get(root, \"contact_params\");\n\tgboolean first = TRUE;\n\tif(params) {\n\t\tif(json_object_size(params) > 0) {\n\t\t\t/* Parse custom Contact URI params */\n\t\t\tconst char *key = NULL;\n\t\t\tjson_t *value = NULL;\n\t\t\tvoid *iter = json_object_iter(params);\n\t\t\twhile(iter != NULL) {\n\t\t\t\tkey = json_object_iter_key(iter);\n\t\t\t\tvalue = json_object_get(params, key);\n\t\t\t\tif(value == NULL || !json_is_string(value)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping param '%s': value is not a string\\n\", key);\n\t\t\t\t\titer = json_object_iter_next(params, iter);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tchar h[1024];\n\t\t\t\tif(first) {\n\t\t\t\t\tfirst = FALSE;\n\t\t\t\t\tg_snprintf(h, sizeof(h), \"%s=%s\", key, json_string_value(value));\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(h, sizeof(h), \";%s=%s\", key, json_string_value(value));\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Adding custom param, %s\\n\", h);\n\t\t\t\tjanus_strlcat(custom_params, h, size);\n\t\t\t\titer = json_object_iter_next(params, iter);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/* Sofia SIP logger function: when the Event Handlers mechanism is enabled,\n * we use this to intercept SIP messages sent by the stack (received\n * messages are more easily recoverable in janus_sip_sofia_callback) */\nstatic void janus_sip_sofia_logger_parse_callid(char *buffer, char *call_id, size_t cidlen) {\n\t/* We use this helper to parse and extract a Call-ID header */\n\tif(buffer == NULL)\n\t\treturn;\n\tconst char *what = \"Call-ID: \";\n\tchar *needle = strstr(buffer, what);\n\tif(needle && call_id && cidlen > 0) {\n\t\t/* URI first */\n\t\t*call_id = '\\0';\n\t\tchar *start = needle + strlen(what);\n\t\tif(start) {\n\t\t\tchar *end = strstr(start, \"\\r\\n\");\n\t\t\tif(end == NULL)\n\t\t\t\tend = strchr(start, '\\n');\n\t\t\tif(end) {\n\t\t\t\tsize_t count = end - start;\n\t\t\t\tcount = count >= cidlen ? cidlen - 1 : count;\n\t\t\t\tmemcpy(call_id, start, count);\n\t\t\t\tcall_id[count] = '\\0';\n\t\t\t}\n\t\t}\n\t}\n}\nstatic void janus_sip_sofia_logger_parse_fromto(char *buffer, const char *what, char *identity, size_t idlen, char *tag, size_t taglen) {\n\t/* We use this helper to parse a From or To header, to extract\n\t * both the SIP uri in there and, if available, the from/to tag */\n\tif(buffer == NULL || what == NULL)\n\t\treturn;\n\tchar *needle = strstr(buffer, what);\n\tif(needle && identity && idlen > 0) {\n\t\t/* URI first */\n\t\t*identity = '\\0';\n\t\tchar *start = strstr(needle + strlen(what), \"<\");\n\t\tif(start) {\n\t\t\tstart++;\n\t\t\tchar *end = strstr(needle, \">\");\n\t\t\tif(end) {\n\t\t\t\tsize_t count = end - start;\n\t\t\t\tcount = count >= idlen ? idlen - 1 : count;\n\t\t\t\tmemcpy(identity, start, count);\n\t\t\t\tidentity[count] = '\\0';\n\t\t\t}\n\t\t}\n\t}\n\tif(needle && tag && taglen > 0) {\n\t\t/* Now the tag */\n\t\t*tag = '\\0';\n\t\tchar *start = strstr(needle + strlen(what), \";tag=\");\n\t\tif(start) {\n\t\t\tstart += strlen(\";tag=\");\n\t\t\tchar *end = strstr(start, \"\\r\\n\");\n\t\t\tif(end == NULL)\n\t\t\t\tend = strchr(start, '\\n');\n\t\t\tif(end) {\n\t\t\t\tsize_t count = end - start;\n\t\t\t\tcount = count >= idlen ? idlen - 1 : count;\n\t\t\t\tmemcpy(tag, start, count);\n\t\t\t\ttag[count] = '\\0';\n\t\t\t}\n\t\t}\n\t}\n}\n\nstatic void janus_sip_sofia_logger_siptrace_callback(void *stream, char const *fmt, va_list ap) {\n\t/* Since the fmt format string in the current Sofia SIP tport_log_msg function implementation is just \"%s\\n\",\n\t * it's more efficient to directly work with the siptrace buffer. */\n\tchar *buffer = va_arg(ap, char *);\n\tJANUS_LOG(LOG_HUGE, \" >>> %s\\n\", buffer);\n\n\t/* Check if this is a message we need */\n\tif(strstr(buffer, \"send \") == buffer) {\n\t\t/* An outgoing message is going to be logged, prepare for that */\n\t\tint length = atoi(&buffer[5]);\n\t\tJANUS_LOG(LOG_HUGE, \"Intercepting message (%d bytes)\\n\", length);\n\t\t/* Skip the stamp line */\n\t\tbuffer = strchr(buffer, '\\n');\n\t\tif(!buffer) {\n\t\t\tJANUS_LOG(LOG_WARN, \"No newline after the stamp in this message, dropping it...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tbuffer++;\n\t\t/* Skip the separator line */\n\t\tbuffer = strchr(buffer, '\\n');\n\t\tif(!buffer) {\n\t\t\tJANUS_LOG(LOG_WARN, \"No newline after the separator in this message, dropping it...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tbuffer++;\n\t\t/* If this is an OPTIONS, we don't care: drop it */\n\t\tif(strstr(buffer, \"OPTIONS\") == buffer)\n\t\t\treturn;\n\t\t/* Parse for the Call, From and To headers */\n\t\tchar call_id[256], to[256], to_tag[256], from[256], from_tag[256], *tag = NULL;\n\t\tjanus_sip_sofia_logger_parse_callid(buffer, call_id, sizeof(call_id));\n\t\tjanus_sip_sofia_logger_parse_fromto(buffer, \"To: \", to, sizeof(to), to_tag, sizeof(to_tag));\n\t\tjanus_sip_sofia_logger_parse_fromto(buffer, \"From: \", from, sizeof(from), from_tag, sizeof(from_tag));\n\t\t/* Look for the session this message belongs to */\n\t\tjanus_sip_call *call = NULL;\n\t\tjanus_sip_session *session = NULL;\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tif(strlen(call_id))\n\t\t\tcall = g_hash_table_lookup(callids, call_id);\n\t\tif(call == NULL) {\n\t\t\t/* Couldn't find any SIP session with that Call-ID, check the request */\n\t\t\tif(strstr(buffer, \"SIP/2.0 \") == buffer) {\n\t\t\t\t/* This is a response code, chech the To */\n\t\t\t\tsession = g_hash_table_lookup(identities, to);\n\t\t\t} else {\n\t\t\t\t/* This is a request, check the From */\n\t\t\t\tsession = g_hash_table_lookup(identities, from);\n\t\t\t}\n\t\t} else {\n\t\t\t/* TODO Use tags to access the actual session in this call */\n\t\t\tif(strstr(buffer, \"SIP/2.0 \") == buffer) {\n\t\t\t\t/* This is a response code, chech the To tag */\n\t\t\t\ttag = to_tag;\n\t\t\t} else {\n\t\t\t\t/* This is a request, check the From tag */\n\t\t\t\ttag = from_tag;\n\t\t\t}\n\t\t\tif(call->caller && call->caller->local_tag && !strcasecmp(call->caller->local_tag, tag)) {\n\t\t\t\tsession = call->caller;\n\t\t\t} else if(call->callee && call->callee->local_tag && !strcasecmp(call->callee->local_tag, tag)) {\n\t\t\t\tsession = call->callee;\n\t\t\t} else if(call->caller && call->caller->local_tag && call->callee && call->callee->local_tag == NULL) {\n\t\t\t\tsession = call->callee;\n\t\t\t}\n\t\t}\n\t\tif(session)\n\t\t\tjanus_refcount_increase(&session->ref);\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tif(session) {\n\t\t\t/* Notify event handlers about the content of the whole outgoing SIP message */\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"sip-out\"));\n\t\t\tjson_object_set_new(info, \"sip\", json_string(buffer));\n\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"Couldn't find a SIP session associated to this message, not notifying event handlers...\\n\");\n\t\t}\n\t}\n}\n\nstatic su_logger_f *janus_sip_sofia_logger = janus_sip_sofia_logger_siptrace_callback;\n\n/* Helpers to ref/unref sessions with active calls */\nstatic void janus_sip_ref_active_call(janus_sip_session *session) {\n\tif(session == NULL)\n\t\treturn;\n\tjanus_sip_session *master = session->master;\n\tif(master) {\n\t\tjanus_mutex_lock(&master->mutex);\n\t\tmaster->active_calls = g_list_append(master->active_calls, session);\n\t\tjanus_refcount_increase(&session->ref);\n\t\tjanus_mutex_unlock(&master->mutex);\n\t} else {\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tsession->active_calls = g_list_append(session->active_calls, session);\n\t\tjanus_refcount_increase(&session->ref);\n\t\tjanus_mutex_unlock(&session->mutex);\n\t}\n}\nstatic void janus_sip_unref_active_call(janus_sip_session *session) {\n\tif(session == NULL)\n\t\treturn;\n\tjanus_sip_session *master = session->master;\n\tif(master) {\n\t\tjanus_mutex_lock(&master->mutex);\n\t\tif(g_list_find(master->active_calls, session) != NULL) {\n\t\t\tmaster->active_calls = g_list_remove(master->active_calls, session);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t}\n\t\tjanus_mutex_unlock(&master->mutex);\n\t} else {\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tif(g_list_find(session->active_calls, session) != NULL) {\n\t\t\tsession->active_calls = g_list_remove(session->active_calls, session);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t}\n\t\tjanus_mutex_unlock(&session->mutex);\n\t}\n}\nstatic janus_sip_session *janus_sip_active_call_from_callid(janus_sip_session *session, const char *callid) {\n\tif(session == NULL || callid == NULL)\n\t\treturn NULL;\n\tjanus_sip_session *needle = session->master ? session->master : session, *result = NULL;\n\tif(needle) {\n\t\tjanus_mutex_lock(&needle->mutex);\n\t\tGList *temp = needle->active_calls;\n\t\twhile(temp) {\n\t\t\tjanus_sip_session *s = (janus_sip_session *)temp->data;\n\t\t\tif(s && s->callid && !strcasecmp(s->callid, callid)) {\n\t\t\t\t/* Found */\n\t\t\t\tresult = s;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tjanus_mutex_unlock(&needle->mutex);\n\t}\n\treturn result;\n}\n\n\n/* Plugin implementation */\nint janus_sip_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* First of all, let's check what version of Sofia SIP is available */\n\tint sofia_major = 0, sofia_minor = 0, sofia_patch = 0;\n\tif(sscanf(SOFIA_SIP_VERSION, \"%d.%d.%d\", &sofia_major, &sofia_minor, &sofia_patch) == 3) {\n\t\tif(sofia_major > 2 || (sofia_major >= 1 && sofia_minor >= 13)) {\n\t\t\t/* Versions of Sofia SIP >= 1.13 apparently don't add a Contact header in\n\t\t\t * dialogs, so we'll query it ourselves using nua_get_params (see #2597) */\n\t\t\tquery_contact_header = TRUE;\n\t\t}\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, sizeof(filename), \"%s/%s.jcfg\", config_path, JANUS_SIP_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_SIP_PACKAGE);\n\t\tg_snprintf(filename, sizeof(filename), \"%s/%s.cfg\", config_path, JANUS_SIP_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\tjanus_config_print(config);\n\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"local_ip\");\n\t\tif(item && item->value) {\n\t\t\t/* Verify that the address is valid */\n\t\t\tstruct ifaddrs *ifas = NULL;\n\t\t\tjanus_network_address iface;\n\t\t\tjanus_network_address_string_buffer ibuf;\n\t\t\tif(getifaddrs(&ifas) == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unable to acquire list of network devices/interfaces; some configurations may not work as expected... %d (%s)\\n\",\n\t\t\t\t\terrno, g_strerror(errno));\n\t\t\t} else {\n\t\t\t\tif(janus_network_lookup_interface(ifas, item->value, &iface) != 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting local IP address to %s, falling back to detecting IP address...\\n\", item->value);\n\t\t\t\t} else {\n\t\t\t\t\tif(janus_network_address_to_string_buffer(&iface, &ibuf) != 0 || janus_network_address_string_buffer_is_null(&ibuf)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error getting local IP address from %s, falling back to detecting IP address...\\n\", item->value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlocal_ip = g_strdup(janus_network_address_string_from_buffer(&ibuf));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfreeifaddrs(ifas);\n\t\t\t}\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"local_media_ip\");\n\t\tif(item && item->value && strlen(item->value) > 0)\n\t\t\tlocal_media_ip = g_strdup(item->value);\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"sdp_ip\");\n\t\tif(item && item->value && strlen(item->value) > 0) {\n\t\t\tsdp_ip = g_strdup(item->value);\n\t\t\tJANUS_LOG(LOG_VERB, \"IP to advertise in SDP: %s\\n\", sdp_ip);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"keepalive_interval\");\n\t\tif(item && item->value)\n\t\t\tkeepalive_interval = atoi(item->value);\n\t\tif(keepalive_interval < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SIP keep-alive interval: %d (falling back to default)\\n\", keepalive_interval);\n\t\t\tkeepalive_interval = 120;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"SIP keep-alive interval set to %d seconds\\n\", keepalive_interval);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"sip_timer_t1x64\");\n\t\tif(item && item->value)\n\t\t\tsip_timer_t1x64 = atoi(item->value);\n\t\tif(sip_timer_t1x64 < 1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SIP Timer T1X64 value: %d (falling back to default)\\n\", sip_timer_t1x64);\n\t\t\tsip_timer_t1x64 = JANUS_DEFAULT_SIP_TIMER_T1X64;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"SIP Timer T1X64 set to %d milliseconds\\n\", sip_timer_t1x64);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"register_ttl\");\n\t\tif(item && item->value)\n\t\t\tregister_ttl = atoi(item->value);\n\t\tif(register_ttl < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SIP registration TTL: %d (falling back to default)\\n\", register_ttl);\n\t\t\tregister_ttl = JANUS_DEFAULT_REGISTER_TTL;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"SIP registration TTL set to %d seconds\\n\", register_ttl);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"behind_nat\");\n\t\tif(item && item->value)\n\t\t\tbehind_nat = janus_is_true(item->value);\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"user_agent\");\n\t\tif(item && item->value)\n\t\t\tuser_agent = g_strdup(item->value);\n\t\telse\n\t\t\tuser_agent = g_strdup(\"Janus WebRTC Server SIP Plugin \"JANUS_SIP_VERSION_STRING);\n\t\tJANUS_LOG(LOG_VERB, \"SIP User-Agent set to %s\\n\", user_agent);\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"rtp_port_range\");\n\t\tif(item && item->value) {\n\t\t\t/* Split in min and max port */\n\t\t\tchar *maxport = strrchr(item->value, '-');\n\t\t\tif(maxport != NULL) {\n\t\t\t\t*maxport = '\\0';\n\t\t\t\tmaxport++;\n\t\t\t\tif(janus_string_to_uint16(item->value, &rtp_range_min) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP min port value: %s (assuming 0)\\n\", item->value);\n\t\t\t\tif(janus_string_to_uint16(maxport, &rtp_range_max) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP max port value: %s (assuming 0)\\n\", maxport);\n\t\t\t\tmaxport--;\n\t\t\t\t*maxport = '-';\n\t\t\t}\n\t\t\tif(rtp_range_min > rtp_range_max) {\n\t\t\t\tuint16_t temp_port = rtp_range_min;\n\t\t\t\trtp_range_min = rtp_range_max;\n\t\t\t\trtp_range_max = temp_port;\n\t\t\t}\n\t\t\tif(rtp_range_max == 0)\n\t\t\t\trtp_range_max = 65535;\n\t\t\tJANUS_LOG(LOG_VERB, \"SIP RTP/RTCP port range: %u -- %u\\n\", rtp_range_min, rtp_range_max);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(item != NULL && item->value != NULL)\n\t\t\tnotify_events = janus_is_true(item->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_SIP_NAME);\n\t\t}\n\n\t\t/* Is there any DSCP TOS to apply? */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"dscp_audio_rtp\");\n\t\tif(item && item->value) {\n\t\t\tint val = atoi(item->value);\n\t\t\tif(val < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring dscp_audio_rtp value as it's not a positive integer\\n\");\n\t\t\t} else {\n\t\t\t\tdscp_audio_rtp = val;\n\t\t\t}\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"dscp_video_rtp\");\n\t\tif(item && item->value) {\n\t\t\tint val = atoi(item->value);\n\t\t\tif(val < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring dscp_video_rtp value as it's not a positive integer\\n\");\n\t\t\t} else {\n\t\t\t\tdscp_video_rtp = val;\n\t\t\t}\n\t\t}\n\n\t\t/* Check if Sofia should find certificates in a custom folder  */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"sips_certs_dir\");\n\t\tif(item && item->value) {\n\t\t\tsips_certs_dir = g_strdup(item->value);\n\t\t\tJANUS_LOG(LOG_VERB, \"Sofia SIP certificates folder: %s\\n\", sips_certs_dir);\n\t\t}\n\n\t\tjanus_config_destroy(config);\n\t}\n\tconfig = NULL;\n\n\tif(local_ip == NULL) {\n\t\tlocal_ip = janus_network_detect_local_ip_as_string(janus_network_query_options_any_ip);\n\t\tif(local_ip == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find any address! using 127.0.0.1 as the local IP... (which is NOT going to work out of your machine)\\n\");\n\t\t\tlocal_ip = g_strdup(\"127.0.0.1\");\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Local IP set to %s\\n\", local_ip);\n\n\t/* Since we might have to derive SDP connection address from local_media_ip make sure it has a meaningful value\n\t * for the purpose of using it in the SDP c= header */\n\tjanus_network_address_nullify(&janus_network_local_media_ip);\n\tif(local_media_ip) {\n\t\tif(janus_network_string_to_address(janus_network_query_options_any_ip, local_media_ip, &janus_network_local_media_ip) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid local media IP address [%s]...\\n\", local_media_ip);\n\t\t\treturn -1;\n\t\t}\n\t\tif((janus_network_local_media_ip.family == AF_INET && janus_network_local_media_ip.ipv4.s_addr == INADDR_ANY) ||\n\t\t\t\t(janus_network_local_media_ip.family == AF_INET6 && IN6_IS_ADDR_UNSPECIFIED(&janus_network_local_media_ip.ipv6))) {\n\t\t\tjanus_network_address_nullify(&janus_network_local_media_ip);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Binding media address set to [%s]...\\n\", janus_network_address_is_null(&janus_network_local_media_ip) ? \"any\" : local_media_ip);\n\tif(!sdp_ip) {\n\t\tchar *ip = janus_network_address_is_null(&janus_network_local_media_ip) ? local_ip : local_media_ip;\n\t\tif(ip) {\n\t\t\tsdp_ip = g_strdup(ip);\n\t\t\tJANUS_LOG(LOG_VERB, \"IP to advertise in SDP: %s\\n\", sdp_ip);\n\t\t}\n\t}\n\n\t/* Setup sofia */\n\tsu_init();\n\tif(notify_events && callback->events_is_enabled()) {\n\t\tif(sofia_major > 2 || (sofia_major >= 1 && sofia_minor >= 13)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"sofia-sip logs are going to be redirected and they will not be shown in the process output\\n\");\n\t\t\t/* Enable the transport logging, as we want to have access to the SIP messages */\n\t\t\tsetenv(\"TPORT_LOG\", \"1\", 1);\n\t\t\tsu_log_redirect(NULL, janus_sip_sofia_logger, NULL);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"sofia-sip logs redirection unavailable on this version of the library\\n\");\n\t\t}\n\t}\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_sip_session_destroy);\n\tidentities = g_hash_table_new(g_str_hash, g_str_equal);\n\tcallids = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free);\n\tmessageids = g_hash_table_new_full(NULL, NULL, (GDestroyNotify)g_free, (GDestroyNotify)janus_sip_session_dereference);\n\tmasters = g_hash_table_new(NULL, NULL);\n\ttransfers = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_sip_transfer_destroy);\n\tunique_ids = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_sip_message_free);\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\tif(janus_network_address_is_null(&janus_network_local_media_ip) ||\n\t\t\tjanus_network_local_media_ip.family == AF_INET6) {\n\t\t/* Finally, let's check if IPv6 is disabled, as we may need to know for RTP/RTCP sockets */\n\t\tint fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\t\tif(fd <= 0) {\n\t\t\tipv6_disabled = TRUE;\n\t\t} else {\n\t\t\tint v6only = 0;\n\t\t\tif(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)\n\t\t\t\tipv6_disabled = TRUE;\n\t\t}\n\t\tif(fd > 0)\n\t\t\tclose(fd);\n\t\tif(ipv6_disabled) {\n\t\t\tif(!janus_network_address_is_null(&janus_network_local_media_ip)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"IPv6 disabled and local media address is IPv6...\\n\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_WARN, \"IPv6 disabled, will only use IPv4 for RTP/RTCP sockets (SIP)\\n\");\n\t\t}\n\t} else if(janus_network_local_media_ip.family == AF_INET) {\n\t\t/* Disable if we have a specified IPv4 address for RTP/RTCP sockets */\n\t\tipv6_disabled = TRUE;\n\t}\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming messages */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"sip handler\", janus_sip_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the SIP handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_SIP_NAME);\n\treturn 0;\n}\n\nvoid janus_sip_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tg_hash_table_destroy(callids);\n\tg_hash_table_destroy(messageids);\n\tg_hash_table_destroy(identities);\n\tg_hash_table_destroy(masters);\n\tg_hash_table_destroy(transfers);\n\tg_hash_table_destroy(unique_ids);\n\tsessions = NULL;\n\tcallids = NULL;\n\tmessageids = NULL;\n\tidentities = NULL;\n\tmasters = NULL;\n\ttransfers = NULL;\n\tunique_ids = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\n\t/* Deinitialize sofia */\n\tsu_deinit();\n\n\tg_free(local_ip);\n\tg_free(local_media_ip);\n\tg_free(sdp_ip);\n\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_SIP_NAME);\n}\n\nint janus_sip_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_sip_get_version(void) {\n\treturn JANUS_SIP_VERSION;\n}\n\nconst char *janus_sip_get_version_string(void) {\n\treturn JANUS_SIP_VERSION_STRING;\n}\n\nconst char *janus_sip_get_description(void) {\n\treturn JANUS_SIP_DESCRIPTION;\n}\n\nconst char *janus_sip_get_name(void) {\n\treturn JANUS_SIP_NAME;\n}\n\nconst char *janus_sip_get_author(void) {\n\treturn JANUS_SIP_AUTHOR;\n}\n\nconst char *janus_sip_get_package(void) {\n\treturn JANUS_SIP_PACKAGE;\n}\n\nstatic janus_sip_session *janus_sip_lookup_session(janus_plugin_session *handle) {\n\tjanus_sip_session *session = NULL;\n\tif(g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_sip_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_sip_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_sip_session *session = g_malloc0(sizeof(janus_sip_session));\n\tsession->handle = handle;\n\tsession->account.identity = NULL;\n\tsession->account.force_udp = FALSE;\n\tsession->account.force_tcp = FALSE;\n\tsession->account.sips = FALSE;\n\tsession->account.rfc2543_cancel = FALSE;\n\tsession->account.automatic_ringing = TRUE;\n\tsession->account.username = NULL;\n\tsession->account.display_name = NULL;\n\tsession->account.user_agent = NULL;\n\tsession->account.authuser = NULL;\n\tsession->account.secret = NULL;\n\tsession->account.secret_type = janus_sip_secret_type_unknown;\n\tsession->account.sip_port = 0;\n\tsession->account.proxy = NULL;\n\tsession->account.outbound_proxy = NULL;\n\tsession->account.registration_status = janus_sip_registration_status_unregistered;\n\tsession->status = janus_sip_call_status_idle;\n\tsession->stack = NULL;\n\tsession->transaction = NULL;\n\tsession->callee = NULL;\n\tsession->callid = NULL;\n\tsession->sdp = NULL;\n\tsession->hangup_reason_header = NULL;\n\tsession->hangup_reason_header_protocol = NULL;\n\tsession->hangup_reason_header_cause = NULL;\n\tsession->hangup_custom_headers = NULL;\n\tsession->media.remote_audio_ip = NULL;\n\tsession->media.remote_video_ip = NULL;\n\tsession->media.earlymedia = FALSE;\n\tsession->media.update = FALSE;\n\tsession->media.autoaccept_reinvites = TRUE;\n\tsession->media.ready = FALSE;\n\tsession->media.require_srtp = FALSE;\n\tsession->media.has_srtp_local_audio = FALSE;\n\tsession->media.has_srtp_local_video = FALSE;\n\tsession->media.has_srtp_remote_audio = FALSE;\n\tsession->media.has_srtp_remote_video = FALSE;\n\tsession->media.srtp_profile = 0;\n\tsession->media.audio_srtp_local_profile = NULL;\n\tsession->media.audio_srtp_local_crypto = NULL;\n\tsession->media.video_srtp_local_profile = NULL;\n\tsession->media.video_srtp_local_crypto = NULL;\n\tsession->media.on_hold = FALSE;\n\tsession->media.has_audio = FALSE;\n\tsession->media.audio_rtp_fd = -1;\n\tsession->media.audio_rtcp_fd= -1;\n\tsession->media.local_audio_rtp_port = 0;\n\tsession->media.remote_audio_rtp_port = 0;\n\tsession->media.local_audio_rtcp_port = 0;\n\tsession->media.remote_audio_rtcp_port = 0;\n\tsession->media.audio_ssrc = 0;\n\tsession->media.audio_ssrc_peer = 0;\n\tsession->media.audio_pt = -1;\n\tsession->media.opusred_pt = -1;\n\tsession->media.audio_pt_name = NULL;\n\tsession->media.audio_send = TRUE;\n\tsession->media.audio_recv = TRUE;\n\tsession->media.hold_audio_dir = JANUS_SDP_SENDONLY;\n\tsession->media.pre_hold_audio_dir = JANUS_SDP_DEFAULT;\n\tsession->media.has_video = FALSE;\n\tsession->media.video_rtp_fd = -1;\n\tsession->media.video_rtcp_fd= -1;\n\tsession->media.local_video_rtp_port = 0;\n\tsession->media.remote_video_rtp_port = 0;\n\tsession->media.local_video_rtcp_port = 0;\n\tsession->media.remote_video_rtcp_port = 0;\n\tsession->media.video_ssrc = 0;\n\tsession->media.video_ssrc_peer = 0;\n\tsession->media.simulcast_ssrc = 0;\n\tsession->media.video_pt = -1;\n\tsession->media.video_pt_name = NULL;\n\tsession->media.video_recv = TRUE;\n\tsession->media.video_pli_supported = FALSE;\n\tsession->media.hold_video_dir = JANUS_SDP_SENDONLY;\n\tsession->media.pre_hold_video_dir = JANUS_SDP_DEFAULT;\n\tsession->media.video_orientation_extension_id = -1;\n\tsession->media.audio_level_extension_id = -1;\n\t/* Initialize the RTP context */\n\tjanus_rtp_switching_context_reset(&session->media.acontext);\n\tjanus_rtp_switching_context_reset(&session->media.vcontext);\n\tsession->media.pipefd[0] = -1;\n\tsession->media.pipefd[1] = -1;\n\tsession->media.updated = FALSE;\n\tsession->media.audio_remote_policy.ssrc.type = ssrc_any_inbound;\n\tsession->media.audio_local_policy.ssrc.type = ssrc_any_inbound;\n\tsession->media.video_remote_policy.ssrc.type = ssrc_any_inbound;\n\tsession->media.video_local_policy.ssrc.type = ssrc_any_inbound;\n\tjanus_mutex_init(&session->rec_mutex);\n\tjanus_mutex_init(&session->rtp_forwarders_mutex);\n\tsession->audio_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->video_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->peer_audio_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->peer_video_forwarders = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\tsession->all_forwarders = g_hash_table_new(NULL, NULL);\n\tsession->udp_sock = -1;\n\tg_atomic_int_set(&session->establishing, 0);\n\tg_atomic_int_set(&session->established, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tjanus_mutex_init(&session->mutex);\n\thandle->plugin_handle = session;\n\tjanus_refcount_init(&session->ref, janus_sip_session_free);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\twhile(session->unique_id == NULL) {\n\t\tsession->unique_id = janus_random_uuid();\n\t\tif(g_hash_table_lookup(unique_ids, session->unique_id) != NULL) {\n\t\t\tg_free(session->unique_id);\n\t\t\tsession->unique_id = NULL;\n\t\t}\n\t\tg_hash_table_insert(unique_ids, g_strdup(session->unique_id), session);\n\t}\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_sip_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_sip_session *session = janus_sip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No SIP session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Destroying SIP session (%s)...\\n\", session->account.username ? session->account.username : \"unregistered user\");\n\tjanus_sip_hangup_media_internal(handle);\n\t/* If this is a master or helper session, update the related sessions */\n\tif(session->master_id != 0) {\n\t\tif(session->master == NULL) {\n\t\t\t/* This is the master, remove it from the list */\n\t\t\tg_hash_table_remove(masters, GUINT_TO_POINTER(session->master_id));\n\t\t\t/* Remove the helper sessions */\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tGList *temp = NULL;\n\t\t\twhile(session->helpers != NULL) {\n\t\t\t\ttemp = session->helpers;\n\t\t\t\tsession->helpers = g_list_remove_link(session->helpers, temp);\n\t\t\t\tjanus_sip_session *helper = (janus_sip_session *)temp->data;\n\t\t\t\tif(helper != NULL && helper->handle != NULL) {\n\t\t\t\t\t/* Get rid of this helper */\n\t\t\t\t\thelper->helper = FALSE;\n\t\t\t\t\thelper->master_id = 0;\n\t\t\t\t\thelper->master = NULL;\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&helper->ref);\n\t\t\t\t\tgateway->end_session(helper->handle);\n\t\t\t\t}\n\t\t\t\tg_list_free(temp);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t} else {\n\t\t\t/* This is a helper session, remove it from the list and remove the references */\n\t\t\tjanus_sip_session *master = session->master;\n\t\t\tjanus_mutex_lock(&master->mutex);\n\t\t\tgboolean found = (g_list_find(master->helpers, session) != NULL);\n\t\t\tif(found) {\n\t\t\t\tmaster->helpers = g_list_remove(master->helpers, session);\n\t\t\t\tsession->helper = FALSE;\n\t\t\t\tsession->master_id = 0;\n\t\t\t\tsession->master = NULL;\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\tjanus_refcount_decrease(&master->ref);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&master->mutex);\n\t\t}\n\t}\n\t/* If this session was involved in a transfer, get rid of the reference */\n\tif(session->refer_id) {\n\t\tg_hash_table_remove(transfers, GUINT_TO_POINTER(session->refer_id));\n\t\tsession->refer_id = 0;\n\t}\n\t/* Shutdown the NUA */\n\tif(session->stack) {\n\t\tjanus_mutex_lock(&session->stack->smutex);\n\t\tif(session->stack->s_nua)\n\t\t\tnua_shutdown(session->stack->s_nua);\n\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t}\n\tif(session->unique_id)\n\t\tg_hash_table_remove(sessions, session->unique_id);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn;\n}\n\njson_t *janus_sip_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_sip_session *session = janus_sip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Provide some generic info, e.g., if we're in a call and with whom */\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"unique_id\", json_string(session->unique_id));\n\tif(session->master != NULL) {\n\t\t/* This is an helper session, provide the details for the master session */\n\t\tjson_object_set_new(info, \"helper\", json_true());\n\t\tjson_t *master = json_object();\n\t\tjson_object_set_new(master, \"username\", session->master->account.username ? json_string(session->master->account.username) : NULL);\n\t\tjson_object_set_new(master, \"authuser\", session->master->account.authuser ? json_string(session->master->account.authuser) : NULL);\n\t\tjson_object_set_new(master, \"secret\", session->master->account.secret ? json_string(\"(hidden)\") : NULL);\n\t\tjson_object_set_new(master, \"display_name\", session->master->account.display_name ? json_string(session->master->account.display_name) : NULL);\n\t\tjson_object_set_new(master, \"user_agent\", session->master->account.user_agent ? json_string(session->master->account.user_agent) : NULL);\n\t\tjson_object_set_new(master, \"identity\", session->master->account.identity ? json_string(session->master->account.identity) : NULL);\n\t\tjson_object_set_new(master, \"registration_status\", json_string(janus_sip_registration_status_string(session->master->account.registration_status)));\n\t\tjson_object_set_new(info, \"master\", master);\n\t}\n\tjson_object_set_new(info, \"username\", session->account.username ? json_string(session->account.username) : NULL);\n\tjson_object_set_new(info, \"authuser\", session->account.authuser ? json_string(session->account.authuser) : NULL);\n\tjson_object_set_new(info, \"secret\", session->account.secret ? json_string(\"(hidden)\") : NULL);\n\tjson_object_set_new(info, \"display_name\", session->account.display_name ? json_string(session->account.display_name) : NULL);\n\tjson_object_set_new(info, \"user_agent\", session->account.user_agent ? json_string(session->account.user_agent) : NULL);\n\tjson_object_set_new(info, \"identity\", session->account.identity ? json_string(session->account.identity) : NULL);\n\tjson_object_set_new(info, \"registration_status\", json_string(janus_sip_registration_status_string(session->account.registration_status)));\n\tjson_object_set_new(info, \"call_status\", json_string(janus_sip_call_status_string(session->status)));\n\tjanus_mutex_lock(&session->mutex);\n\tif(session->helpers != NULL)\n\t\tjson_object_set_new(info, \"helpers\", json_integer(g_list_length(session->helpers)));\n\tif(session->callee) {\n\t\tjson_object_set_new(info, \"callee\", json_string(session->callee));\n\t\tjson_object_set_new(info, \"srtp-required\", json_string(session->media.require_srtp ? \"yes\" : \"no\"));\n\t\tjson_object_set_new(info, \"sdes-local-audio\", json_string(session->media.has_srtp_local_audio ? \"yes\" : \"no\"));\n\t\tjson_object_set_new(info, \"sdes-local-video\", json_string(session->media.has_srtp_local_video ? \"yes\" : \"no\"));\n\t\tjson_object_set_new(info, \"sdes-remote-audio\", json_string(session->media.has_srtp_remote_audio ? \"yes\" : \"no\"));\n\t\tjson_object_set_new(info, \"sdes-remote-video\", json_string(session->media.has_srtp_remote_video ? \"yes\" : \"no\"));\n\t}\n\tjanus_mutex_unlock(&session->mutex);\n\tif(session->arc || session->vrc || session->arc_peer || session->vrc_peer) {\n\t\tjson_t *recording = json_object();\n\t\tif(session->arc && session->arc->filename)\n\t\t\tjson_object_set_new(recording, \"audio\", json_string(session->arc->filename));\n\t\tif(session->vrc && session->vrc->filename)\n\t\t\tjson_object_set_new(recording, \"video\", json_string(session->vrc->filename));\n\t\tif(session->arc_peer && session->arc_peer->filename)\n\t\t\tjson_object_set_new(recording, \"audio-peer\", json_string(session->arc_peer->filename));\n\t\tif(session->vrc_peer && session->vrc_peer->filename)\n\t\t\tjson_object_set_new(recording, \"video-peer\", json_string(session->vrc_peer->filename));\n\t\tjson_object_set_new(info, \"recording\", recording);\n\t}\n\tjson_object_set_new(info, \"establishing\", json_integer(g_atomic_int_get(&session->establishing)));\n\tjson_object_set_new(info, \"established\", json_integer(g_atomic_int_get(&session->established)));\n\tjson_object_set_new(info, \"hangingup\", json_integer(g_atomic_int_get(&session->hangingup)));\n\tjson_object_set_new(info, \"destroyed\", json_integer(g_atomic_int_get(&session->destroyed)));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstruct janus_plugin_result *janus_sip_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_sip_session *session = janus_sip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\t}\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\tjanus_sip_message *msg = g_malloc(sizeof(janus_sip_message));\n\tmsg->handle = handle;\n\tmsg->transaction = transaction;\n\tmsg->message = message;\n\tmsg->jsep = jsep;\n\tg_async_queue_push(messages, msg);\n\n\t/* All the requests to this plugin are handled asynchronously */\n\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n}\n\nvoid janus_sip_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_SIP_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_sip_session *session = janus_sip_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->established, 1);\n\tg_atomic_int_set(&session->establishing, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Only relay RTP/RTCP when we get this event */\n}\n\nvoid janus_sip_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(gateway) {\n\t\t/* Honour the audio/video active flags */\n\t\tjanus_sip_session *session = (janus_sip_session *)handle->plugin_handle;\n\t\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(!janus_sip_call_is_established(session) && session->status != janus_sip_call_status_progress)\n\t\t\treturn;\n\t\tgboolean video = packet->video;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\t/* Forward to our SIP peer */\n\t\tif(video) {\n\t\t\tif(!session->media.video_send) {\n\t\t\t\t/* Dropping video packet, peer doesn't want to receive it */\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(session->media.on_hold && session->media.hold_video_dir != JANUS_SDP_SENDONLY) {\n\t\t\t\t/* Dropping video packet, the call is on hold and we're not sending anything */\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(session->media.simulcast_ssrc) {\n\t\t\t\t/* The user is simulcasting: drop everything except the base layer */\n\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\t\tuint32_t ssrc = ntohl(header->ssrc);\n\t\t\t\tif(ssrc != session->media.simulcast_ssrc) {\n\t\t\t\t\tJANUS_LOG(LOG_DBG, \"Dropping packet (not base simulcast substream)\\n\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->media.video_ssrc == 0) {\n\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\t\tsession->media.video_ssrc = ntohl(header->ssrc);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Got SIP video SSRC: %\"SCNu32\"\\n\", session->media.video_ssrc);\n\t\t\t}\n\t\t\tif(session->media.has_video && session->media.video_rtp_fd != -1) {\n\t\t\t\t/* Check if there are forwarders interested in this traffic */\n\t\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tg_hash_table_iter_init(&iter, session->video_forwarders);\n\t\t\t\twhile(session->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\t\t\t\tif(!rtp_forward->is_video)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tjanus_rtp_forwarder_send_rtp(rtp_forward, buf, len, 0);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t\t/* Save the frame if we're recording */\n\t\t\t\tjanus_recorder_save_frame(session->vrc, buf, len);\n\t\t\t\t/* Is SRTP involved? */\n\t\t\t\tif(session->media.has_srtp_local_video) {\n\t\t\t\t\tchar sbuf[2048];\n\t\t\t\t\tmemcpy(&sbuf, buf, len);\n\t\t\t\t\tint protected = len;\n\t\t\t\t\tint res = srtp_protect(session->media.video_srtp_out, &sbuf, &protected);\n\t\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)&sbuf;\n\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Video SRTP protect error... %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), len, protected, timestamp, seq);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Forward the frame to the peer */\n\t\t\t\t\t\tif(send(session->media.video_rtp_fd, sbuf, protected, 0) < 0) {\n\t\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)&sbuf;\n\t\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending SRTP video packet... %s (len=%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), protected, timestamp, seq);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Forward the frame to the peer */\n\t\t\t\t\tif(send(session->media.video_rtp_fd, buf, len, 0) < 0) {\n\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)&buf;\n\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending RTP video packet... %s (len=%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), len, timestamp, seq);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif(!session->media.audio_send) {\n\t\t\t\t/* Dropping audio packet, peer doesn't want to receive it */\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(session->media.on_hold && session->media.hold_audio_dir != JANUS_SDP_SENDONLY) {\n\t\t\t\t/* Dropping audio packet, the call is on hold and we're not sending anything */\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(session->media.audio_ssrc == 0) {\n\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\t\tsession->media.audio_ssrc = ntohl(header->ssrc);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Got SIP audio SSRC: %\"SCNu32\"\\n\", session->media.audio_ssrc);\n\t\t\t}\n\t\t\tif(session->media.has_audio && session->media.audio_rtp_fd != -1) {\n\t\t\t\t/* Check if there are forwarders interested in this traffic */\n\t\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tg_hash_table_iter_init(&iter, session->audio_forwarders);\n\t\t\t\twhile(session->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\t\t\t\tif(rtp_forward->is_data || rtp_forward->is_video)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tjanus_rtp_forwarder_send_rtp(rtp_forward, buf, len, 0);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t\t/* Save the frame if we're recording */\n\t\t\t\tjanus_recorder_save_frame(session->arc, buf, len);\n\t\t\t\t/* Is SRTP involved? */\n\t\t\t\tif(session->media.has_srtp_local_audio) {\n\t\t\t\t\tchar sbuf[2048];\n\t\t\t\t\tmemcpy(&sbuf, buf, len);\n\t\t\t\t\tint protected = len;\n\t\t\t\t\tint res = srtp_protect(session->media.audio_srtp_out, &sbuf, &protected);\n\t\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)&sbuf;\n\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Audio SRTP protect error... %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), len, protected, timestamp, seq);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Forward the frame to the peer */\n\t\t\t\t\t\tif(send(session->media.audio_rtp_fd, sbuf, protected, 0) < 0) {\n\t\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)&sbuf;\n\t\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending SRTP audio packet... %s (len=%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), protected, timestamp, seq);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Forward the frame to the peer */\n\t\t\t\t\tif(send(session->media.audio_rtp_fd, buf, len, 0) < 0) {\n\t\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)&buf;\n\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending RTP audio packet... %s (len=%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), len, timestamp, seq);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid janus_sip_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(gateway) {\n\t\tjanus_sip_session *session = (janus_sip_session *)handle->plugin_handle;\n\t\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(!janus_sip_call_is_established(session) && session->status != janus_sip_call_status_progress)\n\t\t\treturn;\n\t\tgboolean video = packet->video;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\t/* Forward to our SIP peer */\n\t\tif(video) {\n\t\t\tif(session->media.has_video && session->media.video_rtcp_fd != -1) {\n\t\t\t\t/* Fix SSRCs as the Janus core does */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP] Fixing SSRCs (local %u, peer %u)\\n\",\n\t\t\t\t\tsession->media.video_ssrc, session->media.video_ssrc_peer);\n\t\t\t\tjanus_rtcp_fix_ssrc(NULL, (char *)buf, len, 1, session->media.video_ssrc, session->media.video_ssrc_peer);\n\t\t\t\t/* Is SRTP involved? */\n\t\t\t\tif(session->media.has_srtp_local_video) {\n\t\t\t\t\tchar sbuf[2048];\n\t\t\t\t\tmemcpy(&sbuf, buf, len);\n\t\t\t\t\tint protected = len;\n\t\t\t\t\tint res = srtp_protect_rtcp(session->media.video_srtp_out, &sbuf, &protected);\n\t\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Video SRTCP protect error... %s (len=%d-->%d)...\\n\",\n\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), len, protected);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Forward the message to the peer */\n\t\t\t\t\t\tif(send(session->media.video_rtcp_fd, sbuf, protected, 0) < 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending SRTCP video packet... %s (len=%d)...\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), protected);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Forward the message to the peer */\n\t\t\t\t\tif(send(session->media.video_rtcp_fd, buf, len, 0) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending RTCP video packet... %s (len=%d)...\\n\",\n\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), len);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif(session->media.has_audio && session->media.audio_rtcp_fd != -1) {\n\t\t\t\t/* Fix SSRCs as the Janus core does */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP] Fixing SSRCs (local %u, peer %u)\\n\",\n\t\t\t\t\tsession->media.audio_ssrc, session->media.audio_ssrc_peer);\n\t\t\t\tjanus_rtcp_fix_ssrc(NULL, (char *)buf, len, 1, session->media.audio_ssrc, session->media.audio_ssrc_peer);\n\t\t\t\t/* Is SRTP involved? */\n\t\t\t\tif(session->media.has_srtp_local_audio) {\n\t\t\t\t\tchar sbuf[2048];\n\t\t\t\t\tmemcpy(&sbuf, buf, len);\n\t\t\t\t\tint protected = len;\n\t\t\t\t\tint res = srtp_protect_rtcp(session->media.audio_srtp_out, &sbuf, &protected);\n\t\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Audio SRTCP protect error... %s (len=%d-->%d)...\\n\",\n\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), len, protected);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Forward the message to the peer */\n\t\t\t\t\t\tif(send(session->media.audio_rtcp_fd, sbuf, protected, 0) < 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending SRTCP audio packet... %s (len=%d)...\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), protected);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Forward the message to the peer */\n\t\t\t\t\tif(send(session->media.audio_rtcp_fd, buf, len, 0) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending RTCP audio packet... %s (len=%d)...\\n\",\n\t\t\t\t\t\t\tsession->account.username, g_strerror(errno), len);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nstatic void janus_sip_recorder_close(janus_sip_session *session,\n\t\tgboolean stop_audio, gboolean stop_audio_peer, gboolean stop_video, gboolean stop_video_peer) {\n\tif(session->arc && stop_audio) {\n\t\tjanus_recorder *rc = session->arc;\n\t\tsession->arc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed user's audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->arc_peer && stop_audio_peer) {\n\t\tjanus_recorder *rc = session->arc_peer;\n\t\tsession->arc_peer = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed peer's audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->vrc && stop_video) {\n\t\tjanus_recorder *rc = session->vrc;\n\t\tsession->vrc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed user's video recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->vrc_peer && stop_video_peer) {\n\t\tjanus_recorder *rc = session->vrc_peer;\n\t\tsession->vrc_peer = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed peer's video recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n}\n\nvoid janus_sip_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_SIP_PACKAGE, handle);\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_sip_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_sip_hangup_media_internal(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_sip_session *session = janus_sip_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\tsession->media.simulcast_ssrc = 0;\n\t/* Do cleanup if media thread has not been created */\n\tif(!session->media.ready && !session->relayer_thread) {\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tjanus_sip_media_cleanup(session);\n\t\tjanus_mutex_unlock(&session->mutex);\n\t}\n\t/* Get rid of the recorders, if available */\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_sip_recorder_close(session, TRUE, TRUE, TRUE, TRUE);\n\tjanus_mutex_unlock(&session->rec_mutex);\n\t/* Get rid of RTP forwarders, if any */\n\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\tg_hash_table_remove_all(session->audio_forwarders);\n\tg_hash_table_remove_all(session->video_forwarders);\n\tg_hash_table_remove_all(session->peer_audio_forwarders);\n\tg_hash_table_remove_all(session->peer_video_forwarders);\n\tg_hash_table_remove_all(session->all_forwarders);\n\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t/* Update the SIP status */\n\tif(!(session->status == janus_sip_call_status_inviting ||\n\t\t\tsession->status == janus_sip_call_status_invited ||\n\t\t\tjanus_sip_call_is_established(session))) {\n\t\tg_atomic_int_set(&session->establishing, 0);\n\t\tg_atomic_int_set(&session->established, 0);\n\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\treturn;\n\t}\n\t/* Involve SIP if needed */\n\tjanus_mutex_lock(&session->mutex);\n\tif(session->stack->s_nh_i != NULL && session->callee != NULL) {\n\t\tg_free(session->callee);\n\t\tsession->callee = NULL;\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\t/* Send a BYE */\n\t\tsession->media.earlymedia = FALSE;\n\t\tsession->media.update = FALSE;\n\t\tsession->media.autoaccept_reinvites = TRUE;\n\t\tsession->media.ready = FALSE;\n\t\tsession->media.on_hold = FALSE;\n\n\t\t/* Send a BYE or respond with 480 */\n\t\tif(janus_sip_call_is_established(session) || session->status == janus_sip_call_status_inviting)\n\t\t\tnua_bye(session->stack->s_nh_i, TAG_END());\n\t\telse\n\t\t\tnua_respond(session->stack->s_nh_i, 480, sip_status_phrase(480), TAG_END());\n\n\t\tjanus_sip_call_update_status(session, janus_sip_call_status_closing);\n\n\t\t/* Notify the operation */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"sip\", json_string(\"event\"));\n\t\tjson_t *result = json_object();\n\t\tjson_object_set_new(result, \"event\", json_string(\"hangingup\"));\n\t\tjson_object_set_new(event, \"result\", result);\n\t\tjson_object_set_new(event, \"call_id\", json_string(session->callid));\n\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, NULL, event, NULL);\n\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\tjson_decref(event);\n\t} else {\n\t\tjanus_mutex_unlock(&session->mutex);\n\t}\n\tg_atomic_int_set(&session->establishing, 0);\n\tg_atomic_int_set(&session->established, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_sip_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining SIP handler thread\\n\");\n\tjanus_sip_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_sip_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_sip_session *session = janus_sip_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_sip_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_sip_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = msg->message;\n\t\tif(msg->message == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_SIP_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(!json_is_object(root)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\t\terror_code = JANUS_SIP_ERROR_INVALID_JSON;\n\t\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\t\tgoto error;\n\t\t}\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto error;\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tjson_t *result = NULL;\n\n\t\tif(!strcasecmp(request_text, \"register\")) {\n\t\t\t/* Send a REGISTER */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, register_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tgboolean refresh = json_is_true(json_object_get(root, \"refresh\"));\n\t\t\tif(session->account.registration_status > janus_sip_registration_status_unregistered && !refresh) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already registered (%s)\\n\", session->account.username);\n\t\t\t\terror_code = JANUS_SIP_ERROR_ALREADY_REGISTERED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already registered (%s)\", session->account.username);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Parse the request */\n\t\t\tgboolean guest = FALSE, helper = FALSE;\n\t\t\tjson_t *type = json_object_get(root, \"type\");\n\t\t\tif(type != NULL) {\n\t\t\t\tconst char *type_text = json_string_value(type);\n\t\t\t\tif(!strcmp(type_text, \"guest\")) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Registering as a guest\\n\");\n\t\t\t\t\tguest = TRUE;\n\t\t\t\t} else if(!strcmp(type_text, \"helper\")) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Registering as a helper\\n\");\n\t\t\t\t\thelper = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unknown type '%s', ignoring...\\n\", type_text);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(helper) {\n\t\t\t\t/* This is actually an helper session, for an already registered one */\n\t\t\t\tjson_t *master = json_object_get(root, \"master_id\");\n\t\t\t\tif(master == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing mandatory element for helper (master_id)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Missing mandatory element for helper (master_id)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tguint32 master_id = json_integer_value(master);\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tif(session->master != NULL) {\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Session already a helper (%\"SCNu32\")\\n\", session->master_id);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_HELPER_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Session already a helper (%\"SCNu32\")\", master_id);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_sip_session *ms = g_hash_table_lookup(masters, GUINT_TO_POINTER(master_id));\n\t\t\t\tif(ms == NULL) {\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such master session (%\"SCNu32\")\\n\", master_id);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_HELPER_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such master session (%\"SCNu32\")\", master_id);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Add this session as an helper for the master */\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tjanus_refcount_increase(&ms->ref);\n\t\t\t\tsession->helper = TRUE;\n\t\t\t\tsession->master = ms;\n\t\t\t\tsession->master_id = master_id;\n\t\t\t\tjanus_mutex_lock(&ms->mutex);\n\t\t\t\tms->helpers = g_list_append(ms->helpers, session);\n\t\t\t\tjanus_mutex_unlock(&ms->mutex);\n\t\t\t\tsession->account.registration_status = janus_sip_registration_status_disabled;\n\t\t\t\tg_free(session->account.username);\n\t\t\t\tsession->account.username = ms->account.username ? g_strdup(ms->account.username) : NULL;\n\t\t\t\tif(session->stack == NULL) {\n\t\t\t\t\tsession->stack = g_malloc0(sizeof(ssip_t));\n\t\t\t\t\tsu_home_init(session->stack->s_home);\n\t\t\t\t\tif(session->master->stack->contact_header != NULL)\n\t\t\t\t\t\tsession->stack->contact_header = g_strdup(session->master->stack->contact_header);\n\t\t\t\t}\n\t\t\t\t/* Check if custom headers need to be intercepted */\n\t\t\t\tjson_t *header_prefixes_json = json_object_get(root, \"incoming_header_prefixes\");\n\t\t\t\tif(header_prefixes_json) {\n\t\t\t\t\tsize_t index = 0;\n\t\t\t\t\tjson_t *value = NULL;\n\t\t\t\t\tjson_array_foreach(header_prefixes_json, index, value) {\n\t\t\t\t\t\tconst char *header_prefix = json_string_value(value);\n\t\t\t\t\t\tif(header_prefix)\n\t\t\t\t\t\t\tsession->incoming_header_prefixes = g_list_append(session->incoming_header_prefixes, g_strdup(header_prefix));\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* No custom headers, inherit the parent's if any */\n\t\t\t\t\tif(ms->incoming_header_prefixes != NULL) {\n\t\t\t\t\t\tGList *temp = ms->incoming_header_prefixes;\n\t\t\t\t\t\twhile(temp != NULL) {\n\t\t\t\t\t\t\tchar *header_prefix = (char *)temp->data;\n\t\t\t\t\t\t\tif(header_prefix != NULL)\n\t\t\t\t\t\t\t\tsession->incoming_header_prefixes = g_list_append(session->incoming_header_prefixes, g_strdup(header_prefix));\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsession->stack->session = session;\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t/* Send an event back */\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"registered\"));\n\t\t\t\tjson_object_set_new(result, \"username\", json_string(ms->account.username));\n\t\t\t\tjson_object_set_new(result, \"register_sent\", json_false());\n\t\t\t\tjson_object_set_new(result, \"helper\", json_true());\n\t\t\t\tjson_object_set_new(result, \"master_id\", json_integer(session->master_id));\n\t\t\t\tjson_object_set_new(result, \"unique_id\", json_string(session->unique_id));\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"registered\"));\n\t\t\t\t\tjson_object_set_new(info, \"identity\", json_string(ms->account.identity));\n\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(\"guest\"));\n\t\t\t\t\tjson_object_set_new(info, \"helper\", json_true());\n\t\t\t\t\tjson_object_set_new(info, \"master_id\", json_integer(session->master_id));\n\t\t\t\t\tjson_object_set_new(info, \"unique_id\", json_string(session->unique_id));\n\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\tgoto done;\n\t\t\t}\n\t\t\tif(session->master != NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't register on a helper session\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_HELPER_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't register on a helper session\");\n\t\t\t\tgoto error;\n\t\t\t}\n\n\t\t\tgboolean send_register = TRUE;\n\t\t\tjson_t *do_register = json_object_get(root, \"send_register\");\n\t\t\tif(do_register != NULL) {\n\t\t\t\tsend_register = json_is_true(do_register);\n\t\t\t\tif(guest && send_register) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Conflicting elements: send_register cannot be true if guest is true\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Conflicting elements: send_register cannot be true if guest is true\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tgboolean sips = FALSE;\n\t\t\tjson_t *do_sips = json_object_get(root, \"sips\");\n\t\t\tif(do_sips != NULL) {\n\t\t\t\tsips = json_is_true(do_sips);\n\t\t\t}\n\t\t\tgboolean force_udp = FALSE;\n\t\t\tjson_t *do_udp = json_object_get(root, \"force_udp\");\n\t\t\tif(do_udp != NULL) {\n\t\t\t\tforce_udp = json_is_true(do_udp);\n\t\t\t}\n\t\t\tgboolean force_tcp = FALSE;\n\t\t\tjson_t *do_tcp = json_object_get(root, \"force_tcp\");\n\t\t\tif(do_tcp != NULL) {\n\t\t\t\tforce_tcp = json_is_true(do_tcp);\n\t\t\t}\n\t\t\tif(force_udp && force_tcp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Conflicting elements: force_udp and force_tcp cannot both be true\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Conflicting elements: force_udp and force_tcp cannot both be true\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!force_udp && !force_tcp)\n\t\t\t\tforce_udp = TRUE;\n\t\t\tgboolean rfc2543_cancel = FALSE;\n\t\t\tjson_t *do_rfc2543_cancel = json_object_get(root, \"rfc2543_cancel\");\n\t\t\tif(do_rfc2543_cancel != NULL) {\n\t\t\t\trfc2543_cancel = json_is_true(do_rfc2543_cancel);\n\t\t\t}\n\t\t\tgboolean automatic_ringing = TRUE;\n\t\t\tjson_t *do_automatic_ringing = json_object_get(root, \"automatic_ringing\");\n\t\t\tif(do_automatic_ringing != NULL) {\n\t\t\t\tautomatic_ringing = json_is_true(do_automatic_ringing);\n\t\t\t}\n\n\t\t\t/* Parse addresses */\n\t\t\tjson_t *proxy = json_object_get(root, \"proxy\");\n\t\t\tconst char *proxy_text = NULL;\n\t\t\tif(proxy && !json_is_null(proxy)) {\n\t\t\t\t/* Has to be validated separately because it could be null */\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, proxy_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tgoto error;\n\t\t\t\tproxy_text = json_string_value(proxy);\n\t\t\t\tjanus_sip_uri_t proxy_uri;\n\t\t\t\tif(janus_sip_parse_proxy_uri(&proxy_uri, proxy_text) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid proxy address %s\\n\", proxy_text);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ADDRESS;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid proxy address %s\\n\", proxy_text);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_t *outbound_proxy = json_object_get(root, \"outbound_proxy\");\n\t\t\tconst char *obproxy_text = NULL;\n\t\t\tif(outbound_proxy && !json_is_null(outbound_proxy)) {\n\t\t\t\t/* Has to be validated separately because it could be null */\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, proxy_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tgoto error;\n\t\t\t\tobproxy_text = json_string_value(outbound_proxy);\n\t\t\t\tjanus_sip_uri_t outbound_proxy_uri;\n\t\t\t\tif(janus_sip_parse_proxy_uri(&outbound_proxy_uri, obproxy_text) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid outbound_proxy address %s\\n\", obproxy_text);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ADDRESS;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid outbound_proxy address %s\\n\", obproxy_text);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* Parse register TTL */\n\t\t\tint ttl = register_ttl;\n\t\t\tjson_t *reg_ttl = json_object_get(root, \"register_ttl\");\n\t\t\tif(reg_ttl && json_is_integer(reg_ttl))\n\t\t\t\tttl = json_integer_value(reg_ttl);\n\t\t\tif(ttl <= 0)\n\t\t\t\tttl = JANUS_DEFAULT_REGISTER_TTL;\n\n\t\t\t/* Parse display name */\n\t\t\tconst char *display_name_text = NULL;\n\t\t\tjson_t *display_name = json_object_get(root, \"display_name\");\n\t\t\tif(display_name && json_is_string(display_name))\n\t\t\t\tdisplay_name_text = json_string_value(display_name);\n\n\t\t\t/* Parse user agent */\n\t\t\tconst char *user_agent_text = NULL;\n\t\t\tjson_t *user_agent = json_object_get(root, \"user_agent\");\n\t\t\tif(user_agent && json_is_string(user_agent))\n\t\t\t\tuser_agent_text = json_string_value(user_agent);\n\n\t\t\t/* Now the user part (always needed, even for the guest case) */\n\t\t\tjson_t *username = json_object_get(root, \"username\");\n\t\t\tif(!username) {\n\t\t\t\t/* The username is mandatory even when registering as guests */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing element (username)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing element (username)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tconst char *username_text = NULL;\n\t\t\tconst char *secret_text = NULL;\n\t\t\tconst char *authuser_text = NULL;\n\t\t\tjanus_sip_secret_type secret_type = janus_sip_secret_type_plaintext;\n\t\t\tjanus_sip_uri_t username_uri;\n\t\t\tchar user_id[256];\n\t\t\t/* Parse address */\n\t\t\tusername_text = json_string_value(username);\n\t\t\tif(janus_sip_parse_uri(&username_uri, username_text) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid user address %s\\n\", username_text);\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ADDRESS;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid user address %s\\n\", username_text);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tg_strlcpy(user_id, username_uri.url->url_user, sizeof(user_id));\n\t\t\tif(guest) {\n\t\t\t\t/* Not needed, we can stop here: just say we're registered */\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Guest will have username %s\\n\", user_id);\n\t\t\t\tsend_register = FALSE;\n\t\t\t} else {\n\t\t\t\tjson_t *secret = json_object_get(root, \"secret\");\n\t\t\t\tjson_t *ha1_secret = json_object_get(root, \"ha1_secret\");\n\t\t\t\tjson_t *authuser = json_object_get(root, \"authuser\");\n\t\t\t\tif(!secret && !ha1_secret) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing element (secret or ha1_secret)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Missing element (secret or ha1_secret)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(secret && ha1_secret) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Conflicting elements specified (secret and ha1_secret)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Conflicting elements specified (secret and ha1_secret)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(secret) {\n\t\t\t\t\tsecret_text = json_string_value(secret);\n\t\t\t\t\tsecret_type = janus_sip_secret_type_plaintext;\n\t\t\t\t} else {\n\t\t\t\t\tsecret_text = json_string_value(ha1_secret);\n\t\t\t\t\tsecret_type = janus_sip_secret_type_hashed;\n\t\t\t\t}\n\t\t\t\tif(authuser) {\n\t\t\t\t\tauthuser_text = json_string_value(authuser);\n\t\t\t\t}\n\t\t\t\t/* Got the values, try registering now */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Registering user %s (auth=%s, secret %s) @ %s through %s (outbound proxy: %s)\\n\",\n\t\t\t\t\tusername_text, secret_text, username_uri.url->url_host,\n\t\t\t\t\tauthuser_text != NULL ? authuser_text : username_text,\n\t\t\t\t\tproxy_text != NULL ? proxy_text : \"(null)\",\n\t\t\t\t\tobproxy_text != NULL ? obproxy_text : \"none\");\n\t\t\t}\n\t\t\t/* Create a master ID if we don't have one yet */\n\t\t\tif(session->master_id == 0) {\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\twhile(session->master_id == 0) {\n\t\t\t\t\tsession->master_id = janus_random_uint32();\n\t\t\t\t\tif(g_hash_table_lookup(masters, GUINT_TO_POINTER(session->master_id)) != NULL)\n\t\t\t\t\t\tsession->master_id = 0;\n\t\t\t\t}\n\t\t\t\tg_hash_table_insert(masters, GUINT_TO_POINTER(session->master_id), session);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t}\n\n\t\t\tjson_t *header_prefixes_json = json_object_get(root, \"incoming_header_prefixes\");\n\t\t\tif(header_prefixes_json) {\n\t\t\t\tsize_t index = 0;\n\t\t\t\tjson_t *value = NULL;\n\t\t\t\tjson_array_foreach(header_prefixes_json, index, value) {\n\t\t\t\t\tconst char *header_prefix = json_string_value(value);\n\t\t\t\t\tif(header_prefix)\n\t\t\t\t\t\tsession->incoming_header_prefixes = g_list_append(session->incoming_header_prefixes, g_strdup(header_prefix));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* If this is a refresh, get rid of the old values */\n\t\t\tif(refresh) {\n\t\t\t\t/* Cleanup old values */\n\t\t\t\tif(session->account.identity != NULL) {\n\t\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\t\tg_hash_table_remove(identities, session->account.identity);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tg_free(session->account.identity);\n\t\t\t\t}\n\t\t\t\tsession->account.identity = NULL;\n\t\t\t\tsession->account.force_udp = FALSE;\n\t\t\t\tsession->account.force_tcp = FALSE;\n\t\t\t\tsession->account.sips = FALSE;\n\t\t\t\tsession->account.rfc2543_cancel = FALSE;\n\t\t\t\tsession->account.automatic_ringing = TRUE;\n\t\t\t\tif(session->account.username != NULL)\n\t\t\t\t\tg_free(session->account.username);\n\t\t\t\tsession->account.username = NULL;\n\t\t\t\tif(session->account.display_name != NULL)\n\t\t\t\t\tg_free(session->account.display_name);\n\t\t\t\tsession->account.display_name = NULL;\n\t\t\t\tif(session->account.authuser != NULL)\n\t\t\t\t\tg_free(session->account.authuser);\n\t\t\t\tsession->account.authuser = NULL;\n\t\t\t\tif(session->account.secret != NULL)\n\t\t\t\t\tg_free(session->account.secret);\n\t\t\t\tsession->account.secret = NULL;\n\t\t\t\tsession->account.secret_type = janus_sip_secret_type_unknown;\n\t\t\t\tif(session->account.proxy != NULL)\n\t\t\t\t\tg_free(session->account.proxy);\n\t\t\t\tsession->account.proxy = NULL;\n\t\t\t\tif(session->account.outbound_proxy != NULL)\n\t\t\t\t\tg_free(session->account.outbound_proxy);\n\t\t\t\tsession->account.outbound_proxy = NULL;\n\t\t\t\tif(session->account.user_agent != NULL)\n\t\t\t\t\tg_free(session->account.user_agent);\n\t\t\t\tsession->account.user_agent = NULL;\n\t\t\t\tsession->account.registration_status = janus_sip_registration_status_unregistered;\n\t\t\t}\n\t\t\tsession->account.identity = g_strdup(username_text);\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tg_hash_table_insert(identities, session->account.identity, session);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tsession->account.force_udp = force_udp;\n\t\t\tsession->account.force_tcp = force_tcp;\n\t\t\tsession->account.sips = sips;\n\t\t\tsession->account.rfc2543_cancel = rfc2543_cancel;\n\t\t\tsession->account.automatic_ringing = automatic_ringing;\n\t\t\tsession->account.username = g_strdup(user_id);\n\t\t\tsession->account.authuser = g_strdup(authuser_text ? authuser_text : user_id);\n\t\t\tsession->account.secret = secret_text ? g_strdup(secret_text) : NULL;\n\t\t\tsession->account.secret_type = secret_type;\n\t\t\tif(display_name_text) {\n\t\t\t\tsession->account.display_name = g_strdup(display_name_text);\n\t\t\t}\n\t\t\tif(user_agent_text) {\n\t\t\t\tsession->account.user_agent = g_strdup(user_agent_text);\n\t\t\t}\n\t\t\tif(proxy_text) {\n\t\t\t\tsession->account.proxy = g_strdup(proxy_text);\n\t\t\t}\n\t\t\tif(obproxy_text) {\n\t\t\t\tsession->account.outbound_proxy = g_strdup(obproxy_text);\n\t\t\t}\n\n\t\t\tsession->account.registration_status = janus_sip_registration_status_registering;\n\t\t\tif(!refresh && session->stack == NULL) {\n\t\t\t\t/* Start the thread first */\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"sip %s\", session->account.username);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tg_thread_try_new(tname, janus_sip_sofia_thread, session, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the SIP Sofia thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Got error %d (%s) trying to launch the SIP Sofia thread\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tlong int timeout = 0;\n\t\t\t\twhile(session->stack == NULL || session->stack->s_nua == NULL) {\n\t\t\t\t\tg_usleep(100000);\n\t\t\t\t\ttimeout += 100000;\n\t\t\t\t\tif(timeout >= 2000000) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(timeout >= 2000000) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Two seconds passed and still no NUA, problems with the thread?\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Two seconds passed and still no NUA, problems with the thread?\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session == NULL || session->stack == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing session or Sofia stack\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing session or Sofia stack\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->stack->s_nh_r != NULL) {\n\t\t\t\tnua_handle_destroy(session->stack->s_nh_r);\n\t\t\t\tsession->stack->s_nh_r = NULL;\n\t\t\t}\n\n\t\t\tif(send_register) {\n\t\t\t\t/* Check if the REGISTER needs to be enriched with custom headers */\n\t\t\t\tchar custom_headers[2048];\n\t\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\t\t/* Do the same in case there are custom Contact URI params */\n\t\t\t\tchar custom_params[2048];\n\t\t\t\tjanus_sip_parse_custom_contact_params(root, (char *)&custom_params, sizeof(custom_params));\n\t\t\t\t/* Create a new NUA handle */\n\t\t\t\tjanus_mutex_lock(&session->stack->smutex);\n\t\t\t\tif(session->stack->s_nua == NULL) {\n\t\t\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA destroyed while registering?\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tsession->stack->s_nh_r = nua_handle(session->stack->s_nua, session, TAG_END());\n\t\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\t\tif(session->stack->s_nh_r == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA Handle for REGISTER still null??\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA Handle\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* TTL */\n\t\t\t\tchar ttl_text[20];\n\t\t\t\tg_snprintf(ttl_text, sizeof(ttl_text), \"%d\", ttl);\n\t\t\t\t/* Send the REGISTER */\n\t\t\t\tnua_register(session->stack->s_nh_r,\n\t\t\t\t\tNUTAG_M_USERNAME(session->account.authuser),\n\t\t\t\t\tNUTAG_M_DISPLAY(session->account.display_name),\n\t\t\t\t\tSIPTAG_FROM_STR(username_text),\n\t\t\t\t\tSIPTAG_TO_STR(username_text),\n\t\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\t\tTAG_IF(strlen(custom_params) > 0, NUTAG_M_PARAMS(custom_params)),\n\t\t\t\t\tSIPTAG_EXPIRES_STR(ttl_text),\n\t\t\t\t\tNUTAG_REGISTRAR(proxy_text),\n\t\t\t\t\tNUTAG_PROXY(obproxy_text),\n\t\t\t\t\tTAG_END());\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"registering\"));\n\t\t\t\tjson_object_set_new(result, \"unique_id\", json_string(session->unique_id));\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Not sending a SIP REGISTER: either send_register was set to false or guest mode was enabled\\n\");\n\t\t\t\tsession->account.registration_status = janus_sip_registration_status_disabled;\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"registered\"));\n\t\t\t\tjson_object_set_new(result, \"username\", json_string(session->account.username));\n\t\t\t\tjson_object_set_new(result, \"register_sent\", json_false());\n\t\t\t\tjson_object_set_new(result, \"master_id\", json_integer(session->master_id));\n\t\t\t\tjson_object_set_new(result, \"unique_id\", json_string(session->unique_id));\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"registered\"));\n\t\t\t\t\tjson_object_set_new(info, \"identity\", json_string(session->account.identity));\n\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(\"guest\"));\n\t\t\t\t\tjson_object_set_new(info, \"master_id\", json_integer(session->master_id));\n\t\t\t\t\tjson_object_set_new(info, \"unique_id\", json_string(session->unique_id));\n\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"unregister\")) {\n\t\t\tif(session->stack == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (register first)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (register first)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->helper) {\n\t\t\t\t/* Not really \"unregistering\", we're just removing the association to the \"master\" */\n\t\t\t\tjanus_sip_session *master = session->master;\n\t\t\t\tjanus_mutex_lock(&master->mutex);\n\t\t\t\tgboolean found = (g_list_find(master->helpers, session) != NULL);\n\t\t\t\tif(found) {\n\t\t\t\t\tmaster->helpers = g_list_remove(master->helpers, session);\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&master->ref);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&master->mutex);\n\t\t\t\tsession->helper = FALSE;\n\t\t\t\tsession->master = NULL;\n\t\t\t\tsession->master_id = 0;\n\t\t\t\t/* Done */\n\t\t\t\tsession->account.registration_status = janus_sip_registration_status_unregistered;\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"unregistering\"));\n\t\t\t\tgoto done;\n\t\t\t}\n\t\t\tif(session->account.registration_status < janus_sip_registration_status_registered) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not registered)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not registered)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->stack->s_nh_r == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA Handle for REGISTER still null??\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA Handle\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Unregister now */\n\t\t\tsession->account.registration_status = janus_sip_registration_status_unregistering;\n\t\t\tnua_unregister(session->stack->s_nh_r, TAG_END());\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"unregistering\"));\n\t\t} else if(!strcasecmp(request_text, \"subscribe\")) {\n\t\t\t/* Subscribe to some SIP events */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, subscribe_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tif(session->account.registration_status != janus_sip_registration_status_registered &&\n\t\t\t\t\tsession->account.registration_status != janus_sip_registration_status_disabled) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not registered)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not registered)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tconst char *to = json_string_value(json_object_get(root, \"to\"));\n\t\t\tif(to == NULL)\n\t\t\t\tto = session->account.identity;\n\t\t\tconst char *event_type = json_string_value(json_object_get(root, \"event\"));\n\t\t\tconst char *accept = json_string_value(json_object_get(root, \"accept\"));\n\n\t\t\t/* TTL */\n\t\t\tint ttl = subscribe_ttl;\n\t\t\tjson_t *sub_ttl = json_object_get(root, \"subscribe_ttl\");\n\t\t\tif(sub_ttl && json_is_integer(sub_ttl))\n\t\t\t\tttl = json_integer_value(sub_ttl);\n\t\t\tif(ttl <= 0)\n\t\t\t\tttl = JANUS_DEFAULT_SUBSCRIBE_TTL;\n\t\t\tchar ttl_text[20];\n\t\t\tg_snprintf(ttl_text, sizeof(ttl_text), \"%d\", ttl);\n\n\t\t\t/* Take call-id from request, if it exists */\n\t\t\tconst char *callid = NULL;\n\t\t\tjson_t *request_callid = json_object_get(root, \"call_id\");\n\t\t\tif(request_callid)\n\t\t\t\tcallid = json_string_value(request_callid);\n\n\t\t\t/* Do we have a handle for this subscription already? */\n\t\t\tjanus_mutex_lock(&session->stack->smutex);\n\t\t\tnua_handle_t *nh = NULL;\n\t\t\tif(session->stack->subscriptions != NULL)\n\t\t\t\tnh = g_hash_table_lookup(session->stack->subscriptions, (char *)event_type);\n\t\t\tif(nh == NULL) {\n\t\t\t\t/* We don't, create one now */\n\t\t\t\tif(!session->helper) {\n\t\t\t\t\tif(session->stack->s_nua == NULL) {\n\t\t\t\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA destroyed while subscribing?\\n\");\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tnh = nua_handle(session->stack->s_nua, session, TAG_END());\n\t\t\t\t} else {\n\t\t\t\t\t/* This is a helper, we need to use the master's SIP stack */\n\t\t\t\t\tif(session->master == NULL || session->master->stack == NULL) {\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_HELPER_ERROR;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid master SIP stack\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&session->master->stack->smutex);\n\t\t\t\t\tif(session->master->stack->s_nua == NULL) {\n\t\t\t\t\t\tjanus_mutex_unlock(&session->master->stack->smutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA destroyed while subscribing?\\n\");\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tnh = nua_handle(session->master->stack->s_nua, session, TAG_END());\n\t\t\t\t\tjanus_mutex_unlock(&session->master->stack->smutex);\n\t\t\t\t}\n\t\t\t\tif(session->stack->subscriptions == NULL) {\n\t\t\t\t\t/* We still need a table for mapping these subscriptions as well */\n\t\t\t\t\tsession->stack->subscriptions = g_hash_table_new_full(g_int64_hash, g_int64_equal,\n\t\t\t\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)nua_handle_destroy);\n\t\t\t\t}\n\t\t\t\tg_hash_table_insert(session->stack->subscriptions, g_strdup(event_type), nh);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\t/* Retrieve the Contact header for manually adding if not NULL */\n\t\t\tchar *contact_header = janus_sip_session_contact_header_retrieve(session);\n\t\t\t/* Retrieve the content type */\n\t\t\tconst char *content_type = NULL;\n\t\t\tjson_t *content_type_text = json_object_get(root, \"content_type\");\n\t\t\tif(content_type_text && json_is_string(content_type_text))\n\t\t\t\tcontent_type = json_string_value(content_type_text);\n\t\t\t/* Retrieve the content message */\n\t\t\tconst char *msg_content = NULL;\n\t\t\tjson_t *msg_content_text = json_object_get(root, \"content\");\n\t\t\tif(msg_content_text && json_is_string(msg_content_text))\n\t\t\t\tmsg_content = json_string_value(msg_content_text);\n\t\t\t/* Retrieve the outbound proxy */\n\t\t\tchar *proxy = session->helper && session->master ?\n\t\t\t\tsession->master->account.outbound_proxy : session->account.outbound_proxy;\n\t\t\t/* Send the SUBSCRIBE */\n\t\t\tnua_subscribe(nh,\n\t\t\t\tSIPTAG_TO_STR(to),\n\t\t\t\tSIPTAG_EVENT_STR(event_type),\n\t\t\t\tSIPTAG_CALL_ID_STR(callid),\n\t\t\t\tTAG_IF(contact_header != NULL, SIPTAG_CONTACT_STR(contact_header)),\n\t\t\t\tSIPTAG_ACCEPT_STR(accept),\n\t\t\t\tSIPTAG_EXPIRES_STR(ttl_text),\n\t\t\t\tTAG_IF(proxy != NULL, NUTAG_PROXY(proxy)),\n\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\tTAG_IF(content_type != NULL && msg_content != NULL, SIPTAG_CONTENT_TYPE_STR(content_type)),\n\t\t\t\tTAG_IF(content_type != NULL && msg_content != NULL, SIPTAG_PAYLOAD_STR(msg_content)),\n\t\t\t\tTAG_END());\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"subscribing\"));\n\t\t\tif (callid)\n\t\t\t\tjson_object_set_new(result, \"call_id\", json_string(callid));\n\t\t} else if(!strcasecmp(request_text, \"unsubscribe\")) {\n\t\t\t/* Unsubscribe from some SIP events */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, subscribe_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tif(session->account.registration_status != janus_sip_registration_status_registered &&\n\t\t\t\t\tsession->account.registration_status != janus_sip_registration_status_disabled) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not registered)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not registered)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tconst char *to = json_string_value(json_object_get(root, \"to\"));\n\t\t\tif(to == NULL)\n\t\t\t\tto = session->account.identity;\n\t\t\tconst char *event_type = json_string_value(json_object_get(root, \"event\"));\n\t\t\t/* Get the handle we used for this subscription */\n\t\t\tjanus_mutex_lock(&session->stack->smutex);\n\t\t\tnua_handle_t *nh = NULL;\n\t\t\tif(session->stack->subscriptions != NULL)\n\t\t\t\tnh = g_hash_table_lookup(session->stack->subscriptions, (char *)event_type);\n\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\tif(nh == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not subscribed to this event)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not subscribed to this event)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Send the SUBSCRIBE with Expires set to 0 */\n\t\t\tnua_subscribe(nh, SIPTAG_TO_STR(to), SIPTAG_EVENT_STR(event_type),\n\t\t\t\tSIPTAG_EXPIRES_STR(\"0\"), TAG_END());\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"unsubscribing\"));\n\t\t} else if(!strcasecmp(request_text, \"call\")) {\n\t\t\t/* Call another peer */\n\t\t\tif(session->stack == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (register first)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (register first)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->account.registration_status != janus_sip_registration_status_registered &&\n\t\t\t\t\tsession->account.registration_status != janus_sip_registration_status_disabled) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not registered)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not registered)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->status >= janus_sip_call_status_inviting) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (already in a call? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (already in a call? status=%s)\", janus_sip_call_status_string(session->status));\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, call_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *secret = json_object_get(root, \"secret\");\n\t\t\tjson_t *ha1_secret = json_object_get(root, \"ha1_secret\");\n\t\t\tjson_t *authuser = json_object_get(root, \"authuser\");\n\t\t\tif(secret && ha1_secret) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Conflicting elements specified (secret and ha1_secret)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Conflicting elements specified (secret and ha1_secret)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *uri = json_object_get(root, \"uri\");\n\t\t\t/* Check if the INVITE needs to be enriched with custom headers */\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\t/* SDES-SRTP is disabled by default, let's see if we need to enable it */\n\t\t\tgboolean offer_srtp = FALSE, require_srtp = FALSE;\n\t\t\tjanus_srtp_profile srtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n\t\t\tjson_t *srtp = json_object_get(root, \"srtp\");\n\t\t\tif(srtp) {\n\t\t\t\tconst char *srtp_text = json_string_value(srtp);\n\t\t\t\tif(!strcasecmp(srtp_text, \"sdes_optional\")) {\n\t\t\t\t\t/* Negotiate SDES, but make it optional */\n\t\t\t\t\toffer_srtp = TRUE;\n\t\t\t\t} else if(!strcasecmp(srtp_text, \"sdes_mandatory\")) {\n\t\t\t\t\t/* Negotiate SDES, and require it */\n\t\t\t\t\toffer_srtp = TRUE;\n\t\t\t\t\trequire_srtp = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (srtp can only be sdes_optional or sdes_mandatory)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (srtp can only be sdes_optional or sdes_mandatory)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(offer_srtp) {\n\t\t\t\t\t/* Any SRTP profile different from the default? */\n\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n\t\t\t\t\tconst char *profile = json_string_value(json_object_get(root, \"srtp_profile\"));\n\t\t\t\t\tif(profile) {\n\t\t\t\t\t\tif(!strcmp(profile, \"AES_CM_128_HMAC_SHA1_32\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_32;\n\t\t\t\t\t\t} else if(!strcmp(profile, \"AES_CM_128_HMAC_SHA1_80\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n#ifdef HAVE_SRTP_AESGCM\n\t\t\t\t\t\t} else if(!strcmp(profile, \"AEAD_AES_128_GCM\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AEAD_AES_128_GCM;\n\t\t\t\t\t\t} else if(!strcmp(profile, \"AEAD_AES_256_GCM\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AEAD_AES_256_GCM;\n#endif\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (unsupported SRTP profile)\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (unsupported SRTP profile)\");\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_t *aar = json_object_get(root, \"autoaccept_reinvites\");\n\t\t\tsession->media.autoaccept_reinvites = aar ? json_is_true(aar) : TRUE;\n\t\t\t/* Parse address */\n\t\t\tconst char *uri_text = json_string_value(uri);\n\t\t\tjanus_sip_uri_t target_uri;\n\t\t\tif(janus_sip_parse_uri(&target_uri, uri_text) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid user address %s\\n\", uri_text);\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ADDRESS;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid user address %s\\n\", uri_text);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Any SDP to handle? if not, something's wrong */\n\t\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\t\tif(!msg_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(json_is_true(json_object_get(msg->jsep, \"e2ee\"))) {\n\t\t\t\t/* Media is encrypted, but SIP endpoints will need unencrypted media frames */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Media encryption unsupported by this plugin\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Media encryption unsupported by this plugin\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(strstr(msg_sdp, \"m=application\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"The SIP plugin does not support DataChannels\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"The SIP plugin does not support DataChannels\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"%s is calling %s\\n\", session->account.username, uri_text);\n\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well:\\n%s\\n\", msg_sdp_type, msg_sdp);\n\t\t\t/* Clean up SRTP stuff from before first, in case it's still needed */\n\t\t\tjanus_sip_srtp_cleanup(session);\n\t\t\tsession->media.require_srtp = require_srtp;\n\t\t\tsession->media.has_srtp_local_audio = offer_srtp;\n\t\t\tsession->media.has_srtp_local_video = offer_srtp;\n\t\t\tsession->media.srtp_profile = srtp_profile;\n\t\t\tif(offer_srtp) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate SDES-SRTP (%s)...\\n\", require_srtp ? \"mandatory\" : \"optional\");\n\t\t\t}\n\n\t\t\t/* Get video-orientation extension id from SDP we got */\n\t\t\tsession->media.video_orientation_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t/* Get audio-level extension id from SDP we got */\n\t\t\tsession->media.audio_level_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t/* Parse the SDP we got, manipulate some things, and generate a new one */\n\t\t\tchar sdperror[100];\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(msg_sdp, sdperror, sizeof(sdperror));\n\t\t\tif(!parsed_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing SDP: %s\\n\", sdperror);\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing SDP: %s\", sdperror);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Allocate RTP ports and merge them with the anonymized SDP */\n\t\t\tif(strstr(msg_sdp, \"m=audio\") && !strstr(msg_sdp, \"m=audio 0\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate audio...\\n\");\n\t\t\t\tsession->media.has_audio = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t}\n\t\t\tif(strstr(msg_sdp, \"m=video\") && !strstr(msg_sdp, \"m=video 0\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate video...\\n\");\n\t\t\t\tsession->media.has_video = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(janus_sip_allocate_local_ports(session, FALSE) < 0) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not allocate RTP/RTCP ports\\n\");\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\terror_code = JANUS_SIP_ERROR_IO_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Could not allocate RTP/RTCP ports\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tchar *sdp = janus_sip_sdp_manipulate(session, parsed_sdp, FALSE);\n\t\t\tif(sdp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error manipulating SDP\\n\");\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\terror_code = JANUS_SIP_ERROR_IO_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error manipulating SDP\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Take note of the SDP (may be useful for UPDATEs or re-INVITEs) */\n\t\t\tjanus_sdp_destroy(session->sdp);\n\t\t\tsession->sdp = parsed_sdp;\n\t\t\tJANUS_LOG(LOG_VERB, \"Prepared SDP for INVITE:\\n%s\", sdp);\n\t\t\t/* Prepare the From header */\n\t\t\tchar from_hdr[1024];\n\t\t\tchar *local_tag = g_malloc0(7);\n\t\t\tjanus_sip_random_string(7, local_tag);\n\t\t\t/* Prepare the stack */\n\t\t\tif(session->stack->s_nh_i != NULL)\n\t\t\t\tnua_handle_destroy(session->stack->s_nh_i);\n\t\t\tif(!session->helper) {\n\t\t\t\tjanus_mutex_lock(&session->stack->smutex);\n\t\t\t\tif(session->stack->s_nua == NULL) {\n\t\t\t\t\tg_free(local_tag);\n\t\t\t\t\tg_free(sdp);\n\t\t\t\t\tsession->sdp = NULL;\n\t\t\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA destroyed while calling?\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tsession->stack->s_nh_i = nua_handle(session->stack->s_nua, session, TAG_END());\n\t\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\t\tif(session->account.display_name) {\n\t\t\t\t\tg_snprintf(from_hdr, sizeof(from_hdr), \"\\\"%s\\\" <%s>;tag=%s\",\n\t\t\t\t\t\tsession->account.display_name, session->account.identity, local_tag);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(from_hdr, sizeof(from_hdr), \"%s;tag=%s\",\n\t\t\t\t\t\tsession->account.identity, local_tag);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* This is a helper, we need to use the master's SIP stack */\n\t\t\t\tif(session->master == NULL || session->master->stack == NULL) {\n\t\t\t\t\tg_free(local_tag);\n\t\t\t\t\tg_free(sdp);\n\t\t\t\t\tsession->sdp = NULL;\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_HELPER_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid master SIP stack\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_lock(&session->master->stack->smutex);\n\t\t\t\tif(session->master->stack->s_nua == NULL) {\n\t\t\t\t\tjanus_mutex_unlock(&session->master->stack->smutex);\n\t\t\t\t\tg_free(local_tag);\n\t\t\t\t\tg_free(sdp);\n\t\t\t\t\tsession->sdp = NULL;\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tsession->stack->s_nh_i = nua_handle(session->master->stack->s_nua, session, TAG_END());\n\t\t\t\tjanus_mutex_unlock(&session->master->stack->smutex);\n\t\t\t\tif(session->master->account.display_name) {\n\t\t\t\t\tg_snprintf(from_hdr, sizeof(from_hdr), \"\\\"%s\\\" <%s>;tag=%s\",\n\t\t\t\t\t\tsession->master->account.display_name, session->master->account.identity, local_tag);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(from_hdr, sizeof(from_hdr), \"%s;tag=%s\",\n\t\t\t\t\t\tsession->master->account.identity, local_tag);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->stack->s_nh_i == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"NUA Handle for INVITE still null??\\n\");\n\t\t\t\tg_free(local_tag);\n\t\t\t\tg_free(sdp);\n\t\t\t\tsession->sdp = NULL;\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA Handle\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_inviting);\n\t\t\tchar *callid = NULL;\n\t\t\tjson_t *request_callid = json_object_get(root, \"call_id\");\n\t\t\t/* Take call-id from request, if it exists */\n\t\t\tif(request_callid) {\n\t\t\t\tcallid = g_strdup(json_string_value(request_callid));\n\t\t\t\tif(callid == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid call_id provided, generating a random one\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(callid == NULL) {\n\t\t\t\t/* If call-id does not exist in request, create a random one */\n\t\t\t\tcallid = g_malloc0(24);\n\t\t\t\tjanus_sip_random_string(24, callid);\n\t\t\t}\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"calling\"));\n\t\t\t\tjson_object_set_new(info, \"callee\", json_string(uri_text));\n\t\t\t\tjson_object_set_new(info, \"call-id\", json_string(callid));\n\t\t\t\tjson_object_set_new(info, \"sdp\", json_string(sdp));\n\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* If we're here because of a REFER, tell the transferer the request was accepted */\n\t\t\tguint32 refer_id = json_integer_value(json_object_get(root, \"refer_id\"));\n\t\t\tchar *referred_by = NULL;\n\t\t\tif(refer_id > 0) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Call is after a refer (%\"SCNu32\")\\n\", refer_id);\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tjanus_sip_transfer *transfer = g_hash_table_lookup(transfers, GUINT_TO_POINTER(refer_id));\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tif(transfer != NULL) {\n\t\t\t\t\tif(session->refer_id > 0 && session->refer_id != refer_id) {\n\t\t\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\t\t\tg_hash_table_remove(transfers, GUINT_TO_POINTER(session->refer_id));\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t}\n\t\t\t\t\tsession->refer_id = refer_id;\n\t\t\t\t\treferred_by = transfer->referred_by ? g_strdup(transfer->referred_by) : NULL;\n\t\t\t\t\t/* Any custom headers we should include? (e.g., Replaces) */\n\t\t\t\t\tjanus_strlcat(custom_headers, transfer->custom_headers, sizeof(custom_headers));\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* If the user negotiated simulcasting, just stick with the base substream */\n\t\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\t\tif(msg_simulcast && json_array_size(msg_simulcast) > 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Client negotiated simulcasting which we don't do here, falling back to base substream...\\n\");\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(msg_simulcast); i++) {\n\t\t\t\t\tjson_t *sobj = json_array_get(msg_simulcast, i);\n\t\t\t\t\tjson_t *s = json_object_get(sobj, \"ssrcs\");\n\t\t\t\t\tif(s && json_array_size(s) > 0)\n\t\t\t\t\t\tsession->media.simulcast_ssrc = json_integer_value(json_array_get(s, 0));\n\t\t\t\t\tsession->media.simulcast_ssrc = json_integer_value(json_object_get(s, \"ssrc-0\"));\n\t\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Check if there are new credentials to authenticate the INVITE */\n\t\t\tif(authuser) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Updating credentials (authuser) for authenticating the INVITE\\n\");\n\t\t\t\tif(!session->helper) {\n\t\t\t\t\tg_free(session->account.authuser);\n\t\t\t\t\tsession->account.authuser = g_strdup(json_string_value(authuser));\n\t\t\t\t} else if(session->master != NULL) {\n\t\t\t\t\tg_free(session->master->account.authuser);\n\t\t\t\t\tsession->master->account.authuser = g_strdup(json_string_value(authuser));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(secret) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Updating credentials (secret) for authenticating the INVITE\\n\");\n\t\t\t\tif(!session->helper) {\n\t\t\t\t\tg_free(session->account.secret);\n\t\t\t\t\tsession->account.secret = g_strdup(json_string_value(secret));\n\t\t\t\t\tsession->account.secret_type = janus_sip_secret_type_plaintext;\n\t\t\t\t} else if(session->master != NULL) {\n\t\t\t\t\tg_free(session->master->account.secret);\n\t\t\t\t\tsession->master->account.secret = g_strdup(json_string_value(secret));\n\t\t\t\t\tsession->master->account.secret_type = janus_sip_secret_type_plaintext;\n\t\t\t\t}\n\t\t\t} else if(ha1_secret) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Updating credentials (ha1_secret) for authenticating the INVITE\\n\");\n\t\t\t\tif(!session->helper) {\n\t\t\t\t\tg_free(session->account.secret);\n\t\t\t\t\tsession->account.secret = g_strdup(json_string_value(ha1_secret));\n\t\t\t\t\tsession->account.secret_type = janus_sip_secret_type_hashed;\n\t\t\t\t} else if(session->master != NULL) {\n\t\t\t\t\tg_free(session->master->account.secret);\n\t\t\t\t\tsession->master->account.secret = g_strdup(json_string_value(ha1_secret));\n\t\t\t\t\tsession->master->account.secret_type = janus_sip_secret_type_hashed;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Send INVITE */\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tg_free(session->callee);\n\t\t\tsession->callee = g_strdup(uri_text);\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tg_free(session->callid);\n\t\t\tsession->callid = callid;\n\t\t\tg_free(session->local_tag);\n\t\t\tsession->local_tag = local_tag;\n\t\t\t/* Track this call-id and tag as a caller */\n\t\t\tjanus_sip_call *call = g_malloc0(sizeof(janus_sip_call));\n\t\t\tcall->caller = session;\n\t\t\tg_hash_table_insert(callids, g_strdup(session->callid), call);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tg_atomic_int_set(&session->establishing, 1);\n\t\t\t/* Add a reference for this call */\n\t\t\tjanus_sip_ref_active_call(session);\n\t\t\t/* Retrieve the Contact header for manually adding if not NULL */\n\t\t\tchar *contact_header = janus_sip_session_contact_header_retrieve(session);\n\t\t\t/* Send the INVITE */\n\t\t\tnua_invite(session->stack->s_nh_i,\n\t\t\t\tSIPTAG_FROM_STR(from_hdr),\n\t\t\t\tSIPTAG_TO_STR(uri_text),\n\t\t\t\tSIPTAG_CALL_ID_STR(callid),\n\t\t\t\tTAG_IF(contact_header != NULL, SIPTAG_CONTACT_STR(contact_header)),\n\t\t\t\tSOATAG_USER_SDP_STR(sdp),\n\t\t\t\tNUTAG_PROXY(session->helper && session->master ?\n\t\t\t\t\tsession->master->account.outbound_proxy : session->account.outbound_proxy),\n\t\t\t\tTAG_IF(referred_by != NULL, SIPTAG_REFERRED_BY_STR(referred_by)),\n\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\tNUTAG_AUTOANSWER(0),\n\t\t\t\tNUTAG_AUTOACK(FALSE),\n\t\t\t\tTAG_END());\n\t\t\tg_free(sdp);\n\t\t\tg_free(session->transaction);\n\t\t\tg_free(referred_by);\n\t\t\tsession->transaction = msg->transaction ? g_strdup(msg->transaction) : NULL;\n\t\t\t/* Send an ack back */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"calling\"));\n\t\t\tjson_object_set_new(result, \"call_id\", json_string(session->callid));\n\t\t} else if(!strcasecmp(request_text, \"accept\") || !strcasecmp(request_text, \"progress\")) {\n\t\t\tgboolean progress = !strcasecmp(request_text, \"progress\");\n\t\t\tif(progress && session->status != janus_sip_call_status_invited) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not invited? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not invited? status=%s)\", janus_sip_call_status_string(session->status));\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!progress && session->status != janus_sip_call_status_invited && session->status != janus_sip_call_status_progress) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not invited or progress? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not invited or progress? status=%s)\", janus_sip_call_status_string(session->status));\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no caller?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no caller?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tif(progress) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, progress_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, accept_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\t/* Any SDP to handle? if not, something's wrong */\n\t\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\t\tif(!msg_sdp || !msg_sdp_type) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(json_is_true(json_object_get(msg->jsep, \"e2ee\"))) {\n\t\t\t\t/* Media is encrypted, but SIP endpoints will need unencrypted media frames */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Media encryption unsupported by this plugin\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Media encryption unsupported by this plugin\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* This may be an SDP answer, or an SDP offer (answer to offerless INVITE) */\n\t\t\tgboolean answer = !strcasecmp(msg_sdp_type, \"answer\");\n\t\t\t/* Check if we need to involve SRTP */\n\t\t\tjanus_srtp_profile srtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n\t\t\tjson_t *srtp = json_object_get(root, \"srtp\");\n\t\t\tgboolean answer_srtp = FALSE;\n\t\t\tif(srtp) {\n\t\t\t\tconst char *srtp_text = json_string_value(srtp);\n\t\t\t\tif(!strcasecmp(srtp_text, \"sdes_optional\")) {\n\t\t\t\t\t/* Negotiate SDES, but make it optional */\n\t\t\t\t\tanswer_srtp = TRUE;\n\t\t\t\t} else if(!strcasecmp(srtp_text, \"sdes_mandatory\")) {\n\t\t\t\t\t/* Negotiate SDES, and require it */\n\t\t\t\t\tanswer_srtp = TRUE;\n\t\t\t\t\tsession->media.require_srtp = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (srtp can only be sdes_optional or sdes_mandatory)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (srtp can only be sdes_optional or sdes_mandatory)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tgboolean has_srtp = TRUE;\n\t\t\tif(session->media.has_audio && answer)\n\t\t\t\thas_srtp = (has_srtp && session->media.has_srtp_remote_audio);\n\t\t\tif(session->media.has_video && answer)\n\t\t\t\thas_srtp = (has_srtp && session->media.has_srtp_remote_video);\n\t\t\tif(session->media.require_srtp && !has_srtp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't %s the call: SDES-SRTP required, but caller didn't offer it\\n\", progress ? \"progress\" : \"accept\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_TOO_STRICT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't %s the call: SDES-SRTP required, but caller didn't offer it\", progress ? \"progress\" : \"accept\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tanswer_srtp = answer_srtp || session->media.has_srtp_remote_audio || session->media.has_srtp_remote_video;\n\t\t\tjson_t *aar = json_object_get(root, \"autoaccept_reinvites\");\n\t\t\tsession->media.autoaccept_reinvites = aar ? json_is_true(aar) : TRUE;\n\t\t\t/* Accept/Progress a call from another peer */\n\t\t\tJANUS_LOG(LOG_VERB, \"We're %s the call from %s\\n\", progress ? \"progressing\" : \"accepting\", session->callee);\n\t\t\tif(!answer) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"This is a response to an offerless INVITE\\n\");\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well:\\n%s\\n\", msg_sdp_type, msg_sdp);\n\t\t\tsession->media.has_srtp_local_audio = answer_srtp && (session->media.has_srtp_remote_audio || !answer);\n\t\t\tsession->media.has_srtp_local_video = answer_srtp && (session->media.has_srtp_remote_video || !answer);\n\t\t\tif(answer_srtp) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate SDES-SRTP (%s)...\\n\", session->media.require_srtp ? \"mandatory\" : \"optional\");\n\t\t\t\tif(!answer && !session->media.srtp_profile) {\n\t\t\t\t\t/* We got an offerless-INVITE, any SRTP profile different from the default? */\n\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n\t\t\t\t\tconst char *profile = json_string_value(json_object_get(root, \"srtp_profile\"));\n\t\t\t\t\tif(profile) {\n\t\t\t\t\t\tif(!strcmp(profile, \"AES_CM_128_HMAC_SHA1_32\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_32;\n\t\t\t\t\t\t} else if(!strcmp(profile, \"AES_CM_128_HMAC_SHA1_80\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AES128_CM_SHA1_80;\n#ifdef HAVE_SRTP_AESGCM\n\t\t\t\t\t\t} else if(!strcmp(profile, \"AEAD_AES_128_GCM\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AEAD_AES_128_GCM;\n\t\t\t\t\t\t} else if(!strcmp(profile, \"AEAD_AES_256_GCM\")) {\n\t\t\t\t\t\t\tsrtp_profile = JANUS_SRTP_AEAD_AES_256_GCM;\n#endif\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (unsupported SRTP profile)\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (unsupported SRTP profile)\");\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsession->media.srtp_profile = srtp_profile;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* Get video-orientation extension id from SDP we got */\n\t\t\tsession->media.video_orientation_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t/* Get audio-level extension id from SDP we got */\n\t\t\tsession->media.audio_level_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t/* Parse the SDP we got, manipulate some things, and generate a new one */\n\t\t\tchar sdperror[100];\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(msg_sdp, sdperror, sizeof(sdperror));\n\t\t\tif(!parsed_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing SDP: %s\\n\", sdperror);\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing SDP: %s\", sdperror);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Allocate RTP ports and merge them with the anonymized SDP */\n\t\t\tif(strstr(msg_sdp, \"m=audio\") && !strstr(msg_sdp, \"m=audio 0\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate audio...\\n\");\n\t\t\t\tsession->media.has_audio = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t}\n\t\t\tif(strstr(msg_sdp, \"m=video\") && !strstr(msg_sdp, \"m=video 0\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Going to negotiate video...\\n\");\n\t\t\t\tsession->media.has_video = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(janus_sip_allocate_local_ports(session, session->status == janus_sip_call_status_progress ? TRUE : FALSE) < 0) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not allocate RTP/RTCP ports\\n\");\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\terror_code = JANUS_SIP_ERROR_IO_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Could not allocate RTP/RTCP ports\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tchar *sdp = janus_sip_sdp_manipulate(session, parsed_sdp, TRUE);\n\t\t\tif(sdp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not allocate RTP/RTCP ports\\n\");\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\terror_code = JANUS_SIP_ERROR_IO_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Could not allocate RTP/RTCP ports\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->media.audio_pt > -1) {\n\t\t\t\tsession->media.audio_pt_name = janus_get_codec_from_pt(sdp, session->media.audio_pt);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Detected audio codec: %d (%s)\\n\", session->media.audio_pt, session->media.audio_pt_name);\n\t\t\t}\n\t\t\tif(session->media.video_pt > -1) {\n\t\t\t\tsession->media.video_pt_name = janus_get_codec_from_pt(sdp, session->media.video_pt);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Detected video codec: %d (%s)\\n\", session->media.video_pt, session->media.video_pt_name);\n\t\t\t}\n\t\t\t/* Take note of the SDP (may be useful for UPDATEs or re-INVITEs) */\n\t\t\tjanus_sdp_destroy(session->sdp);\n\t\t\tsession->sdp = parsed_sdp;\n\t\t\tJANUS_LOG(LOG_VERB, \"Prepared SDP for %s:\\n%s\", progress ? \"183 Session Progress\" : \"200 OK\", sdp);\n\t\t\t/* If the user negotiated simulcasting, just stick with the base substream */\n\t\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\t\tif(msg_simulcast) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Client negotiated simulcasting which we don't do here, falling back to base substream...\\n\");\n\t\t\t\tjson_t *s = json_object_get(msg_simulcast, \"ssrcs\");\n\t\t\t\tif(s && json_array_size(s) > 0)\n\t\t\t\t\tsession->media.simulcast_ssrc = json_integer_value(json_array_get(s, 0));\n\t\t\t}\n\t\t\tconst char *event_value;\n\t\t\tif(progress) {\n\t\t\t\tevent_value = \"progressed\";\n\t\t\t} else if(answer) {\n\t\t\t\tevent_value = \"accepted\";\n\t\t\t} else {\n\t\t\t\tevent_value = \"accepting\";\n\t\t\t}\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(event_value));\n\t\t\t\tif(session->callid)\n\t\t\t\t\tjson_object_set_new(info, \"call-id\", json_string(session->callid));\n\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* Check if the OK needs to be enriched with custom headers */\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\t/* Send 200 OK/183 Session progress */\n\t\t\tif(!answer) {\n\t\t\t\tif(session->transaction)\n\t\t\t\t\tg_free(session->transaction);\n\t\t\t\tsession->transaction = msg->transaction ? g_strdup(msg->transaction) : NULL;\n\t\t\t}\n\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\tjanus_sip_call_update_status(session, progress ? janus_sip_call_status_progress : janus_sip_call_status_incall);\n\t\t\tif(session->stack->s_nh_i == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"NUA Handle for %s null\\n\", progress ? \"183 Session Progress\" : \"200 OK\");\n\t\t\t}\n\t\t\tint sip_response = progress ? 183 : 200;\n\t\t\tnua_respond(session->stack->s_nh_i,\n\t\t\t\tsip_response, sip_status_phrase(sip_response),\n\t\t\t\tSOATAG_USER_SDP_STR(sdp),\n\t\t\t\tSOATAG_RTP_SELECT(SOA_RTP_SELECT_COMMON),\n\t\t\t\tNUTAG_AUTOANSWER(0),\n\t\t\t\tNUTAG_AUTOACK(FALSE),\n\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\tTAG_END());\n\t\t\tg_free(sdp);\n\t\t\t/* Send an ack back */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(event_value));\n\t\t\tif(answer) {\n\t\t\t\t/* Start the media */\n\t\t\t\tsession->media.ready = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"siprtp %s\", session->account.username);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tsession->relayer_thread = g_thread_try_new(tname, janus_sip_relay_thread, session, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tsession->relayer_thread = NULL;\n\t\t\t\t\tsession->media.ready = FALSE;\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the RTP/RTCP thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"update\")) {\n\t\t\t/* Update an existing call */\n\t\t\tif(!(session->status == janus_sip_call_status_incall_reinvited || session->status == janus_sip_call_status_incall)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not in a call? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tif(session->sdp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no local SDP?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no local SDP?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Any SDP to handle? if not, something's wrong */\n\t\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\t\tif(!msg_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP update\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP update\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!json_is_true(json_object_get(msg->jsep, \"update\"))) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP update\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP update\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(json_is_true(json_object_get(msg->jsep, \"e2ee\"))) {\n\t\t\t\t/* Media is encrypted, but SIP endpoints will need unencrypted media frames */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Media encryption unsupported by this plugin\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Media encryption unsupported by this plugin\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\t\tgboolean offer = !strcasecmp(msg_sdp_type, \"offer\");\n\t\t\tif(!offer && session->status == janus_sip_call_status_incall) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] SDP type %s is incompatible with session status %s\\n\", session->account.username, msg_sdp_type, janus_sip_call_status_string(session->status));\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"[SIP-%s] SDP type %s is incompatible with session status %s\\n\", session->account.username, msg_sdp_type, janus_sip_call_status_string(session->status));\n\t\t\t\tgoto error;\n\t\t\t}\n\n\t\t\t/* Get video-orientation extension id from SDP we got */\n\t\t\tsession->media.video_orientation_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t/* Get audio-level extension id from SDP we got */\n\t\t\tsession->media.audio_level_extension_id = janus_rtp_header_extension_get_id(msg_sdp, JANUS_RTP_EXTMAP_AUDIO_LEVEL);\n\t\t\t/* Parse the SDP we got, manipulate some things, and generate a new one */\n\t\t\tchar sdperror[100];\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(msg_sdp, sdperror, sizeof(sdperror));\n\t\t\tif(!parsed_sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing SDP: %s\\n\", sdperror);\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing SDP: %s\", sdperror);\n\t\t\t\tgoto error;\n\t\t\t}\n\n\t\t\tif(session->status == janus_sip_call_status_incall_reinvited && offer) {\n\t\t\t\t/* We have re-INVITE in progress */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[SIP-%s] We have incoming offereless re-INVITE in progress\\n\", session->account.username);\n\t\t\t}\n\n\t\t\tif(offer)\n\t\t\t\tsession->sdp->o_version++;\n\n\t\t\tgboolean audio_added = strstr(msg_sdp, \"m=audio\") && !strstr(msg_sdp, \"m=audio 0\") && session->media.local_audio_rtp_port == 0;\n\t\t\tgboolean video_added = strstr(msg_sdp, \"m=video\") && !strstr(msg_sdp, \"m=video 0\") && session->media.local_video_rtp_port == 0;\n\t\t\tif(audio_added)\n\t\t\t\tsession->media.has_audio = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\tif(video_added)\n\t\t\t\tsession->media.has_video = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\n\t\t\tif(offer) {\n\t\t\t\tgboolean offer_srtp = session->media.require_srtp || session->media.has_srtp_local_audio || session->media.has_srtp_local_video;\n\t\t\t\tsession->media.has_srtp_local_audio = offer_srtp;\n\t\t\t\tsession->media.has_srtp_local_video = offer_srtp;\n\t\t\t} else {\n\t\t\t\tgboolean has_srtp = TRUE;\n\t\t\t\tif (session->media.has_audio)\n\t\t\t\t\thas_srtp = (has_srtp && session->media.has_srtp_remote_audio);\n\t\t\t\tif (session->media.has_video)\n\t\t\t\t\thas_srtp = (has_srtp && session->media.has_srtp_remote_video);\n\t\t\t\tif (session->media.require_srtp && !has_srtp) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR,\n\t\t\t\t\t\t  \"Can't update the call: SDES-SRTP required, but caller didn't offer it\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_TOO_STRICT;\n\t\t\t\t\tg_snprintf(error_cause, 512,\n\t\t\t\t\t\t   \"Can't update the call: SDES-SRTP required, but caller didn't offer it\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tsession->media.has_srtp_local_audio = session->media.has_srtp_remote_audio;\n\t\t\t\tsession->media.has_srtp_local_video = session->media.has_srtp_remote_video;\n\t\t\t}\n\t\t\tif(audio_added || video_added) {\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\tif(janus_sip_allocate_local_ports(session, TRUE) < 0) {\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not allocate RTP/RTCP ports\\n\");\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_IO_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not allocate RTP/RTCP ports\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tif(!offer)\n\t\t\t\t\tsession->media.updated = TRUE;\n\t\t\t}\n\t\t\tchar *sdp = janus_sip_sdp_manipulate(session, parsed_sdp, !offer);\n\t\t\tif(sdp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error manipulating SDP\\n\");\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\terror_code = JANUS_SIP_ERROR_IO_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error manipulating SDP\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!offer) {\n\t\t\t\tif(session->media.audio_pt_name == NULL && session->media.audio_pt > -1) {\n\t\t\t\t\tsession->media.audio_pt_name = janus_get_codec_from_pt(sdp, session->media.audio_pt);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Detected audio codec: %d (%s)\\n\", session->media.audio_pt, session->media.audio_pt_name);\n\t\t\t\t}\n\t\t\t\tif(session->media.video_pt_name == NULL && session->media.video_pt > -1) {\n\t\t\t\t\tsession->media.video_pt_name = janus_get_codec_from_pt(sdp, session->media.video_pt);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Detected video codec: %d (%s)\\n\", session->media.video_pt, session->media.video_pt_name);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Take note of the new SDP */\n\t\t\tjanus_sdp_destroy(session->sdp);\n\t\t\tsession->sdp = parsed_sdp;\n\t\t\tsession->media.update = offer;\n\t\t\tJANUS_LOG(LOG_VERB, \"Prepared SDP for update:\\n%s\", sdp);\n\t\t\tif(session->status == janus_sip_call_status_incall) {\n\t\t\t\t/* Retrieve the Contact header for manually adding if not NULL */\n\t\t\t\tchar *contact_header = janus_sip_session_contact_header_retrieve(session);\n\t\t\t\t/* We're sending a re-INVITE ourselves */\n\t\t\t\tnua_invite(session->stack->s_nh_i,\n\t\t\t\t\tTAG_IF(contact_header != NULL, SIPTAG_CONTACT_STR(contact_header)),\n\t\t\t\t\tSOATAG_USER_SDP_STR(sdp),\n\t\t\t\t\tTAG_END());\n\t\t\t} else {\n\t\t\t\t/* We're answering to a re-INVITE we received */\n\t\t\t\tnua_respond(session->stack->s_nh_i,\n\t\t\t\t\t200, sip_status_phrase(200),\n\t\t\t\t\tSOATAG_USER_SDP_STR(sdp),\n\t\t\t\t\tSOATAG_RTP_SELECT(SOA_RTP_SELECT_COMMON),\n\t\t\t\t\tNUTAG_AUTOANSWER(0),\n\t\t\t\t\tTAG_END());\n\t\t\t}\n\t\t\tg_free(sdp);\n\t\t\t/* Send an ack back */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(offer ? \"updating\" : \"updated\"));\n\t\t} else if(!strcasecmp(request_text, \"decline\")) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, decline_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\t/* Wheck if we're declining a call transfer, rather than an incoming call */\n\t\t\tguint32 refer_id = json_integer_value(json_object_get(root, \"refer_id\"));\n\t\t\tif(refer_id > 0) {\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tjanus_sip_transfer *transfer = g_hash_table_lookup(transfers, GUINT_TO_POINTER(refer_id));\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tif(transfer != NULL && transfer->nh_s != NULL) {\n\t\t\t\t\t/* Send a NOTIFY with the error code */\n\t\t\t\t\tint response_code = 603;\n\t\t\t\t\tjson_t *code_json = json_object_get(root, \"code\");\n\t\t\t\t\tif(code_json)\n\t\t\t\t\t\tresponse_code = json_integer_value(code_json);\n\t\t\t\t\tif(response_code <= 399) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid SIP response code specified, using 603 to decline transfer\\n\");\n\t\t\t\t\t\tresponse_code = 603;\n\t\t\t\t\t}\n\t\t\t\t\tchar content[100];\n\t\t\t\t\tg_snprintf(content, sizeof(content), \"SIP/2.0 %d %s\", response_code, sip_status_phrase(response_code));\n\t\t\t\t\tnua_notify(transfer->nh_s,\n\t\t\t\t\t\tNUTAG_SUBSTATE(nua_substate_terminated),\n\t\t\t\t\t\tSIPTAG_CONTENT_TYPE_STR(\"message/sipfrag\"),\n\t\t\t\t\t\tSIPTAG_PAYLOAD_STR(content),\n\t\t\t\t\t\tNUTAG_WITH_SAVED(transfer->saved),\n\t\t\t\t\t\tTAG_END());\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"declined\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"refer_id\", json_integer(refer_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"code\", json_integer(response_code));\n\t\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t\t}\n\t\t\t\t\t/* Notify the operation */\n\t\t\t\t\tresult = json_object();\n\t\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"declining\"));\n\t\t\t\t\tjson_object_set_new(result, \"refer_id\", json_integer(refer_id));\n\t\t\t\t\tjson_object_set_new(result, \"code\", json_integer(response_code));\n\t\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\t\tg_hash_table_remove(transfers, GUINT_TO_POINTER(refer_id));\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tgoto done;\n\t\t\t\t} else {\n\t\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\t\tg_hash_table_remove(transfers, GUINT_TO_POINTER(refer_id));\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no transfer?)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no transfer?)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Reject an incoming call */\n\t\t\tif(session->status != janus_sip_call_status_invited && session->status != janus_sip_call_status_progress) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not invited? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\t/* Ignore */\n\t\t\t\tjanus_sip_message_free(msg);\n\t\t\t\tcontinue;\n\t\t\t\t//~ g_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\t//~ goto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tsession->media.earlymedia = FALSE;\n\t\t\tsession->media.update = FALSE;\n\t\t\tsession->media.autoaccept_reinvites = TRUE;\n\t\t\tsession->media.ready = FALSE;\n\t\t\tsession->media.on_hold = FALSE;\n\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_closing);\n\t\t\tif(session->stack->s_nh_i == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"NUA Handle for 200 OK still null??\\n\");\n\t\t\t}\n\t\t\tint response_code = 603;\n\t\t\tjson_t *code_json = json_object_get(root, \"code\");\n\t\t\tif(code_json)\n\t\t\t\tresponse_code = json_integer_value(code_json);\n\t\t\tif(response_code <= 399) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid SIP response code specified, using 486 to decline call\\n\");\n\t\t\t\tresponse_code = 486;\n\t\t\t}\n\t\t\t/* Check if the response needs to be enriched with custom headers */\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\tnua_respond(session->stack->s_nh_i, response_code, sip_status_phrase(response_code),\n\t\t\t\t    TAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\t    TAG_END());\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"declined\"));\n\t\t\t\tjson_object_set_new(info, \"callee\", json_string(session->callee));\n\t\t\t\tif(session->callid)\n\t\t\t\t\tjson_object_set_new(info, \"call-id\", json_string(session->callid));\n\t\t\t\tjson_object_set_new(info, \"code\", json_integer(response_code));\n\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t}\n\t\t\tg_free(session->callee);\n\t\t\tsession->callee = NULL;\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t/* Notify the operation */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"declining\"));\n\t\t\tjson_object_set_new(result, \"code\", json_integer(response_code));\n\t\t\tif(session->callid)\n\t\t\t\tjson_object_set_new(result, \"call_id\", json_string(session->callid));\n\t\t} else if(!strcasecmp(request_text, \"transfer\")) {\n\t\t\t/* Transfer an existing call */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, transfer_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tif(!janus_sip_call_is_established(session)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not in a call? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tif(session->sdp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no local SDP?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no local SDP?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Transfer to the following URI */\n\t\t\tjson_t *uri = json_object_get(root, \"uri\");\n\t\t\tconst char *uri_text = json_string_value(uri);\n\t\t\tjanus_sip_uri_t target_uri;\n\t\t\tif(janus_sip_parse_uri(&target_uri, uri_text) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid user address %s\\n\", uri_text);\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ADDRESS;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid user address %s\\n\", uri_text);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Is this a blind (unattended) or warm (attended) transfer? (default=blind) */\n\t\t\tconst char *callid = json_string_value(json_object_get(root, \"replace\"));\n\t\t\tsip_refer_to_t *refer_to = NULL;\n\t\t\tif(callid != NULL) {\n\t\t\t\t/* This is an attended transfer, make sure this call exists */\n\t\t\t\tjanus_sip_session *replaced = janus_sip_active_call_from_callid(session, callid);\n\t\t\t\tif(replaced == NULL || replaced->stack == NULL || replaced->stack->s_nh_i == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such call-ID %s\\n\", callid);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_NO_SUCH_CALLID;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such call-ID %s\", callid);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Craft the Replaces header field */\n\t\t\t\tsip_replaces_t *r = nua_handle_make_replaces(replaced->stack->s_nh_i, session->stack->s_home, 0);\n\t\t\t\tchar *replaces = sip_headers_as_url_query(session->stack->s_home, SIPTAG_REPLACES(r), TAG_END());\n\t\t\t\trefer_to = sip_refer_to_format(session->stack->s_home, \"<%s?%s>\", uri_text, replaces);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Attended transfer: <%s?%s>\\n\", uri_text, replaces);\n\t\t\t\tsu_free(session->stack->s_home, r);\n\t\t\t\tsu_free(session->stack->s_home, replaces);\n\t\t\t}\n\t\t\tif(refer_to == NULL)\n\t\t\t\trefer_to = sip_refer_to_format(session->stack->s_home, \"<%s>\", uri_text);\n\t\t\t/* Send the REFER */\n\t\t\tnua_refer(session->stack->s_nh_i,\n\t\t\t\tSIPTAG_REFER_TO(refer_to),\n\t\t\t\tTAG_END());\n\n\t\t\t/* Notify the operation */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"transferring\"));\n\t\t} else if(!strcasecmp(request_text, \"hold\") || !strcasecmp(request_text, \"unhold\")) {\n\t\t\t/* We either need to put the call on-hold, or resume it */\n\t\t\tif(session->status != janus_sip_call_status_incall) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not in a call? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\t/* Ignore */\n\t\t\t\tjanus_sip_message_free(msg);\n\t\t\t\tcontinue;\n\t\t\t\t//~ g_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\t//~ goto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tif(session->sdp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no SDP?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no SDP?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tgboolean hold = !strcasecmp(request_text, \"hold\");\n\t\t\tif(hold != session->media.on_hold) {\n\t\t\t\t/* To put the call on-hold, we need to change the media direction:\n\t\t\t\t * resuming it means resuming the direction we had before */\n\t\t\t\tjanus_sdp_mdirection hold_dir = JANUS_SDP_SENDONLY;\n\t\t\t\tif(hold) {\n\t\t\t\t\t/* By default when holding we use recvonly, but the\n\t\t\t\t\t * actual direction to set can be passed via API too */\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, hold_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0)\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\tjson_t *hdir = json_object_get(root, \"direction\");\n\t\t\t\t\tif(hdir != NULL) {\n\t\t\t\t\t\tconst char *dir = json_string_value(hdir);\n\t\t\t\t\t\thold_dir = janus_sdp_parse_mdirection(dir);\n\t\t\t\t\t\tif(hold_dir != JANUS_SDP_SENDONLY && hold_dir != JANUS_SDP_RECVONLY &&\n\t\t\t\t\t\t\t\thold_dir != JANUS_SDP_INACTIVE) {\n\t\t\t\t\t\t\t/* Invalid direction */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid direction (can only be sendonly, recvonly or inactive)\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid direction (can only be sendonly, recvonly or inactive)\");\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsession->media.on_hold = hold;\n\t\t\t\tjanus_sdp_mline *m = janus_sdp_mline_find(session->sdp, JANUS_SDP_AUDIO);\n\t\t\t\tif(m) {\n\t\t\t\t\tif(hold) {\n\t\t\t\t\t\t/* Take note of the original media direction */\n\t\t\t\t\t\tsession->media.hold_audio_dir = hold_dir;\n\t\t\t\t\t\tsession->media.pre_hold_audio_dir = m->direction;\n\t\t\t\t\t\tif(m->direction != hold_dir) {\n\t\t\t\t\t\t\t/* Update the media direction */\n\t\t\t\t\t\t\tswitch(m->direction) {\n\t\t\t\t\t\t\t\tcase JANUS_SDP_DEFAULT:\n\t\t\t\t\t\t\t\tcase JANUS_SDP_SENDRECV:\n\t\t\t\t\t\t\t\t\tm->direction = hold_dir;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tm->direction = session->media.pre_hold_audio_dir;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tm = janus_sdp_mline_find(session->sdp, JANUS_SDP_VIDEO);\n\t\t\t\tif(m) {\n\t\t\t\t\tif(hold) {\n\t\t\t\t\t\t/* Take note of the original media direction */\n\t\t\t\t\t\tsession->media.hold_video_dir = hold_dir;\n\t\t\t\t\t\tsession->media.pre_hold_video_dir = m->direction;\n\t\t\t\t\t\tif(m->direction != hold_dir) {\n\t\t\t\t\t\t\t/* Update the media direction */\n\t\t\t\t\t\t\tswitch(m->direction) {\n\t\t\t\t\t\t\t\tcase JANUS_SDP_DEFAULT:\n\t\t\t\t\t\t\t\tcase JANUS_SDP_SENDRECV:\n\t\t\t\t\t\t\t\t\tm->direction = hold_dir;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tm->direction = session->media.pre_hold_video_dir;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Check if the INVITE needs to be enriched with custom headers */\n\t\t\t\tchar custom_headers[2048];\n\t\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\n\t\t\t\t/* Retrieve the Contact header for manually adding if not NULL */\n\t\t\t\tchar *contact_header = janus_sip_session_contact_header_retrieve(session);\n\t\t\t\t/* Send the re-INVITE */\n\t\t\t\tchar *sdp = janus_sdp_write(session->sdp);\n\t\t\t\tnua_invite(session->stack->s_nh_i,\n\t\t\t\t\tTAG_IF(contact_header != NULL, SIPTAG_CONTACT_STR(contact_header)),\n\t\t\t\t\tSOATAG_USER_SDP_STR(sdp),\n\t\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\t\tTAG_END());\n\t\t\t\tg_free(sdp);\n\t\t\t}\n\t\t\t/* Send an ack back */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(hold ? \"holding\" : \"resuming\"));\n\t\t} else if(!strcasecmp(request_text, \"hangup\")) {\n\t\t\t/* Hangup an ongoing call */\n\t\t\tif(!janus_sip_call_is_established(session) && session->status != janus_sip_call_status_inviting && session->status != janus_sip_call_status_progress) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not established/inviting/progress? status=%s)\\n\",\n\t\t\t\t\tjanus_sip_call_status_string(session->status));\n\t\t\t\t/* Ignore */\n\t\t\t\tjanus_sip_message_free(msg);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tsession->media.earlymedia = FALSE;\n\t\t\tsession->media.update = FALSE;\n\t\t\tsession->media.autoaccept_reinvites = TRUE;\n\t\t\tsession->media.ready = FALSE;\n\t\t\tsession->media.on_hold = FALSE;\n\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_closing);\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\tnua_bye(session->stack->s_nh_i,\n\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\tTAG_END());\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tg_free(session->callee);\n\t\t\tsession->callee = NULL;\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t/* Notify the operation */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"hangingup\"));\n\t\t} else if(!strcasecmp(request_text, \"recording\")) {\n\t\t\t/* Start or stop recording */\n\t\t\tif(!(session->status == janus_sip_call_status_inviting || /* Presume it makes sense to start recording with early media? */\n\t\t\t\t\tsession->status == janus_sip_call_status_progress ||\n\t\t\t\t\tjanus_sip_call_is_established(session))) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not in a call? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, recording_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *action = json_object_get(root, \"action\");\n\t\t\tconst char *action_text = json_string_value(action);\n\t\t\tif(strcasecmp(action_text, \"start\") && strcasecmp(action_text, \"stop\") &&\n\t\t\t\t\tstrcasecmp(action_text, \"pause\") && strcasecmp(action_text, \"resume\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid action (should be start|stop|pause|resume)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid action (should be start|stop|pause|resume)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tgboolean record_audio = FALSE, record_video = FALSE,\t/* No media is recorded by default */\n\t\t\t\trecord_peer_audio = FALSE, record_peer_video = FALSE, send_peer_pli = FALSE;\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\trecord_audio = audio ? json_is_true(audio) : FALSE;\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\trecord_video = video ? json_is_true(video) : FALSE;\n\t\t\tjson_t *peer_audio = json_object_get(root, \"peer_audio\");\n\t\t\trecord_peer_audio = peer_audio ? json_is_true(peer_audio) : FALSE;\n\t\t\tjson_t *peer_video = json_object_get(root, \"peer_video\");\n\t\t\trecord_peer_video = peer_video ? json_is_true(peer_video) : FALSE;\n\t\t\tjson_t *peer_pli = json_object_get(root, \"send_peer_pli\");\n\t\t\tsend_peer_pli = peer_pli ? json_is_true(peer_pli) : FALSE;\n\t\t\tif(!record_audio && !record_video && !record_peer_audio && !record_peer_video) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid request (at least one of audio, video, peer_audio and peer_video should be true)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_RECORDING_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid request (at least one of audio, video, peer_audio and peer_video should be true)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\t\tconst char *recording_base = json_string_value(recfile);\n\t\t\tjanus_mutex_lock(&session->rec_mutex);\n\t\t\tif(!strcasecmp(action_text, \"start\")) {\n\t\t\t\t/* Start recording something */\n\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\tchar filename[255];\n\t\t\t\tgint64 now = janus_get_real_time();\n\t\t\t\tif(record_peer_audio || record_peer_video) {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Starting recording of peer's %s (user %s, call %s)\\n\",\n\t\t\t\t\t\t(record_peer_audio && record_peer_video ? \"audio and video\" : (record_peer_audio ? \"audio\" : \"video\")),\n\t\t\t\t\t\tsession->account.username, session->transaction);\n\t\t\t\t\t/* Start recording this peer's audio and/or video */\n\t\t\t\t\tif(record_peer_audio) {\n\t\t\t\t\t\tmemset(filename, 0, sizeof(filename));\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"%s-peer-audio\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"sip-%s-%s-%\"SCNi64\"-peer-audio\",\n\t\t\t\t\t\t\t\tsession->account.username ? session->account.username : \"unknown\",\n\t\t\t\t\t\t\t\tsession->transaction ? session->transaction : \"unknown\",\n\t\t\t\t\t\t\t\tnow);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this peer!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* If RED is in use, take note of it */\n\t\t\t\t\t\t\tif(session->media.opusred_pt > 0)\n\t\t\t\t\t\t\t\tjanus_recorder_opusred(rc, session->media.opusred_pt);\n\t\t\t\t\t\t\tsession->arc_peer = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(record_peer_video) {\n\t\t\t\t\t\tmemset(filename, 0, sizeof(filename));\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"%s-peer-video\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"sip-%s-%s-%\"SCNi64\"-peer-video\",\n\t\t\t\t\t\t\t\tsession->account.username ? session->account.username : \"unknown\",\n\t\t\t\t\t\t\t\tsession->transaction ? session->transaction : \"unknown\",\n\t\t\t\t\t\t\t\tnow);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* If the video-orientation extension has been negotiated, mark it in the recording */\n\t\t\t\t\t\tif(session->media.video_orientation_extension_id > 0)\n\t\t\t\t\t\t\tjanus_recorder_add_extmap(session->vrc_peer, session->media.video_orientation_extension_id, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t\t\t\t/* If we detected PLI support in the remote SDP, craft and send a PLI to the peer */\n\t\t\t\t\t\tif(session->media.video_pli_supported || send_peer_pli)\n\t\t\t\t\t\t\tjanus_sip_rtcp_pli_send(session);\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an video recording file for this peer!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsession->vrc_peer = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(record_audio || record_video) {\n\t\t\t\t\t/* Start recording the user's audio and/or video */\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Starting recording of user's %s (user %s, call %s)\\n\",\n\t\t\t\t\t\t(record_audio && record_video ? \"audio and video\" : (record_audio ? \"audio\" : \"video\")),\n\t\t\t\t\t\tsession->account.username, session->transaction);\n\t\t\t\t\tif(record_audio) {\n\t\t\t\t\t\tmemset(filename, 0, sizeof(filename));\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"%s-user-audio\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"sip-%s-%s-%\"SCNi64\"-own-audio\",\n\t\t\t\t\t\t\t\tsession->account.username ? session->account.username : \"unknown\",\n\t\t\t\t\t\t\t\tsession->transaction ? session->transaction : \"unknown\",\n\t\t\t\t\t\t\t\tnow);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.audio_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this user!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* If RED is in use, take note of it */\n\t\t\t\t\t\t\tif(session->media.opusred_pt > 0)\n\t\t\t\t\t\t\t\tjanus_recorder_opusred(rc, session->media.opusred_pt);\n\t\t\t\t\t\t\tsession->arc = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(record_video) {\n\t\t\t\t\t\tmemset(filename, 0, sizeof(filename));\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"%s-user-video\", recording_base);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, sizeof(filename), \"sip-%s-%s-%\"SCNi64\"-own-video\",\n\t\t\t\t\t\t\t\tsession->account.username ? session->account.username : \"unknown\",\n\t\t\t\t\t\t\t\tsession->transaction ? session->transaction : \"unknown\",\n\t\t\t\t\t\t\t\tnow);\n\t\t\t\t\t\t\t/* FIXME This only works if offer/answer happened */\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, session->media.video_pt_name, filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open a video recording file for this user!\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsession->vrc = rc;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* If the video-orientation extension has been negotiated, mark it in the recording */\n\t\t\t\t\t\tif(session->media.video_orientation_extension_id > 0)\n\t\t\t\t\t\t\tjanus_recorder_add_extmap(session->vrc, session->media.video_orientation_extension_id, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording video, sending a PLI to kickstart it\\n\");\n\t\t\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(action_text, \"pause\")) {\n\t\t\t\tif(record_audio)\n\t\t\t\t\tjanus_recorder_pause(session->arc);\n\t\t\t\tif(record_video)\n\t\t\t\t\tjanus_recorder_pause(session->vrc);\n\t\t\t\tif(record_peer_audio)\n\t\t\t\t\tjanus_recorder_pause(session->arc_peer);\n\t\t\t\tif(record_peer_video)\n\t\t\t\t\tjanus_recorder_pause(session->vrc_peer);\n\t\t\t} else if(!strcasecmp(action_text, \"resume\")) {\n\t\t\t\tif(record_audio)\n\t\t\t\t\tjanus_recorder_resume(session->arc);\n\t\t\t\tif(record_video && !janus_recorder_resume(session->vrc))\n\t\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t\tif(record_peer_audio)\n\t\t\t\t\tjanus_recorder_resume(session->arc_peer);\n\t\t\t\tif(record_peer_video)\n\t\t\t\t\tjanus_recorder_resume(session->vrc_peer);\n\t\t\t} else {\n\t\t\t\t/* Stop recording something: notice that this never returns an error, even when we were not recording anything */\n\t\t\t\tjanus_sip_recorder_close(session, record_audio, record_peer_audio, record_video, record_peer_video);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rec_mutex);\n\t\t\t/* Notify the result */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"recordingupdated\"));\n\t\t} else if(!strcasecmp(request_text, \"info\")) {\n\t\t\t/* Send a SIP INFO request: we'll need the payload type and content */\n\t\t\tif(!janus_sip_call_is_established(session)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not established? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, info_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tconst char *info_type = json_string_value(json_object_get(root, \"type\"));\n\t\t\tconst char *info_content = json_string_value(json_object_get(root, \"content\"));\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\tnua_info(session->stack->s_nh_i,\n\t\t\t\tSIPTAG_CONTENT_TYPE_STR(info_type),\n\t\t\t\tSIPTAG_PAYLOAD_STR(info_content),\n\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\tTAG_END());\n\t\t\t/* Notify the operation */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"infosent\"));\n\t\t} else if(!strcasecmp(request_text, \"send_ringing\")) {\n\t\t\tif(session->status != janus_sip_call_status_invited && session->status != janus_sip_call_status_progress) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not invited or progress? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->stack->s_nh_i)\n\t\t\t\tnua_respond(session->stack->s_nh_i, 180, sip_status_phrase(180), TAG_END());\n\t\t} else if(!strcasecmp(request_text, \"message\")) {\n\t\t\t/* Send a SIP MESSAGE request: we'll only need the content and optional payload type */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, sipmessage_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tgboolean in_dialog_message = TRUE;\n\t\t\tjson_t *uri = json_object_get(root, \"uri\");\n\t\t\tconst char *uri_text = json_string_value(uri);\n\t\t\tif(uri != NULL)\n\t\t\t\tin_dialog_message = FALSE;\n\n\t\t\tif(in_dialog_message) {\n\t\t\t\tif(!(session->status == janus_sip_call_status_inviting || janus_sip_call_is_established(session))) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not established? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\tif(session->callee == NULL) {\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t} else {\n\t\t\t\tif(session->account.registration_status != janus_sip_registration_status_registered &&\n\t\t\t\t   session->account.registration_status != janus_sip_registration_status_disabled) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not registered)\\n\");\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not registered)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_sip_uri_t target_uri;\n\t\t\t\tif(janus_sip_parse_uri(&target_uri, uri_text) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid user address %s\\n\", uri_text);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ADDRESS;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid user address %s\\n\", uri_text);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst char *content_type = \"text/plain\";\n\t\t\tjson_t *content_type_text = json_object_get(root, \"content_type\");\n\t\t\tif(content_type_text && json_is_string(content_type_text))\n\t\t\t\tcontent_type = json_string_value(content_type_text);\n\n\t\t\tconst char *msg_content = json_string_value(json_object_get(root, \"content\"));\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\n\t\t\tchar *message_callid = NULL;\n\t\t\tif(in_dialog_message) {\n\t\t\t\t/* Take Call-ID, later used to report delivery status */\n\t\t\t\tmessage_callid = g_strdup(session->callid) ;\n\t\t\t\tnua_message(session->stack->s_nh_i,\n\t\t\t\t\tSIPTAG_CONTENT_TYPE_STR(content_type),\n\t\t\t\t\tSIPTAG_PAYLOAD_STR(msg_content),\n\t\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\t\tTAG_END());\n\t\t\t} else {\n\t\t\t\t/* Get appropriate handle */\n\t\t\t\tnua_handle_t *nh = NULL;\n\t\t\t\tif(!session->helper) {\n\t\t\t\t\tjanus_mutex_lock(&session->stack->smutex);\n\t\t\t\t\tif(session->stack->s_nua == NULL) {\n\t\t\t\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA destroyed while sending message?\\n\");\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tnh = nua_handle(session->stack->s_nua, session, TAG_END());\n\t\t\t\t\tjanus_mutex_unlock(&session->stack->smutex);\n\t\t\t\t} else {\n\t\t\t\t\t/* This is a helper, we need to use the master's SIP stack */\n\t\t\t\t\tif(session->master == NULL || session->master->stack == NULL) {\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_HELPER_ERROR;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid master SIP stack\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&session->master->stack->smutex);\n\t\t\t\t\tif(session->master->stack->s_nua == NULL) {\n\t\t\t\t\t\tjanus_mutex_unlock(&session->master->stack->smutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"NUA destroyed while sending message?\\n\");\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid NUA\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tnh = nua_handle(session->master->stack->s_nua, session, TAG_END());\n\t\t\t\t\tjanus_mutex_unlock(&session->master->stack->smutex);\n\t\t\t\t}\n\t\t\t\tjson_t *request_callid = json_object_get(root, \"call_id\");\n\t\t\t\t/* Use call-id from the request, if it exists */\n\t\t\t\tif(request_callid) {\n\t\t\t\t\tmessage_callid = g_strdup(json_string_value(request_callid));\n\t\t\t\t\tif(message_callid == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid call_id provided, generating a random one\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(message_callid == NULL) {\n\t\t\t\t\t/* If call-id does not exist in request, create a random one */\n\t\t\t\t\tmessage_callid = g_malloc0(24);\n\t\t\t\t\tjanus_sip_random_string(24, message_callid);\n\t\t\t\t}\n\t\t\t\tnua_message(nh,\n\t\t\t\t\tSIPTAG_TO_STR(uri_text),\n\t\t\t\t\tSIPTAG_CONTENT_TYPE_STR(content_type),\n\t\t\t\t\tSIPTAG_PAYLOAD_STR(msg_content),\n\t\t\t\t\tNUTAG_PROXY(session->helper && session->master ?\n\t\t\t\t\t\tsession->master->account.outbound_proxy : session->account.outbound_proxy),\n\t\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\t\tSIPTAG_CALL_ID_STR(message_callid),\n\t\t\t\t\tTAG_END());\n\t\t\t}\n\t\t\t/* Notify the application */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"messagesent\"));\n\t\t\tjson_object_set_new(result, \"call_id\", json_string(message_callid));\n\t\t\t/* Store message id and session */\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\tg_hash_table_insert(messageids, g_strdup(message_callid), session);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tg_free(message_callid);\n\t\t} else if(!strcasecmp(request_text, \"dtmf_info\")) {\n\t\t\t/* Send DMTF tones using SIP INFO\n\t\t\t * (https://tools.ietf.org/html/draft-kaplan-dispatch-info-dtmf-package-00)\n\t\t\t */\n\t\t\tif(!janus_sip_call_is_established(session)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not established? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, dtmf_info_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *digit = json_object_get(root, \"digit\");\n\t\t\tconst char *digit_text = json_string_value(digit);\n\t\t\tif(strlen(digit_text) != 1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (digit should be one character))\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (digit should be one character)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tint duration_ms = 0;\n\t\t\tjson_t *duration = json_object_get(root, \"duration\");\n\t\t\tduration_ms = duration ? json_integer_value(duration) : 0;\n\t\t\tif(duration_ms <= 0 || duration_ms > 5000) {\n\t\t\t\tduration_ms = 160; /* default value */\n\t\t\t}\n\t\t\tchar payload[64];\n\t\t\tg_snprintf(payload, sizeof(payload), \"Signal=%s\\r\\nDuration=%d\", digit_text, duration_ms);\n\t\t\tchar custom_headers[2048];\n\t\t\tjanus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));\n\t\t\tnua_info(session->stack->s_nh_i,\n\t\t\t\tSIPTAG_CONTENT_TYPE_STR(\"application/dtmf-relay\"),\n\t\t\t\tSIPTAG_PAYLOAD_STR(payload),\n\t\t\t\tTAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),\n\t\t\t\tTAG_END());\n\t\t\t/* Notify the result */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"dtmfsent\"));\n\t\t} else if(!strcasecmp(request_text, \"keyframe\")) {\n\t\t\t/* Programmatically send a keyframe request via RTCP PLI to\n\t\t\t * either the WebRTC user, the SIP peer, or both of them */\n\t\t\tif(!janus_sip_call_is_established(session)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (not established? status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (not in a call?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tif(session->callee == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Wrong state (no callee?)\\n\");\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Wrong state (no callee?)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, keyframe_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tgboolean user = json_is_true(json_object_get(root, \"user\"));\n\t\t\tgboolean peer = json_is_true(json_object_get(root, \"peer\"));\n\t\t\tif(user) {\n\t\t\t\t/* Send a PLI to the WebRTC user */\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t\tif(peer) {\n\t\t\t\t/* Send a PLI to the SIP peer (but only if they negotiated it) */\n\t\t\t\tif(session->media.video_pli_supported)\n\t\t\t\t\tjanus_sip_rtcp_pli_send(session);\n\t\t\t}\n\t\t\t/* Notify the result */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"keyframesent\"));\n\t\t} else if(!strcasecmp(request_text, \"rtp_forward\")) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_forward_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tif(!janus_sip_call_is_established(session)) {\n\t\t\t\terror_code = JANUS_SIP_ERROR_WRONG_STATE;\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Wrong state (not in a call)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Iterate on the provided streams array */\n\t\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\t\tif(streams == NULL || json_array_size(streams) == 0) {\n\t\t\t\terror_code = JANUS_SIP_ERROR_MISSING_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Missing mandatory element streams, or empty array\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* Iterate on the streams objects and validate them all */\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, rtp_forward_stream_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tgoto error;\n\t\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\t\tif(strcasecmp(type, \"audio\") && strcasecmp(type, \"video\") &&\n\t\t\t\t\t\tstrcasecmp(type, \"peer_audio\") && strcasecmp(type, \"peer_video\")) {\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Invalid element (type)\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Make sure we have a host attribute, either global or stream-specific */\n\t\t\t\tjson_t *stream_host = json_object_get(s, \"host\");\n\t\t\t\tconst char *s_host = json_string_value(stream_host), *resolved_host = NULL;\n\t\t\t\tjson_t *stream_host_family = json_object_get(s, \"host_family\");\n\t\t\t\tconst char *s_host_family = json_string_value(stream_host_family);\n\t\t\t\tint s_family = AF_INET;\n\t\t\t\tif(s_host_family) {\n\t\t\t\t\tif(!strcasecmp(s_host_family, \"ipv4\")) {\n\t\t\t\t\t\ts_family = AF_INET;\n\t\t\t\t\t} else if(!strcasecmp(s_host_family, \"ipv6\")) {\n\t\t\t\t\t\ts_family = AF_INET6;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported protocol family (%s)\\n\", s_host_family);\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported protocol family (%s)\", s_host_family);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstruct addrinfo *res = NULL, *start = NULL;\n\t\t\t\tjanus_network_address addr;\n\t\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\t\tstruct addrinfo hints;\n\t\t\t\tmemset(&hints, 0, sizeof(hints));\n\t\t\t\tif(s_family != 0)\n\t\t\t\t\thints.ai_family = s_family;\n\t\t\t\tif(getaddrinfo(s_host, NULL, s_family != 0 ? &hints : NULL, &res) == 0) {\n\t\t\t\t\tstart = res;\n\t\t\t\t\twhile(res != NULL) {\n\t\t\t\t\t\tif(janus_network_address_from_sockaddr(res->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t\t\t/* Resolved */\n\t\t\t\t\t\t\tresolved_host = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\t\t\tstart = NULL;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tres = res->ai_next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(resolved_host == NULL) {\n\t\t\t\t\tif(start)\n\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s)...\\n\", s_host);\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not resolve address (%s)...\", s_host);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Add the resolved address to the JSON object, so that we can use it later */\n\t\t\t\tjson_object_set_new(s, \"host\", json_string(resolved_host));\n\t\t\t\t/* We may need to SRTP-encrypt this stream */\n\t\t\t\tint srtp_suite = 0;\n\t\t\t\tconst char *srtp_crypto = NULL;\n\t\t\t\tjson_t *s_suite = json_object_get(s, \"srtp_suite\");\n\t\t\t\tjson_t *s_crypto = json_object_get(s, \"srtp_crypto\");\n\t\t\t\tif(s_suite && s_crypto) {\n\t\t\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\t\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP suite (%d)\\n\", srtp_suite);\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP suite (%d)\", srtp_suite);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t\t\t\tif(srtp_crypto == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP crypto\\n\");\n\t\t\t\t\t\terror_code = JANUS_SIP_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP crypto\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_refcount_increase(&session->ref);\t/* This is just to handle the request for now */\n\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\tif(session->udp_sock <= 0) {\n\t\t\t\tsession->udp_sock = socket(!ipv6_disabled ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\t\tint v6only = 0;\n\t\t\t\tif(session->udp_sock <= 0 ||\n\t\t\t\t\t\t(!ipv6_disabled && setsockopt(session->udp_sock, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)) {\n\t\t\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not open UDP socket for RTP forwarder, %d (%s)\\n\",\n\t\t\t\t\t\terrno, g_strerror(errno));\n\t\t\t\t\terror_code = JANUS_SIP_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not open UDP socket for RTP forwarder\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Iterate on all objects, and create the related forwarder(s) */\n\t\t\tresult = json_object();\n\t\t\tjson_t *new_forwarders = json_array();\n\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\t\tjson_t *stream_host = json_object_get(s, \"host\");\n\t\t\t\tconst char *host = json_string_value(stream_host);\n\t\t\t\tjson_t *stream_port = json_object_get(s, \"port\");\n\t\t\t\tuint16_t port = json_integer_value(stream_port);\n\t\t\t\tjson_t *stream_pt = json_object_get(s, \"pt\");\n\t\t\t\tjson_t *stream_ssrc = json_object_get(s, \"ssrc\");\n\t\t\t\tint srtp_suite = 0;\n\t\t\t\tconst char *srtp_crypto = NULL;\n\t\t\t\tjson_t *s_suite = json_object_get(s, \"srtp_suite\");\n\t\t\t\tjson_t *s_crypto = json_object_get(s, \"srtp_crypto\");\n\t\t\t\tif(s_suite && s_crypto) {\n\t\t\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t\t\t}\n\t\t\t\t/* Create the forwarder */\n\t\t\t\tjanus_rtp_forwarder *f = janus_sip_rtp_forwarder_add_helper(session, type,\n\t\t\t\t\thost, port, json_integer_value(stream_pt), json_integer_value(stream_ssrc), srtp_suite, srtp_crypto);\n\t\t\t\tif(f) {\n\t\t\t\t\tjson_t *rtpf = janus_sip_rtp_forwarder_summary(f);\n\t\t\t\t\tjson_array_append_new(new_forwarders, rtpf);\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = janus_sip_rtp_forwarder_summary(f);\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(type));\n\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, NULL, info);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tif(new_forwarders != NULL)\n\t\t\t\tjson_object_set_new(result, \"forwarders\", new_forwarders);\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"rtp_forward\"));\n\t\t} else if(!strcasecmp(request_text, \"stop_rtp_forward\")) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, stop_rtp_forward_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_SIP_ERROR_MISSING_ELEMENT, JANUS_SIP_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *id = json_object_get(root, \"stream_id\");\n\t\t\tguint32 stream_id = json_integer_value(id);\n\t\t\tjanus_refcount_increase(&session->ref);\t/* Just to handle the message now */\n\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\t/* Find the forwarder by iterating on all the streams */\n\t\t\tgboolean found = g_hash_table_remove(session->audio_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(!found)\n\t\t\t\tfound = g_hash_table_remove(session->video_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(!found)\n\t\t\t\tfound = g_hash_table_remove(session->peer_audio_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(!found)\n\t\t\t\tfound = g_hash_table_remove(session->peer_video_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(found)\n\t\t\t\tg_hash_table_remove(session->all_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tif(!found) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No such stream (%\"SCNu32\")\\n\", stream_id);\n\t\t\t\terror_code = JANUS_SIP_ERROR_NO_SUCH_STREAM;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such stream (%\"SCNu32\")\", stream_id);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"stop_rtp_forward\"));\n\t\t\tjson_object_set_new(result, \"stream_id\", json_integer(stream_id));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"stop_rtp_forward\"));\n\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(stream_id));\n\t\t\t\tgateway->notify_event(&janus_sip_plugin, NULL, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"listforwarders\")) {\n\t\t\t/* Return a list of all forwarders for this call */\n\t\t\tjson_t *list = json_array();\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\tg_hash_table_iter_init(&iter, session->all_forwarders);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_rtp_forwarder *rf = (janus_rtp_forwarder *)value;\n\t\t\t\tjson_t *fl = janus_sip_rtp_forwarder_summary(rf);\n\t\t\t\tjson_array_append_new(list, fl);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"forwarders\"));\n\t\t\tjson_object_set_new(result, \"rtp_forwarders\", list);\n\t\t} else if(!strcasecmp(request_text, \"reset\")) {\n\t\t\t/* Apparently, under some particular circumstances that we haven't\n\t\t\t * managed to replicate ourselves yet, it can sometimes happen that\n\t\t\t * a janus_sip_session remains with the establishing atomic set to\n\t\t\t * 1, even though the last call has been correctly closed. This\n\t\t\t * prevents further incoming calls to be established, as the\n\t\t\t * plugin automatically answers with a 486 busy thinking another\n\t\t\t * call is currently in progress. This \"reset\" request is here\n\t\t\t * to reset the establishing flag back to 0, in case the call\n\t\t\t * state is idle but the flag is still set to 1 instead */\n\t\t\tif(session->status == janus_sip_call_status_idle) {\n\t\t\t\tg_atomic_int_set(&session->established, 0);\n\t\t\t\tg_atomic_int_set(&session->establishing, 0);\n\t\t\t}\n\t\t\t/* Notify the result */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"reset\"));\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unknown request (%s)\\n\", request_text);\n\t\t\terror_code = JANUS_SIP_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown request (%s)\", request_text);\n\t\t\tgoto error;\n\t\t}\n\ndone:\n\t\t{\n\t\t\t/* Prepare JSON event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"sip\", json_string(\"event\"));\n\t\t\tif(result != NULL)\n\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\tjson_object_set_new(event, \"call_id\", json_string(session->callid));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_sip_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_sip_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"sip\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tjson_object_set_new(event, \"call_id\", json_string(session->callid));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_sip_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_sip_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving SIP handler thread\\n\");\n\treturn NULL;\n}\n\n\n/* Sofia callbacks */\nvoid janus_sip_sofia_callback(nua_event_t event, int status, char const *phrase, nua_t *nua, nua_magic_t *magic, nua_handle_t *nh, nua_hmagic_t *hmagic, sip_t const *sip, tagi_t tags[])\n{\n\tjanus_sip_session *session = (janus_sip_session *)(hmagic ? hmagic : magic);\n\tssip_t *ssip = session->stack;\n\n\t/* Notify event handlers about the content of the whole incoming SIP message, if any */\n\tif(notify_events && gateway->events_is_enabled() && ssip) {\n\t\t/* Print the incoming message */\n\t\tsize_t msg_size = 0;\n\t\tmsg_t *msg = nua_current_request(nua);\n\t\tif(msg) {\n\t\t\tchar *msg_str = msg_as_string(ssip->s_home, msg, NULL, 0, &msg_size);\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"sip-in\"));\n\t\t\tjson_object_set_new(info, \"sip\", json_string(msg_str));\n\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\tsu_free(ssip->s_home, msg_str);\n\t\t}\n\t}\n\n\tswitch (event) {\n\t/* Status or Error Indications */\n\t\tcase nua_i_active:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_i_error:\n\t\t\tJANUS_LOG(LOG_WARN, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_i_fork:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_i_media_error:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_i_subscription:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_i_state:;\n\t\t\ttagi_t const *ti = tl_find(tags, nutag_callstate);\n\t\t\tenum nua_callstate callstate = ti ? ti->t_value : nua_callstate_init;\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s, call state [%s]\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\", nua_callstate_name(callstate));\n\t\t\t/* There are several call states, but we care about the terminated state in order to send the 'hangup' event\n\t\t\t * and the proceeding state in order to send the 'proceeding' event so the client can play a ringback tone for\n\t\t\t * the user since we don't send early media. (assuming this is the right session, of course).\n\t\t\t * http://sofia-sip.sourceforge.net/refdocs/nua/nua__tag_8h.html#a516dc237722dc8ca4f4aa3524b2b444b\n\t\t\t */\n\t\t\tif(callstate == nua_callstate_proceeding &&\n\t\t\t\t\t(session->stack->s_nh_i == nh || session->stack->s_nh_i == NULL)) {\n\t\t\t\tjson_t *call = json_object();\n\t\t\t\tjson_object_set_new(call, \"sip\", json_string(\"event\"));\n\t\t\t\tjson_t *calling = json_object();\n\t\t\t\tjson_object_set_new(calling, \"event\", json_string(\"proceeding\"));\n\t\t\t\tjson_object_set_new(calling, \"code\", json_integer(status));\n\t\t\t\tjson_object_set_new(call, \"result\", calling);\n\t\t\t\tjson_object_set_new(call, \"call_id\", json_string(session->callid));\n\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, call, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(call);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"proceeding\"));\n\t\t\t\t\tif(session->callid)\n\t\t\t\t\t\tjson_object_set_new(info, \"call-id\", json_string(session->callid));\n\t\t\t\t\tjson_object_set_new(info, \"code\", json_integer(status));\n\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t} else if(callstate == nua_callstate_terminated &&\n\t\t\t\t\t(session->stack->s_nh_i == nh || session->stack->s_nh_i == NULL)) {\n\t\t\t\tsession->media.earlymedia = FALSE;\n\t\t\t\tsession->media.update = FALSE;\n\t\t\t\tsession->media.autoaccept_reinvites = TRUE;\n\t\t\t\tsession->media.ready = FALSE;\n\t\t\t\tsession->media.on_hold = FALSE;\n\t\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_idle);\n\t\t\t\tsession->stack->s_nh_i = NULL;\n\t\t\t\tjson_t *call = json_object();\n\t\t\t\tjson_object_set_new(call, \"sip\", json_string(\"event\"));\n\t\t\t\tjson_t *calling = json_object();\n\t\t\t\tjson_object_set_new(calling, \"event\", json_string(\"hangup\"));\n\t\t\t\tjson_object_set_new(calling, \"code\", json_integer(status));\n\t\t\t\tjson_object_set_new(calling, \"reason\", json_string(phrase ? phrase : \"\"));\n\t\t\t\tif(session->hangup_reason_header)\n\t\t\t\t\tjson_object_set_new(calling, \"reason_header\", json_string(session->hangup_reason_header));\n\t\t\t\tif(session->hangup_reason_header_protocol)\n\t\t\t\t\tjson_object_set_new(calling, \"reason_header_protocol\", json_string(session->hangup_reason_header_protocol));\n\t\t\t\tif(session->hangup_reason_header_cause)\n\t\t\t\t\tjson_object_set_new(calling, \"reason_header_cause\", json_string(session->hangup_reason_header_cause));\n\t\t\t\tif(session->hangup_custom_headers) {\n\t\t\t\t\tjson_t *custom_headers_copy = json_deep_copy(session->hangup_custom_headers);\n\t\t\t\t\tjson_object_set_new(calling, \"headers\", custom_headers_copy);\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(call, \"result\", calling);\n\t\t\t\tjson_object_set_new(call, \"call_id\", json_string(session->callid));\n\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, call, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(call);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"hangup\"));\n\t\t\t\t\tif(session->callid)\n\t\t\t\t\t\tjson_object_set_new(info, \"call-id\", json_string(session->callid));\n\t\t\t\t\tjson_object_set_new(info, \"code\", json_integer(status));\n\t\t\t\t\tif(phrase)\n\t\t\t\t\t\tjson_object_set_new(info, \"reason\", json_string(phrase));\n\t\t\t\t\tif(session->hangup_reason_header)\n\t\t\t\t\t\tjson_object_set_new(info, \"reason_header\", json_string(session->hangup_reason_header));\n\t\t\t\t\tif(session->hangup_reason_header_protocol)\n\t\t\t\t\t\tjson_object_set_new(info, \"reason_header_protocol\", json_string(session->hangup_reason_header_protocol));\n\t\t\t\t\tif(session->hangup_reason_header_cause)\n\t\t\t\t\t\tjson_object_set_new(info, \"reason_header_cause\", json_string(session->hangup_reason_header_cause));\n\t\t\t\t\tif(session->hangup_custom_headers) {\n\t\t\t\t\t\tjson_t *custom_headers_notify_copy = json_deep_copy(session->hangup_custom_headers);\n\t\t\t\t\t\tjson_object_set_new(info, \"headers\", custom_headers_notify_copy);\n\t\t\t\t\t}\n\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\t/* Get rid of any PeerConnection that may have been set up */\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tif(session->callid) {\n\t\t\t\t\tjanus_sip_call *call = g_hash_table_lookup(callids, session->callid);\n\t\t\t\t\tif(call) {\n\t\t\t\t\t\tif(call->caller == session)\n\t\t\t\t\t\t\tcall->caller = NULL;\n\t\t\t\t\t\telse if(call->callee == session)\n\t\t\t\t\t\t\tcall->callee = NULL;\n\t\t\t\t\t\tif(call->caller == NULL && call->callee == NULL)\n\t\t\t\t\t\t\tg_hash_table_remove(callids, session->callid);\n\t\t\t\t\t}\n\t\t\t\t\tg_free(session->callid);\n\t\t\t\t\tsession->callid = NULL;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tg_free(session->transaction);\n\t\t\t\tsession->transaction = NULL;\n\t\t\t\tg_free(session->hangup_reason_header);\n\t\t\t\tg_free(session->hangup_reason_header_protocol);\n\t\t\t\tg_free(session->hangup_reason_header_cause);\n\t\t\t\tsession->hangup_reason_header = NULL;\n\t\t\t\tsession->hangup_reason_header_protocol = NULL;\n\t\t\t\tsession->hangup_reason_header_cause = NULL;\n\t\t\t\tif(session->hangup_custom_headers) {\n\t\t\t\t\tjson_decref(session->hangup_custom_headers);\n\t\t\t\t\tsession->hangup_custom_headers = NULL;\n\t\t\t\t}\n\t\t\t\tif(g_atomic_int_get(&session->establishing) || g_atomic_int_get(&session->established)) {\n\t\t\t\t\t/* Get rid of the PeerConnection in the core */\n\t\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\t\t/* Also clean up locally, in case there was no PC */\n\t\t\t\t\tjanus_sip_hangup_media_internal(session->handle);\n\t\t\t\t}\n\t\t\t} else if(session->stack->s_nh_i == nh && callstate == nua_callstate_calling && session->status == janus_sip_call_status_incall) {\n\t\t\t\t/* Have just sent re-INVITE */\n\t\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_incall_reinviting);\n\t\t\t} else if(session->stack->s_nh_i == nh && callstate == nua_callstate_ready &&\n\t\t\t\t\t(session->status == janus_sip_call_status_incall_reinviting || session->status == janus_sip_call_status_incall_reinvited)) {\n\t\t\t\t/* Clear re-INVITE progress status */\n\t\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_incall);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase nua_i_terminated: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We had a reference to this session for this call, get rid of it */\n\t\t\tjanus_sip_unref_active_call(session);\n\t\t\tbreak;\n\t\t}\n\t/* SIP requests */\n\t\tcase nua_i_ack: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We're only interested in this when there's been an offerless INVITE, as here's where we'd get our answer */\n\t\t\tif(sip->sip_payload && sip->sip_payload->pl_data) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"This ACK contains a payload, probably as a result of an offerless INVITE: simulating 200 OK...\\n\");\n\t\t\t\tjanus_sip_sofia_callback(nua_r_invite, 700, \"ACK\", nua, magic, nh, hmagic, sip, tags);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_outbound:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_i_bye: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tjanus_sip_save_reason(sip, session);\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_cancel: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tjanus_sip_save_reason(sip, session);\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_invite: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* Add a reference for this call */\n\t\t\tjanus_sip_ref_active_call(session);\n\t\t\tif(ssip == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tInvalid SIP stack\\n\");\n\t\t\t\tnua_respond(nh, 500, sip_status_phrase(500), TAG_END());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(sip->sip_from == NULL || sip->sip_to == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tInvalid request (missing From or To)\\n\");\n\t\t\t\tnua_respond(nh, 400, sip_status_phrase(400), TAG_END());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tgboolean reinvite = FALSE, busy = FALSE;\n\t\t\tif(session->stack->s_nh_i == NULL) {\n\t\t\t\tif(g_atomic_int_get(&session->establishing) || g_atomic_int_get(&session->established) || session->relayer_thread != NULL) {\n\t\t\t\t\t/* Still busy establishing another call (or maybe still cleaning up the previous call) */\n\t\t\t\t\tbusy = TRUE;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif(session->stack->s_nh_i == nh) {\n\t\t\t\t\t/* re-INVITE, we'll check what changed later */\n\t\t\t\t\treinvite = TRUE;\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Got a re-INVITE...\\n\");\n\t\t\t\t} else if(session->status >= janus_sip_call_status_inviting) {\n\t\t\t\t\t/* Busy with another call */\n\t\t\t\t\tbusy = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(busy) {\n\t\t\t\t/* This session is busy, any helper that can take it? */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Busy... maybe a helper can help?\\n\");\n\t\t\t\tjanus_sip_session *helper = NULL;\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t/* Find a free helper */\n\t\t\t\tGList *temp = session->helpers;\n\t\t\t\twhile(temp != NULL) {\n\t\t\t\t\thelper = (janus_sip_session *)temp->data;\n\t\t\t\t\tif(helper->stack->s_nh_i == NULL && !g_atomic_int_get(&helper->establishing) &&\n\t\t\t\t\t\t\t!g_atomic_int_get(&helper->established) && helper->relayer_thread == NULL) {\n\t\t\t\t\t\t/* Found! */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Helper %p is busy too...\\n\", helper);\n\t\t\t\t\thelper = NULL;\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tif(helper != NULL) {\n\t\t\t\t\t/* Bind the call to the helper and handle it there */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Passing INVITE to helper %p\\n\", helper);\n\t\t\t\t\tnua_handle_bind(nh, helper);\n\t\t\t\t\t/* This session won't need the reference anymore, the helper will */\n\t\t\t\t\tjanus_sip_unref_active_call(session);\n\t\t\t\t\tjanus_sip_sofia_callback(event, status, phrase, nua, magic, nh, helper, sip, tags);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\tAlready in a call (busy, status=%s)\\n\", janus_sip_call_status_string(session->status));\n\t\t\t\tnua_respond(nh, 486, sip_status_phrase(486), TAG_END());\n\t\t\t\t/* Notify the web app about the missed invite */\n\t\t\t\tjson_t *missed = json_object();\n\t\t\t\tjson_object_set_new(missed, \"sip\", json_string(\"event\"));\n\t\t\t\tjson_t *result = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"missed_call\"));\n\t\t\t\tchar *caller_text = url_as_string(session->stack->s_home, sip->sip_from->a_url);\n\t\t\t\tjson_object_set_new(result, \"caller\", json_string(caller_text));\n\t\t\t\tif(sip->sip_from->a_display) {\n\t\t\t\t\tjson_object_set_new(result, \"displayname\", json_string(sip->sip_from->a_display));\n\t\t\t\t}\n\t\t\t\tchar *callee_text = url_as_string(session->stack->s_home, sip->sip_to->a_url);\n\t\t\t\tjson_object_set_new(result, \"callee\", json_string(callee_text));\n\t\t\t\tjson_object_set_new(missed, \"result\", result);\n\t\t\t\tjson_object_set_new(missed, \"call_id\", json_string(sip->sip_call_id->i_id));\n\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, missed, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(missed);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"missed_call\"));\n\t\t\t\t\tjson_object_set_new(info, \"caller\", json_string(caller_text));\n\t\t\t\t\tjson_object_set_new(info, \"callee\", json_string(callee_text));\n\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\tsu_free(session->stack->s_home, caller_text);\n\t\t\t\tsu_free(session->stack->s_home, callee_text);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(!reinvite) {\n\t\t\t\tg_atomic_int_set(&session->establishing, 1);\n\t\t\t} else {\n\t\t\t\t/* This is a re-INVITE, we have a reference already */\n\t\t\t\tjanus_sip_unref_active_call(session);\n\t\t\t}\n\t\t\t/* Check if there's an SDP to process */\n\t\t\tjanus_sdp *sdp = NULL;\n\t\t\tif(!sip->sip_payload) {\n\t\t\t\tJANUS_LOG(LOG_VERB,\"Received offerless %s\\n\", reinvite ? \"re-INVITE\" : \"INVITE\");\n\t\t\t} else {\n\t\t\t\tchar sdperror[100];\n\t\t\t\tsdp = janus_sdp_parse(sip->sip_payload->pl_data, sdperror, sizeof(sdperror));\n\t\t\t\tif(!sdp) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tError parsing SDP! %s\\n\", sdperror);\n\t\t\t\t\tg_atomic_int_set(&session->establishing, 0);\n\t\t\t\t\tnua_respond(nh, 488, sip_status_phrase(488), TAG_END());\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!reinvite) {\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t/* New incoming call */\n\t\t\t\tg_free(session->callee);\n\t\t\t\tchar *caller_text = url_as_string(session->stack->s_home, sip->sip_from->a_url);\n\t\t\t\tsession->callee = g_strdup(caller_text);\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tsu_free(session->stack->s_home, caller_text);\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tg_free(session->callid);\n\t\t\t\tsession->callid = sip && sip->sip_call_id ? g_strdup(sip->sip_call_id->i_id) : NULL;\n\t\t\t\tif(session->callid) {\n\t\t\t\t\t/* Track this call-id and tag as a callee */\n\t\t\t\t\tjanus_sip_call *call = g_hash_table_lookup(callids, session->callid);\n\t\t\t\t\tif(call) {\n\t\t\t\t\t\t/* The caller is in this Janus instance too, update the mapping */\n\t\t\t\t\t\tcall->callee = session;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* External caller, create a new mapping */\n\t\t\t\t\t\tcall = g_malloc0(sizeof(janus_sip_call));\n\t\t\t\t\t\tcall->callee = session;\n\t\t\t\t\t\tg_hash_table_insert(callids, g_strdup(session->callid), call);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_invited);\n\t\t\t\t/* Clean up SRTP stuff from before first, in case it's still needed */\n\t\t\t\tjanus_sip_srtp_cleanup(session);\n\t\t\t}\n\t\t\t/* Parse SDP */\n\t\t\tJANUS_LOG(LOG_VERB, \"Someone is %s a call:\\n%s\",\n\t\t\t\treinvite ? \"updating\" : \"inviting us in\",\n\t\t\t\tsip->sip_payload ? sip->sip_payload->pl_data : \"(no SDP)\");\n\t\t\tgboolean changed = FALSE;\n\t\t\tif(sdp) {\n\t\t\t\tjanus_sip_sdp_process(session, sdp, FALSE, reinvite, &changed);\n\t\t\t\t/* Check if offer has neither audio nor video, fail with 488 */\n\t\t\t\tif(!session->media.has_audio && !session->media.has_video) {\n\t\t\t\t\tg_atomic_int_set(&session->establishing, 0);\n\t\t\t\t\tnua_respond(nh, 488, sip_status_phrase(488), TAG_END());\n\t\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* Also fail with 488 if there's no remote IP addresses that can be used for RTP */\n\t\t\t\tif(!session->media.remote_audio_ip && !session->media.remote_video_ip) {\n\t\t\t\t\tg_atomic_int_set(&session->establishing, 0);\n\t\t\t\t\tnua_respond(nh, 488, sip_status_phrase(488), TAG_END());\n\t\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(reinvite && session->media.autoaccept_reinvites) {\n\t\t\t\t/* No need to involve the application: we reply ourselves */\n\t\t\t\tnua_respond(nh, 200, sip_status_phrase(200), TAG_END());\n\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t/* Check if there's an isfocus feature parameter in the Contact header */\n\t\t\tgboolean is_focus = FALSE;\n\t\t\tif(sip->sip_contact && sip->sip_contact->m_params) {\n\t\t\t\tint i=0;\n\t\t\t\tfor(i=0; sip->sip_contact->m_params[i]; i++) {\n\t\t\t\t\tif(!strcasecmp(sip->sip_contact->m_params[i], \"isfocus\")) {\n\t\t\t\t\t\t/* The peer is a conference bridge */\n\t\t\t\t\t\tis_focus = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* If this is a re-INVITE, take note of that */\n\t\t\tif(reinvite) {\n\t\t\t\tsession->media.update = TRUE;\n\t\t\t\t/* Mark status as janus_sip_call_status_incall_reinvited only when handling reinvites ourselves*/\n\t\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_incall_reinvited);\n\t\t\t}\n\n\t\t\t/* Notify the application about the new incoming call or re-INVITE */\n\t\t\tjson_t *jsep = NULL;\n\t\t\tif(sdp)\n\t\t\t\tjsep = json_pack(\"{ssss}\", \"type\", \"offer\", \"sdp\", sip->sip_payload->pl_data);\n\t\t\tjson_t *call = json_object();\n\t\t\tjson_object_set_new(call, \"sip\", json_string(\"event\"));\n\t\t\tjson_t *calling = json_object();\n\t\t\tjson_object_set_new(calling, \"event\", json_string(reinvite ? \"updatingcall\" : \"incomingcall\"));\n\t\t\tjson_object_set_new(calling, \"username\", json_string(session->callee));\n\t\t\tif(session->callid)\n\t\t\t\tjson_object_set_new(calling, \"call_id\", json_string(session->callid));\n\t\t\tif(sip->sip_from->a_display) {\n\t\t\t\tjson_object_set_new(calling, \"displayname\", json_string(sip->sip_from->a_display));\n\t\t\t}\n\t\t\tchar *callee_text = url_as_string(session->stack->s_home, sip->sip_to->a_url);\n\t\t\tjson_object_set_new(calling, \"callee\", json_string(callee_text));\n\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\tjson_object_set_new(calling, \"headers\", headers);\n\t\t\t}\n\t\t\tchar *referred_by = NULL;\n\t\t\tif(sip->sip_referred_by) {\n\t\t\t\tchar *rby_text = sip_header_as_string(session->stack->s_home, (const sip_header_t *)sip->sip_referred_by);\n\t\t\t\treferred_by = g_strdup(rby_text);\n\t\t\t\tsu_free(session->stack->s_home, rby_text);\n\t\t\t\tjson_object_set_new(calling, \"referred_by\", json_string(referred_by));\n\t\t\t}\n\t\t\tif(sip->sip_replaces && sip->sip_replaces->rp_call_id) {\n\t\t\t\tjson_object_set_new(calling, \"replaces\", json_string(sip->sip_replaces->rp_call_id));\n\t\t\t}\n\t\t\tif(is_focus)\n\t\t\t\tjson_object_set_new(calling, \"isfocus\", json_true());\n\t\t\tif(sdp && (session->media.has_srtp_remote_audio || session->media.has_srtp_remote_video)) {\n\t\t\t\t/* FIXME Maybe a true/false instead? */\n\t\t\t\tjson_object_set_new(calling, \"srtp\", json_string(session->media.require_srtp ? \"sdes_mandatory\" : \"sdes_optional\"));\n\t\t\t}\n\t\t\tjson_object_set_new(call, \"result\", calling);\n\t\t\tjson_object_set_new(call, \"call_id\", json_string(session->callid));\n\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, call, jsep);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(call);\n\t\t\tif(jsep)\n\t\t\t\tjson_decref(jsep);\n\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(reinvite ? \"updatingcall\" : \"incomingcall\"));\n\t\t\t\tif(session->callid)\n\t\t\t\t\tjson_object_set_new(info, \"call-id\", json_string(session->callid));\n\t\t\t\tjson_object_set_new(info, \"username\", json_string(session->callee));\n\t\t\t\tif(sip->sip_from->a_display)\n\t\t\t\t\tjson_object_set_new(info, \"displayname\", json_string(sip->sip_from->a_display));\n\t\t\t\tjson_object_set_new(info, \"callee\", json_string(callee_text));\n\t\t\t\tif(referred_by)\n\t\t\t\t\tjson_object_set_new(info, \"referred_by\", json_string(referred_by));\n\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t}\n\t\t\tsu_free(session->stack->s_home, callee_text);\n\t\t\tg_free(referred_by);\n\t\t\tif(!reinvite) {\n\t\t\t\tif(session->account.automatic_ringing) {\n\t\t\t\t\t/* Send a Ringing back */\n\t\t\t\t\tnua_respond(nh, 180, sip_status_phrase(180), TAG_END());\n\t\t\t\t}\n\t\t\t\tsession->stack->s_nh_i = nh;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_refer: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We're being asked to transfer a call */\n\t\t\tif(sip == NULL || sip->sip_refer_to == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing Refer-To header\\n\");\n\t\t\t\tnua_respond(nh, 400, sip_status_phrase(400), TAG_END());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t/* Access the headers we need */\n\t\t\tchar *refer_to = NULL, *referred_by = NULL, *custom_headers = NULL, *replaces = NULL;\n\t\t\tconst char *url_headers = sip->sip_refer_to->r_url->url_headers;\n\t\t\tif(url_headers != NULL) {\n\t\t\t\t/* Convert to SIP headers */\n\t\t\t\tsip->sip_refer_to->r_url->url_headers = NULL;\n\t\t\t\tcustom_headers = url_query_as_header_string(session->stack->s_home, url_headers);\n\t\t\t\t/* FIXME Look for the \"replaces\" part, to extract the call-id */\n\t\t\t\tchar *start = strstr(custom_headers, \"replaces:\");\n\t\t\t\tif(start != NULL) {\n\t\t\t\t\tstart += strlen(\"replaces:\");\n\t\t\t\t\tchar *end = strchr(start, ';');\n\t\t\t\t\tif(end != NULL) {\n\t\t\t\t\t\t/* Found */\n\t\t\t\t\t\t*end = '\\0';\n\t\t\t\t\t\treplaces = g_strdup(start);\n\t\t\t\t\t\t*end = ';';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\trefer_to = url_as_string(session->stack->s_home, sip->sip_refer_to->r_url);\n\t\t\tsip->sip_refer_to->r_url->url_headers = url_headers;\n\t\t\tif(sip->sip_referred_by != NULL)\n\t\t\t\treferred_by = sip_header_as_string(session->stack->s_home, (const sip_header_t *)sip->sip_referred_by);\n\t\t\telse if(sip->sip_from != NULL)\n\t\t\t\treferred_by = url_as_string(session->stack->s_home, sip->sip_from->a_url);\n\t\t\tJANUS_LOG(LOG_VERB, \"Incoming REFER: %s (by %s, headers: %s)\\n\",\n\t\t\t\trefer_to, referred_by ? referred_by : \"unknown\", custom_headers ? custom_headers : \"unknown\");\n\t\t\t/* Send a 202 back */\n\t\t\tnua_respond(nh, 202, sip_status_phrase(202), NUTAG_WITH_CURRENT(nua), TAG_END());\n\t\t\tJANUS_LOG(LOG_VERB, \"[%p] 202\\n\", nh);\n\t\t\t/* Take note of the session and NUA handle we got the REFER from (for NOTIFY) */\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tguint32 refer_id = 0;\n\t\t\twhile(refer_id == 0) {\n\t\t\t\trefer_id = janus_random_uint32();\n\t\t\t\tif(g_hash_table_lookup(transfers, GUINT_TO_POINTER(refer_id)) != NULL) {\n\t\t\t\t\trefer_id = 0;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjanus_sip_transfer *t = g_malloc(sizeof(janus_sip_transfer));\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tt->session = session;\n\t\t\t\tt->referred_by = referred_by ? g_strdup(referred_by) : NULL;\n\t\t\t\tt->custom_headers = custom_headers ? g_strdup(custom_headers) : NULL;\n\t\t\t\tt->nh_s = nh;\n\t\t\t\tnua_save_event(nua, t->saved);\n\t\t\t\tg_hash_table_insert(transfers, GUINT_TO_POINTER(refer_id), t);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* Notify the application */\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"sip\", json_string(\"event\"));\n\t\t\tjson_t *result = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"transfer\"));\n\t\t\tjson_object_set_new(result, \"refer_id\", json_integer(refer_id));\n\t\t\tjson_object_set_new(result, \"refer_to\", json_string(refer_to));\n\t\t\tif(referred_by != NULL) {\n\t\t\t\tjson_object_set_new(result, \"referred_by\", json_string(referred_by));\n\t\t\t\tsu_free(session->stack->s_home, referred_by);\n\t\t\t}\n\t\t\tif(replaces != NULL) {\n\t\t\t\tjson_object_set_new(result, \"replaces\", json_string(replaces));\n\t\t\t\tg_free(replaces);\n\t\t\t}\n\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t}\n\t\t\tsu_free(session->stack->s_home, refer_to);\n\t\t\tif(custom_headers != NULL)\n\t\t\t\tsu_free(session->stack->s_home, custom_headers);\n\t\t\tjson_object_set_new(info, \"result\", result);\n\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, info, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(info);\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_info: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We expect a payload */\n\t\t\tif(!sip->sip_content_type || !sip->sip_content_type->c_type || !sip->sip_payload || !sip->sip_payload->pl_data) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst char *type = sip->sip_content_type->c_type;\n\t\t\tchar *payload = sip->sip_payload->pl_data;\n\t\t\t/* Notify the application */\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"sip\", json_string(\"event\"));\n\t\t\tjson_t *result = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"info\"));\n\t\t\tchar *caller_text = url_as_string(session->stack->s_home, sip->sip_from->a_url);\n\t\t\tjson_object_set_new(result, \"sender\", json_string(caller_text));\n\t\t\tsu_free(session->stack->s_home, caller_text);\n\t\t\tif(sip->sip_from && sip->sip_from->a_display && strlen(sip->sip_from->a_display) > 0) {\n\t\t\t\tjson_object_set_new(result, \"displayname\", json_string(sip->sip_from->a_display));\n\t\t\t}\n\t\t\tjson_object_set_new(result, \"type\", json_string(type));\n\t\t\tjson_object_set_new(result, \"content\", json_string(payload));\n\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t}\n\t\t\tif(session->callid)\n\t\t\t\tjson_object_set_new(info, \"call_id\", json_string(session->callid));\n\t\t\tjson_object_set_new(info, \"result\", result);\n\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, info, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(info);\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_message: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We expect a payload */\n\t\t\tif(!sip->sip_content_type || !sip->sip_content_type->c_type || !sip->sip_payload || !sip->sip_payload->pl_data) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst char *content_type = sip->sip_content_type->c_type;\n\t\t\tchar *payload = sip->sip_payload->pl_data;\n\t\t\t/* Notify the application */\n\t\t\tjson_t *message = json_object();\n\t\t\tjson_object_set_new(message, \"sip\", json_string(\"event\"));\n\t\t\tjson_t *result = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"message\"));\n\t\t\tchar *caller_text = url_as_string(session->stack->s_home, sip->sip_from->a_url);\n\t\t\tjson_object_set_new(result, \"sender\", json_string(caller_text));\n\t\t\tsu_free(session->stack->s_home, caller_text);\n\t\t\tif(sip->sip_from && sip->sip_from->a_display && strlen(sip->sip_from->a_display) > 0) {\n\t\t\t\tjson_object_set_new(result, \"displayname\", json_string(sip->sip_from->a_display));\n\t\t\t}\n\t\t\tjson_object_set_new(result, \"content\", json_string(payload));\n\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t}\n\t\t\tif(session->callid)\n\t\t\t\tjson_object_set_new(message, \"call_id\", json_string(session->callid));\n\t\t\tjson_object_set_new(result, \"content_type\", json_string(content_type));\n\t\t\tjson_object_set_new(message, \"result\", result);\n\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, message, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(message);\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_notify: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We expect a payload */\n\t\t\tif(!sip) {\n\t\t\t\t/* No SIP message? Maybe an internal message? */\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(!sip->sip_payload || !sip->sip_payload->pl_data) {\n\t\t\t\t/* Send a 200 back and ignore the message */\n\t\t\t\tnua_respond(nh, 200, sip_status_phrase(200), TAG_END());\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t/* Notify the application */\n\t\t\tjson_t *notify = json_object();\n\t\t\tjson_object_set_new(notify, \"sip\", json_string(\"event\"));\n\t\t\tjson_object_set_new(notify, \"call_id\", json_string(sip->sip_call_id->i_id));\n\t\t\tjson_t *result = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"notify\"));\n\t\t\tif(sip->sip_event != NULL)\n\t\t\t\tjson_object_set_new(result, \"notify\", json_string(sip->sip_event->o_type));\n\t\t\tconst tagi_t *t = tl_find(tags, nutag_substate);\n\t\t\tif(t != NULL) {\n\t\t\t\tenum nua_substate substate = (enum nua_substate)(t->t_value);\n\t\t\t\tjson_object_set_new(result, \"substate\", json_string(nua_substate_name(substate)));\n\t\t\t}\n\t\t\tif(sip->sip_content_type != NULL)\n\t\t\t\tjson_object_set_new(result, \"content-type\", json_string(sip->sip_content_type->c_type));\n\t\t\tjson_object_set_new(result, \"content\", json_string(sip->sip_payload->pl_data));\n\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t}\n\t\t\tjson_object_set_new(notify, \"result\", result);\n\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, notify, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(notify);\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_i_options:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* Stack responds automatically to OPTIONS request unless OPTIONS is\n\t\t\t * included in the set of application methods, set by NUTAG_APPL_METHOD(). */\n\t\t\tbreak;\n\t/* Responses */\n\t\tcase nua_r_get_params:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tconst tagi_t* from = NULL;\n\t\t\tif((status != 200) || ((from = tl_find(tags, siptag_from_str)) == NULL)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unable to find 'siptag_from_str' among all the tags\\n\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst char *from_value = (const char *)from->t_value;\n\t\t\tif(from_value == NULL || strlen(from_value) < 2) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid 'siptag_from_str' value '%s'\\n\", from_value);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"'siptag_from_str': %s\\n\", from_value);\n\t\t\tg_free(ssip->contact_header);\n\t\t\tssip->contact_header = g_strdup(from_value);\n\t\t\tbreak;\n\t\tcase nua_r_set_params:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_r_notifier:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_r_shutdown:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tif(status < 200 && !g_atomic_int_get(&stopping)) {\n\t\t\t\t/* shutdown in progress -> return */\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(status >= 200 && ssip != NULL) {\n\t\t\t\t/* Check if this session (and/or its helpers) had dangling\n\t\t\t\t * references for ongoing calls: we won't receive other events\n\t\t\t\t * after this, so it's up to us to clean up after ourselves */\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\twhile(session->active_calls) {\n\t\t\t\t\tjanus_sip_session *s = (janus_sip_session *)session->active_calls->data;\n\t\t\t\t\tif(s != NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%p] Removing reference\\n\", s);\n\t\t\t\t\t\tjanus_refcount_decrease(&s->ref);\n\t\t\t\t\t}\n\t\t\t\t\tsession->active_calls = g_list_remove(session->active_calls, s);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t/* End the event loop: su_root_run() will return */\n\t\t\t\tsu_root_break(ssip->s_root);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase nua_r_terminate:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t/* SIP responses */\n\t\tcase nua_r_bye:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_r_cancel:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tbreak;\n\t\tcase nua_r_info:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* FIXME Should we notify the user, in case the SIP INFO returned an error? */\n\t\t\tbreak;\n\t\tcase nua_r_message:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* Handle authentication for SIP MESSAGE - eg. SippySoft Softswitch requires 401 authentication even if SIP user is registered */\n\t\t\tif(status == 401 || status == 407) {\n\t\t\t\tconst char *scheme = NULL;\n\t\t\t\tconst char *realm = NULL;\n\t\t\t\tif(status == 401) {\n\t\t\t\t\t/* Get scheme/realm from 401 error */\n\t\t\t\t\tsip_www_authenticate_t const* www_auth = sip->sip_www_authenticate;\n\t\t\t\t\tif(www_auth == NULL)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tscheme = www_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(www_auth->au_params, \"realm=\");\n\t\t\t\t} else {\n\t\t\t\t\t/* Get scheme/realm from 407 error, proxy-auth */\n\t\t\t\t\tsip_proxy_authenticate_t const* proxy_auth = sip->sip_proxy_authenticate;\n\t\t\t\t\tif(proxy_auth == NULL)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tscheme = proxy_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(proxy_auth->au_params, \"realm=\");\n\t\t\t\t}\n\t\t\t\tchar authuser[100], secret[100];\n\t\t\t\tmemset(authuser, 0, sizeof(authuser));\n\t\t\t\tmemset(secret, 0, sizeof(secret));\n\t\t\t\tif(session->helper) {\n\t\t\t\t\t/* This is an helper session, we'll need the credentials from the master */\n\t\t\t\t\tif(session->master == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No master session for this helper, authentication will fail...\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsession = session->master;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(session->account.authuser && strchr(session->account.authuser, ':')) {\n\t\t\t\t\t/* The authuser contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"\\\"%s\\\"\", session->account.authuser);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"%s\", session->account.authuser);\n\t\t\t\t}\n\t\t\t\tif(session->account.secret && strchr(session->account.secret, ':')) {\n\t\t\t\t\t/* The secret contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"\\\"%s\\\"\", session->account.secret);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"%s\", session->account.secret);\n\t\t\t\t}\n\t\t\t\tchar auth[256];\n\t\t\t\tmemset(auth, 0, sizeof(auth));\n\t\t\t\tg_snprintf(auth, sizeof(auth), \"%s%s:%s:%s:%s%s\",\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tscheme,\n\t\t\t\t\trealm,\n\t\t\t\t\tauthuser,\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tsecret);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t%s\\n\", auth);\n\t\t\t\t/* Authenticate */\n\t\t\t\tnua_authenticate(nh,\n\t\t\t\t\tNUTAG_AUTH(auth),\n\t\t\t\t\tTAG_END());\n\t\t\t}  else {\n\t\t\t\tchar *messageid = g_strdup(sip->sip_call_id->i_id);\n\t\t\t\t/* Find session associated with the message */\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tjanus_sip_session *message_session = g_hash_table_lookup(messageids, messageid);\n\t\t\t\tif (!message_session) {\n\t\t\t\t\tmessage_session = session;\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Message (%s) not associated with any session, event will be reported to master\\n\", messageid);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t/* MESSAGE response, notify the application */\n\t\t\t\tjson_t *result = json_object();\n\t\t\t\t/* SIP code and reason */\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"messagedelivery\"));\n\t\t\t\tjson_object_set_new(result, \"code\", json_integer(status));\n\t\t\t\tjson_object_set_new(result, \"reason\", json_string(phrase));\n\t\t\t\t/* Build the delivery receipt */\n\t\t\t\tjson_t *dr = json_object();\n\t\t\t\tjson_object_set_new(dr, \"sip\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(dr, \"result\", result);\n\t\t\t\tjson_object_set_new(dr, \"call_id\", json_string(messageid));\n\t\t\t\t/* Report delivery */\n\t\t\t\tint ret = gateway->push_event(message_session->handle, &janus_sip_plugin, message_session->transaction, dr, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(dr);\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tg_hash_table_remove(messageids, messageid);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tg_free(messageid);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase nua_r_refer: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We got a response to our REFER */\n\t\t\tJANUS_LOG(LOG_VERB, \"Response to REFER received\\n\");\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_r_invite: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\n\t\t\t/* If this INVITE was triggered by a REFER, notify the transferer */\n\t\t\tif(session->refer_id > 0) {\n\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\tjanus_sip_transfer *transfer = g_hash_table_lookup(transfers, GUINT_TO_POINTER(session->refer_id));\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tif(transfer != NULL && transfer->nh_s != NULL) {\n\t\t\t\t\t/* Send a NOTIFY */\n\t\t\t\t\tchar content[100];\n\t\t\t\t\tg_snprintf(content, sizeof(content), \"SIP/2.0 %d %s\", status, phrase);\n\t\t\t\t\tnua_notify(transfer->nh_s,\n\t\t\t\t\t\tNUTAG_SUBSTATE(nua_substate_active),\n\t\t\t\t\t\tSIPTAG_CONTENT_TYPE_STR(\"message/sipfrag\"),\n\t\t\t\t\t\tSIPTAG_PAYLOAD_STR(content),\n\t\t\t\t\t\tTAG_END());\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tgboolean in_progress = FALSE;\n\t\t\tif(status < 200) {\n\t\t\t\t/* Not ready yet, either notify the user (e.g., \"ringing\") or handle early media */\n\t\t\t\tif(status == 180 || status == 183) {\n\t\t\t\t\t/* If's a Session Progress: check if there's an SDP, and if so, treat it like a 200 */\n\t\t\t\t\tif(sip->sip_payload && sip->sip_payload->pl_data) {\n\t\t\t\t\t\tin_progress = TRUE;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Ringing, notify the application */\n\t\t\t\t\t\tjson_t *ringing = json_object();\n\t\t\t\t\t\tjson_object_set_new(ringing, \"sip\", json_string(\"event\"));\n\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"ringing\"));\n\t\t\t\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_object_set_new(ringing, \"result\", result);\n\t\t\t\t\t\tjson_object_set_new(ringing, \"call_id\", json_string(session->callid));\n\t\t\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, ringing, NULL);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\t\t\tjson_decref(ringing);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Nothing to do, let's wait for a 200 OK */\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else if(status == 401 || status == 407) {\n\t\t\t\tjanus_sip_save_reason(sip, session);\n\t\t\t\tconst char *scheme = NULL;\n\t\t\t\tconst char *realm = NULL;\n\t\t\t\tif(status == 401) {\n\t\t\t\t\t/* Get scheme/realm from 401 error */\n\t\t\t\t\tsip_www_authenticate_t const* www_auth = sip->sip_www_authenticate;\n\t\t\t\t\tif(www_auth == NULL)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tscheme = www_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(www_auth->au_params, \"realm=\");\n\t\t\t\t} else {\n\t\t\t\t\t/* Get scheme/realm from 407 error, proxy-auth */\n\t\t\t\t\tsip_proxy_authenticate_t const* proxy_auth = sip->sip_proxy_authenticate;\n\t\t\t\t\tif(proxy_auth == NULL)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tscheme = proxy_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(proxy_auth->au_params, \"realm=\");\n\t\t\t\t}\n\t\t\t\tchar authuser[100], secret[100];\n\t\t\t\tmemset(authuser, 0, sizeof(authuser));\n\t\t\t\tmemset(secret, 0, sizeof(secret));\n\t\t\t\tif(session->helper) {\n\t\t\t\t\t/* This is an helper session, we'll need the credentials from the master */\n\t\t\t\t\tif(session->master == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No master session for this helper, authentication will fail...\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsession = session->master;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(session->account.authuser && strchr(session->account.authuser, ':')) {\n\t\t\t\t\t/* The authuser contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"\\\"%s\\\"\", session->account.authuser);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"%s\", session->account.authuser);\n\t\t\t\t}\n\t\t\t\tif(session->account.secret && strchr(session->account.secret, ':')) {\n\t\t\t\t\t/* The secret contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"\\\"%s\\\"\", session->account.secret);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"%s\", session->account.secret);\n\t\t\t\t}\n\t\t\t\tchar auth[256];\n\t\t\t\tmemset(auth, 0, sizeof(auth));\n\t\t\t\tg_snprintf(auth, sizeof(auth), \"%s%s:%s:%s:%s%s\",\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tscheme,\n\t\t\t\t\trealm,\n\t\t\t\t\tauthuser,\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tsecret);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t%s\\n\", auth);\n\t\t\t\t/* Authenticate */\n\t\t\t\tnua_authenticate(nh,\n\t\t\t\t\tNUTAG_AUTH(auth),\n\t\t\t\t\tTAG_END());\n\t\t\t\tbreak;\n\t\t\t} else if(status == 700) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Handling SDP answer in ACK\\n\");\n\t\t\t} else if(status >= 400 && status != 700) {\n\t\t\t\tjanus_sip_save_reason(sip, session);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(ssip == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tInvalid SIP stack\\n\");\n\t\t\t\tnua_respond(nh, 500, sip_status_phrase(500), TAG_END());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(sip->sip_payload == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tMissing SDP\\n\");\n\t\t\t\tnua_respond(nh, 488, sip_status_phrase(488), TAG_END());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tchar sdperror[100];\n\t\t\tjanus_sdp *sdp = janus_sdp_parse(sip->sip_payload->pl_data, sdperror, sizeof(sdperror));\n\t\t\tif(!sdp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tError parsing SDP! %s\\n\", sdperror);\n\t\t\t\tnua_respond(nh, 488, sip_status_phrase(488), TAG_END());\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t/* Send an ACK, if needed */\n\t\t\tif(!in_progress) {\n\t\t\t\tchar *route = NULL;\n\t\t\t\tsip_record_route_t *srr = sip->sip_record_route;\n\t\t\t\tif(srr != NULL) {\n\t\t\t\t\twhile(srr->r_next != NULL)\n\t\t\t\t\t\tsrr = srr->r_next;\n\t\t\t\t\troute = srr ? url_as_string(session->stack->s_home, srr->r_url) : NULL;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Sending ACK (route=%s)\\n\", route ? route : \"none\");\n\t\t\t\tnua_ack(nh,\n\t\t\t\t\tTAG_IF(route, NTATAG_DEFAULT_PROXY(route)),\n\t\t\t\t\tTAG_END());\n\t\t\t\tif(route != NULL)\n\t\t\t\t\tsu_free(session->stack->s_home, route);\n\t\t\t}\n\t\t\t/* Parse SDP */\n\t\t\tJANUS_LOG(LOG_VERB, \"Peer accepted our call:\\n%s\", sip->sip_payload->pl_data);\n\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_incall);\n\t\t\tchar *fixed_sdp = sip->sip_payload->pl_data;\n\t\t\tgboolean changed = FALSE;\n\t\t\tgboolean update = session->media.ready;\n\t\t\tjanus_sip_sdp_process(session, sdp, TRUE, update, &changed);\n\t\t\t/* If we asked for SRTP and are not getting it, fail */\n\t\t\tgboolean has_srtp = TRUE;\n\t\t\tif(session->media.has_audio)\n\t\t\t\thas_srtp = (has_srtp && session->media.has_srtp_remote_audio);\n\t\t\tif(session->media.has_video)\n\t\t\t\thas_srtp = (has_srtp && session->media.has_srtp_remote_video);\n\t\t\tif(session->media.require_srtp && !has_srtp) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"We asked for mandatory SRTP but didn't get any in the reply!\\n\");\n\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\t/* Hangup immediately */\n\t\t\t\tsession->media.earlymedia = FALSE;\n\t\t\t\tsession->media.update = FALSE;\n\t\t\t\tsession->media.autoaccept_reinvites = TRUE;\n\t\t\t\tsession->media.ready = FALSE;\n\t\t\t\tsession->media.on_hold = FALSE;\n\t\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_closing);\n\t\t\t\tnua_bye(nh, TAG_END());\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\tg_free(session->callee);\n\t\t\t\tsession->callee = NULL;\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(!session->media.remote_audio_ip && !session->media.remote_video_ip) {\n\t\t\t\t/* No remote address parsed? Give up */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"\\tNo remote IP address found for RTP, something's wrong with the SDP!\\n\");\n\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\t/* Hangup immediately */\n\t\t\t\tsession->media.earlymedia = FALSE;\n\t\t\t\tsession->media.update = FALSE;\n\t\t\t\tsession->media.autoaccept_reinvites = TRUE;\n\t\t\t\tsession->media.ready = FALSE;\n\t\t\t\tsession->media.on_hold = FALSE;\n\t\t\t\tjanus_sip_call_update_status(session, janus_sip_call_status_closing);\n\t\t\t\tnua_bye(nh, TAG_END());\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\tg_free(session->callee);\n\t\t\t\tsession->callee = NULL;\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(session->media.audio_pt > -1) {\n\t\t\t\tsession->media.audio_pt_name = janus_get_codec_from_pt(fixed_sdp, session->media.audio_pt);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Detected audio codec: %d (%s)\\n\", session->media.audio_pt, session->media.audio_pt_name);\n\t\t\t}\n\t\t\tif(session->media.video_pt > -1) {\n\t\t\t\tsession->media.video_pt_name = janus_get_codec_from_pt(fixed_sdp, session->media.video_pt);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Detected video codec: %d (%s)\\n\", session->media.video_pt, session->media.video_pt_name);\n\t\t\t}\n\t\t\tsession->media.ready = TRUE;\t/* FIXME Maybe we need a better way to signal this */\n\t\t\tif(update && !session->media.earlymedia && !session->media.update) {\n\t\t\t\t/* Don't push to the application if this is in response to a hold/unhold we sent ourselves */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"This is an update to an existing call (possibly in response to hold/unhold)\\n\");\n\t\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(!session->media.earlymedia && !session->media.update) {\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"siprtp %s\", session->account.username);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tsession->relayer_thread = g_thread_try_new(tname, janus_sip_relay_thread, session, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tsession->relayer_thread = NULL;\n\t\t\t\t\tsession->media.ready = FALSE;\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the RTP/RTCP thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Check if there's an isfocus feature parameter in the Contact header */\n\t\t\tgboolean is_focus = FALSE;\n\t\t\tif(sip->sip_contact && sip->sip_contact->m_params) {\n\t\t\t\tint i=0;\n\t\t\t\tfor(i=0; sip->sip_contact->m_params[i]; i++) {\n\t\t\t\t\tif(!strcasecmp(sip->sip_contact->m_params[i], \"isfocus\")) {\n\t\t\t\t\t\t/* The peer is a conference bridge */\n\t\t\t\t\t\tis_focus = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Send event back to the application */\n\t\t\tjson_t *jsep = NULL;\n\t\t\tif(!session->media.earlymedia) {\n\t\t\t\tjsep = json_pack(\"{ssss}\", \"type\", \"answer\", \"sdp\", fixed_sdp);\n\t\t\t} else {\n\t\t\t\t/* We've received the 200 OK after the 183, we can remove the flag now */\n\t\t\t\tsession->media.earlymedia = FALSE;\n\t\t\t}\n\t\t\tif(in_progress) {\n\t\t\t\t/* If we just received the 183, set the flag instead so that we can handle the 200 OK differently */\n\t\t\t\tsession->media.earlymedia = TRUE;\n\t\t\t}\n\t\t\tjson_t *call = json_object();\n\t\t\tjson_object_set_new(call, \"sip\", json_string(\"event\"));\n\t\t\tjson_t *calling = json_object();\n\t\t\tjson_object_set_new(calling, \"event\", json_string(in_progress ? \"progress\" : \"accepted\"));\n\t\t\tjson_object_set_new(calling, \"username\", json_string(session->callee));\n\t\t\tif(is_focus)\n\t\t\t\tjson_object_set_new(calling, \"isfocus\", json_true());\n\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\tjson_object_set_new(calling, \"headers\", headers);\n\t\t\t}\n\t\t\tjson_object_set_new(call, \"result\", calling);\n\t\t\tjson_object_set_new(call, \"call_id\", json_string(session->callid));\n\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, call, jsep);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(call);\n\t\t\tjson_decref(jsep);\n\t\t\tjanus_sdp_destroy(sdp);\n\t\t\t/* Also notify event handlers */\n\t\t\tif(!session->media.update && notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(in_progress ? \"progress\" : \"accepted\"));\n\t\t\t\tif(session->callid)\n\t\t\t\t\tjson_object_set_new(info, \"call-id\", json_string(session->callid));\n\t\t\t\tjson_object_set_new(info, \"username\", json_string(session->callee));\n\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t}\n\t\t\tif(session->media.update) {\n\t\t\t\t/* We just received a 200 OK to an update we sent */\n\t\t\t\tsession->media.update = FALSE;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_r_register:\n\t\tcase nua_r_unregister: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tif(status == 200) {\n\t\t\t\tif(event == nua_r_register) {\n\t\t\t\t\tif(session->account.registration_status < janus_sip_registration_status_registered)\n\t\t\t\t\t\tsession->account.registration_status = janus_sip_registration_status_registered;\n\t\t\t\t} else {\n\t\t\t\t\tsession->account.registration_status = janus_sip_registration_status_unregistered;\n\t\t\t\t}\n\t\t\t\tconst char *event_name = (event == nua_r_register ? \"registered\" : \"unregistered\");\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Successfully %s\\n\", event_name);\n\t\t\t\t/* Notify the application */\n\t\t\t\tjson_t *reg = json_object();\n\t\t\t\tjson_object_set_new(reg, \"sip\", json_string(\"event\"));\n\t\t\t\tjson_t *reging = json_object();\n\t\t\t\tjson_object_set_new(reging, \"event\", json_string(event_name));\n\t\t\t\tjson_object_set_new(reging, \"username\", json_string(session->account.username));\n\t\t\t\tif(event == nua_r_register) {\n\t\t\t\t\tjson_object_set_new(reging, \"register_sent\", json_true());\n\t\t\t\t\tjson_object_set_new(reging, \"master_id\", json_integer(session->master_id));\n\t\t\t\t}\n\t\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\t\tjson_object_set_new(reging, \"headers\", headers);\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(reg, \"unique_id\", json_string(session->unique_id));\n\t\t\t\tjson_object_set_new(reg, \"result\", reging);\n\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, reg, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(reg);\n\t\t\t\t/* If we unregistered and this session had helpers, get rid of them */\n\t\t\t\tif(event == nua_r_unregister) {\n\t\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t\tGList *temp = NULL;\n\t\t\t\t\twhile(session->helpers != NULL) {\n\t\t\t\t\t\ttemp = session->helpers;\n\t\t\t\t\t\tsession->helpers = g_list_remove_link(session->helpers, temp);\n\t\t\t\t\t\tjanus_sip_session *helper = (janus_sip_session *)temp->data;\n\t\t\t\t\t\tif(helper != NULL && helper->handle != NULL) {\n\t\t\t\t\t\t\t/* Get rid of this helper */\n\t\t\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&helper->ref);\n\t\t\t\t\t\t\tgateway->end_session(helper->handle);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg_list_free(temp);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t}\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(event_name));\n\t\t\t\t\tjson_object_set_new(info, \"identity\", json_string(session->account.identity));\n\t\t\t\t\tif(session->account.proxy)\n\t\t\t\t\t\tjson_object_set_new(info, \"proxy\", json_string(session->account.proxy));\n\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t} else if(status == 401 || status == 407) {\n\t\t\t\tconst char *scheme = NULL;\n\t\t\t\tconst char *realm = NULL;\n\t\t\t\tif(status == 401) {\n\t\t\t\t\t/* Get scheme/realm from 401 error */\n\t\t\t\t\tsip_www_authenticate_t const* www_auth = sip->sip_www_authenticate;\n\t\t\t\t\tif(www_auth == NULL) {\n\t\t\t\t\t\t/* No WWW-Authenticate header, give up */\n\t\t\t\t\t\tgoto auth_failed;\n\t\t\t\t\t}\n\t\t\t\t\tscheme = www_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(www_auth->au_params, \"realm=\");\n\t\t\t\t} else {\n\t\t\t\t\t/* Get scheme/realm from 407 error, proxy-auth */\n\t\t\t\t\tsip_proxy_authenticate_t const* proxy_auth = sip->sip_proxy_authenticate;\n\t\t\t\t\tif(proxy_auth == NULL) {\n\t\t\t\t\t\t/* No Proxy-Authenticate header, give up */\n\t\t\t\t\t\tgoto auth_failed;\n\t\t\t\t\t}\n\t\t\t\t\tscheme = proxy_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(proxy_auth->au_params, \"realm=\");\n\t\t\t\t}\n\t\t\t\tchar authuser[100], secret[100];\n\t\t\t\tmemset(authuser, 0, sizeof(authuser));\n\t\t\t\tmemset(secret, 0, sizeof(secret));\n\t\t\t\tif(session->account.authuser && strchr(session->account.authuser, ':')) {\n\t\t\t\t\t/* The authuser contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"\\\"%s\\\"\", session->account.authuser);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"%s\", session->account.authuser);\n\t\t\t\t}\n\t\t\t\tif(session->account.secret && strchr(session->account.secret, ':')) {\n\t\t\t\t\t/* The secret contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"\\\"%s\\\"\", session->account.secret);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"%s\", session->account.secret);\n\t\t\t\t}\n\t\t\t\tchar auth[256];\n\t\t\t\tmemset(auth, 0, sizeof(auth));\n\t\t\t\tg_snprintf(auth, sizeof(auth), \"%s%s:%s:%s:%s%s\",\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tscheme,\n\t\t\t\t\trealm,\n\t\t\t\t\tauthuser,\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tsecret);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t%s\\n\", auth);\n\t\t\t\t/* Authenticate */\n\t\t\t\tnua_authenticate(nh,\n\t\t\t\t\tNUTAG_AUTH(auth),\n\t\t\t\t\tTAG_END());\n\t\t\t} else if(status >= 400) {\nauth_failed:\n\t\t\t\t/* Authentication failed? */\n\t\t\t\tsession->account.registration_status = janus_sip_registration_status_failed;\n\t\t\t\t/* Cleanup registration values */\n\t\t\t\tif(session->account.identity != NULL) {\n\t\t\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\t\t\tg_hash_table_remove(identities, session->account.identity);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tg_free(session->account.identity);\n\t\t\t\t}\n\t\t\t\tsession->account.identity = NULL;\n\t\t\t\tsession->account.force_udp = FALSE;\n\t\t\t\tsession->account.force_tcp = FALSE;\n\t\t\t\tsession->account.sips = FALSE;\n\t\t\t\tsession->account.rfc2543_cancel = FALSE;\n\t\t\t\tsession->account.automatic_ringing = TRUE;\n\t\t\t\tif(session->account.username != NULL)\n\t\t\t\t\tg_free(session->account.username);\n\t\t\t\tsession->account.username = NULL;\n\t\t\t\tif(session->account.display_name != NULL)\n\t\t\t\t\tg_free(session->account.display_name);\n\t\t\t\tsession->account.display_name = NULL;\n\t\t\t\tif(session->account.authuser != NULL)\n\t\t\t\t\tg_free(session->account.authuser);\n\t\t\t\tsession->account.authuser = NULL;\n\t\t\t\tif(session->account.secret != NULL)\n\t\t\t\t\tg_free(session->account.secret);\n\t\t\t\tsession->account.secret = NULL;\n\t\t\t\tsession->account.secret_type = janus_sip_secret_type_unknown;\n\t\t\t\tif(session->account.proxy != NULL)\n\t\t\t\t\tg_free(session->account.proxy);\n\t\t\t\tsession->account.proxy = NULL;\n\t\t\t\tif(session->account.outbound_proxy != NULL)\n\t\t\t\t\tg_free(session->account.outbound_proxy);\n\t\t\t\tsession->account.outbound_proxy = NULL;\n\t\t\t\tif(session->account.user_agent != NULL)\n\t\t\t\t\tg_free(session->account.user_agent);\n\t\t\t\tsession->account.user_agent = NULL;\n\t\t\t\tsession->account.registration_status = janus_sip_registration_status_unregistered;\n\t\t\t\t/* Tell the application... */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"sip\", json_string(\"event\"));\n\t\t\t\tjson_t *result = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"registration_failed\"));\n\t\t\t\tjson_object_set_new(result, \"code\", json_integer(status));\n\t\t\t\tjson_object_set_new(result, \"reason\", json_string(phrase ? phrase : \"\"));\n\t\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(event);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"registration_failed\"));\n\t\t\t\t\tjson_object_set_new(info, \"code\", json_integer(status));\n\t\t\t\t\tjson_object_set_new(info, \"reason\", json_string(phrase ? phrase : \"\"));\n\t\t\t\t\tgateway->notify_event(&janus_sip_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_r_subscribe: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\tif(status == 200 || status == 202) {\n\t\t\t\t/* Success */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"sip\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"call_id\", json_string(sip->sip_call_id->i_id));\n\t\t\t\tjson_t *result = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"subscribe_succeeded\"));\n\t\t\t\tjson_object_set_new(result, \"code\", json_integer(status));\n\t\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t\t}\n\t\t\t\tif (sip->sip_expires)\n\t\t\t\t\tjson_object_set_new(result, \"expires\", json_integer(sip->sip_expires->ex_delta));\n\t\t\t\tjson_object_set_new(result, \"reason\", json_string(phrase ? phrase : \"\"));\n\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(event);\n\t\t\t} else if(status == 401 || status == 407) {\n\t\t\t\tconst char *scheme = NULL;\n\t\t\t\tconst char *realm = NULL;\n\t\t\t\tif(status == 401) {\n\t\t\t\t\t/* Get scheme/realm from 401 error */\n\t\t\t\t\tsip_www_authenticate_t const* www_auth = sip->sip_www_authenticate;\n\t\t\t\t\tif(www_auth == NULL)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tscheme = www_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(www_auth->au_params, \"realm=\");\n\t\t\t\t} else {\n\t\t\t\t\t/* Get scheme/realm from 407 error, proxy-auth */\n\t\t\t\t\tsip_proxy_authenticate_t const* proxy_auth = sip->sip_proxy_authenticate;\n\t\t\t\t\tif(proxy_auth == NULL)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tscheme = proxy_auth->au_scheme;\n\t\t\t\t\trealm = msg_params_find(proxy_auth->au_params, \"realm=\");\n\t\t\t\t}\n\t\t\t\tchar authuser[100], secret[100];\n\t\t\t\tmemset(authuser, 0, sizeof(authuser));\n\t\t\t\tmemset(secret, 0, sizeof(secret));\n\t\t\t\tif(session->helper) {\n\t\t\t\t\t/* This is an helper session, we'll need the credentials from the master */\n\t\t\t\t\tif(session->master == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No master session for this helper, authentication will fail...\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsession = session->master;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(session->account.authuser && strchr(session->account.authuser, ':')) {\n\t\t\t\t\t/* The authuser contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"\\\"%s\\\"\", session->account.authuser);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(authuser, sizeof(authuser), \"%s\", session->account.authuser);\n\t\t\t\t}\n\t\t\t\tif(session->account.secret && strchr(session->account.secret, ':')) {\n\t\t\t\t\t/* The secret contains a colon: wrap it in quotes */\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"\\\"%s\\\"\", session->account.secret);\n\t\t\t\t} else {\n\t\t\t\t\tg_snprintf(secret, sizeof(secret), \"%s\", session->account.secret);\n\t\t\t\t}\n\t\t\t\tchar auth[256];\n\t\t\t\tmemset(auth, 0, sizeof(auth));\n\t\t\t\tg_snprintf(auth, sizeof(auth), \"%s%s:%s:%s:%s%s\",\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tscheme,\n\t\t\t\t\trealm,\n\t\t\t\t\tauthuser,\n\t\t\t\t\tsession->account.secret_type == janus_sip_secret_type_hashed ? \"HA1+\" : \"\",\n\t\t\t\t\tsecret);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"\\t%s\\n\", auth);\n\t\t\t\t/* Authenticate */\n\t\t\t\tnua_authenticate(nh,\n\t\t\t\t\tNUTAG_AUTH(auth),\n\t\t\t\t\tTAG_END());\n\t\t\t\tbreak;\n\t\t\t} else if(status >= 400) {\n\t\t\t\t/* Something went wrong */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] SUBSCRIBE failed: %d %s\\n\", session->account.username, status, phrase ? phrase : \"\");\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"sip\", json_string(\"event\"));\n\t\t\t\tif(sip && sip->sip_call_id)\n\t\t\t\t\tjson_object_set_new(event, \"call_id\", json_string(sip->sip_call_id->i_id));\n\t\t\t\tjson_t *result = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"subscribe_failed\"));\n\t\t\t\tjson_object_set_new(result, \"code\", json_integer(status));\n\t\t\t\tjson_object_set_new(result, \"reason\", json_string(phrase ? phrase : \"\"));\n\t\t\t\tif(session->incoming_header_prefixes) {\n\t\t\t\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\t\t\t\tjson_object_set_new(result, \"headers\", headers);\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase nua_r_notify: {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%s][%s]: %d %s\\n\", session->account.username, nua_event_name(event), status, phrase ? phrase : \"??\");\n\t\t\t/* We got a response to a NOTIFY we sent, but we really don't care */\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\t/* unknown event -> print out error message */\n\t\t\tJANUS_LOG(LOG_ERR, \"Unknown event %d (%s)\\n\", event, nua_event_name(event));\n\t\t\tbreak;\n\t}\n}\n\nvoid janus_sip_save_reason(sip_t const *sip, janus_sip_session *session) {\n\tif(!sip || !session)\n\t\treturn;\n\n\tif(sip->sip_reason && sip->sip_reason->re_text) {\n\t\tg_free(session->hangup_reason_header);\n\t\tsession->hangup_reason_header = g_strdup(sip->sip_reason->re_text);\n\t\tjanus_sip_remove_quotes(session->hangup_reason_header);\n\t}\n\tif(sip->sip_reason && sip->sip_reason->re_protocol) {\n\t\tg_free(session->hangup_reason_header_protocol);\n\t\tsession->hangup_reason_header_protocol = g_strdup(sip->sip_reason->re_protocol);\n\t}\n\tif(sip->sip_reason && sip->sip_reason->re_cause) {\n\t\tg_free(session->hangup_reason_header_cause);\n\t\tsession->hangup_reason_header_cause = g_strdup(sip->sip_reason->re_cause);\n\t}\n\tif(session->incoming_header_prefixes) {\n\t\tif(session->hangup_custom_headers) {\n\t\t\tjson_decref(session->hangup_custom_headers);\n\t\t}\n\t\tjson_t *headers = janus_sip_get_incoming_headers(sip, session);\n\t\tsession->hangup_custom_headers = headers;\n\t}\n}\n\nvoid janus_sip_sdp_process(janus_sip_session *session, janus_sdp *sdp, gboolean answer, gboolean update, gboolean *changed) {\n\tif(!session || !sdp)\n\t\treturn;\n\t/* c= */\n\tint opusred_pt = answer ? janus_sdp_get_opusred_pt(sdp, -1) : -1;\n\tif(sdp->c_addr) {\n\t\tif(update) {\n\t\t\tif(changed && (!session->media.remote_audio_ip || strcmp(sdp->c_addr, session->media.remote_audio_ip))) {\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t*changed = TRUE;\n\t\t\t}\n\t\t\tif(changed && (!session->media.remote_video_ip || strcmp(sdp->c_addr, session->media.remote_video_ip))) {\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t*changed = TRUE;\n\t\t\t}\n\t\t}\n\t\t/* Regardless if we audio and video are being negotiated we set their connection addresses\n\t\t * from session level c= header by default. If media level connection addresses are available\n\t\t * they will be set when processing appropriate media description.*/\n\t\tg_free(session->media.remote_audio_ip);\n\t\tsession->media.remote_audio_ip = g_strdup(sdp->c_addr);\n\t\tg_free(session->media.remote_video_ip);\n\t\tsession->media.remote_video_ip = g_strdup(sdp->c_addr);\n\t}\n\tGList *temp = sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tsession->media.require_srtp = session->media.require_srtp || (m->proto && !strcasecmp(m->proto, \"RTP/SAVP\"));\n\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\tif(m->port) {\n\t\t\t\tif(m->port != session->media.remote_audio_rtp_port) {\n\t\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t\tif(changed)\n\t\t\t\t\t\t*changed = TRUE;\n\t\t\t\t}\n\t\t\t\tsession->media.has_audio = TRUE;\n\t\t\t\tsession->media.remote_audio_rtp_port = m->port;\n\t\t\t\tsession->media.remote_audio_rtcp_port = m->port+1;\t/* FIXME We're assuming RTCP is on the next port */\n\t\t\t\tsession->media.dtmf_pt = janus_sdp_get_codec_pt(sdp, -1, \"dtmf\");\n\t\t\t\tif(m->direction == JANUS_SDP_SENDONLY || m->direction == JANUS_SDP_INACTIVE)\n\t\t\t\t\tsession->media.audio_send = FALSE;\n\t\t\t\telse\n\t\t\t\t\tsession->media.audio_send = TRUE;\n\t\t\t\tif(m->direction == JANUS_SDP_RECVONLY || m->direction == JANUS_SDP_INACTIVE)\n\t\t\t\t\tsession->media.audio_recv = FALSE;\n\t\t\t\telse\n\t\t\t\t\tsession->media.audio_recv = TRUE;\n\t\t\t} else {\n\t\t\t\tsession->media.audio_send = FALSE;\n\t\t\t\tsession->media.audio_recv = FALSE;\n\t\t\t}\n\t\t} else if(m->type == JANUS_SDP_VIDEO) {\n\t\t\tif(m->port) {\n\t\t\t\tif(m->port != session->media.remote_video_rtp_port) {\n\t\t\t\t\t/* This is an update and an address changed */\n\t\t\t\t\tif(changed)\n\t\t\t\t\t\t*changed = TRUE;\n\t\t\t\t}\n\t\t\t\tsession->media.has_video = TRUE;\n\t\t\t\tsession->media.remote_video_rtp_port = m->port;\n\t\t\t\tsession->media.remote_video_rtcp_port = m->port+1;\t/* FIXME We're assuming RTCP is on the next port */\n\t\t\t\tif(m->direction == JANUS_SDP_SENDONLY || m->direction == JANUS_SDP_INACTIVE)\n\t\t\t\t\tsession->media.video_send = FALSE;\n\t\t\t\telse\n\t\t\t\t\tsession->media.video_send = TRUE;\n\t\t\t\tif(m->direction == JANUS_SDP_RECVONLY || m->direction == JANUS_SDP_INACTIVE)\n\t\t\t\t\tsession->media.video_recv = FALSE;\n\t\t\t\telse\n\t\t\t\t\tsession->media.video_recv = TRUE;\n\t\t\t} else {\n\t\t\t\tsession->media.video_send = FALSE;\n\t\t\t\tsession->media.video_recv = FALSE;\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported media line (not audio/video)\\n\");\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(m->c_addr && m->type == JANUS_SDP_AUDIO) {\n\t\t\tif(update && (!session->media.remote_audio_ip || strcmp(m->c_addr, session->media.remote_audio_ip))) {\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\tif(changed)\n\t\t\t\t\t*changed = TRUE;\n\t\t\t}\n\t\t\tg_free(session->media.remote_audio_ip);\n\t\t\tsession->media.remote_audio_ip = g_strdup(m->c_addr);\n\t\t}\n\t\telse if(m->c_addr && m->type == JANUS_SDP_VIDEO) {\n\t\t\tif(update && (!session->media.remote_video_ip || strcmp(m->c_addr, session->media.remote_video_ip))) {\n\t\t\t\t/* This is an update and an address changed */\n\t\t\t\tif(changed)\n\t\t\t\t\t*changed = TRUE;\n\t\t\t}\n\t\t\tg_free(session->media.remote_video_ip);\n\t\t\tsession->media.remote_video_ip = g_strdup(m->c_addr);\n\t\t}\n\n\t\tGList *tempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name) {\n\t\t\t\tif(!strcasecmp(a->name, \"crypto\")) {\n\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\t\t\t\tif((m->type == JANUS_SDP_AUDIO && session->media.audio_srtp_in != NULL) || (m->type == JANUS_SDP_VIDEO && session->media.video_srtp_in != NULL)) {\n\t\t\t\t\t\t\t/* Remote SRTP is already set */\n\t\t\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgint32 tag = 0;\n\t\t\t\t\t\tchar profile[101], crypto[101];\n\t\t\t\t\t\tint res = a->value ? (sscanf(a->value, \"%\"SCNi32\" %100s inline:%100s\",\n\t\t\t\t\t\t\t&tag, profile, crypto)) : 0;\n\t\t\t\t\t\tif(res != 3) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Failed to parse crypto line, ignoring... %s\\n\", a->value);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tgboolean video = (m->type == JANUS_SDP_VIDEO);\n\t\t\t\t\t\t\tif(answer && ((!video && tag != session->media.audio_srtp_tag) || (video && tag != session->media.video_srtp_tag))) {\n\t\t\t\t\t\t\t\t/* Not the tag for the crypto line we offered */\n\t\t\t\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(janus_sip_srtp_set_remote(session, video, profile, crypto) < 0) {\n\t\t\t\t\t\t\t\t/* Unsupported profile? */\n\t\t\t\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(!video) {\n\t\t\t\t\t\t\t\tsession->media.audio_srtp_tag = tag;\n\t\t\t\t\t\t\t\tsession->media.has_srtp_remote_audio = TRUE;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tsession->media.video_srtp_tag = tag;\n\t\t\t\t\t\t\t\tsession->media.has_srtp_remote_video = TRUE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if(m->type == JANUS_SDP_VIDEO && !strcasecmp(a->name, \"rtcp-fb\") && a->value) {\n\t\t\t\t\tif(strstr(a->value, \" pli\"))\n\t\t\t\t\t\tsession->media.video_pli_supported = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\n\t\tif(answer && (m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO)) {\n\t\t\t/* Check which codec was negotiated eventually */\n\t\t\tint pt = -1;\n\t\t\tif(m->ptypes)\n\t\t\t\tpt = GPOINTER_TO_INT(m->ptypes->data);\n\t\t\tif(pt > -1) {\n\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\tif(pt == opusred_pt) {\n\t\t\t\t\t\tsession->media.opusred_pt = pt;\n\t\t\t\t\t\tsession->media.audio_pt = m->ptypes->next ? GPOINTER_TO_INT(m->ptypes->next->data) : -1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsession->media.audio_pt = pt;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsession->media.video_pt = pt;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\n\tif(update && changed && *changed) {\n\t\t/* Something changed: mark this on the session, so that the thread can update the sockets */\n\t\tsession->media.updated = TRUE;\n\t\tif(session->media.pipefd[1] > 0) {\n\t\t\tint code = 1;\n\t\t\tssize_t res = 0;\n\t\t\tdo {\n\t\t\t\tres = write(session->media.pipefd[1], &code, sizeof(int));\n\t\t\t} while(res == -1 && errno == EINTR);\n\t\t}\n\t}\n}\n\nchar *janus_sip_sdp_manipulate(janus_sip_session *session, janus_sdp *sdp, gboolean answer) {\n\tif(!session || !session->stack || !sdp)\n\t\treturn NULL;\n\tGHashTable *codecs = NULL;\n\tGList *pts_to_remove = NULL;\n\t/* Start replacing stuff */\n\tJANUS_LOG(LOG_VERB, \"Setting protocol to %s\\n\", session->media.require_srtp ? \"RTP/SAVP\" : \"RTP/AVP\");\n\tif(sdp->c_addr) {\n\t\tg_free(sdp->c_addr);\n\t\tsdp->c_addr = g_strdup(sdp_ip);\n\t}\n\tint opusred_pt = answer ? janus_sdp_get_opusred_pt(sdp, -1) : -1;\n\tGList *temp = sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tg_free(m->proto);\n\t\tm->proto = g_strdup(session->media.require_srtp ? \"RTP/SAVP\" : \"RTP/AVP\");\n\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\tm->port = session->media.local_audio_rtp_port;\n\t\t\tif(session->media.has_srtp_local_audio) {\n\t\t\t\tif(!session->media.audio_srtp_local_profile || !session->media.audio_srtp_local_crypto) {\n\t\t\t\t\tjanus_sip_srtp_set_local(session, FALSE, &session->media.audio_srtp_local_profile, &session->media.audio_srtp_local_crypto);\n\t\t\t\t}\n\t\t\t\tif(session->media.audio_srtp_tag == 0)\n\t\t\t\t\tsession->media.audio_srtp_tag = 1;\n\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"crypto\", \"%\"SCNi32\" %s inline:%s\",\n\t\t\t\t\tsession->media.audio_srtp_tag, session->media.audio_srtp_local_profile, session->media.audio_srtp_local_crypto);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t}\n\t\t} else if(m->type == JANUS_SDP_VIDEO) {\n\t\t\tm->port = session->media.local_video_rtp_port;\n\t\t\tif(session->media.has_srtp_local_video) {\n\t\t\t\tif(!session->media.video_srtp_local_profile || !session->media.video_srtp_local_crypto) {\n\t\t\t\t\tjanus_sip_srtp_set_local(session, TRUE, &session->media.video_srtp_local_profile, &session->media.video_srtp_local_crypto);\n\t\t\t\t}\n\t\t\t\tif(session->media.video_srtp_tag == 0)\n\t\t\t\t\tsession->media.video_srtp_tag = 1;\n\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"crypto\", \"%\"SCNi32\" %s inline:%s\",\n\t\t\t\t\tsession->media.video_srtp_tag, session->media.video_srtp_local_profile, session->media.video_srtp_local_crypto);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t}\n\t\t}\n\t\tg_free(m->c_addr);\n\t\tm->c_addr = g_strdup(sdp_ip);\n\t\t/* Get rid of some extra attributes to try and keep the SDP short enough */\n\t\tGList *tempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\t/* These are attributes we handle ourselves, the plugins don't need them */\n\t\t\tif(!strcasecmp(a->name, \"mid\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"msid\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"bundle-only\")\n\t\t\t\t\t|| (!strcasecmp(a->name, \"rtcp-fb\") && a->value && strstr(a->value, \"nack pli\") == NULL)\n\t\t\t\t\t|| (!strcasecmp(a->name, \"extmap\") && a->value &&\n\t\t\t\t\t\tstrstr(a->value, JANUS_RTP_EXTMAP_AUDIO_LEVEL) == NULL &&\n\t\t\t\t\t\tstrstr(a->value, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION) == NULL)) {\n\t\t\t\tm->attributes = g_list_remove(m->attributes, a);\n\t\t\t\ttempA = m->attributes;\n\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(answer && (m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO)) {\n\t\t\t/* Check which codec was negotiated eventually */\n\t\t\tint pt = -1;\n\t\t\tif(m->ptypes)\n\t\t\t\tpt = GPOINTER_TO_INT(m->ptypes->data);\n\t\t\tif(pt > -1) {\n\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\tif(pt == opusred_pt) {\n\t\t\t\t\t\tsession->media.opusred_pt = pt;\n\t\t\t\t\t\tsession->media.audio_pt = m->ptypes->next ? GPOINTER_TO_INT(m->ptypes->next->data) : -1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsession->media.audio_pt = pt;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsession->media.video_pt = pt;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* If this is an answer, get rid of multiple versions of the same\n\t\t * codec as well (e.g., video profiles), as that confuses the hell\n\t\t * out of SOATAG_RTP_SELECT(SOA_RTP_SELECT_COMMON) in nua_respond() */\n\t\tif(answer) {\n\t\t\tif(codecs == NULL)\n\t\t\t\tcodecs = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\t\t/* Check all rtpmap attributes */\n\t\t\tint pt = -1;\n\t\t\tchar codec[50];\n\t\t\tGList *ma = m->attributes;\n\t\t\twhile(ma) {\n\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\tif(a->name != NULL && a->value != NULL && !strcasecmp(a->name, \"rtpmap\")) {\n\t\t\t\t\tif(sscanf(a->value, \"%3d %49s\", &pt, codec) == 2) {\n\t\t\t\t\t\tif(g_hash_table_lookup(codecs, codec) != NULL) {\n\t\t\t\t\t\t\t/* We already have a version of this codec, remove the payload type */\n\t\t\t\t\t\t\tpts_to_remove = g_list_append(pts_to_remove, GINT_TO_POINTER(pt));\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Removing %d (%s)\\n\", pt, codec);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Keep track of this codec */\n\t\t\t\t\t\t\tg_hash_table_insert(codecs, g_strdup(codec), GINT_TO_POINTER(pt));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tma = ma->next;\n\t\t\t}\n\t\t\t/* If we need to remove some payload types from this m-line, do it now */\n\t\t\tif(pts_to_remove != NULL) {\n\t\t\t\tGList *temp = pts_to_remove;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tint pt = GPOINTER_TO_INT(temp->data);\n\t\t\t\t\tjanus_sdp_remove_payload_type(sdp, m->index, pt);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tg_list_free(pts_to_remove);\n\t\t\t\tpts_to_remove = NULL;\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tif(codecs != NULL)\n\t\tg_hash_table_destroy(codecs);\n\t/* Generate a SDP string out of our changes */\n\treturn janus_sdp_write(sdp);\n}\n\n /* Bind local RTP/RTCP sockets */\nstatic int janus_sip_allocate_local_ports(janus_sip_session *session, gboolean update) {\n\tif(session == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid session\\n\");\n\t\treturn -1;\n\t}\n\tif(!update) {\n\t\t/* Reset status */\n\t\tif(session->media.audio_rtp_fd != -1) {\n\t\t\tclose(session->media.audio_rtp_fd);\n\t\t\tsession->media.audio_rtp_fd = -1;\n\t\t}\n\t\tif(session->media.audio_rtcp_fd != -1) {\n\t\t\tclose(session->media.audio_rtcp_fd);\n\t\t\tsession->media.audio_rtcp_fd = -1;\n\t\t}\n\t\tsession->media.local_audio_rtp_port = 0;\n\t\tsession->media.local_audio_rtcp_port = 0;\n\t\tsession->media.audio_ssrc = 0;\n\t\tif(session->media.video_rtp_fd != -1) {\n\t\t\tclose(session->media.video_rtp_fd);\n\t\t\tsession->media.video_rtp_fd = -1;\n\t\t}\n\t\tif(session->media.video_rtcp_fd != -1) {\n\t\t\tclose(session->media.video_rtcp_fd);\n\t\t\tsession->media.video_rtcp_fd = -1;\n\t\t}\n\t\tsession->media.local_video_rtp_port = 0;\n\t\tsession->media.local_video_rtcp_port = 0;\n\t\tsession->media.video_ssrc = 0;\n\t\tif(session->media.pipefd[0] > 0) {\n\t\t\tclose(session->media.pipefd[0]);\n\t\t\tsession->media.pipefd[0] = -1;\n\t\t}\n\t\tif(session->media.pipefd[1] > 0) {\n\t\t\tclose(session->media.pipefd[1]);\n\t\t\tsession->media.pipefd[1] = -1;\n\t\t}\n\t}\n\tgboolean use_ipv6_address_family = !ipv6_disabled &&\n\t\t(janus_network_address_is_null(&janus_network_local_media_ip) || janus_network_local_media_ip.family == AF_INET6);\n\tsocklen_t addrlen = use_ipv6_address_family? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);\n\t/* Start */\n\tint attempts = 100;\t/* FIXME Don't retry forever */\n\tif(session->media.has_audio) {\n\t\tJANUS_LOG(LOG_VERB, \"Allocating audio ports using address [%s]\\n\",\n\t\t\tjanus_network_address_is_null(&janus_network_local_media_ip) ? \"any\" : local_media_ip);\n\t\tstruct sockaddr_storage audio_rtp_address, audio_rtcp_address;\n\t\twhile(session->media.local_audio_rtp_port == 0 || session->media.local_audio_rtcp_port == 0) {\n\t\t\tif(attempts == 0)\t/* Too many failures */\n\t\t\t\treturn -1;\n\t\t\tmemset(&audio_rtp_address, 0, sizeof(audio_rtp_address));\n\t\t\tmemset(&audio_rtcp_address, 0, sizeof(audio_rtcp_address));\n\t\t\tif(session->media.audio_rtp_fd == -1) {\n\t\t\t\tsession->media.audio_rtp_fd = socket(use_ipv6_address_family ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);\n\t\t\t\tint v6only = 0;\n\t\t\t\tif(use_ipv6_address_family && session->media.audio_rtp_fd != -1 &&\n\t\t\t\t\t\tsetsockopt(session->media.audio_rtp_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting v6only to false on audio RTP socket (error=%s)\\n\",\n\t\t\t\t\t\tg_strerror(errno));\n\t\t\t\t}\n\t\t\t\t/* Set the DSCP value if set in the config file */\n\t\t\t\tif(session->media.audio_rtp_fd != -1 && dscp_audio_rtp > 0) {\n\t\t\t\t\tint optval = dscp_audio_rtp << 2;\n\t\t\t\t\tint ret = setsockopt(session->media.audio_rtp_fd, IPPROTO_IP, IP_TOS, &optval, sizeof(optval));\n\t\t\t\t\tif(ret < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting IP_TOS %d on audio RTP socket (error=%s)\\n\",\n\t\t\t\t\t\t\toptval, g_strerror(errno));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->media.audio_rtcp_fd == -1) {\n\t\t\t\tsession->media.audio_rtcp_fd = socket(use_ipv6_address_family ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);\n\t\t\t\tint v6only = 0;\n\t\t\t\tif(use_ipv6_address_family && session->media.audio_rtcp_fd != -1 &&\n\t\t\t\t\t\tsetsockopt(session->media.audio_rtcp_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting v6only to false on audio RTCP socket (error=%s)\\n\",\n\t\t\t\t\t\tg_strerror(errno));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->media.audio_rtp_fd == -1 || session->media.audio_rtcp_fd == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating audio sockets...\\n\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tint rtp_port = g_random_int_range(rtp_range_min, rtp_range_max);\n\t\t\tif(rtp_port % 2)\n\t\t\t\trtp_port++;\t/* Pick an even port for RTP */\n\t\t\tif(use_ipv6_address_family) {\n\t\t\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)&audio_rtp_address;\n\t\t\t\taddr->sin6_family = AF_INET6;\n\t\t\t\taddr->sin6_port = htons(rtp_port);\n\t\t\t\taddr->sin6_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? in6addr_any : janus_network_local_media_ip.ipv6;\n\t\t\t} else {\n\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&audio_rtp_address;\n\t\t\t\taddr->sin_family = AF_INET;\n\t\t\t\taddr->sin_port = htons(rtp_port);\n\t\t\t\taddr->sin_addr.s_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? INADDR_ANY : janus_network_local_media_ip.ipv4.s_addr;\n\t\t\t}\n\t\t\tif(bind(session->media.audio_rtp_fd, (struct sockaddr *)(&audio_rtp_address), addrlen) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Bind failed for audio RTP (port %d), error (%s), trying a different one...\\n\", rtp_port, g_strerror(errno));\n\t\t\t\tclose(session->media.audio_rtp_fd);\n\t\t\t\tsession->media.audio_rtp_fd = -1;\n\t\t\t\tattempts--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Audio RTP listener bound to [%s]:%d(%d)\\n\",\n\t\t\t\tjanus_network_address_is_null(&janus_network_local_media_ip) ? \"any\" : local_media_ip, rtp_port, session->media.audio_rtp_fd);\n\t\t\tint rtcp_port = rtp_port+1;\n\t\t\tif(use_ipv6_address_family) {\n\t\t\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)&audio_rtcp_address;\n\t\t\t\taddr->sin6_family = AF_INET6;\n\t\t\t\taddr->sin6_port = htons(rtcp_port);\n\t\t\t\taddr->sin6_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? in6addr_any : janus_network_local_media_ip.ipv6;\n\t\t\t} else {\n\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&audio_rtcp_address;\n\t\t\t\taddr->sin_family = AF_INET;\n\t\t\t\taddr->sin_port = htons(rtcp_port);\n\t\t\t\taddr->sin_addr.s_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? INADDR_ANY : janus_network_local_media_ip.ipv4.s_addr;\n\t\t\t}\n\t\t\tif(bind(session->media.audio_rtcp_fd, (struct sockaddr *)(&audio_rtcp_address), addrlen) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Bind failed for audio RTCP (port %d), error (%s), trying a different one...\\n\", rtcp_port, g_strerror(errno));\n\t\t\t\t/* RTP socket is not valid anymore, reset it */\n\t\t\t\tclose(session->media.audio_rtp_fd);\n\t\t\t\tsession->media.audio_rtp_fd = -1;\n\t\t\t\tclose(session->media.audio_rtcp_fd);\n\t\t\t\tsession->media.audio_rtcp_fd = -1;\n\t\t\t\tattempts--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Audio RTCP listener bound to [%s]:%d(%d)\\n\",\n\t\t\t\tjanus_network_address_is_null(&janus_network_local_media_ip) ? \"any\" : local_media_ip, rtcp_port, session->media.audio_rtcp_fd);\n\t\t\tsession->media.local_audio_rtp_port = rtp_port;\n\t\t\tsession->media.local_audio_rtcp_port = rtcp_port;\n\t\t}\n\t}\n\tif(session->media.has_video) {\n\t\tJANUS_LOG(LOG_VERB, \"Allocating video ports using address [%s]\\n\",\n\t\t\tjanus_network_address_is_null(&janus_network_local_media_ip  ) ?\"any\" : local_media_ip);\n\t\tstruct sockaddr_storage video_rtp_address, video_rtcp_address;\n\t\twhile(session->media.local_video_rtp_port == 0 || session->media.local_video_rtcp_port == 0) {\n\t\t\tif(attempts == 0)\t/* Too many failures */\n\t\t\t\treturn -1;\n\t\t\tmemset(&video_rtp_address, 0, sizeof(video_rtp_address));\n\t\t\tmemset(&video_rtcp_address, 0, sizeof(video_rtcp_address));\n\t\t\tif(session->media.video_rtp_fd == -1) {\n\t\t\t\tsession->media.video_rtp_fd = socket(use_ipv6_address_family ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);\n\t\t\t\tint v6only = 0;\n\t\t\t\tif(use_ipv6_address_family && session->media.video_rtp_fd != -1 &&\n\t\t\t\t\t\tsetsockopt(session->media.video_rtp_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting v6only to false on video RTP socket (error=%s)\\n\",\n\t\t\t\t\t\tg_strerror(errno));\n\t\t\t\t}\n\t\t\t\t/* Set the DSCP value if set in the config file */\n\t\t\t\tif(session->media.video_rtp_fd != -1 && dscp_video_rtp > 0) {\n\t\t\t\t\tint optval = dscp_video_rtp << 2;\n\t\t\t\t\tint ret = setsockopt(session->media.video_rtp_fd, IPPROTO_IP, IP_TOS, &optval, sizeof(optval));\n\t\t\t\t\tif(ret < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting IP_TOS %d on video RTP socket (error=%s)\\n\",\n\t\t\t\t\t\t\toptval, g_strerror(errno));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->media.video_rtcp_fd == -1) {\n\t\t\t\tsession->media.video_rtcp_fd = socket(use_ipv6_address_family ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);\n\t\t\t\tint v6only = 0;\n\t\t\t\tif(use_ipv6_address_family && session->media.video_rtcp_fd != -1 &&\n\t\t\t\t\t\tsetsockopt(session->media.video_rtcp_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error setting v6only to false on video RTCP socket (error=%s)\\n\",\n\t\t\t\t\t\tg_strerror(errno));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->media.video_rtp_fd == -1 || session->media.video_rtcp_fd == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating video sockets...\\n\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tint rtp_port = g_random_int_range(rtp_range_min, rtp_range_max);\n\t\t\tif(rtp_port % 2)\n\t\t\t\trtp_port++;\t/* Pick an even port for RTP */\n\t\t\tif(use_ipv6_address_family) {\n\t\t\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)&video_rtp_address;\n\t\t\t\taddr->sin6_family = AF_INET6;\n\t\t\t\taddr->sin6_port = htons(rtp_port);\n\t\t\t\taddr->sin6_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? in6addr_any : janus_network_local_media_ip.ipv6;\n\t\t\t} else {\n\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&video_rtp_address;\n\t\t\t\taddr->sin_family = AF_INET;\n\t\t\t\taddr->sin_port = htons(rtp_port);\n\t\t\t\taddr->sin_addr.s_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? INADDR_ANY : janus_network_local_media_ip.ipv4.s_addr;\n\t\t\t}\n\t\t\tif(bind(session->media.video_rtp_fd, (struct sockaddr *)(&video_rtp_address), addrlen) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Bind failed for video RTP (port %d), error (%s), trying a different one...\\n\", rtp_port, g_strerror(errno));\n\t\t\t\tclose(session->media.video_rtp_fd);\n\t\t\t\tsession->media.video_rtp_fd = -1;\n\t\t\t\tattempts--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Video RTP listener bound to [%s]:%d(%d)\\n\",\n\t\t\t\tjanus_network_address_is_null(&janus_network_local_media_ip) ? \"any\" : local_media_ip, rtp_port, session->media.video_rtp_fd);\n\t\t\tint rtcp_port = rtp_port+1;\n\t\t\tif(use_ipv6_address_family) {\n\t\t\t\tstruct sockaddr_in6 *addr = (struct sockaddr_in6 *)&video_rtcp_address;\n\t\t\t\taddr->sin6_family = AF_INET6;\n\t\t\t\taddr->sin6_port = htons(rtcp_port);\n\t\t\t\taddr->sin6_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? in6addr_any : janus_network_local_media_ip.ipv6;\n\t\t\t} else {\n\t\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)&video_rtcp_address;\n\t\t\t\taddr->sin_family = AF_INET;\n\t\t\t\taddr->sin_port = htons(rtcp_port);\n\t\t\t\taddr->sin_addr.s_addr = janus_network_address_is_null(&janus_network_local_media_ip) ? INADDR_ANY : janus_network_local_media_ip.ipv4.s_addr;\n\t\t\t}\n\t\t\tif(bind(session->media.video_rtcp_fd, (struct sockaddr *)(&video_rtcp_address), addrlen) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Bind failed for video RTCP (port %d), error (%s), trying a different one...\\n\", rtcp_port, g_strerror(errno));\n\t\t\t\t/* RTP socket is not valid anymore, reset it */\n\t\t\t\tclose(session->media.video_rtp_fd);\n\t\t\t\tsession->media.video_rtp_fd = -1;\n\t\t\t\tclose(session->media.video_rtcp_fd);\n\t\t\t\tsession->media.video_rtcp_fd = -1;\n\t\t\t\tattempts--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Video RTCP listener bound to [%s]:%d(%d)\\n\",\n\t\t\t\tjanus_network_address_is_null(&janus_network_local_media_ip) ? \"any\" : local_media_ip, rtcp_port, session->media.video_rtcp_fd);\n\t\t\tsession->media.local_video_rtp_port = rtp_port;\n\t\t\tsession->media.local_video_rtcp_port = rtcp_port;\n\t\t}\n\t}\n\tif(!update) {\n\t\t/* We need this to quickly interrupt the poll when it's time to update a session or wrap up */\n\t\tpipe(session->media.pipefd);\n\t}\n\treturn 0;\n}\n\n/* Helper method to (re)connect RTP/RTCP sockets */\nstatic void janus_sip_connect_sockets(janus_sip_session *session, struct sockaddr_storage *audio_server_addr, struct sockaddr_storage *video_server_addr) {\n\tif(!session || (!audio_server_addr && !video_server_addr))\n\t\treturn;\n\n\t/* Connect peers (FIXME This pretty much sucks right now) */\n\tif(session->media.remote_audio_rtp_port && audio_server_addr && session->media.audio_rtp_fd != -1) {\n\t\tif(audio_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)audio_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_audio_rtp_port);\n\t\t} else if(audio_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)audio_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_audio_rtp_port);\n\t\t}\n\t\tif(connect(session->media.audio_rtp_fd, (struct sockaddr *)audio_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Couldn't connect audio RTP? (%s:%d)\\n\", session->account.username,\n\t\t\t\tsession->media.remote_audio_ip, session->media.remote_audio_rtp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s]   -- %d (%s)\\n\", session->account.username, errno, g_strerror(errno));\n\t\t}\n\t}\n\tif(session->media.remote_audio_rtcp_port && audio_server_addr && session->media.audio_rtcp_fd != -1) {\n\t\tif(audio_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)audio_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_audio_rtcp_port);\n\t\t} else if(audio_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)audio_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_audio_rtcp_port);\n\t\t}\n\t\tif(connect(session->media.audio_rtcp_fd, (struct sockaddr *)audio_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Couldn't connect audio RTCP? (%s:%d)\\n\", session->account.username,\n\t\t\t\tsession->media.remote_audio_ip, session->media.remote_audio_rtcp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s]   -- %d (%s)\\n\", session->account.username, errno, g_strerror(errno));\n\t\t}\n\t}\n\tif(session->media.remote_video_rtp_port && video_server_addr && session->media.video_rtp_fd != -1) {\n\t\tif(video_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)video_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_video_rtp_port);\n\t\t} else if(video_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)video_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_video_rtp_port);\n\t\t}\n\t\tif(connect(session->media.video_rtp_fd, (struct sockaddr *)video_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Couldn't connect video RTP? (%s:%d)\\n\", session->account.username,\n\t\t\t\tsession->media.remote_video_ip, session->media.remote_video_rtp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s]   -- %d (%s)\\n\", session->account.username, errno, g_strerror(errno));\n\t\t}\n\t}\n\tif(session->media.remote_video_rtcp_port && video_server_addr && session->media.video_rtcp_fd != -1) {\n\t\tif(video_server_addr->ss_family == AF_INET6) {\n\t\t\tstruct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)video_server_addr;\n\t\t\taddr6->sin6_port = htons(session->media.remote_video_rtcp_port);\n\t\t} else if(video_server_addr->ss_family == AF_INET) {\n\t\t\tstruct sockaddr_in *addr = (struct sockaddr_in *)video_server_addr;\n\t\t\taddr->sin_port = htons(session->media.remote_video_rtcp_port);\n\t\t}\n\t\tif(connect(session->media.video_rtcp_fd, (struct sockaddr *)video_server_addr, sizeof(struct sockaddr_storage)) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Couldn't connect video RTCP? (%s:%d)\\n\", session->account.username,\n\t\t\t\tsession->media.remote_video_ip, session->media.remote_video_rtcp_port);\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s]   -- %d (%s)\\n\", session->account.username, errno, g_strerror(errno));\n\t\t}\n\t}\n}\n\nstatic void janus_sip_media_cleanup(janus_sip_session *session) {\n\tif(session->media.audio_rtp_fd != -1) {\n\t\tclose(session->media.audio_rtp_fd);\n\t\tsession->media.audio_rtp_fd = -1;\n\t}\n\tif(session->media.audio_rtcp_fd != -1) {\n\t\tclose(session->media.audio_rtcp_fd);\n\t\tsession->media.audio_rtcp_fd = -1;\n\t}\n\tsession->media.local_audio_rtp_port = 0;\n\tsession->media.local_audio_rtcp_port = 0;\n\tsession->media.remote_audio_rtp_port = 0;\n\tsession->media.remote_audio_rtcp_port = 0;\n\tsession->media.audio_ssrc = 0;\n\tsession->media.audio_ssrc_peer = 0;\n\tif(session->media.video_rtp_fd != -1) {\n\t\tclose(session->media.video_rtp_fd);\n\t\tsession->media.video_rtp_fd = -1;\n\t}\n\tif(session->media.video_rtcp_fd != -1) {\n\t\tclose(session->media.video_rtcp_fd);\n\t\tsession->media.video_rtcp_fd = -1;\n\t}\n\tsession->media.local_video_rtp_port = 0;\n\tsession->media.local_video_rtcp_port = 0;\n\tsession->media.remote_video_rtp_port = 0;\n\tsession->media.remote_video_rtcp_port = 0;\n\tsession->media.video_ssrc = 0;\n\tsession->media.video_ssrc_peer = 0;\n\tsession->media.simulcast_ssrc = 0;\n\tif(session->media.pipefd[0] > 0) {\n\t\tclose(session->media.pipefd[0]);\n\t\tsession->media.pipefd[0] = -1;\n\t}\n\tif(session->media.pipefd[1] > 0) {\n\t\tclose(session->media.pipefd[1]);\n\t\tsession->media.pipefd[1] = -1;\n\t}\n\t/* Clean up SRTP stuff, if needed */\n\tjanus_sip_srtp_cleanup(session);\n\n\t/* Media fields not cleaned up elsewhere */\n\tjanus_sip_media_reset(session);\n}\n\n/* Thread to relay RTP/RTCP frames coming from the SIP peer */\nstatic void *janus_sip_relay_thread(void *data) {\n\tjanus_sip_session *session = (janus_sip_session *)data;\n\tif(!session) {\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tif(!session->account.username || !session->callee) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Starting relay thread (%s <--> %s)\\n\", session->account.username, session->callee);\n\n\tif(!session->callee) {\n\t\tJANUS_LOG(LOG_WARN, \"[SIP-%s] Leaving thread, no callee...\\n\", session->account.username);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\t/* File descriptors */\n\tsocklen_t addrlen;\n\tstruct sockaddr_in remote;\n\tint resfd = 0, bytes = 0, pollerrs = 0;\n\tstruct pollfd fds[5];\n\tint pipe_fd = session->media.pipefd[0];\n\tchar buffer[1500];\n\tmemset(buffer, 0, 1500);\n\tif(pipe_fd == -1) {\n\t\t/* If the pipe file descriptor doesn't exist, it means we're done already,\n\t\t * and/or we may never be notified about sessions being closed, so give up */\n\t\tJANUS_LOG(LOG_WARN, \"[SIP-%s] Leaving thread, no pipe file descriptor...\\n\", session->account.username);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\t/* Loop */\n\tint num = 0;\n\tgboolean goon = TRUE;\n\n\tsession->media.updated = TRUE; /* Connect UDP sockets upon loop entry */\n\tgboolean have_audio_server_ip = TRUE;\n\tgboolean have_video_server_ip = TRUE;\n\n\twhile(goon && session != NULL && !g_atomic_int_get(&session->destroyed) &&\n\t\t\tsession->status > janus_sip_call_status_idle &&\n\t\t\tsession->status < janus_sip_call_status_closing) {\t/* FIXME We need a per-call watchdog as well */\n\n\t\tif(session->media.updated) {\n\t\t\t/* Apparently there was a session update, or the loop has just been entered */\n\t\t\tsession->media.updated = FALSE;\n\n\t\t\t/* Resolve the addresses, if needed */\n\t\t\thave_audio_server_ip = FALSE;\n\t\t\thave_video_server_ip = FALSE;\n\t\t\tstruct sockaddr_storage audio_server_addr = { 0 }, video_server_addr = { 0 };\n\t\t\tif(session->media.remote_audio_ip && strcmp(session->media.remote_audio_ip, \"0.0.0.0\")) {\n\t\t\t\tif(janus_network_resolve_address(session->media.remote_audio_ip, &audio_server_addr) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Couldn't resolve audio address '%s'\\n\",\n\t\t\t\t\t\tsession->account.username, session->media.remote_audio_ip);\n\t\t\t\t} else {\n\t\t\t\t\t/* Address resolved */\n\t\t\t\t\thave_audio_server_ip = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(session->media.remote_video_ip && strcmp(session->media.remote_video_ip, \"0.0.0.0\")) {\n\t\t\t\tif(janus_network_resolve_address(session->media.remote_video_ip, &video_server_addr) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Couldn't resolve video address '%s'\\n\",\n\t\t\t\t\t\tsession->account.username, session->media.remote_video_ip);\n\t\t\t\t} else {\n\t\t\t\t\t/* Address resolved */\n\t\t\t\t\thave_video_server_ip = TRUE;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif(have_audio_server_ip || have_video_server_ip) {\n\t\t\t\tjanus_sip_connect_sockets(session, have_audio_server_ip ? &audio_server_addr : NULL,\n\t\t\t\t\thave_video_server_ip ? &video_server_addr : NULL);\n\t\t\t} else if(session->media.remote_audio_ip == NULL && session->media.remote_video_ip == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%p] Couldn't update session details: both audio and video remote IP addresses are NULL\\n\",\n\t\t\t\t\tsession->account.username);\n\t\t\t} else {\n\t\t\t\tif(session->media.remote_audio_ip)\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%p] Couldn't update session details: audio remote IP address (%s) is invalid\\n\",\n\t\t\t\t\t\tsession->account.username, session->media.remote_audio_ip);\n\t\t\t\tif(session->media.remote_video_ip)\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%p] Couldn't update session details: video remote IP address (%s) is invalid\\n\",\n\t\t\t\t\t\tsession->account.username, session->media.remote_video_ip);\n\t\t\t}\n\n\t\t\t/* In case we're on hold (remote address is 0.0.0.0) set the send properties to FALSE */\n\t\t\tif(have_audio_server_ip && !strcmp(session->media.remote_audio_ip, \"0.0.0.0\")) {\n\t\t\t\tsession->media.audio_send = FALSE;\n\t\t\t\tsession->media.audio_recv = FALSE;\n\t\t\t}\n\t\t\tif(have_video_server_ip && !strcmp(session->media.remote_video_ip, \"0.0.0.0\")) {\n\t\t\t\tsession->media.video_send = FALSE;\n\t\t\t\tsession->media.video_recv = FALSE;\n\t\t\t}\n\t\t}\n\n\t\t/* Prepare poll */\n\t\tnum = 0;\n\t\tif(session->media.audio_rtp_fd != -1) {\n\t\t\tfds[num].fd = session->media.audio_rtp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(session->media.audio_rtcp_fd != -1) {\n\t\t\tfds[num].fd = session->media.audio_rtcp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(session->media.video_rtp_fd != -1) {\n\t\t\tfds[num].fd = session->media.video_rtp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(session->media.video_rtcp_fd != -1) {\n\t\t\tfds[num].fd = session->media.video_rtcp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\t/* Finally, let's add the pipe */\n\t\tpipe_fd = session->media.pipefd[0];\n\t\tif(pipe_fd == -1) {\n\t\t\t/* Pipe was closed? Means the call is over */\n\t\t\tbreak;\n\t\t}\n\t\tfds[num].fd = pipe_fd;\n\t\tfds[num].events = POLLIN;\n\t\tfds[num].revents = 0;\n\t\tnum++;\n\t\t/* Wait for some data */\n\t\tresfd = poll(fds, num, 1000);\n\t\tif(resfd < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Got an EINTR (%s), ignoring...\\n\", session->account.username, g_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Error polling...\\n\", session->account.username);\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s]   -- %d (%s)\\n\", session->account.username, errno, g_strerror(errno));\n\t\t\tbreak;\n\t\t} else if(resfd == 0) {\n\t\t\t/* No data, keep going */\n\t\t\tcontinue;\n\t\t}\n\t\tif(session == NULL || g_atomic_int_get(&session->destroyed) ||\n\t\t\t\tsession->status <= janus_sip_call_status_idle ||\n\t\t\t\tsession->status >= janus_sip_call_status_closing)\n\t\t\tbreak;\n\t\tint i = 0;\n\t\tfor(i=0; i<num; i++) {\n\t\t\tif(fds[i].revents & (POLLERR | POLLHUP)) {\n\t\t\t\t/* If we just updated the session, let's wait until things have calmed down */\n\t\t\t\tif(session->media.updated)\n\t\t\t\t\tbreak;\n\t\t\t\t/* Check the socket error */\n\t\t\t\tint error = 0;\n\t\t\t\tsocklen_t errlen = sizeof(error);\n\t\t\t\tgetsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen);\n\t\t\t\tif(error == 0) {\n\t\t\t\t\t/* Maybe not a breaking error after all? */\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(error == 111) {\n\t\t\t\t\t/* ICMP error? If it's related to RTCP, let's just close the RTCP socket and move on */\n\t\t\t\t\tif(fds[i].fd == session->media.audio_rtcp_fd) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[SIP-%s] Got a '%s' on the audio RTCP socket, closing it\\n\",\n\t\t\t\t\t\t\tsession->account.username, g_strerror(error));\n\t\t\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t\t\tclose(session->media.audio_rtcp_fd);\n\t\t\t\t\t\tsession->media.audio_rtcp_fd = -1;\n\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if(fds[i].fd == session->media.video_rtcp_fd) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[SIP-%s] Got a '%s' on the video RTCP socket, closing it\\n\",\n\t\t\t\t\t\t\tsession->account.username, g_strerror(error));\n\t\t\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t\t\tclose(session->media.video_rtcp_fd);\n\t\t\t\t\t\tsession->media.video_rtcp_fd = -1;\n\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* FIXME Should we be more tolerant of ICMP errors on RTP sockets as well? */\n\t\t\t\tpollerrs++;\n\t\t\t\tif(pollerrs < 100)\n\t\t\t\t\tcontinue;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Too many errors polling %d (socket #%d): %s...\\n\", session->account.username,\n\t\t\t\t\tfds[i].fd, i, fds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\");\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s]   -- %d (%s)\\n\", session->account.username, error, g_strerror(error));\n\t\t\t\tgoon = FALSE;\t/* Can we assume it's pretty much over, after a POLLERR? */\n\t\t\t\t/* FIXME Simulate a \"hangup\" coming from the application */\n\t\t\t\tjanus_sip_hangup_media(session->handle);\n\t\t\t\tbreak;\n\t\t\t} else if(fds[i].revents & POLLIN) {\n\t\t\t\tif(pipe_fd != -1 && fds[i].fd == pipe_fd) {\n\t\t\t\t\t/* Poll interrupted for a reason, go on */\n\t\t\t\t\tint code = 0;\n\t\t\t\t\t(void)read(pipe_fd, &code, sizeof(int));\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* Got an RTP/RTCP packet */\n\t\t\t\tif(session->media.audio_rtp_fd != -1 && fds[i].fd == session->media.audio_rtp_fd) {\n\t\t\t\t\t/* Got something audio (RTP) */\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(session->media.audio_rtp_fd, buffer, 1500, 0, (struct sockaddr*)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || !janus_is_rtp(buffer, bytes)) {\n\t\t\t\t\t\t/* Failed to read or not an RTP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpollerrs = 0;\n\t\t\t\t\tif(!session->media.audio_recv) {\n\t\t\t\t\t\t/* Dropping audio packet, we weren't expecting anything */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(session->media.on_hold && session->media.hold_audio_dir != JANUS_SDP_RECVONLY) {\n\t\t\t\t\t\t/* Dropping video packet, the call is on hold and we're not receiving anything */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buffer;\n\t\t\t\t\tjanus_sip_check_rfc2833(session, buffer, bytes);\n\t\t\t\t\tif(session->media.audio_ssrc_peer == 0) {\n\t\t\t\t\t\tsession->media.audio_ssrc_peer = ntohl(header->ssrc);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Got SIP peer audio SSRC: %\"SCNu32\"\\n\", session->media.audio_ssrc_peer);\n\t\t\t\t\t}\n\t\t\t\t\t/* Is this SRTP? */\n\t\t\t\t\tif(session->media.has_srtp_remote_audio) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect(session->media.audio_srtp_in, buffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok && res != srtp_err_status_replay_fail && res != srtp_err_status_replay_old) {\n\t\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Audio SRTP unprotect error: %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), bytes, buflen, timestamp, seq);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* Check if the SSRC changed (e.g., after a re-INVITE or UPDATE) */\n\t\t\t\t\tjanus_rtp_header_update(header, &session->media.acontext, FALSE, 0);\n\t\t\t\t\t/* Check if there are forwarders interested in this traffic */\n\t\t\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\t\t\tGHashTableIter iter;\n\t\t\t\t\tgpointer value;\n\t\t\t\t\tg_hash_table_iter_init(&iter, session->peer_audio_forwarders);\n\t\t\t\t\twhile(session->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\t\t\t\t\tif(rtp_forward->is_data || rtp_forward->is_video)\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\tjanus_rtp_forwarder_send_rtp(rtp_forward, buffer, bytes, 0);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t\t\t/* Save the frame if we're recording */\n\t\t\t\t\theader->ssrc = htonl(session->media.audio_ssrc_peer);\n\t\t\t\t\tjanus_recorder_save_frame(session->arc_peer, buffer, bytes);\n\t\t\t\t\t/* Relay to application */\n\t\t\t\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = FALSE, .buffer = buffer, .length = bytes };\n\t\t\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\t\t\t/* Add audio-level extension, if present */\n\t\t\t\t\tif(session->media.audio_level_extension_id != -1) {\n\t\t\t\t\t\tgboolean vad = FALSE;\n\t\t\t\t\t\tint level = -1;\n\t\t\t\t\t\tif(janus_rtp_header_extension_parse_audio_level(buffer, bytes,\n\t\t\t\t\t\t\t\tsession->media.audio_level_extension_id, &vad, &level) == 0) {\n\t\t\t\t\t\t\trtp.extensions.audio_level = level;\n\t\t\t\t\t\t\trtp.extensions.audio_level_vad = vad;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(session->media.audio_rtcp_fd != -1 && fds[i].fd == session->media.audio_rtcp_fd) {\n\t\t\t\t\t/* Got something audio (RTCP) */\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(session->media.audio_rtcp_fd, buffer, 1500, 0, (struct sockaddr*)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || !janus_is_rtcp(buffer, bytes)) {\n\t\t\t\t\t\t/* Failed to read or not an RTCP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpollerrs = 0;\n\t\t\t\t\tif(!session->media.video_recv) {\n\t\t\t\t\t\t/* Dropping video packet, we weren't expecting anything */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(session->media.on_hold && session->media.hold_video_dir != JANUS_SDP_RECVONLY) {\n\t\t\t\t\t\t/* Dropping video packet, the call is on hold and we're not receiving anything */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Is this SRTCP? */\n\t\t\t\t\tif(session->media.has_srtp_remote_audio) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect_rtcp(session->media.audio_srtp_in, buffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok && res != srtp_err_status_replay_fail && res != srtp_err_status_replay_old) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Audio SRTCP unprotect error: %s (len=%d-->%d)\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), bytes, buflen);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* Relay to application */\n\t\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = -1, .video = FALSE, .buffer = buffer, bytes };\n\t\t\t\t\tgateway->relay_rtcp(session->handle, &rtcp);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(session->media.video_rtp_fd != -1 && fds[i].fd == session->media.video_rtp_fd) {\n\t\t\t\t\t/* Got something video (RTP) */\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(session->media.video_rtp_fd, buffer, 1500, 0, (struct sockaddr*)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || !janus_is_rtp(buffer, bytes)) {\n\t\t\t\t\t\t/* Failed to read or not an RTP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpollerrs = 0;\n\t\t\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buffer;\n\t\t\t\t\tif(session->media.video_ssrc_peer == 0) {\n\t\t\t\t\t\tsession->media.video_ssrc_peer = ntohl(header->ssrc);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Got SIP peer video SSRC: %\"SCNu32\"\\n\", session->media.video_ssrc_peer);\n\t\t\t\t\t}\n\t\t\t\t\t/* Is this SRTP? */\n\t\t\t\t\tif(session->media.has_srtp_remote_video) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect(session->media.video_srtp_in, buffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok && res != srtp_err_status_replay_fail && res != srtp_err_status_replay_old) {\n\t\t\t\t\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\t\t\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Video SRTP unprotect error: %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), bytes, buflen, timestamp, seq);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* Check if the SSRC changed (e.g., after a re-INVITE or UPDATE) */\n\t\t\t\t\tjanus_rtp_header_update(header, &session->media.vcontext, TRUE, 0);\n\t\t\t\t\t/* Check if there are forwarders interested in this traffic */\n\t\t\t\t\tjanus_mutex_lock(&session->rtp_forwarders_mutex);\n\t\t\t\t\tGHashTableIter iter;\n\t\t\t\t\tgpointer value;\n\t\t\t\t\tg_hash_table_iter_init(&iter, session->peer_video_forwarders);\n\t\t\t\t\twhile(session->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\t\t\t\t\tif(!rtp_forward->is_video)\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\tjanus_rtp_forwarder_send_rtp(rtp_forward, buffer, bytes, 0);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&session->rtp_forwarders_mutex);\n\t\t\t\t\t/* Save the frame if we're recording */\n\t\t\t\t\theader->ssrc = htonl(session->media.video_ssrc_peer);\n\t\t\t\t\tjanus_recorder_save_frame(session->vrc_peer, buffer, bytes);\n\t\t\t\t\t/* Relay to application */\n\t\t\t\t\tjanus_plugin_rtp rtp = { .mindex = -1, .video = TRUE, .buffer = buffer, .length = bytes };\n\t\t\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\t\t\t/* Add video-orientation extension, if present */\n\t\t\t\t\tif(session->media.video_orientation_extension_id > 0) {\n\t\t\t\t\t\tgboolean c = FALSE, f = FALSE, r1 = FALSE, r0 = FALSE;\n\t\t\t\t\t\tif(janus_rtp_header_extension_parse_video_orientation(buffer, bytes,\n\t\t\t\t\t\t\t\tsession->media.video_orientation_extension_id, &c, &f, &r1, &r0) == 0) {\n\t\t\t\t\t\t\trtp.extensions.video_rotation = 0;\n\t\t\t\t\t\t\tif(r1 && r0)\n\t\t\t\t\t\t\t\trtp.extensions.video_rotation = 270;\n\t\t\t\t\t\t\telse if(r1)\n\t\t\t\t\t\t\t\trtp.extensions.video_rotation = 180;\n\t\t\t\t\t\t\telse if(r0)\n\t\t\t\t\t\t\t\trtp.extensions.video_rotation = 90;\n\t\t\t\t\t\t\trtp.extensions.video_back_camera = c;\n\t\t\t\t\t\t\trtp.extensions.video_flipped = f;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(session->media.video_rtcp_fd != -1 && fds[i].fd == session->media.video_rtcp_fd) {\n\t\t\t\t\t/* Got something video (RTCP) */\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(session->media.video_rtcp_fd, buffer, 1500, 0, (struct sockaddr*)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || !janus_is_rtcp(buffer, bytes)) {\n\t\t\t\t\t\t/* Failed to read or not an RTCP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tpollerrs = 0;\n\t\t\t\t\t/* Is this SRTCP? */\n\t\t\t\t\tif(session->media.has_srtp_remote_video) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect_rtcp(session->media.video_srtp_in, buffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok && res != srtp_err_status_replay_fail && res != srtp_err_status_replay_old) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Video SRTP unprotect error: %s (len=%d-->%d)\\n\",\n\t\t\t\t\t\t\t\tsession->account.username, janus_srtp_error_str(res), bytes, buflen);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* Relay to application */\n\t\t\t\t\tjanus_plugin_rtcp rtcp = { .mindex = -1, .video = TRUE, .buffer = buffer, bytes };\n\t\t\t\t\tgateway->relay_rtcp(session->handle, &rtcp);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* Cleanup the media session */\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_sip_media_cleanup(session);\n\tjanus_mutex_unlock(&session->mutex);\n\t/* Done */\n\tJANUS_LOG(LOG_VERB, \"Leaving SIP relay thread\\n\");\n\tsession->relayer_thread = NULL;\n\tjanus_refcount_decrease(&session->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\n\n/* Sofia Event thread */\ngpointer janus_sip_sofia_thread(gpointer user_data) {\n\tjanus_sip_session *session = (janus_sip_session *)user_data;\n\tif(session == NULL) {\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tif(session->account.username == NULL) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Joining sofia loop thread (%s)...\\n\", session->account.username);\n\tsession->stack = g_malloc0(sizeof(ssip_t));\n\tsu_home_init(session->stack->s_home);\n\tsession->stack->session = session;\n\tsession->stack->s_nua = NULL;\n\tsession->stack->s_nh_r = NULL;\n\tsession->stack->s_nh_i = NULL;\n\tsession->stack->s_nh_m = NULL;\n\tsession->stack->s_root = su_root_create(session->stack);\n\tsession->stack->subscriptions = NULL;\n\tjanus_mutex_init(&session->stack->smutex);\n\tJANUS_LOG(LOG_VERB, \"Setting up sofia stack (sip:%s@%s)\\n\", session->account.username, local_ip);\n\tchar sip_url[128];\n\tchar sips_url[128];\n\tchar *ipv6;\n\tipv6 = strstr(local_ip, \":\");\n\tif(session->account.force_tcp)\n\t\tg_snprintf(sip_url, sizeof(sip_url), \"sip:%s%s%s:*;transport=tcp\", ipv6 ? \"[\" : \"\", local_ip, ipv6 ? \"]\" : \"\");\n\telse\n\t\tg_snprintf(sip_url, sizeof(sip_url), \"sip:%s%s%s:*;transport=udp\", ipv6 ? \"[\" : \"\", local_ip, ipv6 ? \"]\" : \"\");\n\tg_snprintf(sips_url, sizeof(sips_url), \"sips:%s%s%s:*;transport=tls\", ipv6 ? \"[\" : \"\", local_ip, ipv6 ? \"]\" : \"\");\n\tchar outbound_options[256] = \"use-rport no-validate\";\n\tif(keepalive_interval > 0)\n\t\tjanus_strlcat(outbound_options, \" options-keepalive\", sizeof(outbound_options));\n\tif(!behind_nat)\n\t\tjanus_strlcat(outbound_options, \" no-natify\", sizeof(outbound_options));\n\tsession->stack->s_nua = nua_create(session->stack->s_root,\n\t\t\t\tjanus_sip_sofia_callback,\n\t\t\t\tsession,\n\t\t\t\tSIPTAG_ALLOW_STR(\"INVITE, ACK, BYE, CANCEL, OPTIONS, REFER, MESSAGE, INFO, NOTIFY\"),\n\t\t\t\tNUTAG_M_USERNAME(session->account.username),\n\t\t\t\tNUTAG_URL(sip_url),\n\t\t\t\tTAG_IF(session->account.sips, NUTAG_SIPS_URL(sips_url)),\n\t\t\t\tTAG_IF(session->account.sips && sips_certs_dir, NUTAG_CERTIFICATE_DIR(sips_certs_dir)),\n\t\t\t\tSIPTAG_USER_AGENT_STR(session->account.user_agent ? session->account.user_agent : user_agent),\n\t\t\t\tNUTAG_KEEPALIVE(keepalive_interval * 1000),\t/* Sofia expects it in milliseconds */\n\t\t\t\tNUTAG_OUTBOUND(outbound_options),\n\t\t\t\tNUTAG_APPL_METHOD(\"REFER\"),\t\t\t/* We'll respond to incoming REFER messages ourselves */\n\t\t\t\tSIPTAG_SUPPORTED_STR(\"replaces\"),\t/* Advertise that we support the Replaces header */\n\t\t\t\tSIPTAG_SUPPORTED(NULL),\n\t\t\t\tNTATAG_CANCEL_2543(session->account.rfc2543_cancel),\n\t\t\t\tNTATAG_SIP_T1X64(sip_timer_t1x64),\n\t\t\t\tTAG_NULL());\n\tif(query_contact_header)\n\t\tnua_get_params(session->stack->s_nua, SIPTAG_FROM_STR(\"\"), TAG_END());\n\tsu_root_run(session->stack->s_root);\n\t/* When we get here, we're done */\n\tjanus_mutex_lock(&session->stack->smutex);\n\tnua_t *s_nua = session->stack->s_nua;\n\tsession->stack->s_nua = NULL;\n\tjanus_mutex_unlock(&session->stack->smutex);\n\tif(session->stack->s_nh_r != NULL) {\n\t\tnua_handle_destroy(session->stack->s_nh_r);\n\t\tsession->stack->s_nh_r = NULL;\n\t}\n\tif(session->stack->s_nh_i != NULL) {\n\t\tnua_handle_destroy(session->stack->s_nh_i);\n\t\tsession->stack->s_nh_i = NULL;\n\t}\n\tif(session->stack->s_nh_m != NULL) {\n\t\tnua_handle_destroy(session->stack->s_nh_m);\n\t\tsession->stack->s_nh_m = NULL;\n\t}\n\tjanus_mutex_lock(&session->stack->smutex);\n\tif(session->stack->subscriptions != NULL)\n\t\tg_hash_table_unref(session->stack->subscriptions);\n\tsession->stack->subscriptions = NULL;\n\tjanus_mutex_unlock(&session->stack->smutex);\n\tnua_destroy(s_nua);\n\tsu_root_destroy(session->stack->s_root);\n\tsession->stack->s_root = NULL;\n\tjanus_refcount_decrease(&session->ref);\n\tJANUS_LOG(LOG_VERB, \"Leaving sofia loop thread...\\n\");\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\n/* Check peer RTP has RFC2833 and push event */\nstatic void janus_sip_check_rfc2833(janus_sip_session *session, char *buffer, int len) {\n\tif(session->media.dtmf_pt <= 0)\n\t\treturn;\n\tjanus_rtp_header *rtp_header = (janus_rtp_header *)buffer;\n\tif(rtp_header->type != session->media.dtmf_pt)\n\t\treturn;\n\tint plen = 0;\n\tchar *payload_buffer = janus_rtp_payload(buffer, len, &plen);\n\tif(plen < 0 || (size_t)plen < sizeof(janus_rtp_rfc2833_payload))\n\t\treturn;\n\tjanus_rtp_rfc2833_payload *rfc2833_payload = (janus_rtp_rfc2833_payload *)payload_buffer;\n\tuint16_t duration = ntohs(rfc2833_payload->duration);\n\tif(rfc2833_payload->end == 0)\n\t\treturn;\n\n\t/* Set up last dtmf to avoid duplication */\n\tif(session->latest_dtmf.dtmf_event_id == rfc2833_payload->event && session->latest_dtmf.timestamp == rtp_header->timestamp)\n\t\treturn;\n\tsession->latest_dtmf.dtmf_event_id = rfc2833_payload->event;\n\tsession->latest_dtmf.timestamp = rtp_header->timestamp;\n\n\t/* Parse dtmf key */\n\tuint16_t dtmf_key;\n\tif(rfc2833_payload->event > 15)\n\t\treturn;\n  \tdtmf_key = dtmf_keys[rfc2833_payload->event];\n\tchar dtmf_key_str[2];\n\tdtmf_key_str[0] = dtmf_key;\n\tdtmf_key_str[1] = '\\0';\n\n\t/* Notify the application */\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"sip\", json_string(\"event\"));\n\tjson_t *result = json_object();\n\tjson_object_set_new(result, \"event\", json_string(\"dtmf\"));\n\tjson_object_set_new(result, \"sender\", json_string(session->callee));\n\tjson_object_set_new(result, \"signal\", json_string(dtmf_key_str));\n\tjson_object_set_new(result, \"duration\", json_integer(duration));\n\tif(session->callid)\n\t\tjson_object_set_new(info, \"call_id\", json_string(session->callid));\n\tjson_object_set_new(info, \"result\", result);\n\tint ret = gateway->push_event(session->handle, &janus_sip_plugin, session->transaction, info, NULL);\n\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\tjson_decref(info);\n\treturn;\n}\n\n/* Helper method to send an RTCP PLI to the SIP peer */\nstatic void janus_sip_rtcp_pli_send(janus_sip_session *session) {\n\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(!janus_sip_call_is_established(session))\n\t\treturn;\n\tif(!session->media.has_video || session->media.video_rtcp_fd == -1)\n\t\treturn;\n\t/* Generate a PLI */\n\tchar rtcp_buf[12];\n\tint rtcp_len = 12;\n\tjanus_rtcp_pli((char *)&rtcp_buf, rtcp_len);\n\t/* Fix SSRCs as the Janus core does */\n\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Fixing SSRCs (local %u, peer %u)\\n\",\n\t\tsession->account.username, session->media.video_ssrc, session->media.video_ssrc_peer);\n\tjanus_rtcp_fix_ssrc(NULL, (char *)rtcp_buf, rtcp_len, 1, session->media.video_ssrc, session->media.video_ssrc_peer);\n\t/* Is SRTP involved? */\n\tif(session->media.has_srtp_local_video) {\n\t\tchar sbuf[50];\n\t\tmemcpy(&sbuf, rtcp_buf, rtcp_len);\n\t\tint protected = rtcp_len;\n\t\tint res = srtp_protect_rtcp(session->media.video_srtp_out, &sbuf, &protected);\n\t\tif(res != srtp_err_status_ok) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[SIP-%s] Video SRTCP protect error... %s (len=%d-->%d)...\\n\",\n\t\t\t\tsession->account.username, janus_srtp_error_str(res), rtcp_len, protected);\n\t\t} else {\n\t\t\t/* Forward the message to the peer */\n\t\t\tif(send(session->media.video_rtcp_fd, sbuf, protected, 0) < 0) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending SRTCP video packet... %s (len=%d)...\\n\",\n\t\t\t\t\tsession->account.username, g_strerror(errno), protected);\n\t\t\t}\n\t\t}\n\t} else {\n\t\t/* Forward the message to the peer */\n\t\tif(send(session->media.video_rtcp_fd, rtcp_buf, rtcp_len, 0) < 0) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"[SIP-%s] Error sending RTCP video packet... %s (len=%d)...\\n\",\n\t\t\t\tsession->account.username, g_strerror(errno), rtcp_len);\n\t\t}\n\t}\n}\n\n/* RTP forwarder helpers */\nstatic janus_rtp_forwarder *janus_sip_rtp_forwarder_add_helper(janus_sip_session *session, const char *type,\n\t\tconst gchar *host, int port, int pt, uint32_t ssrc, int srtp_suite, const char *srtp_crypto) {\n\tif(!session || !type || !host)\n\t\treturn NULL;\n\tgboolean is_video = !strcasecmp(type, \"video\") || !strcasecmp(type, \"peer_video\");\n\tgboolean is_peer = !strcasecmp(type, \"peer_audio\") || !strcasecmp(type, \"peer_video\");\n\t/* Create a new RTP forwarder */\n\tjanus_rtp_forwarder *rf = janus_rtp_forwarder_create(JANUS_SIP_NAME, 0,\n\t\tsession->udp_sock, host, port, ssrc, pt, srtp_suite, srtp_crypto, FALSE, 0, is_video, FALSE);\n\tif(rf == NULL)\n\t\treturn NULL;\n\trf->metadata = g_strdup(type);\n\t/* Add the forwarder to the ones we have for the publisher stream */\n\tg_hash_table_insert(session->all_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\tif(!is_video && !is_peer) {\n\t\tg_hash_table_insert(session->audio_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t} else if(is_video && !is_peer) {\n\t\tg_hash_table_insert(session->video_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t\tgateway->send_pli(session->handle);\n\t} else if(!is_video && is_peer) {\n\t\tg_hash_table_insert(session->peer_audio_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t} else if(is_video && is_peer) {\n\t\tg_hash_table_insert(session->peer_video_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\t\tjanus_sip_rtcp_pli_send(session);\n\t}\n\t/* Done */\n\tJANUS_LOG(LOG_VERB, \"[SIP-%s] Added %s' %s rtp_forward: %s:%d stream_id: %\"SCNu32\"\\n\",\n\t\tsession->account.username, is_peer ? \"peer\" : \"user\", is_video ? \"video\" : \"audio\", host, port, rf->stream_id);\n\treturn rf;\n}\n\nstatic json_t *janus_sip_rtp_forwarder_summary(janus_rtp_forwarder *f) {\n\tif(f == NULL)\n\t\treturn NULL;\n\tjson_t *json = json_object();\n\tjson_object_set_new(json, \"stream_id\", json_integer(f->stream_id));\n\tif(f->metadata)\n\t\tjson_object_set_new(json, \"type\", json_string((const char *)f->metadata));\n\tchar address[100];\n\tif(f->serv_addr.sin_family == AF_INET) {\n\t\tjson_object_set_new(json, \"host\", json_string(\n\t\t\tinet_ntop(AF_INET, &f->serv_addr.sin_addr, address, sizeof(address))));\n\t} else {\n\t\tjson_object_set_new(json, \"host\", json_string(\n\t\t\tinet_ntop(AF_INET6, &f->serv_addr6.sin6_addr, address, sizeof(address))));\n\t}\n\tjson_object_set_new(json, \"port\", json_integer(ntohs(f->serv_addr.sin_port)));\n\tjson_object_set_new(json, \"media\", json_string(f->is_video ? \"video\" : \"audio\"));\n\tif(f->payload_type > 0)\n\t\tjson_object_set_new(json, \"pt\", json_integer(f->payload_type));\n\tif(f->ssrc)\n\t\tjson_object_set_new(json, \"ssrc\", json_integer(f->ssrc));\n\tif(f->is_srtp)\n\t\tjson_object_set_new(json, \"srtp\", json_true());\n\treturn json;\n}\n"
  },
  {
    "path": "src/plugins/janus_streaming.c",
    "content": "/*! \\file   janus_streaming.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Streaming plugin\n * \\details Check the \\ref streaming for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page streaming Streaming plugin documentation\n * This is a streaming plugin for Janus, allowing WebRTC peers\n * to watch/listen to pre-recorded files or media generated by another tool.\n * Specifically, the plugin currently supports three different type of streams:\n *\n * -# on-demand streaming of pre-recorded media files (different\n * streaming context for each peer);\n * -# live streaming of pre-recorded media files (shared streaming\n * context for all peers attached to the stream);\n * -# live streaming of media generated by another tool (shared\n * streaming context for all peers attached to the stream).\n *\n * For what concerns types 1. and 2., considering the proof of concept\n * nature of the implementation the only pre-recorded media files\n * that the plugins supports right now are Opus, raw mu-Law and a-Law files:\n * support is of course planned for other additional widespread formats\n * as well.\n *\n * For what concerns type 3., instead, the plugin is configured\n * to listen on a few ports for RTP: this means that the plugin\n * is implemented to receive RTP on those ports and relay them to all\n * peers attached to that stream. Any tool that can generate audio/video\n * RTP streams and specify a destination is good for the purpose: the\n * examples section contains samples that make use of GStreamer (http://gstreamer.freedesktop.org/)\n * but other tools like FFmpeg (http://www.ffmpeg.org/), LibAV (http://libav.org/)\n * or others are fine as well. This makes it really easy to capture and\n * encode whatever you want using your favourite tool, and then have it\n * transparently broadcasted via WebRTC using Janus. Notice that we recently\n * added  the possibility to also add a datachannel track to an RTP streaming\n * mountpoint: this allows you to send, via UDP, a text-based message to\n * relay via datachannels (e.g., the title of the current song, if this\n * is a radio streaming channel). When using this feature, though, beware\n * that you'll have to stay within the boundaries of the MTU, as each\n * message will have to stay within the size of an UDP packet.\n *\n * Streams to make available are listed in the plugin configuration file.\n * A pre-filled configuration file is provided in \\c conf/janus.plugin.streaming.jcfg\n * and includes some examples you can start from.\n *\n * To add more streams or modify the existing ones, you can use the following\n * syntax:\n *\n * \\verbatim\nstream-name: {\n\t[settings]\n}\n\\endverbatim\n *\n * with the allowed settings listed below. Notice that, for the sake of\n * simplicity, only the global and legacy properties are listed in the\n * following text: you can use the new stream-based syntax as well (see\n * the 'multistream-test' example below for reference), but in that case\n * all properties whose names start with audio/video/data should be\n * renamed to have that prefix removed. Please refer to the plugin API\n * documentation for more details, as the static configuration in that\n * case mirrors the dynamic API syntax.\n *\n * \\verbatim\ntype = rtp|live|ondemand|rtsp\n       rtp = stream originated by an external tool (e.g., gstreamer or\n             ffmpeg) and sent to the plugin via RTP\n       live = local file streamed live to multiple viewers\n              (multiple viewers = same streaming context)\n       ondemand = local file streamed on-demand to a single listener\n                  (multiple viewers = different streaming contexts)\n       rtsp = stream originated by an external RTSP feed (only\n              available if libcurl support was compiled)\nid = <unique numeric ID>\ndescription = This is my awesome stream\nmetadata = An optional string that can contain any metadata (e.g., JSON)\n\t\t\tassociated with the stream you want users to receive\nis_private = true|false (private streams don't appear when you do a 'list' request)\nfilename = path to the local file to stream (only for live/ondemand)\nsecret = <optional password needed for manipulating (e.g., destroying\n\t\tor enabling/disabling) the stream>\npin = <optional password needed for watching the stream>\naudio = true|false (do/don't stream audio)\nvideo = true|false (do/don't stream video)\n   The following options are only valid for the 'rtp' type:\ndata = true|false (do/don't stream text via datachannels)\naudioport = local port for receiving audio frames\naudiortcpport = local port for receiving and sending audio RTCP feedback\naudiomcast = multicast group for receiving audio frames, if any\naudioiface = network interface or IP address to bind to, if any (binds to all otherwise)\naudiopt = <audio RTP payload type> (e.g., 111)\naudiocodec = name of the audio codec (opus)\naudiofmtp = Codec specific parameters, if any\naudioskew = true|false (whether the plugin should perform skew\n\tanalysis and compensation on incoming audio RTP stream, EXPERIMENTAL)\nvideoport = local port for receiving video frames (only for rtp)\nvideortcpport = local port for receiving and sending video RTCP feedback\nvideomcast = multicast group for receiving video frames, if any\nvideoiface = network interface or IP address to bind to, if any (binds to all otherwise)\nvideopt = <video RTP payload type> (e.g., 100)\nvideocodec = name of the video codec (vp8)\nvideofmtp = Codec specific parameters, if any\nvideosimulcast = true|false (do|don't enable video simulcasting)\nvideoport2 = second local port for receiving video frames (only for rtp, and simulcasting)\nvideoport3 = third local port for receiving video frames (only for rtp, and simulcasting)\nvideoskew = true|false (whether the plugin should perform skew\n\tanalysis and compensation on incoming video RTP stream, EXPERIMENTAL)\nvideosvc = true|false (whether the video will have SVC support; works only for VP9-SVC, default=false)\nh264sps = if using H.264 as a video codec, value of the sprop-parameter-sets\n\tthat would normally be sent via SDP, but that we'll use to instead\n\tmanually ingest SPS and PPS packets via RTP for streams that miss it\ncollision = in case of collision (more than one SSRC hitting the same port), the plugin\n\twill discard incoming RTP packets with a new SSRC unless this many milliseconds\n\tpassed, which would then change the current SSRC (0=disabled)\ndataport = local port for receiving data messages to relay\ndatamcast = multicast group for receiving data messages, if any\ndataiface = network interface or IP address to bind to, if any (binds to all otherwise)\ndatatype = text|binary (type of data this mountpoint will relay, default=text)\ndatabuffermsg = true|false (whether the plugin should store the latest\n\tmessage and send it immediately for new viewers)\nthreads = number of threads to assist with the relaying part, which can help\n\tif you expect a lot of viewers that may cause the RTP receiving part\n\tin the Streaming plugin to slow down and fail to catch up (default=0)\n\nNote: by default, the Streaming plugin only forwards the latest packets\nit receives, never performing any buffering. This means that, for video\nstreams, new viewers may initially start receiving frames they cannot\ndecode right away, since they'd refer to keyframes that were sent before\nthey joined. In such scenarios, they'd have to wait until the next keyframe\narrives before video can be decoded and displayed, which could take a\nwhile depending on the frequency of keyframes encoded by the source.\nFor forwarded streams (e.g., from the VideoRoom) this can be easily\naddressed by using the RTCP support. For sources that can't or won't\nhonour dynamic keyframe requests, a partial and experimental solution\nmight be storing the latest keyframe and the following deltas, to send\nto new viewers before new live packet are delivered. This feature can\nbe enabled in the Streaming plugin using the 'bufferkf_ms' and/or\nthe 'bufferkf_bytes' properties, which configure how many milliseconds\nor how many bytes (in total) should be stored any time a keyframe is\nreceived: data exceeding those limits won't be stored, until a new\nkeyframe arrives. The two properties are not mutually exclusive, and\ncan be configured at the same time: in that case, the first one that\nhits the limit stops the buffering of the current keyframe. Notice\nthat, again, this feature should be considered highly experimental,\nand that it comes with a few considerable drawbacks: the most obvious\none is that, depending on how many packets were stored, new viewers\nmay be hit with a considerable burst of data as soon as they connect,\nwhich may negatively impact performance or even cause issues of its own.\nAlso notice that this is a global, and not per-stream, feature: this\nmeans that if you create a mountpoint with two video streams, the\nfeature will impact both of them, and both streams will have a buffer\nof their own. This also works for RTSP mountpoints.\n\tbufferkf_ms = how many milliseconds of packets to store, starting\n\t\tfrom a new keyframe (default=0)\n\tbufferkf_bytes = how many bytes of packets to store, starting\n\t\tfrom a new keyframe (default=0)\n\nIn case you want to use SRTP for your RTP-based mountpoint, you'll need\nto configure the SRTP-related properties as well, namely the suite to\nuse for hashing (32 or 80) and the crypto information for decrypting\nthe stream (as a base64 encoded string the way SDES does it). Notice\nthat with SRTP involved you'll have to pay extra attention to what you\nfeed the mountpoint, as you may risk getting SRTP decrypt errors:\nsrtpsuite = 32\nsrtpcrypto = WbTBosdVUZqEb6Htqhn+m3z7wUh4RJVR8nE15GbN\n\nThe Streaming plugin can also be used to (re)stream media that has been\nencrypted using something that can be consumed via Insertable Streams.\nIn that case, we only need to be aware of it, so that we can send the\ninfo along with the SDP. How to decrypt the media is out of scope, and\nup to the application since, again, this is end-to-end encryption and\nso neither Janus nor the Streaming plugin have access to anything.\nDO NOT SET THIS PROPERTY IF YOU DON'T KNOW WHAT YOU'RE DOING!\ne2ee = true\n\nTo allow mountpoints to negotiate the playout-delay RTP extension,\nyou can set the 'playoutdelay_ext' property to true: this way, any\nsubscriber can customize the playout delay of incoming video streams,\nassuming the browser supports the RTP extension in the first place.\nplayoutdelay_ext = true\n\nTo allow mountpoints to negotiate the abs-capture-time RTP extension,\nyou can set the 'abscapturetime_src_ext_id' property to value in range 1..14 inclusive: this way, any\nsubscriber can receive the abs-capture-time of incoming RTP streams,\nassuming the browser supports the RTP extension in the first place.\nIncoming RTP stream should provide abs-capture-time exactly in the same header id.\nabscapturetime_src_ext_id = 1\n\nThe following options are only valid for the 'rtsp' type:\nurl = RTSP stream URL\nrtsp_user = RTSP authorization username, if needed\nrtsp_pwd = RTSP authorization password, if needed\nrtsp_quirk = Some RTSP servers offer the stream using only the path, instead of the fully qualified URL.\n\tIf set true, this boolean informs Janus that we should try a path-only DESCRIBE request if the initial request returns 404.\nrtsp_failcheck = whether an error should be returned if connecting to the RTSP server fails (default=true)\nrtspiface = network interface IP address or device name to listen on when receiving RTSP streams\nrtsp_reconnect_delay = after n seconds passed and no media assumed, the RTSP server has gone and schedule a reconnect (default=5s)\nrtsp_session_timeout = by default the streaming plugin will check the RTSP connection with an OPTIONS query,\n\tthe value of the timeout comes from the RTSP session initializer and by default\n\tthis session timeout is the half of this value In some cases this value can be too high (for example more than one minute)\n\tbecause of the media server. In that case this plugin will calculate the timeout with this\n\tformula: timeout = min(session_timeout, rtsp_session_timeout / 2). (default=0s)\nrtsp_timeout = communication timeout (CURLOPT_TIMEOUT) for cURL call gathering the RTSP information (default=10s)\nrtsp_conn_timeout = connection timeout for cURL (CURLOPT_CONNECTTIMEOUT) call gathering the RTSP information (default=5s)\nrtsp_notify_changes = if set to true, will send an event to connected users when the RTSP session\n\tgets disconnected, and when it's reconnected (default=false)\n\\endverbatim\n *\n * Notice that attributes like \\c audioport or \\c videopt only make sense\n * when you're creating a mountpoint with a single audio and/or video stream,\n * as the plugin in that case assumes that limitation is fine by you. In\n * case you're interested in creating multistream mountpoints, that is\n * mountpoints that can contain more than one audio and/or video stream\n * at the same time, you HAVE to use a different syntax. Specifically,\n * you'll need to use a \\c media array/list, containing the different\n * streams, in the right order, that you want to make available: each\n * stream will then need to contain the related info, e.g., port to bind\n * to, type of media, codec name and so on. An example is provided below:\n *\n\\verbatim\nmultistream-test: {\n\ttype = \"rtp\"\n\tid = 123\n\tdescription = \"Multistream test (1 audio, 2 video)\"\n\tmedia = (\n\t\t{\n\t\t\ttype = \"audio\"\n\t\t\tmid = \"a\"\n\t\t\tlabel = \"Audio stream\"\n\t\t\tport = 5102\n\t\t\tpt = 111\n\t\t\tcodec = \"opus\"\n\t\t},\n\t\t{\n\t\t\ttype = \"video\"\n\t\t\tmid = \"v1\"\n\t\t\tlabel = \"Video stream #1\"\n\t\t\tport = 5104\n\t\t\tpt = 100\n\t\t\tcodec = \"vp8\"\n\t\t},\n\t\t{\n\t\t\ttype = \"video\"\n\t\t\tmid = \"v2\"\n\t\t\tlabel = \"Video stream #2\"\n\t\t\tport = 5106\n\t\t\tpt = 100\n\t\t\tcodec = \"vp8\"\n\t\t}\n\t)\n}\n\\endverbatim\n *\n * In the above example, we're creating a mountpoint with a single audio\n * stream and two different video streams: each stream has a unique \\c mid\n * (that you MUST provide) which is what will be used for the SDP offer\n * to send to viewers, and their unique configuration properties. As you\n * can see, it's much cleaner in the way you create and configure\n * mountpoints: there's no hardcoded audio/video prefix for the name of\n * properties, you configure media streams the same way and just add them\n * to a list. Notice that of course this also works with the simple one\n * audio/one video mountpoints you've used so far, and that has been\n * documented before: as such, you're encouraged to start using this\n * new approach as soon as possible, since in the next versions we\n * might deprecate the old one.\n *\n * \\section streamapi Streaming API\n *\n * The Streaming API supports several requests, some of which are\n * synchronous and some asynchronous. There are some situations, though,\n * (invalid JSON, invalid request) which will always result in a\n * synchronous error response even for asynchronous requests.\n *\n * \\c list , \\c info , \\c create , \\c destroy , \\c recording , \\c edit ,\n * \\c enable and \\c disable are synchronous requests, which means you'll\n * get a response directly within the context of the transaction. \\c list\n * lists all the available streams; \\c create allows you to create a new\n * mountpoint dynamically, as an alternative to using the configuration\n * file; \\c destroy removes a mountpoint and destroys it; \\c recording\n * instructs the plugin on whether or not a live RTP stream should be\n * recorded while it's broadcasted; \\c enable and \\c disable respectively\n * enable and disable a mountpoint, that is decide whether or not a\n * mountpoint should be available to users without destroying it.\n * \\c edit allows you to dynamically edit some mountpoint properties (e.g., the PIN);\n *\n * The \\c watch , \\c start , \\c configure , \\c pause , \\c switch and \\c stop requests\n * instead are all asynchronous, which means you'll get a notification\n * about their success or failure in an event. \\c watch asks the plugin\n * to prepare the playout of one of the available streams; \\c start\n * starts the actual playout; \\c pause allows you to pause a playout\n * without tearing down the PeerConnection; \\c switch allows you to\n * switch to a different mountpoint of the same kind (note: only live\n * RTP mountpoints supported as of now) without having to stop and watch\n * the new one; \\c stop stops the playout and tears the PeerConnection\n * down.\n *\n * Notice that, in general, all users can create mountpoints, no matter\n * what type they are. If you want to limit this functionality, you can\n * configure an admin \\c admin_key in the plugin settings. When\n * configured, only \"create\" requests that include the correct\n * \\c admin_key value in an \"admin_key\" property will succeed, and will\n * be rejected otherwise.\n *\n * \\subsection streamingsync Synchronous requests\n *\n * To list the available Streaming mountpoints (both those created via\n * configuration file and those created via API), you can use the \\c list\n * request:\n *\n\\verbatim\n{\n\t\"request\" : \"list\"\n}\n\\endverbatim\n *\n * If successful, it will return an array with a list of all the mountpoints.\n * Notice that only the public mountpoints will be returned: those with\n * an \\c is_private set to yes/true will be skipped. The response will\n * be formatted like this:\n *\n\\verbatim\n{\n\t\"streaming\" : \"list\",\n\t\"list\" : [\n\t\t{\n\t\t\t\"id\" : <unique ID of mountpoint #1>,\n\t\t\t\"type\" : \"<type of mountpoint #1, in line with the types introduced above>\",\n\t\t\t\"description\" : \"<description of mountpoint #1>\",\n\t\t\t\"metadata\" : \"<metadata of mountpoint #1, if any>\",\n\t\t\t\"enabled\" : <true|false, depending on whether the mountpoint is currently enabled or not>,\n\t\t\t\"media\" : [\n\t\t\t\t{\n\t\t\t\t\t\"mid\" : \"<unique mid of this stream>\",\n\t\t\t\t\t\"label\" : \"<unique text label of this stream>\",\n\t\t\t\t\t\"msid\" : \"<msid of this stream, if configured>\",\n\t\t\t\t\t\"type\" : \"<audio|video|data\">,\n\t\t\t\t\t\"age_ms\" : <how much time passed since we last received media for this stream; optional>,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t// Other streams, if available\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"id\" : <unique ID of mountpoint #2>,\n\t\t\t\"type\" : \"<type of mountpoint #2, in line with the types introduced above>\",\n\t\t\t\"description\" : \"<description of mountpoint #2>\",\n\t\t\t\"metadata\" : \"<metadata of mountpoint #2, if any>\",\n\t\t\t\"media\" : [..]\n\t\t},\n\t\t...\n\t]\n}\n\\endverbatim\n *\n * As you can see, the \\c list request only returns very generic info on\n * each mountpoint. In case you're interested in learning more details about\n * a specific mountpoint, you can use the \\c info request instead, which\n * returns more information, or all of it if the mountpoint secret is\n * provided in the request. An \\c info request must be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"info\"\n\t\"id\" : <unique ID of mountpoint to query>,\n\t\"secret\" : <mountpoint secret; optional, can be used to return more info>\"\n}\n\\endverbatim\n *\n * If successful, this will have the plugin return an object containing\n * more info on the mountpoint:\n *\n\\verbatim\n{\n\t\"streaming\" : \"info\",\n\t\"info\" : {\n\t\t\"id\" : <unique ID of mountpoint>,\n\t\t\"name\" : \"<unique name of mountpoint>\",\n\t\t\"description\" : \"<description of mountpoint>\",\n\t\t\"metadata\" : \"<metadata of mountpoint, if any>\",\n\t\t\"secret\" : \"<secret of mountpoint; only available if a valid secret was provided>\",\n\t\t\"pin\" : \"<PIN to access mountpoint; only available if a valid secret was provided>\",\n\t\t\"is_private\" : <true|false, depending on whether the mountpoint is listable; only available if a valid secret was provided>,\n\t\t\"viewers\" : <count of current subscribers, if any>,\n\t\t\"enabled\" : <true|false, depending on whether the mountpoint is currently enabled or not>,\n\t\t\"type\" : \"<type of mountpoint>\",\n\t\t\"media\" : [\n\t\t\t{\n\t\t\t\t\"mid\" : \"<unique mid of this stream>\",\n\t\t\t\t\"mindex\" : \"<unique mindex of this stream>\",\n\t\t\t\t\"type\" : \"<audio|video|data\">,\n\t\t\t\t\"label\" : \"<unique text label of this stream>\",\n\t\t\t\t\"msid\" : \"<msid of this stream, if configured>\",\n\t\t\t\t\"age_ms\" : <how much time passed since we last received media for this stream; optional>,\n\t\t\t\t\"pt\" : <payload type, only present if RTP and configured>,\n\t\t\t\t\"codec\" : \"<cocec name value, only present if RTP and configured>\",\n\t\t\t\t\"rtpmap\" : \"<SDP rtpmap value, only present if RTP and configured>\",\n\t\t\t\t\"fmtp\" : \"<audio SDP fmtp value, only present if RTP and configured>\",\n\t\t\t\t...\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Other streams, if available\n\t\t\t}\n\t\t]\n\t}\n}\n\\endverbatim\n *\n * Considering the different mountpoint types that you can create in this\n * plugin, the nature of the rest of the returned info obviously depends\n * on which mountpoint you're querying. This is especially true for RTP\n * and RTSP mountpoints. Notice that info like the ports an RTP mountpoint\n * is listening on will only be returned if you provide the correct secret,\n * as otherwise they're treated like sensitive information and are not\n * returned to generic \\c info calls.\n *\n * We've seen how you can create a new mountpoint via configuration file,\n * but you can create one via API as well, using the \\c create request.\n * Most importantly, you can also choose whether or not a \\c create\n * request should result in the mountpoint being saved to configuration\n * file so that it's still available after a server restart. The common\n * syntax for all \\c create requests is the following:\n *\n\\verbatim\n{\n\t\"request\" : \"create\",\n\t\"admin_key\" : \"<plugin administrator key; mandatory if configured>\",\n\t\"type\" : \"<type of the mountpoint to create; mandatory>\",\n\t\"id\" : <unique ID to assign the mountpoint; optional, will be chosen by the server if missing>,\n\t\"name\" : \"<unique name for the mountpoint; optional, will be chosen by the server if missing>\",\n\t\"description\" : \"<description of mountpoint; optional>\",\n\t\"metadata\" : \"<metadata of mountpoint; optional>\",\n\t\"secret\" : \"<secret to query/edit the mountpoint later; optional>\",\n\t\"pin\" : \"<PIN required for viewers to access mountpoint; optional>\",\n\t\"is_private\" : <true|false, whether the mountpoint should be listable; true by default>,\n\t\"media\" : [\n\t\t{\n\t\t\t\"type\" : \"<audio|video|data>\",\n\t\t\t\"mid\" : \"<unique mid to assign to this stream in negotiated PeerConnections>\",\n\t\t\t\"msid\" : \"<msid to add to the m-line, if needed>\",\n\t\t\t\"label\", \"<optional label to name the track>\",\n\t\t\t\"mcast\", \"<multicast group for receiving packets, if any>\",\n\t\t\t\"iface\", \"<network interface or IP address to bind to, if any (binds to all otherwise)>\",\n\t\t\t\"port\" : <port to bind to, to receive media to relay>\",\n\t\t\t\"rtcpport\", <port to bind to for receiving and sending audio RTCP feedback, if any>,\n\t\t\t\"pt\", <RTP payload type (audio/video only)>,\n\t\t\t\"codec\", \"<name of the codec that will be used for this track (audio/video only)>\",\n\t\t\t\"fmtp\", \"<codec specific parameters, if any (audio/video only)>\",\n\t\t\t\"skew\", <true|false (whether the plugin should perform skew\n\t\t\t\tanalysis and compensation on incoming RTP streams, EXPERIMENTAL)>\n\t\t\t\"simulcast\", <true|false (do|don't enable video simulcasting)>,\n\t\t\t\"port2\", <second local port for receiving video frames (only for RTP video, and simulcasting)>,\n\t\t\t\"port3\", <third local port for receiving video frames (only for RTP video, and simulcasting)>,\n\t\t\t\"svc\", <true|false (whether the RTP video will have SVC support; works only for VP9-SVC, default=false)>,\n\t\t\t\"datatype\", \"<text|binary (type of data this mountpoint will relay, default=text)>\",\n\t\t\t...\n\t\t}.\n\t\t... other streams, if any ...\n\t],\n\t...\n\t\"permanent\" : <true|false, whether the mountpoint should be saved to configuration file or not; false by default>,\n\t...\n}\n\\endverbatim\n *\n * Of course, different mountpoint types will have different properties\n * you can specify in a \\c create. Please refer to the documentation on\n * configuration files to see the fields you can pass. The only important\n * difference to highlight is that, unlike in configuration files, you will\n * NOT have to escape semicolons with a trailing slash, in those properties\n * where a semicolon might be needed (e.g., \\c audiofmtp or \\c videofmtp ).\n *\n * Notice that, just as we introduced the possibility of configuring multistream\n * mountpoints statically with a \\c media array, the same applies when using\n * the API to create them: just add a \\c media JSON array containing the\n * list of streams to create and the related properties as you would do\n * statically (that is, using generic properties like \\c port, \\c fmtp,\n * etc., rather than the hardcoded \\c audioport and the like), and it\n * will work for dynamically created mountpoints as well.\n *\n * A successful \\c create will result in a \\c created response:\n *\n\\verbatim\n{\n\t\"streaming\" : \"created\",\n\t\"create\" : \"<unique name of the just created mountpoint>\",\n\t\"permanent\" : <true|false, depending on whether the mountpoint was saved to configuration file or not>,\n\t\"stream\": {\n\t\t\"id\" : <unique ID of the just created mountpoint>,\n\t\t\"type\" : \"<type of the just created mountpoint>\",\n\t\t\"description\" : \"<description of the just created mountpoint>\",\n\t\t\"is_private\" : <true|false, depending on whether the new mountpoint is listable>,\n\t\t\"ports\" : [\t\t// Only for RTP mountpoints\n\t\t\t{\n\t\t\t\t\"type\" : \"<audio|video|data>\",\n\t\t\t\t\"mid\" : \"<unique mid of stream #1>\",\n\t\t\t\t\"msid\" : \"<msid of this stream, if configured>\",\n\t\t\t\t\"port\" : <port the plugin is listening on for this stream's media>\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Other streams, if available\n\t\t\t}\n\t\t]\n\t\t...\n\t}\n}\n\\endverbatim\n *\n * Notice that additional information, namely the ports the mountpoint\n * bound to, will only be added for new RTP mountpoints, otherwise this\n * is all that a \\c created request will contain. If you want to double\n * check everything in your \\c create request went as expected, you may\n * want to issue a followup \\c info request to compare the results.\n *\n * Once you created a mountpoint, you can modify some (not all) of its\n * properties via an \\c edit request. Namely, you can only modify generic\n * properties like the mountpoint description, the secret, the PIN and\n * whether or not the mountpoint should be listable. All other properties\n * are considered to be immutable. Again, you can choose whether the changes\n * should be permanent, e.g., saved to configuration file, or not. Notice\n * that an \\c edit request requires the right secret to be provided, if\n * the mountpoint has one, or will return an error instead. The \\c edit\n * request must be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"edit\",\n\t\"id\" : <unique ID of the mountpoint to edit; mandatory>,\n\t\"secret\" : \"<secret to edit the mountpoint; mandatory if configured>\",\n\t\"new_description\" : \"<new description for the mountpoint; optional>\",\n\t\"new_metadata\" : \"<new metadata for the mountpoint; optional>\",\n\t\"new_secret\" : \"<new secret for the mountpoint; optional>\",\n\t\"new_pin\" : \"<new PIN for the mountpoint, PIN will be removed if set to an empty string; optional>\",\n\t\"new_is_private\" : <true|false, depending on whether the mountpoint should be now listable; optional>,\n\t\"permanent\" : <true|false, whether the mountpoint should be saved to configuration file or not; false by default>,\n\t\"edited_event\" : <true|false, whether an event will be sent to all viewers when metadata is updated; false by default>\n}\n\\endverbatim\n *\n * A successful \\c edit will result in an \\c edited response:\n *\n\\verbatim\n{\n\t\"streaming\" : \"edited\",\n\t\"id\" : <unique ID of the just edited mountpoint>,\n\t\"permanent\" : <true|false, depending on whether the changes were saved to configuration file or not>\n}\n\\endverbatim\n *\n * In case \\c edited_event was set to \\c true in the request, a successful \\c edit will\n * also result in an \\c edited event sent to all viewers when the metadata has changed:\n *\n\\verbatim\n{\n\t\"streaming\" : \"edited\",\n\t\"id\" : <unique ID of the just edited mountpoint>,\n\t\"metadata\" : \"<updated metadata for the mountpoint>\",\n}\n\\endverbatim\n *\n * Just as you can create and edit mountpoints, you can of course also destroy\n * them. Again, this applies to all mountpoints, whether created statically\n * via configuration file or dynamically via API, and the mountpoint destruction\n * can be made permanent in the configuration file as well. A \\c destroy\n * request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"destroy\",\n\t\"id\" : <unique ID of the mountpoint to destroy; mandatory>,\n\t\"secret\" : \"<secret to destroy the mountpoint; mandatory if configured>\",\n\t\"permanent\" : <true|false, whether the mountpoint should be removed from the configuration file or not; false by default>\n}\n\\endverbatim\n *\n * If successful, the result will be confirmed in a \\c destroyed event:\n *\n\\verbatim\n{\n\t\"streaming\" : \"destroyed\",\n\t\"id\" : <unique ID of the just destroyed mountpoint>\n}\n\\endverbatim\n *\n * Notice that destroying a mountpoint while viewers are still subscribed\n * to it will result in all viewers being removed, and their PeerConnection\n * closed as a consequence.\n *\n * You can also dynamically enable and disable mountpoints via API. A\n * disabled mountpoint is a mountpoint that exists, and still works as\n * expected, but is not accessible to viewers until it's enabled again.\n * This is a useful property, especially in case of mountpoints that\n * need to be prepared in advance but must not be accessible until a\n * specific moment, and a much better alternative to just create the\n * mountpoint at the very last minute and destroy it otherwise. The\n * syntax for both the \\c enable and \\c disable requests is the same,\n * and looks like the following:\n *\n\\verbatim\n{\n\t\"request\" : \"enable\",\n\t\"id\" : <unique ID of the mountpoint to enable; mandatory>,\n\t\"secret\" : \"<secret to enable the mountpoint; mandatory if configured>\"\n}\n\\endverbatim\n *\n * If successful, a generic \\c ok is returned:\n *\n\\verbatim\n{\n\t\"streaming\" : \"ok\"\n}\n\\endverbatim\n\\verbatim\n{\n\t\"request\" : \"disable\",\n\t\"id\" : <unique ID of the mountpoint to disable; mandatory>,\n\t\"stop_recording\" : <true|false, whether the recording should also be stopped or not; true by default>\n\t\"secret\" : \"<secret to disable the mountpoint; mandatory if configured>\"\n}\n\\endverbatim\n *\n * If successful, a generic \\c ok is returned:\n *\n\\verbatim\n{\n\t\"streaming\" : \"ok\"\n}\n\\endverbatim\n *\n * You can kick all viewers from a mountpoint using the \\c kick_all request. Notice\n * that this only removes all viewers, but does not prevent them from starting to watch\n * the mountpoint again. Please note this request works with all mountpoint types,\n * except for on-demand streaming. The \\c kick_all request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"kick_all\",\n\t\"id\" : <unique ID of the mountpoint to disable; mandatory>,\n\t\"secret\" : \"<mountpoint secret; mandatory if configured>\",\n}\n\\endverbatim\n *\n * If successful, a \\c kicked_all response is returned:\n *\n\\verbatim\n{\n\t\"streaming\" : \"kicked_all\",\n}\n\\endverbatim\n *\n * Finally, you can record a mountpoint to the internal Janus .mjr format\n * using the \\c recording request. The same request can also be used to\n * stop recording. Although the same request is used in both cases, though,\n * the syntax for the two use cases differs a bit, namely in terms of the\n * type of some properties. Notice that, while for backwards compatibility\n * you can still use the old \\c audio, \\c video and \\c data named properties,\n * they're now deprecated and so you're highly encouraged to use the new drill-down\n * \\c media list instead.\n *\n * To start recording a new mountpoint, the request should be formatted\n * like this:\n *\n\\verbatim\n{\n\t\"request\" : \"recording\",\n\t\"action\" : \"start\",\n\t\"id\" : <unique ID of the mountpoint to manipulate; mandatory>,\n\t\"media\" : [\t\t// Drill-down recording controls\n\t\t{\n\t\t\t\"mid\" : \"<mid of the stream to start recording>\",\n\t\t\t\"filename\" : \"<base path/filename to use for the recording>\"\n\t\t},\n\t\t{\n\t\t\t// Recording controls for other streams, if provided\n\t\t}\n\t]\n}\n\\endverbatim\n *\n * To stop a recording, instead, this is the request syntax:\n *\n\\verbatim\n{\n\t\"request\" : \"recording\",\n\t\"action\" : \"stop\",\n\t\"id\" : <unique ID of the mountpoint to manipulate; mandatory>,\n\t\"media\" : [\t\t// Drill-down recording controls\n\t\t{\n\t\t\t\"mid\" : \"<mid of the stream to stop recording>\"\n\t\t},\n\t\t{\n\t\t\t// Recording controls for other streams, if provided\n\t\t}\n\t]\n}\n\\endverbatim\n *\n * When using the deprecated properties, when starting a recording the \\c audio ,\n * \\c video and \\c data properties are strings, and specify the base path\n * to use for the recording filename; when stopping a recording, instead,\n * they're interpreted as boolean properties. This is one more reason why\n * you should migrate to the new \\c media list instead, as it doesn't have\n * this ambiguity between the two different requests. Notice that, as with all\n * APIs that wrap .mjr recordings, the filename you specify here is not\n * the actual filename: an \\c .mjr extension is always going to be added\n * by the Janus core, so you should take this into account when tracking\n * the related recording files.\n *\n * Whether you started or stopped a recording, a successful request will\n * always result in a simple \\c ok response:\n *\n\\verbatim\n{\n\t\"streaming\" : \"ok\"\n}\n\\endverbatim\n *\n * \\subsection streamingasync Asynchronous requests\n *\n * All the requests we've gone through so far are synchronous. This means\n * that they return a response right away. That said, many of the requests\n * this plugin supports are asynchronous instead, which means Janus will\n * send an ack when they're received, and a response will only follow\n * later on. This is especially true for requests dealing with the\n * management and setup of mountpoint viewers, e.g., for the purpose of\n * negotiating a WebRTC PeerConnection to receive media from a mountpoint.\n *\n * To subscribe to a specific mountpoint, an interested viewer can make\n * use of the \\c watch request. As suggested by the request name, this\n * instructs the plugin to setup a new PeerConnection to allow the new\n * viewer to watch the specified mountpoint. The \\c watch request must\n * be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"watch\",\n\t\"id\" : <unique ID of the mountpoint to subscribe to; mandatory>,\n\t\"pin\" : \"<PIN required to access the mountpoint; mandatory if configured>\",\n\t\"media\" : [\n\t\t<array of mids to subscribe to, as strings; optional, missing or empty array subscribes to all mids>\n\t]\n\t\"offer_audio\" : <true|false; deprecated; whether or not audio should be negotiated; true by default if the mountpoint has audio>,\n\t\"offer_video\" : <true|false; deprecated; whether or not video should be negotiated; true by default if the mountpoint has video>,\n\t\"offer_data\" : <true|false; deprecated; whether or not datachannels should be negotiated; true by default if the mountpoint has datachannels>\n}\n\\endverbatim\n *\n * As you can see, it's just a matter of specifying the ID of the mountpoint to\n * subscribe to and, if needed, the PIN to access the mountpoint in case\n * it's protected. The \\c media array is particularly interesting, as it\n * allows you to only subscribe to a subset of the mountpoint media, which\n * you can address by the related \\c mid property of each stream. By default,\n * in fact, a \\c watch request will result in the plugin preparing a new\n * SDP offer trying to negotiate all the media streams available in the\n * mountpoint; in case the viewer knows they don't support one of the\n * mountpoint codecs, though (e.g., the video in the mountpoint is VP8,\n * but they only support H.264), or are not interested in getting all the\n * media (e.g., they're ok with just audio and not video, or don't have\n * enough bandwidth for both), they can use those properties to shape the\n * SDP offer to their needs. The \\c media array is optional, as a missing\n * or empty array will simply be interpreted as a willingness to subscribe\n * to all the streams in the mountpoint, which is the default behaviour.\n * Notice that the order of the mids in the \\c media array is irrelevant,\n * as is how many times the same mid is listed in the array: the presence\n * of a mid is just interpreted as an \"on\" switch for that stream, meaning\n * it will be offered in the SDP.\n *\n * \\note For backwards compatibility, the deprecated \\c offer_audio ,\n * \\c offer_video and \\c offer_data properties are also available. They\n * also allow you to only subscribe to a subset of the mountpoint media,\n * but with a more crude approach: specifically, they dictate whether or\n * not any audio, video or data stream should be offered or not.\n *\n * As anticipated, if successful this request will generate a new JSEP SDP\n * offer, which will be attached to a \\c preparing status event:\n *\n\\verbatim\n{\n\t\"status\" : \"preparing\"\n}\n\\endverbatim\n *\n * At this stage, to complete the setup of a subscription the viewer is\n * supposed to send a JSEP SDP answer back to the plugin. This is done\n * by means of a \\c start request, which in this case MUST be associated\n * with a JSEP SDP answer but otherwise requires no arguments:\n *\n\\verbatim\n{\n\t\"request\" : \"start\"\n}\n\\endverbatim\n *\n * If successful this request returns a \\c starting status event:\n *\n\\verbatim\n{\n\t\"status\" : \"starting\"\n}\n\\endverbatim\n *\n * Once this is done, all that's needed is waiting for the WebRTC PeerConnection\n * establishment to succeed. As soon as that happens, the Streaming plugin\n * can start relaying media from the mountpoint the viewer subscribed to\n * to the viewer themselves.\n *\n * Notice that the same exact steps we just went through (\\c watch request,\n * followed by JSEP offer by the plugin, followed by \\c start request with\n * JSEP answer by the viewer) is what you also use when renegotiations are\n * needed, e.g., for the purpose of ICE restarts.\n *\n * As a viewer, you can temporarily pause and resume the whole media delivery\n * with a \\c pause and, again, \\c start request (in this case without any JSEP\n * SDP answer attached). Neither expect other arguments, as the context\n * is implicitly derived from the handle they're sent on:\n *\n\\verbatim\n{\n\t\"request\" : \"pause\"\n}\n\\endverbatim\n *\n\\verbatim\n{\n\t\"request\" : \"start\"\n}\n\\endverbatim\n *\n * Unsurprisingly, they just result in, respectively, \\c pausing and\n * \\c starting events:\n *\n\\verbatim\n{\n\t\"status\" : \"pausing\"\n}\n\\endverbatim\n *\n\\verbatim\n{\n\t\"status\" : \"starting\"\n}\n\\endverbatim\n *\n * For more drill-down manipulations of a subscription, a \\c configure\n * request can be used instead. This request allows viewers to dynamically\n * change some properties associated to their media subscription, e.g.,\n * in terms of what should and should not be sent at a specific time. A\n * \\c configure request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"configure\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"mid\" : <mid of the m-line to tweak>,\n\t\t\t\"send\" : <true|false, depending on whether the media addressed by the above mid should be relayed or not; optional>,\n\t\t\t\"substream\" : <substream to receive (0-2), in case simulcasting is enabled; optional>,\n\t\t\t\"temporal\" : <temporal layers to receive (0-2), in case simulcasting is enabled; optional>,\n\t\t\t\"fallback\" : <How much time (in us, default 250000) without receiving packets will make us drop to the substream below>,\n\t\t\t\"spatial_layer\" : <spatial layer to receive (0-1), in case VP9-SVC is enabled; optional>,\n\t\t\t\"temporal_layer\" : <temporal layers to receive (0-2), in case VP9-SVC is enabled; optional>,\n\t\t\t\"min_delay\" : <minimum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>,\n\t\t\t\"max_delay\" : <maximum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>\n\t\t},\n\t\t// Other streams, if any\n\t]\n}\n\\endverbatim\n *\n * While the deprecated \\c audio , \\c video and \\c data properties can still be\n * used as a media-level pause/resume functionality, a better option is to\n * specify the \\c mid of the stream instead, and a \\c send boolean property\n * to specify if this specific stream should be relayed or not. The \\c pause\n * and \\c start requests instead pause and resume all streams at the same time.\n * The \\c substream and \\c temporal properties, finally, only make sense\n * when the mountpoint is configured with video simulcasting support, and\n * as such the viewer is interested in receiving a specific substream\n * or temporal layer, rather than any other of the available ones.\n * The \\c spatial_layer and \\c temporal_layer have exactly the same meaning,\n * but within the context of VP9-SVC mountpoints, and will have no effect\n * on mountpoints involving a different video codec. In both cases, make\n * sure you specify the \\c mid of the stream in case multiple videos are\n * available in a mountpoint, or the request may have no effect.\n *\n * Another interesting feature in the Streaming plugin is the so-called\n * mountpoint \"switching\". Basically, when subscribed to a specific\n * mountpoint and receiving media from there, you can at any time \"switch\"\n * to a different mountpoint, and as such start receiving media from that\n * other mountpoint instead. Think of it as changing channel on a TV: you\n * keep on using the same PeerConnection, the plugin simply changes the\n * source of the media transparently. Of course, while powerful and effective\n * this request has some limitations. First of all, it only works with RTP\n * mountpoints, and not other mountpoint types; besides, the two mountpoints\n * must have the same media configuration, that is, use the same codecs,\n * the same payload types, etc. In fact, since the same PeerConnection is\n * used for this feature, switching to a mountpoint with a different\n * configuration might result in media incompatible with the PeerConnection\n * setup being relayed to the viewer, and as such in no audio/video being\n * played. That said, a \\c switch request must be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"switch\",\n\t\"id\" : <unique ID of the new mountpoint to switch to; mandatory>\n}\n\\endverbatim\n *\n * If successful, you'll be unsubscribed from the previous mountpoint,\n * and subscribed to the new mountpoint instead. The event to confirm\n * the switch was successful will look like this:\n *\n\\verbatim\n{\n\t\"switched\" : \"ok\",\n\t\"id\" : <unique ID of the new mountpoint>\n}\n\\endverbatim\n *\n * Finally, to stop the subscription to the mountpoint and tear down the\n * related PeerConnection, you can use the \\c stop request. Since context\n * is implicit, no other argument is required:\n *\n\\verbatim\n{\n\t\"request\" : \"stop\"\n}\n\\endverbatim\n *\n * If successful, the plugin will attempt to tear down the PeerConnection,\n * and will send back a \\c stopping status event:\n *\n\\verbatim\n{\n\t\"status\" : \"stopping\"\n}\n\\endverbatim\n *\n * Once a PeerConnection has been torn down and the subscription closed,\n * as a viewer you're free to subscribe to a different mountpoint instead.\n * In fact, while you can't watch more than one mountpoint at the same\n * time on the same handle, there's no limit on how many mountpoints\n * you can watch in sequence, again on the same handle. If you're interested\n * in subscribing to multiple mountpoints at the same time, instead, you'll\n * have to create multiple handles for the purpose.\n */\n\n\n#include \"plugin.h\"\n\n#include <errno.h>\n#include <netdb.h>\n#include <sys/poll.h>\n#include <sys/socket.h>\n#include <sys/time.h>\n\n#include <jansson.h>\n\n#ifdef HAVE_LIBCURL\n#include <curl/curl.h>\n#ifndef CURL_AT_LEAST_VERSION\n#define CURL_AT_LEAST_VERSION(x,y,z) 0\n#endif\n#endif\n\n#ifdef HAVE_LIBOGG\n#include <ogg/ogg.h>\n#endif\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../rtp.h\"\n#include \"../rtpsrtp.h\"\n#include \"../rtcp.h\"\n#include \"../record.h\"\n#include \"../utils.h\"\n#include \"../sdp-utils.h\"\n#include \"../ip-utils.h\"\n\n/* Default settings */\n#define JANUS_STREAMING_DEFAULT_SESSION_TIMEOUT 0 /* Overwrite the RTSP session timeout. If set to zero, the RTSP timeout is derived from a session. */\n#define JANUS_STREAMING_DEFAULT_RECONNECT_DELAY 5 /* Reconnecting delay in seconds. */\n#define JANUS_STREAMING_DEFAULT_CURL_TIMEOUT 10L /* Communication timeout for cURL. */\n#define JANUS_STREAMING_DEFAULT_CURL_CONNECT_TIMEOUT 5L /* Connection timeout for cURL. */\n\n/* Plugin information */\n#define JANUS_STREAMING_VERSION\t\t\t11\n#define JANUS_STREAMING_VERSION_STRING\t\"0.0.11\"\n#define JANUS_STREAMING_DESCRIPTION\t\t\"This is a streaming plugin for Janus, allowing WebRTC peers to watch/listen to pre-recorded files or media generated by an external source.\"\n#define JANUS_STREAMING_NAME\t\t\t\"JANUS Streaming plugin\"\n#define JANUS_STREAMING_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_STREAMING_PACKAGE\t\t\t\"janus.plugin.streaming\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_streaming_init(janus_callbacks *callback, const char *config_path);\nvoid janus_streaming_destroy(void);\nint janus_streaming_get_api_compatibility(void);\nint janus_streaming_get_version(void);\nconst char *janus_streaming_get_version_string(void);\nconst char *janus_streaming_get_description(void);\nconst char *janus_streaming_get_name(void);\nconst char *janus_streaming_get_author(void);\nconst char *janus_streaming_get_package(void);\nvoid janus_streaming_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_streaming_handle_admin_message(json_t *message);\nvoid janus_streaming_setup_media(janus_plugin_session *handle);\nvoid janus_streaming_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_streaming_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_streaming_data_ready(janus_plugin_session *handle);\nvoid janus_streaming_hangup_media(janus_plugin_session *handle);\nvoid janus_streaming_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_streaming_query_session(janus_plugin_session *handle);\nstatic int janus_streaming_get_fd_port(int fd);\n\n/* Plugin setup */\nstatic janus_plugin janus_streaming_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_streaming_init,\n\t\t.destroy = janus_streaming_destroy,\n\n\t\t.get_api_compatibility = janus_streaming_get_api_compatibility,\n\t\t.get_version = janus_streaming_get_version,\n\t\t.get_version_string = janus_streaming_get_version_string,\n\t\t.get_description = janus_streaming_get_description,\n\t\t.get_name = janus_streaming_get_name,\n\t\t.get_author = janus_streaming_get_author,\n\t\t.get_package = janus_streaming_get_package,\n\n\t\t.create_session = janus_streaming_create_session,\n\t\t.handle_message = janus_streaming_handle_message,\n\t\t.handle_admin_message = janus_streaming_handle_admin_message,\n\t\t.setup_media = janus_streaming_setup_media,\n\t\t.incoming_rtp = janus_streaming_incoming_rtp,\n\t\t.incoming_rtcp = janus_streaming_incoming_rtcp,\n\t\t.data_ready = janus_streaming_data_ready,\n\t\t.hangup_media = janus_streaming_hangup_media,\n\t\t.destroy_session = janus_streaming_destroy_session,\n\t\t.query_session = janus_streaming_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_STREAMING_NAME);\n\treturn &janus_streaming_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter id_parameters[] = {\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter idopt_parameters[] = {\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter idstr_parameters[] = {\n\t{\"id\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter idstropt_parameters[] = {\n\t{\"id\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter watch_parameters[] = {\n\t{\"pin\", JSON_STRING, 0},\n\t{\"media\", JANUS_JSON_ARRAY, 0},\n\t{\"restart\", JANUS_JSON_BOOL, 0},\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"offer_audio\", JANUS_JSON_BOOL, 0},\n\t{\"offer_video\", JANUS_JSON_BOOL, 0},\n\t{\"offer_data\", JANUS_JSON_BOOL, 0},\n};\nstatic struct janus_json_parameter adminkey_parameters[] = {\n\t{\"admin_key\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter edit_parameters[] = {\n\t{\"new_description\", JSON_STRING, 0},\n\t{\"new_metadata\", JSON_STRING, 0},\n\t{\"new_secret\", JSON_STRING, 0},\n\t{\"new_pin\", JSON_STRING, 0},\n\t{\"new_is_private\", JANUS_JSON_BOOL, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0},\n\t{\"edited_event\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter create_parameters[] = {\n\t{\"name\", JSON_STRING, 0},\n\t{\"description\", JSON_STRING, 0},\n\t{\"metadata\", JSON_STRING, 0},\n\t{\"is_private\", JANUS_JSON_BOOL, 0},\n\t{\"type\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"secret\", JSON_STRING, 0},\n\t{\"pin\", JSON_STRING, 0},\n\t{\"media\", JANUS_JSON_ARRAY, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0},\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"data\", JANUS_JSON_BOOL, 0},\n};\nstatic struct janus_json_parameter rtp_parameters[] = {\n\t{\"collision\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"bufferkf_ms\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"bufferkf_bytes\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"threads\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtpsuite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtpcrypto\", JSON_STRING, 0},\n\t{\"e2ee\", JANUS_JSON_BOOL, 0},\n\t{\"playoutdelay_ext\", JANUS_JSON_BOOL, 0},\n\t{\"abscapturetime_src_ext_id\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter live_parameters[] = {\n\t{\"filename\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"audiortpmap\", JSON_STRING, 0},\t/* Deprecated */\n\t{\"audiofmtp\", JSON_STRING, 0},\n\t{\"audiopt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter ondemand_parameters[] = {\n\t{\"filename\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"audiortpmap\", JSON_STRING, 0},\t/* Deprecated */\n\t{\"audiofmtp\", JSON_STRING, 0},\n\t{\"audiopt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\n#ifdef HAVE_LIBCURL\nstatic struct janus_json_parameter rtsp_parameters[] = {\n\t{\"url\", JSON_STRING, 0},\n\t{\"rtsp_user\", JSON_STRING, 0},\n\t{\"rtsp_pwd\", JSON_STRING, 0},\n\t{\"rtsp_quirk\", JANUS_JSON_BOOL, 0},\n\t{\"rtsp_failcheck\", JANUS_JSON_BOOL, 0},\n\t{\"rtspiface\", JSON_STRING, 0},\n\t{\"rtsp_reconnect_delay\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"rtsp_session_timeout\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"rtsp_timeout\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"rtsp_conn_timeout\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"rtsp_notify_changes\", JANUS_JSON_BOOL, 0},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"audiortpmap\", JSON_STRING, 0},\t/* Deprecated */\n\t{\"audiofmtp\", JSON_STRING, 0},\n\t{\"audiopt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"videocodec\", JSON_STRING, 0},\n\t{\"videortpmap\", JSON_STRING, 0},\t/* Deprecated */\n\t{\"videofmtp\", JSON_STRING, 0},\n\t{\"videopt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"videobufferkf\", JANUS_JSON_BOOL, 0},\t/* Deprecated for the properties below */\n\t{\"bufferkf_ms\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"bufferkf_bytes\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"threads\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n};\n#endif\nstatic struct janus_json_parameter rtp_media_parameters[] = {\n\t{\"type\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"mid\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"label\", JANUS_JSON_STRING, 0},\n\t{\"msid\", JANUS_JSON_STRING, 0},\n\t{\"mcast\", JANUS_JSON_STRING, 0},\n\t{\"iface\", JANUS_JSON_STRING, 0},\n\t{\"port\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"rtcpport\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"pt\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"codec\", JANUS_JSON_STRING, 0},\n\t{\"rtpmap\", JANUS_JSON_STRING, 0},\t/* Deprecated */\n\t{\"fmtp\", JANUS_JSON_STRING, 0},\n\t{\"skew\", JANUS_JSON_BOOL, 0},\n\t/* Video only */\n\t{\"bufferkf\", JANUS_JSON_BOOL, 0},\t/* Deprecated: see global bufferkf_ms and bufferkf_bytes */\n\t{\"simulcast\", JANUS_JSON_BOOL, 0},\n\t{\"port2\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"port3\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"svc\", JANUS_JSON_BOOL, 0},\n\t/* Data only */\n\t{\"datatype\", JANUS_JSON_STRING, 0},\n\t{\"buffermsg\", JANUS_JSON_BOOL, 0},\n};\nstatic struct janus_json_parameter rtp_audio_parameters[] = {\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"audiomcast\", JSON_STRING, 0},\n\t{\"audioport\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"audiortcpport\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audiopt\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"audiortpmap\", JSON_STRING, 0},\t/* Deprecated */\n\t{\"audiofmtp\", JSON_STRING, 0},\n\t{\"audioiface\", JSON_STRING, 0},\n\t{\"audioskew\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter rtp_video_parameters[] = {\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"videomcast\", JSON_STRING, 0},\n\t{\"videoport\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"videortcpport\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"videopt\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"videocodec\", JSON_STRING, 0},\n\t{\"videortpmap\", JSON_STRING, 0},\t/* Deprecated */\n\t{\"videofmtp\", JSON_STRING, 0},\n\t{\"videobufferkf\", JANUS_JSON_BOOL, 0},\t/* Deprecated: see global bufferkf_ms and bufferkf_bytes */\n\t{\"videoiface\", JSON_STRING, 0},\n\t{\"videosimulcast\", JANUS_JSON_BOOL, 0},\n\t{\"videoport2\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"videoport3\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"videoskew\", JANUS_JSON_BOOL, 0},\n\t{\"videosvc\", JANUS_JSON_BOOL, 0},\n\t{\"h264sps\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter rtp_data_parameters[] = {\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"dataport\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"databuffermsg\", JANUS_JSON_BOOL, 0},\n\t{\"datatype\", JSON_STRING, 0},\n\t{\"dataiface\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter destroy_parameters[] = {\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter recording_parameters[] = {\n\t{\"action\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter recording_start_parameters[] = {\n\t{\"media\", JANUS_JSON_ARRAY, 0},\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"audio\", JSON_STRING, 0},\n\t{\"video\", JSON_STRING, 0},\n\t{\"data\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter recording_stop_parameters[] = {\n\t{\"media\", JANUS_JSON_ARRAY, 0},\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"data\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter simulcast_parameters[] = {\n\t{\"substream\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"fallback\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter svc_parameters[] = {\n\t{\"spatial_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"mid\", JANUS_JSON_STRING, 0},\n\t{\"streams\", JANUS_JSON_ARRAY, 0},\n\t{\"send\", JANUS_JSON_BOOL, 0},\n\t/* For VP8 (or H.264) simulcast */\n\t{\"substream\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"fallback\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For VP9 SVC */\n\t{\"spatial_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For the playout-delay RTP extension, if negotiated */\n\t{\"min_delay\", JSON_INTEGER, 0},\n\t{\"max_delay\", JSON_INTEGER, 0},\n\t/* Deprecated parameters: still there only for\n\t * backwards compatibility, but not for long */\n\t{\"audio\", JSON_STRING, 0},\n\t{\"video\", JSON_STRING, 0},\n\t{\"data\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter disable_parameters[] = {\n\t{\"stop_recording\", JANUS_JSON_BOOL, 0}\n};\n\n/* Static configuration instance */\nstatic janus_config *config = NULL;\nstatic const char *config_folder = NULL;\nstatic janus_mutex config_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic gboolean string_ids = FALSE;\nstatic gboolean ipv6_disabled = FALSE;\nstatic janus_callbacks *gateway = NULL;\nstatic GThread *handler_thread;\nstatic void *janus_streaming_handler(void *data);\n\n/* RTP range to use for random ports */\n#define DEFAULT_RTP_RANGE_MIN 10000\n#define DEFAULT_RTP_RANGE_MAX 60000\nstatic uint16_t rtp_range_min = DEFAULT_RTP_RANGE_MIN;\nstatic uint16_t rtp_range_max = DEFAULT_RTP_RANGE_MAX;\nstatic uint16_t rtp_range_slider = DEFAULT_RTP_RANGE_MIN;\nstatic janus_mutex fd_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void *janus_streaming_ondemand_thread(void *data);\nstatic void *janus_streaming_filesource_thread(void *data);\nstatic void janus_streaming_relay_rtp_packet(gpointer data, gpointer user_data);\nstatic void janus_streaming_relay_rtcp_packet(gpointer data, gpointer user_data);\nstatic void *janus_streaming_relay_thread(void *data);\nstatic void janus_streaming_hangup_media_internal(janus_plugin_session *handle);\n\ntypedef enum janus_streaming_type {\n\tjanus_streaming_type_none = 0,\n\tjanus_streaming_type_live,\n\tjanus_streaming_type_on_demand,\n} janus_streaming_type;\n\ntypedef enum janus_streaming_source {\n\tjanus_streaming_source_none = 0,\n\tjanus_streaming_source_file,\n\tjanus_streaming_source_rtp,\n} janus_streaming_source;\n\ntypedef struct janus_streaming_rtp_keyframe {\n\tgboolean enabled;\n\tuint16_t bufferkf_ms;\n\tuint32_t bufferkf_bytes;\n\t/* If enabled, we store the packets of the last keyframe plus the\n\t * following deltas (assuming they are within the ms/bytes limits),\n\t * so that we can send them as a burst for new viewers */\n\tGList *latest_keyframe;\n\tuint32_t kf_ssrc, kf_ts, kf_bytes;\n\tint64_t kf_start;\n\tgboolean first_ts;\n\tjanus_mutex mutex;\n} janus_streaming_rtp_keyframe;\n\ntypedef struct janus_streaming_rtp_relay_packet {\n\tint mindex;\n\tjanus_rtp_header *data;\n\tgint length;\n\tgboolean is_rtp;\t/* This may be a data packet and not RTP */\n\tgboolean is_data;\n\tgboolean is_video;\n\tgboolean is_kfburst;\n\tgboolean simulcast;\n\tuint32_t ssrc[3];\n\tjanus_videocodec codec;\n\tint substream;\n\tint ptype;\n\tuint32_t timestamp;\n\tuint16_t seq_number;\n\t/* The following are only relevant for VP9 SVC*/\n\tgboolean svc;\n\tjanus_vp9_svc_info svc_info;\n\t/* The following is only relevant for datachannels */\n\tgboolean textdata;\n} janus_streaming_rtp_relay_packet;\nstatic janus_streaming_rtp_relay_packet exit_packet;\nstatic void janus_streaming_rtp_relay_packet_free(janus_streaming_rtp_relay_packet *pkt) {\n\tif(pkt == NULL || pkt == &exit_packet)\n\t\treturn;\n\tg_free(pkt->data);\n\tg_free(pkt);\n\n}\n\n#ifdef HAVE_LIBCURL\ntypedef struct janus_streaming_buffer {\n\tchar *buffer;\n\tsize_t size;\n} janus_streaming_buffer;\n#endif\n\ntypedef struct janus_streaming_codecs {\n\tgint pt;\n\tchar *fmtp;\n\tjanus_audiocodec audio_codec;\n\tjanus_videocodec video_codec;\n} janus_streaming_codecs;\n\ntypedef struct janus_streaming_rtp_source {\n\tGList *media;\t\t\t\t/* List of media streams in this RTP source (audio, video and/or data) */\n\tGHashTable *media_byid;\t\t/* As above, indexed by mindex */\n\tGHashTable *media_byfd;\t\t/* As above, indexed by file descriptor */\n\tjanus_mutex rec_mutex;\t\t/* Mutex to protect the recorders of all media streams from race conditions */\n\tint pipefd[2];\t\t\t\t/* Just needed to quickly interrupt the poll when it's time to wrap up */\n\tint rtp_collision;\t\t\t/* Whether we should take care of potential RTP collisions */\n\tuint32_t lowest_bitrate;\t/* Lowest bitrate received by viewers via REMB since last update */\n\tgint64 remb_latest;\t\t\t/* Time of latest sent REMB (to avoid flooding) */\n\tgboolean rtsp;\n#ifdef HAVE_LIBCURL\n\tCURL *curl;\n\tchar *curl_errbuf;\n\tjanus_streaming_buffer *curldata;\n\tchar *rtsp_url;\n\tchar *rtsp_username, *rtsp_password;\n\tchar *rtsp_stream_uri;\n\tgboolean rtsp_quirk;\n\tgboolean rtsp_notify_changes;\n\tgint64 ka_timeout;\n\tchar *rtsp_ahost, *rtsp_vhost;\n\tjanus_streaming_codecs rtsp_acodecs, rtsp_vcodecs;\n\tgboolean reconnecting;\n\tgint64 reconnect_timer;\n\tgint64 reconnect_delay;\n\tgint64 session_timeout;\n\tint rtsp_timeout;\n\tint rtsp_conn_timeout;\n\tjanus_mutex rtsp_mutex;\n#endif\n\t/* Optional keyframe (and deltas) buffering */\n\tuint16_t bufferkf_ms;\n\tuint32_t bufferkf_bytes;\n\t/* Only needed for SRTP support */\n\tgboolean is_srtp;\n\tint srtpsuite;\n\tchar *srtpcrypto;\n\tsrtp_t srtp_ctx;\n\tsrtp_policy_t srtp_policy;\n\t/* If the media is end-to-end encrypted, we may need to know */\n\tgboolean e2ee;\n\t/* Whether the playout-delay extension should be negotiated or not for new subscribers */\n\tgboolean playoutdelay_ext;\n\t/* Extension header id in RTP source with abs-capture-time */\n\tint abscapturetime_src_ext_id;\n} janus_streaming_rtp_source;\n\ntypedef enum janus_streaming_media {\n\tJANUS_STREAMING_MEDIA_NONE = 0,\n\tJANUS_STREAMING_MEDIA_AUDIO,\n\tJANUS_STREAMING_MEDIA_VIDEO,\n\tJANUS_STREAMING_MEDIA_DATA\n} janus_streaming_media;\nstatic const char *janus_streaming_media_str(janus_streaming_media type) {\n\tswitch(type) {\n\t\tcase JANUS_STREAMING_MEDIA_AUDIO: return \"audio\";\n\t\tcase JANUS_STREAMING_MEDIA_VIDEO: return \"video\";\n\t\tcase JANUS_STREAMING_MEDIA_DATA: return \"data\";\n\t\tcase JANUS_STREAMING_MEDIA_NONE:\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\nstatic janus_streaming_media janus_streaming_parse_media(const char *type) {\n\tif(type == NULL)\n\t\treturn JANUS_STREAMING_MEDIA_NONE;\n\telse if(!strcasecmp(type, \"audio\"))\n\t\treturn JANUS_STREAMING_MEDIA_AUDIO;\n\telse if(!strcasecmp(type, \"video\"))\n\t\treturn JANUS_STREAMING_MEDIA_VIDEO;\n\telse if(!strcasecmp(type, \"data\"))\n\t\treturn JANUS_STREAMING_MEDIA_DATA;\n\treturn JANUS_STREAMING_MEDIA_NONE;\n}\n\ntypedef struct janus_streaming_rtp_source_stream {\n\tint mindex;\n\tjanus_streaming_media type;\n\tchar *mid, *label, *msid, *mstid;\n\tjanus_streaming_codecs codecs;\n\tchar *host;\n\tgint port[3], remote_port;\n\tgint rtcp_port, remote_rtcp_port;\n\tin_addr_t mcast;\n\tchar *mcast_str;\n\tjanus_network_address iface;\n\tchar *iface_str;\n\tjanus_recorder *rc;\t/* The Janus recorder instance for this streams, if enabled */\n\tjanus_rtp_switching_context context[3];\n\tint fd[3];\n\tint rtcp_fd;\n\tgboolean simulcast;\n\tgboolean svc;\n\tchar *h264_spspps;\n\tint h264_spspps_len;\n\tgboolean skew;\n\tgint64 last_received[3];\n\tuint32_t ssrc;\t\t\t\t/* Only needed for fixing outgoing RTCP packets */\n\tuint32_t last_ssrc[3];\t\t/* Only needed for detecting new sources */\n\tvolatile gint need_pli;\t\t/* Whether we need to send a PLI later */\n\tvolatile gint sending_pli;\t/* Whether we're currently sending a PLI */\n\tgint64 pli_latest;\t\t\t/* Time of latest sent PLI (to avoid flooding) */\n\tstruct sockaddr_storage rtcp_addr;\n\tjanus_streaming_rtp_keyframe keyframe;\n\tgboolean textdata;\n\tgboolean buffermsg;\n\tvoid *last_msg;\n\tjanus_mutex buffermsg_mutex;\n\tjanus_refcount ref;\n} janus_streaming_rtp_source_stream;\nstatic void janus_streaming_rtp_source_stream_unref(janus_streaming_rtp_source_stream *stream) {\n\t/* Decrease the counter */\n\tif(stream)\n\t\tjanus_refcount_decrease(&stream->ref);\n}\n\ntypedef struct janus_streaming_file_source {\n\tchar *filename;\n\tgboolean opus;\n\tjanus_streaming_codecs codecs;\n} janus_streaming_file_source;\n\n/* used for audio/video fd and RTCP fd */\ntypedef struct multiple_fds {\n\tint fd;\n\tint rtcp_fd;\n} multiple_fds;\n\ntypedef struct janus_streaming_mountpoint {\n\tguint64 id;\t\t\t/* Unique mountpoint ID (when using integers) */\n\tgchar *id_str;\t\t/* Unique mountpoint ID (when using strings) */\n\tchar *name;\n\tchar *description;\n\tchar *metadata;\n\tgboolean is_private;\n\tchar *secret;\n\tchar *pin;\n\tgboolean enabled;\n\tgboolean active;\n\tgboolean audio, video, data;\n\tGThread *thread;\t/* A mountpoint may or may not have a thread */\n\tjanus_streaming_type streaming_type;\n\tjanus_streaming_source streaming_source;\n\tvoid *source;\t/* Can differ according to the source type */\n\tGDestroyNotify source_destroy;\n\tGList *viewers;\n\tint helper_threads;\t\t/* Only relevant for RTP/RTSP mountpoints */\n\tGList *threads;\t\t\t/* Only relevant for RTP/RTSP mountpoints */\n\tvolatile gint destroyed;\n\tjanus_mutex mutex;\n\tjanus_refcount ref;\n} janus_streaming_mountpoint;\nGHashTable *mountpoints = NULL, *mountpoints_temp = NULL;\njanus_mutex mountpoints_mutex = JANUS_MUTEX_INITIALIZER;\nstatic char *admin_key = NULL;\n\ntypedef struct janus_streaming_helper {\n\tjanus_streaming_mountpoint *mp;\n\tguint id;\n\tGThread *thread;\n\tint num_viewers;\n\tGList *viewers;\n\tGAsyncQueue *queued_packets;\n\tvolatile gint destroyed;\n\tjanus_mutex mutex;\n\tjanus_refcount ref;\n} janus_streaming_helper;\nstatic void janus_streaming_helper_destroy(janus_streaming_helper *helper) {\n\tif(helper && g_atomic_int_compare_and_exchange(&helper->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&helper->ref);\n}\nstatic void janus_streaming_helper_free(const janus_refcount *helper_ref) {\n\tjanus_streaming_helper *helper = janus_refcount_containerof(helper_ref, janus_streaming_helper, ref);\n\t/* This helper can be destroyed, free all the resources */\n\tg_async_queue_unref(helper->queued_packets);\n\tif(helper->viewers != NULL)\n\t\tg_list_free(helper->viewers);\n\tjanus_mutex_destroy(&helper->mutex);\n\tg_free(helper);\n}\nstatic void *janus_streaming_helper_thread(void *data);\nstatic void janus_streaming_helper_rtprtcp_packet(gpointer data, gpointer user_data);\n\n/* Helpers to create an RTP live source (e.g., from gstreamer/ffmpeg/vlc/etc.) */\njanus_streaming_rtp_source_stream *janus_streaming_create_rtp_source_stream(\n\t\tconst char *name, int mindex, const char *type, const char *mid, const char *label, const char *msid,\n\t\tchar *mcast, char *miface, const janus_network_address *iface,\n\t\tuint16_t port, uint16_t port2, uint16_t port3, gboolean dortcp, uint16_t rtcpport,\n\t\tuint8_t pt, char *codec, char *fmtp, char *sprop,\n\t\tgboolean doskew, uint16_t bufferkf_ms, uint32_t bufferkf_bytes, gboolean simulcast, gboolean svc,\n\t\tgboolean textdata, gboolean buffermsg);\njanus_streaming_mountpoint *janus_streaming_create_rtp_source(\n\t\tuint64_t id, char *id_str, char *name, char *desc, char *metadata,\n\t\tGList *media, int srtpsuite, char *srtpcrypto, int threads, int rtp_collision,\n\t\tuint16_t bufferkf_ms, uint32_t bufferkf_bytes,\n\t\tgboolean e2ee, gboolean playoutdelay_ext, int abscapturetime_src_ext_id);\n/* Helper to create a file/ondemand live source */\njanus_streaming_mountpoint *janus_streaming_create_file_source(\n\t\tuint64_t id, char *id_str, char *name, char *desc, char *metadata, char *filename, gboolean live,\n\t\tgboolean doaudio, uint8_t apt, char *acodec, char *afmtp, gboolean dovideo);\n/* Helper to create a rtsp live source */\njanus_streaming_mountpoint *janus_streaming_create_rtsp_source(\n\t\tuint64_t id, char *id_str, char *name, char *desc, char *metadata,\n\t\tchar *url, char *username, char *password,\n\t\tgboolean quirk, gboolean notify_changes,\n\t\tgboolean doaudio, int audiopt, char *acodec, char *afmtp,\n\t\tgboolean dovideo, int videopt, char *vcodec, char *vfmtp,\n\t\tuint16_t bufferkf_ms, uint32_t bufferkf_bytes,\n\t\tconst janus_network_address *iface, int threads,\n\t\tgint64 reconnect_delay, gint64 session_timeout, int rtsp_timeout, int rtsp_conn_timeout,\n\t\tgboolean error_on_failure);\n\ntypedef struct janus_streaming_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_streaming_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_streaming_message exit_message;\n\ntypedef struct janus_streaming_session_stream {\n\tint mindex;\t\t\t\t/* The media index of this stream (may not be the same as the mountpoint stream) */\n\tjanus_streaming_rtp_source_stream *stream;\n\tgboolean send;\t\t\t/* Whether this stream media must be sent to this subscriber */\n\tint pt;\n\tjanus_rtp_switching_context context;\n\tjanus_rtp_simulcasting_context sim_context;\n\tjanus_vp8_simulcast_context vp8_context;\n\t/* The following are only relevant the mountpoint is VP9-SVC, and are not to be confused with VP8\n\t * simulcast, which has similar info (substream/templayer) but in a completely different context */\n\tint spatial_layer, target_spatial_layer;\n\tgint64 last_spatial_layer[3];\n\tint temporal_layer, target_temporal_layer;\n\t/* Playout delays to enforce when relaying this stream, if the extension has been negotiated */\n\tint16_t min_delay, max_delay;\n} janus_streaming_session_stream;\nstatic void janus_streaming_session_stream_free(janus_streaming_session_stream *s) {\n\tif(s && s->stream)\n\t\tjanus_streaming_rtp_source_stream_unref(s->stream);\n\tg_free(s);\n}\n\ntypedef struct janus_streaming_session {\n\tjanus_plugin_session *handle;\n\tjanus_streaming_mountpoint *mountpoint;\n\tgint64 sdp_sessid;\n\tgint64 sdp_version;\n\tvolatile gint started;\n\tvolatile gint paused;\n\tGList *streams;\t\t\t\t/* List of streams this session is subscribed to */\n\tGHashTable *streams_byid;\t/* Map of streams this session is subscribed to, indexed by mountpoint mindex */\n\t/* If the media is end-to-end encrypted, we may need to know */\n\tgboolean e2ee;\n\t/* Whether the playout-delay extension should be negotiated */\n\tgboolean playoutdelay_ext;\n\t/* Extension header id in RTP source with abs-capture-time */\n\tint abscapturetime_src_ext_id;\n\tjanus_mutex mutex;\n\tvolatile gint dataready;\n\tvolatile gint stopping;\n\tvolatile gint renegotiating;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_streaming_session;\nstatic GHashTable *sessions;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_streaming_session_destroy(janus_streaming_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_streaming_session_free(const janus_refcount *session_ref) {\n\tjanus_streaming_session *session = janus_refcount_containerof(session_ref, janus_streaming_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tjanus_mutex_destroy(&session->mutex);\n\tg_free(session);\n}\n\nstatic void janus_streaming_mountpoint_destroy(janus_streaming_mountpoint *mountpoint) {\n\tif(!mountpoint)\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&mountpoint->destroyed, 0, 1))\n\t\treturn;\n\t/* If this is an RTP source, interrupt the poll */\n\tif(mountpoint->streaming_source == janus_streaming_source_rtp) {\n\t\tjanus_streaming_rtp_source *source = mountpoint->source;\n\t\tif(source != NULL && source->pipefd[1] > 0) {\n\t\t\tint code = 1;\n\t\t\tssize_t res = 0;\n\t\t\tdo {\n\t\t\t\tres = write(source->pipefd[1], &code, sizeof(int));\n\t\t\t} while(res == -1 && errno == EINTR);\n\t\t}\n\t}\n\t/* Wait for the thread to finish */\n\tif(mountpoint->thread != NULL)\n\t\tg_thread_join(mountpoint->thread);\n\t/* Get rid of the helper threads, if any */\n\tif(mountpoint->helper_threads > 0) {\n\t\tGList *l = mountpoint->threads;\n\t\twhile(l) {\n\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\tg_async_queue_push(ht->queued_packets, &exit_packet);\n\t\t\tjanus_streaming_helper_destroy(ht);\n\t\t\tl = l->next;\n\t\t}\n\t}\n\t/* Decrease the counter */\n\tjanus_refcount_decrease(&mountpoint->ref);\n}\n\nstatic void janus_streaming_mountpoint_free(const janus_refcount *mp_ref) {\n\tjanus_streaming_mountpoint *mp = janus_refcount_containerof(mp_ref, janus_streaming_mountpoint, ref);\n\t/* This mountpoint can be destroyed, free all the resources */\n\tg_free(mp->id_str);\n\tg_free(mp->name);\n\tg_free(mp->description);\n\tg_free(mp->metadata);\n\tg_free(mp->secret);\n\tg_free(mp->pin);\n\tjanus_mutex_lock(&mp->mutex);\n\tif(mp->viewers != NULL)\n\t\tg_list_free(mp->viewers);\n\tif(mp->threads != NULL) {\n\t\t/* Remove the last reference to the helper threads, if any */\n\t\tGList *l = mp->threads;\n\t\twhile(l) {\n\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\tjanus_refcount_decrease(&ht->ref);\n\t\t\tl = l->next;\n\t\t}\n\t\t/* Destroy the list */\n\t\tg_list_free(mp->threads);\n\t}\n\tjanus_mutex_unlock(&mp->mutex);\n\tif(mp->source != NULL && mp->source_destroy != NULL) {\n\t\tmp->source_destroy(mp->source);\n\t}\n\tjanus_mutex_destroy(&mp->mutex);\n\tg_free(mp);\n}\n\nstatic void janus_streaming_message_free(janus_streaming_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_streaming_session *session = (janus_streaming_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\n/* Helper to notify mountpoint subscribers about events */\nstatic void janus_streaming_notify_subscribers(janus_streaming_mountpoint *mountpoint, json_t *info) {\n\t/* mountpoint->mutex has to be locked. */\n\tif(mountpoint == NULL || info == NULL)\n\t\treturn;\n\tGList *subscriber = g_list_first(mountpoint->viewers);\n\twhile(subscriber) {\n\t\tjanus_streaming_session *s = (janus_streaming_session *)subscriber->data;\n\t\tif(s == NULL || g_atomic_int_get(&s->destroyed)) {\n\t\t\tsubscriber = g_list_next(subscriber);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&s->mutex);\n\t\tgateway->push_event(s->handle, &janus_streaming_plugin, NULL, info, NULL);\n\t\tjanus_mutex_unlock(&s->mutex);\n\t\tsubscriber = g_list_next(subscriber);\n\t}\n}\n\n#ifdef HAVE_LIBOGG\n/* Helper struct to handle the playout of Opus files */\ntypedef struct janus_streaming_opus_context {\n\tchar *name, *filename;\n\tFILE *file;\n\togg_sync_state sync;\n\togg_stream_state stream;\n\togg_page page;\n\togg_packet pkt;\n\tchar *oggbuf;\n\tgint state, headers;\n} janus_streaming_opus_context;\n/* Helper method to open an Opus file, and make sure it's valid */\nstatic int janus_streaming_opus_context_init(janus_streaming_opus_context *ctx) {\n\tif(ctx == NULL || ctx->file == NULL)\n\t\treturn -1;\n\tfseek(ctx->file, 0, SEEK_SET);\n\togg_stream_clear(&ctx->stream);\n\togg_sync_clear(&ctx->sync);\n\tif(ogg_sync_init(&ctx->sync) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Error re-initializing Ogg sync state...\\n\", ctx->name);\n\t\treturn -1;\n\t}\n\tctx->headers = 0;\n\treturn 0;\n}\n/* Helper method to check if an Ogg page begins with an Ogg stream */\nstatic gboolean janus_streaming_ogg_is_opus(ogg_page *page) {\n\togg_stream_state state;\n\togg_packet pkt;\n\togg_stream_init(&state, ogg_page_serialno(page));\n\togg_stream_pagein(&state, page);\n\tif(ogg_stream_packetout(&state, &pkt) == 1) {\n\t\tif(pkt.bytes >= 19 && !memcmp(pkt.packet, \"OpusHead\", 8)) {\n\t\t\togg_stream_clear(&state);\n\t\t\treturn 1;\n\t\t}\n\t}\n\togg_stream_clear(&state);\n\treturn FALSE;\n}\n/* Helper method to traverse the Opus file until we get a packet we can send */\nstatic int janus_streaming_opus_context_read(janus_streaming_opus_context *ctx, char *buffer, int length) {\n\tif(ctx == NULL || ctx->file == NULL || buffer == NULL)\n\t\treturn -1;\n\t/* Check our current state in processing the Ogg file */\n\tint read = 0;\n\tif(ctx->state == 0) {\n\t\t/* Prepare a buffer, and read from the Ogg file... */\n\t\tctx->oggbuf = ogg_sync_buffer(&ctx->sync, 8192);\n\t\tif(ctx->oggbuf == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_sync_buffer failed...\\n\", ctx->name);\n\t\t\treturn -2;\n\t\t}\n\t\tread = fread(ctx->oggbuf, 1, 8192, ctx->file);\n\t\tif(read == 0 && feof(ctx->file)) {\n\t\t\t/* FIXME We're doing this forever... should this be configurable? */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s] Rewind! (%s)\\n\", ctx->name, ctx->filename);\n\t\t\tif(janus_streaming_opus_context_init(ctx) < 0)\n\t\t\t\treturn -3;\n\t\t\treturn janus_streaming_opus_context_read(ctx, buffer, length);\n\t\t}\n\t\tif(ogg_sync_wrote(&ctx->sync, read) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_sync_wrote failed...\\n\", ctx->name);\n\t\t\treturn -4;\n\t\t}\n\t\t/* Next state: sync pageout */\n\t\tctx->state = 1;\n\t}\n\tif(ctx->state == 1) {\n\t\t/* Prepare an ogg_page out of the buffer */\n\t\twhile((read = ogg_sync_pageout(&ctx->sync, &ctx->page)) == 1) {\n\t\t\t/* Let's look for an Opus stream, first of all */\n\t\t\tif(ctx->headers == 0) {\n\t\t\t\tif(janus_streaming_ogg_is_opus(&ctx->page)) {\n\t\t\t\t\t/* This is the start of an Opus stream */\n\t\t\t\t\tif(ogg_stream_init(&ctx->stream, ogg_page_serialno(&ctx->page)) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_stream_init failed...\\n\", ctx->name);\n\t\t\t\t\t\treturn -5;\n\t\t\t\t\t}\n\t\t\t\t\tctx->headers++;\n\t\t\t\t} else if(!ogg_page_bos(&ctx->page)) {\n\t\t\t\t\t/* No Opus stream? */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] No Opus stream...\\n\", ctx->name);\n\t\t\t\t\treturn -6;\n\t\t\t\t} else {\n\t\t\t\t\t/* Still waiting for an Opus stream */\n\t\t\t\t\treturn janus_streaming_opus_context_read(ctx, buffer, length);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Submit the page for packetization */\n\t\t\tif(ogg_stream_pagein(&ctx->stream, &ctx->page) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] ogg_stream_pagein failed...\\n\", ctx->name);\n\t\t\t\treturn -7;\n\t\t\t}\n\t\t\t/* Time to start reading packets */\n\t\t\tctx->state = 2;\n\t\t\tbreak;\n\t\t}\n\t\tif(read != 1) {\n\t\t\t/* Go back to reading from the file */\n\t\t\tctx->state = 0;\n\t\t\treturn janus_streaming_opus_context_read(ctx, buffer, length);\n\t\t}\n\t}\n\tif(ctx->state == 2) {\n\t\t/* Read and process available packets */\n\t\tif(ogg_stream_packetout(&ctx->stream, &ctx->pkt) != 1) {\n\t\t\t/* Go back to reading pages */\n\t\t\tctx->state = 1;\n\t\t\treturn janus_streaming_opus_context_read(ctx, buffer, length);\n\t\t} else {\n\t\t\t/* Skip header packets */\n\t\t\tif(ctx->headers == 1 && ctx->pkt.bytes >= 19 && !memcmp(ctx->pkt.packet, \"OpusHead\", 8)) {\n\t\t\t\tctx->headers++;\n\t\t\t\treturn janus_streaming_opus_context_read(ctx, buffer, length);\n\t\t\t}\n\t\t\tif(ctx->headers == 2 && ctx->pkt.bytes >= 16 && !memcmp(ctx->pkt.packet, \"OpusTags\", 8)) {\n\t\t\t\tctx->headers++;\n\t\t\t\treturn janus_streaming_opus_context_read(ctx, buffer, length);\n\t\t\t}\n\t\t\t/* Get the packet duration */\n\t\t\tif(length < ctx->pkt.bytes) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Buffer too short for Opus packet (%d < %ld)\\n\",\n\t\t\t\t\tctx->name, length, ctx->pkt.bytes);\n\t\t\t\treturn -8;\n\t\t\t}\n\t\t\tmemcpy(buffer, ctx->pkt.packet, ctx->pkt.bytes);\n\t\t\tlength = ctx->pkt.bytes;\n\t\t\treturn length;\n\t\t}\n\t}\n\t/* If we got here, continue with the iteration */\n\treturn -9;\n}\n/* Helper method to cleanup an Opus context */\nstatic void janus_streaming_opus_context_cleanup(janus_streaming_opus_context *ctx) {\n\tif(ctx == NULL)\n\t\treturn;\n\tif(ctx->headers > 0)\n\t\togg_stream_clear(&ctx->stream);\n\togg_sync_clear(&ctx->sync);\n}\n#endif\n\n\n/* Helper method to send an RTCP PLI */\nstatic void janus_streaming_rtcp_pli_send(janus_streaming_rtp_source_stream *stream) {\n\tif(stream == NULL || stream->rtcp_fd < 0 || stream->rtcp_addr.ss_family == 0)\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&stream->sending_pli, 0, 1))\n\t\treturn;\n\tgint64 now = janus_get_monotonic_time();\n\tif(now - stream->pli_latest < G_USEC_PER_SEC) {\n\t\t/* We just sent a PLI less than a second ago, schedule a new delivery later */\n\t\tg_atomic_int_set(&stream->need_pli, 1);\n\t\tg_atomic_int_set(&stream->sending_pli, 0);\n\t\treturn;\n\t}\n\t/* Update the time of when we last sent a keyframe request */\n\tg_atomic_int_set(&stream->need_pli, 0);\n\tstream->pli_latest = janus_get_monotonic_time();\n\tJANUS_LOG(LOG_HUGE, \"Sending PLI\\n\");\n\t/* Generate a PLI */\n\tchar rtcp_buf[12];\n\tint rtcp_len = 12;\n\tjanus_rtcp_pli((char *)&rtcp_buf, rtcp_len);\n\tjanus_rtcp_fix_ssrc(NULL, rtcp_buf, rtcp_len, 1, 1, stream->ssrc);\n\t/* Send the packet */\n\tsocklen_t addrlen = stream->rtcp_addr.ss_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);\n\tint sent = 0;\n\tif((sent = sendto(stream->rtcp_fd, rtcp_buf, rtcp_len, 0,\n\t\t\t(struct sockaddr *)&stream->rtcp_addr, addrlen)) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error in sendto... %d (%s)\\n\", errno, g_strerror(errno));\n\t} else {\n\t\tJANUS_LOG(LOG_HUGE, \"Sent %d/%d bytes\\n\", sent, rtcp_len);\n\t}\n\tg_atomic_int_set(&stream->sending_pli, 0);\n}\n\n/* Helper method to send an RTCP REMB */\nstatic void janus_streaming_rtcp_remb_send(janus_streaming_rtp_source *source, janus_streaming_rtp_source_stream *stream) {\n\tif(stream == NULL || stream->rtcp_fd < 0 || stream->rtcp_addr.ss_family == 0)\n\t\treturn;\n\t/* Update the time of when we last sent REMB feedback */\n\tsource->remb_latest = janus_get_monotonic_time();\n\t/* Generate a REMB */\n\tchar rtcp_buf[24];\n\tint rtcp_len = 24;\n\tjanus_rtcp_remb((char *)(&rtcp_buf), rtcp_len, source->lowest_bitrate);\n\tjanus_rtcp_fix_ssrc(NULL, rtcp_buf, rtcp_len, 1, 1, stream->ssrc);\n\tJANUS_LOG(LOG_HUGE, \"Sending REMB: %\"SCNu32\"\\n\", source->lowest_bitrate);\n\t/* Reset the lowest bitrate */\n\tsource->lowest_bitrate = 0;\n\t/* Send the packet */\n\tsocklen_t addrlen = stream->rtcp_addr.ss_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);\n\tint sent = 0;\n\tif((sent = sendto(stream->rtcp_fd, rtcp_buf, rtcp_len, 0,\n\t\t\t(struct sockaddr *)&stream->rtcp_addr, addrlen)) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error in sendto... %d (%s)\\n\", errno, g_strerror(errno));\n\t} else {\n\t\tJANUS_LOG(LOG_HUGE, \"Sent %d/%d bytes\\n\", sent, rtcp_len);\n\t}\n}\n\n/* Helper method to parse a base64-encoded sprop-parameter-sets and update a stream */\nstatic char *janus_streaming_parse_sprop(char *sprop, int *len) {\n\tif(sprop == NULL || len == NULL)\n\t\treturn NULL;\n\tchar *sps = NULL;\n\tif(strstr(sprop, \",\")) {\n\t\tchar **parts = g_strsplit(sprop, \",\", -1);\n\t\tchar *sps_p = parts[0];\n\t\tchar *pps_p = parts[1];\n\t\tif(sps_p && pps_p) {\n\t\t\t/* Base64 decode both fields */\n\t\t\tgsize slen = 0, plen = 0;\n\t\t\tguchar *sps_dec = g_base64_decode(sps_p, &slen);\n\t\t\tguchar *pps_dec = g_base64_decode(pps_p, &plen);\n\t\t\tif(sps_dec && pps_dec) {\n\t\t\t\t/* Prepare the RTP packet with the NAL units */\n\t\t\t\tint flen = 12 + 1 + 2 + slen + 2 + plen;\n\t\t\t\tchar *buf = g_malloc0(flen);\n\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\t\t\t\trtp->version = 2;\n\t\t\t\t/* STAP-A */\n\t\t\t\tchar *nal = buf + 12;\n\t\t\t\t*nal = 0x18;\n\t\t\t\tint offset = 1;\n\t\t\t\t/* Add SPS */\n\t\t\t\tuint16_t nsize = htons(slen);\n\t\t\t\tmemcpy(nal + offset, &nsize, sizeof(nsize));\n\t\t\t\toffset += sizeof(nsize);\n\t\t\t\tmemcpy(nal + offset, sps_dec, slen);\n\t\t\t\toffset += slen;\n\t\t\t\t/* Add PPS */\n\t\t\t\tnsize = htons(plen);\n\t\t\t\tmemcpy(nal + offset, &nsize, sizeof(nsize));\n\t\t\t\toffset += sizeof(nsize);\n\t\t\t\tmemcpy(nal + offset, pps_dec, plen);\n\t\t\t\t/* Keep track of the packet */\n\t\t\t\tsps = buf;\n\t\t\t\t*len = flen;\n\t\t\t}\n\t\t\tg_free(sps_dec);\n\t\t\tg_free(pps_dec);\n\t\t}\n\t\tg_strfreev(parts);\n\t}\n\treturn sps;\n}\n\n/* Error codes */\n#define JANUS_STREAMING_ERROR_NO_MESSAGE\t\t\t450\n#define JANUS_STREAMING_ERROR_INVALID_JSON\t\t\t451\n#define JANUS_STREAMING_ERROR_INVALID_REQUEST\t\t452\n#define JANUS_STREAMING_ERROR_MISSING_ELEMENT\t\t453\n#define JANUS_STREAMING_ERROR_INVALID_ELEMENT\t\t454\n#define JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT\t455\n#define JANUS_STREAMING_ERROR_CANT_CREATE\t\t\t456\n#define JANUS_STREAMING_ERROR_UNAUTHORIZED\t\t\t457\n#define JANUS_STREAMING_ERROR_CANT_SWITCH\t\t\t458\n#define JANUS_STREAMING_ERROR_CANT_RECORD\t\t\t459\n#define JANUS_STREAMING_ERROR_INVALID_STATE\t\t\t460\n#define JANUS_STREAMING_ERROR_INVALID_SDP\t\t\t461\n#define JANUS_STREAMING_ERROR_UNKNOWN_ERROR\t\t\t470\n\n\n/* Plugin implementation */\nint janus_streaming_init(janus_callbacks *callback, const char *config_path) {\n#ifdef HAVE_LIBCURL\n\tcurl_global_init(CURL_GLOBAL_ALL);\n#else\n\tJANUS_LOG(LOG_WARN, \"libcurl not available, Streaming plugin will not have RTSP support\\n\");\n#endif\n#ifndef HAVE_LIBOGG\n\tJANUS_LOG(LOG_WARN, \"libogg not available, Streaming plugin will not have file-based Opus streaming\\n\");\n#endif\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\tstruct ifaddrs *ifas = NULL;\n\tif(getifaddrs(&ifas) == -1) {\n\t\tJANUS_LOG(LOG_ERR, \"Unable to acquire list of network devices/interfaces; some configurations may not work as expected... %d (%s)\\n\",\n\t\t\terrno, g_strerror(errno));\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_STREAMING_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tconfig = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_STREAMING_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_STREAMING_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tconfig_folder = config_path;\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\n\t/* Let's check if IPv6 is disabled, as we may need when creating sockets */\n\tint fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\tif(fd < 0) {\n\t\tipv6_disabled = TRUE;\n\t} else {\n\t\tint v6only = 0;\n\t\tif(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)\n\t\t\tipv6_disabled = TRUE;\n\t}\n\tif(fd >= 0)\n\t\tclose(fd);\n\tif(ipv6_disabled) {\n\t\tJANUS_LOG(LOG_WARN, \"IPv6 disabled, will only use IPv4 sockets for mountpoints\\n\");\n\t}\n\n\t/* Threads will expect this to be set */\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Parse configuration to populate the mountpoints */\n\tif(config != NULL) {\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\t/* Any admin key to limit who can \"create\"? */\n\t\tjanus_config_item *key = janus_config_get(config, config_general, janus_config_type_item, \"admin_key\");\n\t\tif(key != NULL && key->value != NULL)\n\t\t\tadmin_key = g_strdup(key->value);\n\t\tjanus_config_item *range = janus_config_get(config, config_general, janus_config_type_item, \"rtp_port_range\");\n\t\tif(range && range->value) {\n\t\t\t/* Split in min and max port */\n\t\t\tchar *maxport = strrchr(range->value, '-');\n\t\t\tif(maxport != NULL) {\n\t\t\t\t*maxport = '\\0';\n\t\t\t\tmaxport++;\n\t\t\t\tif(janus_string_to_uint16(range->value, &rtp_range_min) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP min port value: %s (assuming 0)\\n\", range->value);\n\t\t\t\tif(janus_string_to_uint16(maxport, &rtp_range_max) < 0)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid RTP max port value: %s (assuming 0)\\n\", maxport);\n\t\t\t\tmaxport--;\n\t\t\t\t*maxport = '-';\n\t\t\t}\n\t\t\tif(rtp_range_min > rtp_range_max) {\n\t\t\t\tuint16_t temp_port = rtp_range_min;\n\t\t\t\trtp_range_min = rtp_range_max;\n\t\t\t\trtp_range_max = temp_port;\n\t\t\t}\n\t\t\tif(rtp_range_min % 2)\n\t\t\t\trtp_range_min++;\t/* Pick an even port for RTP */\n\t\t\tif(rtp_range_min > rtp_range_max) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Incorrect port range (%u -- %u), switching min and max\\n\", rtp_range_min, rtp_range_max);\n\t\t\t\tuint16_t range_temp = rtp_range_max;\n\t\t\t\trtp_range_max = rtp_range_min;\n\t\t\t\trtp_range_min = range_temp;\n\t\t\t}\n\t\t\tif(rtp_range_max == 0)\n\t\t\t\trtp_range_max = 65535;\n\t\t\trtp_range_slider = rtp_range_min;\n\t\t\tJANUS_LOG(LOG_VERB, \"Streaming RTP/RTCP port range: %u -- %u\\n\", rtp_range_min, rtp_range_max);\n\t\t}\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_STREAMING_NAME);\n\t\t}\n\t\tjanus_config_item *ids = janus_config_get(config, config_general, janus_config_type_item, \"string_ids\");\n\t\tif(ids != NULL && ids->value != NULL)\n\t\t\tstring_ids = janus_is_true(ids->value);\n\t\tif(string_ids) {\n\t\t\tJANUS_LOG(LOG_INFO, \"Streaming will use alphanumeric IDs, not numeric\\n\");\n\t\t}\n\t}\n\t/* Iterate on all mountpoints */\n\tmountpoints = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_streaming_mountpoint_destroy);\n\tmountpoints_temp = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t(GDestroyNotify)g_free, NULL);\n\tif(config != NULL) {\n\t\tGList *clist = janus_config_get_categories(config, NULL), *cl = clist;\n\t\twhile(cl != NULL) {\n\t\t\tjanus_config_category *cat = (janus_config_category *)cl->data;\n\t\t\tif(cat->name == NULL || !strcasecmp(cat->name, \"general\")) {\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Adding Streaming mountpoint '%s'\\n\", cat->name);\n\t\t\tjanus_config_item *type = janus_config_get(config, cat, janus_config_type_item, \"type\");\n\t\t\tif(type == NULL || type->value == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid type, skipping mountpoint '%s'...\\n\", cat->name);\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_config_item *id = janus_config_get(config, cat, janus_config_type_item, \"id\");\n\t\t\tguint64 mpid = 0;\n\t\t\tif(id == NULL || id->value == NULL) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Missing id for mountpoint '%s', will generate a random one...\\n\", cat->name);\n\t\t\t} else {\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tif(!string_ids) {\n\t\t\t\t\tmpid = g_ascii_strtoull(id->value, 0, 10);\n\t\t\t\t\t/* Make sure the ID is completely numeric */\n\t\t\t\t\tchar mpid_str[30];\n\t\t\t\t\tg_snprintf(mpid_str, sizeof(mpid_str), \"%\"SCNu64, mpid);\n\t\t\t\t\tif(strcmp(id->value, mpid_str)) {\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the Streaming mountpoint '%s', ID '%s' is not numeric...\\n\",\n\t\t\t\t\t\t\tcat->name, id->value);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(mpid == 0) {\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the Streaming mountpoint '%s', invalid ID '%s'...\\n\",\n\t\t\t\t\t\t\tcat->name, id->value);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Let's make sure the mountpoint doesn't exist already */\n\t\t\t\tif(g_hash_table_lookup(mountpoints, string_ids ? (gpointer)id->value : (gpointer)&mpid) != NULL) {\n\t\t\t\t\t/* It does... */\n\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the Streaming mountpoint '%s', ID '%s' already exists...\\n\",\n\t\t\t\t\t\tcat->name, id->value);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t}\n\t\t\tif(!strcasecmp(type->value, \"rtp\")) {\n\t\t\t\t/* RTP live source (e.g., from gstreamer/ffmpeg/vlc/etc.) */\n\t\t\t\tGList *streams = NULL;\n\t\t\t\tjanus_config_item *desc = janus_config_get(config, cat, janus_config_type_item, \"description\");\n\t\t\t\tjanus_config_item *md = janus_config_get(config, cat, janus_config_type_item, \"metadata\");\n\t\t\t\tjanus_config_item *priv = janus_config_get(config, cat, janus_config_type_item, \"is_private\");\n\t\t\t\tjanus_config_item *secret = janus_config_get(config, cat, janus_config_type_item, \"secret\");\n\t\t\t\tjanus_config_item *pin = janus_config_get(config, cat, janus_config_type_item, \"pin\");\n\t\t\t\tjanus_config_item *media = janus_config_get(config, cat, janus_config_type_array, \"media\");\n\t\t\t\tjanus_config_item *rtpcollision = janus_config_get(config, cat, janus_config_type_item, \"collision\");\n\t\t\t\tjanus_config_item *vkf_ms = janus_config_get(config, cat, janus_config_type_item, \"bufferkf_ms\");\n\t\t\t\tjanus_config_item *vkf_bytes = janus_config_get(config, cat, janus_config_type_item, \"bufferkf_bytes\");\n\t\t\t\tjanus_config_item *threads = janus_config_get(config, cat, janus_config_type_item, \"threads\");\n\t\t\t\tjanus_config_item *ssuite = janus_config_get(config, cat, janus_config_type_item, \"srtpsuite\");\n\t\t\t\tjanus_config_item *scrypto = janus_config_get(config, cat, janus_config_type_item, \"srtpcrypto\");\n\t\t\t\tjanus_config_item *e2ee = janus_config_get(config, cat, janus_config_type_item, \"e2ee\");\n\t\t\t\tjanus_config_item *pd = janus_config_get(config, cat, janus_config_type_item, \"playoutdelay_ext\");\n\t\t\t\tjanus_config_item *abscaptime_src_id = janus_config_get(config, cat, janus_config_type_item, \"abscapturetime_src_ext_id\");\n\t\t\t\tgboolean is_private = priv && priv->value && janus_is_true(priv->value);\n\t\t\t\tif(ssuite && ssuite->value && atoi(ssuite->value) != 32 && atoi(ssuite->value) != 80) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid SRTP suite...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(rtpcollision && rtpcollision->value && atoi(rtpcollision->value) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid collision configuration...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tuint16_t bufferkf_ms = 0;\n\t\t\t\tif(vkf_ms && vkf_ms->value && janus_string_to_uint16(vkf_ms->value, &bufferkf_ms) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid bufferkf_ms configuration...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tuint32_t bufferkf_bytes = 0;\n\t\t\t\tif(vkf_bytes && vkf_bytes->value && janus_string_to_uint32(vkf_bytes->value, &bufferkf_bytes) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid bufferkf_bytes configuration...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(threads && threads->value && atoi(threads->value) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid threads configuration...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tint abscaptime_src_id_int = 0;\n\t\t\t\tif(abscaptime_src_id && abscaptime_src_id->value) {\n\t\t\t\t\tabscaptime_src_id_int = atoi(abscaptime_src_id->value);\n\t\t\t\t\tif(abscaptime_src_id_int < 0 || abscaptime_src_id_int > 14) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid abscaptime_src_id configuration...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* How are we adding media? */\n\t\t\t\tif(media != NULL) {\n\t\t\t\t\t/* We're using the new media-based configuration, iterate on all media objects */\n\t\t\t\t\tgboolean failed = FALSE;\n\t\t\t\t\tuint16_t s_bufferkf_ms = 0;\n\t\t\t\t\tuint32_t s_bufferkf_bytes = 0;\n\t\t\t\t\tGList *ml = media->list;\n\t\t\t\t\twhile(ml) {\n\t\t\t\t\t\tjanus_config_item *m = (janus_config_item *)ml->data;\n\t\t\t\t\t\tif(m == NULL || m->type != janus_config_type_category) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid media item (not a category?), skipping in '%s'...\\n\", cat->name);\n\t\t\t\t\t\t\tml = ml->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_config_item *type = janus_config_get(config, m, janus_config_type_item, \"type\");\n\t\t\t\t\t\tif(type == NULL || type->value == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid media type, skipping in '%s'...\\n\", cat->name);\n\t\t\t\t\t\t\tml = ml->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(strcasecmp(type->value, \"audio\") && strcasecmp(type->value, \"video\") && strcasecmp(type->value, \"data\")) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Unsupported media type '%s', skipping in '%s'...\\n\", type->value, cat->name);\n\t\t\t\t\t\t\tml = ml->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgboolean audio = !strcasecmp(type->value, \"audio\");\n\t\t\t\t\t\tgboolean video = !strcasecmp(type->value, \"video\");\n\t\t\t\t\t\tgboolean data = !strcasecmp(type->value, \"data\");\n\t\t\t\t\t\t/* We need mid and label for addressing this stream on the client side */\n\t\t\t\t\t\tjanus_config_item *mid = janus_config_get(config, m, janus_config_type_item, \"mid\");\n\t\t\t\t\t\tif(mid == NULL || mid->value == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Missing media mid, skipping in '%s'...\\n\", cat->name);\n\t\t\t\t\t\t\tml = ml->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_config_item *label = janus_config_get(config, m, janus_config_type_item, \"label\");\n\t\t\t\t\t\tjanus_config_item *msid = janus_config_get(config, m, janus_config_type_item, \"msid\");\n\t\t\t\t\t\t/* These are the attributes we can configure per each media stream */\n\t\t\t\t\t\tjanus_network_address media_iface;\n\t\t\t\t\t\tjanus_config_item *iface = janus_config_get(config, m, janus_config_type_item, \"iface\");\n\t\t\t\t\t\tjanus_config_item *mcast = janus_config_get(config, m, janus_config_type_item, \"mcast\");\n\t\t\t\t\t\tjanus_config_item *port = janus_config_get(config, m, janus_config_type_item, \"port\");\n\t\t\t\t\t\tjanus_config_item *rtcpport = janus_config_get(config, m, janus_config_type_item, \"rtcpport\");\n\t\t\t\t\t\tjanus_config_item *pt = janus_config_get(config, m, janus_config_type_item, \"pt\");\n\t\t\t\t\t\tjanus_config_item *codec = janus_config_get(config, m, janus_config_type_item, \"codec\");\n\t\t\t\t\t\tjanus_config_item *rtpmap = janus_config_get(config, m, janus_config_type_item, \"rtpmap\");\n\t\t\t\t\t\tjanus_config_item *fmtp = janus_config_get(config, m, janus_config_type_item, \"fmtp\");\n\t\t\t\t\t\tjanus_config_item *vsps = janus_config_get(config, m, janus_config_type_item, \"h264sps\");\n\t\t\t\t\t\tjanus_config_item *vkf = janus_config_get(config, m, janus_config_type_item, \"bufferkf\");\n\t\t\t\t\t\tjanus_config_item *vsc = janus_config_get(config, m, janus_config_type_item, \"simulcast\");\n\t\t\t\t\t\tjanus_config_item *dbm = janus_config_get(config, m, janus_config_type_item, \"buffermsg\");\n\t\t\t\t\t\tjanus_config_item *dt = janus_config_get(config, m, janus_config_type_item, \"datatype\");\n\t\t\t\t\t\tjanus_config_item *vport2 = janus_config_get(config, m, janus_config_type_item, \"port2\");\n\t\t\t\t\t\tjanus_config_item *vport3 = janus_config_get(config, m, janus_config_type_item, \"port3\");\n\t\t\t\t\t\tjanus_config_item *vsvc = janus_config_get(config, m, janus_config_type_item, \"svc\");\n\t\t\t\t\t\tjanus_config_item *skew = janus_config_get(config, m, janus_config_type_item, \"skew\");\n\t\t\t\t\t\tgboolean doskew = skew && skew->value && janus_is_true(skew->value);\n\t\t\t\t\t\tgboolean dosvc = video && vsvc && vsvc->value && janus_is_true(vsvc->value);\n\t\t\t\t\t\tif(video && vkf && vkf->value && janus_is_true(vkf->value)) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The bufferkf property has been deprecated, please refer to bufferkf_ms and/or bufferkf_bytes\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\ts_bufferkf_ms = video ? bufferkf_ms : 0;\n\t\t\t\t\t\ts_bufferkf_bytes = video ? bufferkf_bytes : 0;\n\t\t\t\t\t\tgboolean simulcast = video && vsc && vsc->value && janus_is_true(vsc->value);\n\t\t\t\t\t\tif(simulcast && (s_bufferkf_ms || s_bufferkf_bytes)) {\n\t\t\t\t\t\t\t/* FIXME We'll need to take care of this */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Simulcasting enabled, so disabling buffering of keyframes for this stream\\n\");\n\t\t\t\t\t\t\ts_bufferkf_ms = 0;\n\t\t\t\t\t\t\ts_bufferkf_bytes = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgboolean buffermsg = data && dbm && dbm->value && janus_is_true(dbm->value);\n\t\t\t\t\t\tgboolean textdata = TRUE;\n\t\t\t\t\t\tif(data && dt && dt->value) {\n\t\t\t\t\t\t\tif(!strcasecmp(dt->value, \"text\"))\n\t\t\t\t\t\t\t\ttextdata = TRUE;\n\t\t\t\t\t\t\telse if(!strcasecmp(dt->value, \"binary\"))\n\t\t\t\t\t\t\t\ttextdata = FALSE;\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid data type '%s'...\\n\", cat->name, dt->value);\n\t\t\t\t\t\t\t\tfailed = TRUE;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst char *streamcodec = (codec && codec->value ? codec->value : NULL);\n\t\t\t\t\t\tif((audio || video) && streamcodec == NULL) {\n\t\t\t\t\t\t\t/* No codec property, check the deprecated rtpmap */\n\t\t\t\t\t\t\tif(rtpmap && rtpmap->value)\n\t\t\t\t\t\t\t\tstreamcodec = janus_sdp_get_rtpmap_codec(rtpmap->value);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif((audio || video) &&\n\t\t\t\t\t\t\t\t(port == NULL || port->value == NULL || atoi(port->value) < 0 ||\n\t\t\t\t\t\t\t\tpt == NULL || pt->value == NULL || streamcodec == NULL)) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', missing mandatory information for audio/video stream...\\n\", cat->name);\n\t\t\t\t\t\t\tfailed = TRUE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t} else if(data && (port == NULL || port->value == NULL || atoi(port->value) < 0)) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', missing mandatory information for data stream...\\n\", cat->name);\n\t\t\t\t\t\t\tfailed = TRUE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(iface) {\n\t\t\t\t\t\t\tif(!ifas) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add '%s' stream '%s', it relies on network configuration but network device information is unavailable...\\n\", type->value, cat->name);\n\t\t\t\t\t\t\t\tfailed = TRUE;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, iface->value, &media_iface) != 0) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add '%s' stream '%s', invalid network interface configuration for media stream...\\n\", type->value, cat->name);\n\t\t\t\t\t\t\t\tfailed = TRUE;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Create the source stream */\n\t\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\t\tcat->name, g_list_length(streams),\n\t\t\t\t\t\t\ttype->value, mid->value, (label && label->value ? label->value : type->value),\n\t\t\t\t\t\t\t(msid && msid->value ? msid->value : NULL),\n\t\t\t\t\t\t\tmcast ? (char *)mcast->value : NULL,\n\t\t\t\t\t\t\tiface && iface->value ? (char *)iface->value : NULL,\n\t\t\t\t\t\t\tiface && iface->value ? &media_iface : NULL,\n\t\t\t\t\t\t\t(port && port->value) ? atoi(port->value) : 0,\n\t\t\t\t\t\t\t(vport2 && vport2->value) ? atoi(vport2->value) : 0,\n\t\t\t\t\t\t\t(vport3 && vport3->value) ? atoi(vport3->value) : 0,\n\t\t\t\t\t\t\t(rtcpport && rtcpport->value),\n\t\t\t\t\t\t\t\t(rtcpport && rtcpport->value) ? atoi(rtcpport->value) : 0,\n\t\t\t\t\t\t\t(pt && pt->value) ? atoi(pt->value) : 0,\n\t\t\t\t\t\t\t(char *)streamcodec,\n\t\t\t\t\t\t\tfmtp ? (char *)fmtp->value : NULL,\n\t\t\t\t\t\t\tvsps ? (char *)vsps->value : NULL,\n\t\t\t\t\t\t\tdoskew, s_bufferkf_ms, s_bufferkf_bytes,\n\t\t\t\t\t\t\tsimulcast, dosvc, textdata, buffermsg);\n\t\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add '%s' stream '%s', error creating source stream...\\n\", type->value, cat->name);\n\t\t\t\t\t\t\tfailed = TRUE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\t\tstreams = g_list_append(streams, stream);\n\t\t\t\t\t\t/* Go on */\n\t\t\t\t\t\tml = ml->next;\n\t\t\t\t\t}\n\t\t\t\t\tif(failed) {\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* If we got here, we create a mountpoint the \"old\" way */\n\t\t\t\t\tjanus_network_address video_iface, audio_iface, data_iface;\n\t\t\t\t\tjanus_config_item *audio = janus_config_get(config, cat, janus_config_type_item, \"audio\");\n\t\t\t\t\tjanus_config_item *askew = janus_config_get(config, cat, janus_config_type_item, \"audioskew\");\n\t\t\t\t\tjanus_config_item *video = janus_config_get(config, cat, janus_config_type_item, \"video\");\n\t\t\t\t\tjanus_config_item *vskew = janus_config_get(config, cat, janus_config_type_item, \"videoskew\");\n\t\t\t\t\tjanus_config_item *vsvc = janus_config_get(config, cat, janus_config_type_item, \"videosvc\");\n\t\t\t\t\tjanus_config_item *data = janus_config_get(config, cat, janus_config_type_item, \"data\");\n\t\t\t\t\tjanus_config_item *amcast = janus_config_get(config, cat, janus_config_type_item, \"audiomcast\");\n\t\t\t\t\tjanus_config_item *aiface = janus_config_get(config, cat, janus_config_type_item, \"audioiface\");\n\t\t\t\t\tjanus_config_item *aport = janus_config_get(config, cat, janus_config_type_item, \"audioport\");\n\t\t\t\t\tjanus_config_item *artcpport = janus_config_get(config, cat, janus_config_type_item, \"audiortcpport\");\n\t\t\t\t\tjanus_config_item *apt = janus_config_get(config, cat, janus_config_type_item, \"audiopt\");\n\t\t\t\t\tjanus_config_item *acodec = janus_config_get(config, cat, janus_config_type_item, \"audiocodec\");\n\t\t\t\t\tjanus_config_item *artpmap = janus_config_get(config, cat, janus_config_type_item, \"audiortpmap\");\n\t\t\t\t\tjanus_config_item *afmtp = janus_config_get(config, cat, janus_config_type_item, \"audiofmtp\");\n\t\t\t\t\tjanus_config_item *vmcast = janus_config_get(config, cat, janus_config_type_item, \"videomcast\");\n\t\t\t\t\tjanus_config_item *viface = janus_config_get(config, cat, janus_config_type_item, \"videoiface\");\n\t\t\t\t\tjanus_config_item *vport = janus_config_get(config, cat, janus_config_type_item, \"videoport\");\n\t\t\t\t\tjanus_config_item *vrtcpport = janus_config_get(config, cat, janus_config_type_item, \"videortcpport\");\n\t\t\t\t\tjanus_config_item *vpt = janus_config_get(config, cat, janus_config_type_item, \"videopt\");\n\t\t\t\t\tjanus_config_item *vcodec = janus_config_get(config, cat, janus_config_type_item, \"videocodec\");\n\t\t\t\t\tjanus_config_item *vrtpmap = janus_config_get(config, cat, janus_config_type_item, \"videortpmap\");\n\t\t\t\t\tjanus_config_item *vfmtp = janus_config_get(config, cat, janus_config_type_item, \"videofmtp\");\n\t\t\t\t\tjanus_config_item *vsps = janus_config_get(config, cat, janus_config_type_item, \"h264sps\");\n\t\t\t\t\tjanus_config_item *vkf = janus_config_get(config, cat, janus_config_type_item, \"videobufferkf\");\n\t\t\t\t\tjanus_config_item *vsc = janus_config_get(config, cat, janus_config_type_item, \"videosimulcast\");\n\t\t\t\t\tjanus_config_item *vport2 = janus_config_get(config, cat, janus_config_type_item, \"videoport2\");\n\t\t\t\t\tjanus_config_item *vport3 = janus_config_get(config, cat, janus_config_type_item, \"videoport3\");\n\t\t\t\t\tjanus_config_item *dmcast = janus_config_get(config, cat, janus_config_type_item, \"datamcast\");\n\t\t\t\t\tjanus_config_item *diface = janus_config_get(config, cat, janus_config_type_item, \"dataiface\");\n\t\t\t\t\tjanus_config_item *dport = janus_config_get(config, cat, janus_config_type_item, \"dataport\");\n\t\t\t\t\tjanus_config_item *dbm = janus_config_get(config, cat, janus_config_type_item, \"databuffermsg\");\n\t\t\t\t\tjanus_config_item *dt = janus_config_get(config, cat, janus_config_type_item, \"datatype\");\n\t\t\t\t\tgboolean doaudio = audio && audio->value && janus_is_true(audio->value);\n\t\t\t\t\tgboolean doaskew = audio && askew && askew->value && janus_is_true(askew->value);\n\t\t\t\t\tgboolean dovideo = video && video->value && janus_is_true(video->value);\n\t\t\t\t\tgboolean dovskew = video && vskew && vskew->value && janus_is_true(vskew->value);\n\t\t\t\t\tgboolean dosvc = video && vsvc && vsvc->value && janus_is_true(vsvc->value);\n\t\t\t\t\tgboolean dodata = data && data->value && janus_is_true(data->value);\n\t\t\t\t\tif(video && vkf && vkf->value && janus_is_true(vkf->value)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The videobufferkf property has been deprecated, please refer to bufferkf_ms and/or bufferkf_bytes\\n\");\n\t\t\t\t\t}\n\t\t\t\t\tgboolean simulcast = video && vsc && vsc->value && janus_is_true(vsc->value);\n\t\t\t\t\tif(simulcast && (bufferkf_ms > 0 || bufferkf_bytes > 0)) {\n\t\t\t\t\t\t/* FIXME We'll need to take care of this */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Simulcasting enabled, so disabling buffering of keyframes for this stream\\n\");\n\t\t\t\t\t\tbufferkf_ms = 0;\n\t\t\t\t\t\tbufferkf_bytes = 0;\n\t\t\t\t\t}\n\t\t\t\t\tgboolean buffermsg = data && dbm && dbm->value && janus_is_true(dbm->value);\n\t\t\t\t\tgboolean textdata = TRUE;\n\t\t\t\t\tif(data && dt && dt->value) {\n\t\t\t\t\t\tif(!strcasecmp(dt->value, \"text\"))\n\t\t\t\t\t\t\ttextdata = TRUE;\n\t\t\t\t\t\telse if(!strcasecmp(dt->value, \"binary\"))\n\t\t\t\t\t\t\ttextdata = FALSE;\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid data type '%s'...\\n\", cat->name, dt->value);\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(!doaudio && !dovideo && !dodata) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', no audio, video or data have to be streamed...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tuint16_t audio_port = 0, audio_rtcp_port = 0;\n\t\t\t\t\tconst char *audiocodec = (acodec && acodec->value ? acodec->value : NULL);\n\t\t\t\t\tif(audiocodec == NULL) {\n\t\t\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\t\t\tif(artpmap && artpmap->value)\n\t\t\t\t\t\t\taudiocodec = janus_sdp_get_rtpmap_codec(artpmap->value);\n\t\t\t\t\t}\n\t\t\t\t\tif(doaudio &&\n\t\t\t\t\t\t\t(aport == NULL || aport->value == NULL ||\n\t\t\t\t\t\t\tjanus_string_to_uint16(aport->value, &audio_port) < 0 ||\n\t\t\t\t\t\t\tapt == NULL || apt->value == NULL || audiocodec == NULL)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', missing mandatory information for audio...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(doaudio && artcpport != NULL && artcpport->value != NULL &&\n\t\t\t\t\t\t\t(janus_string_to_uint16(artcpport->value, &audio_rtcp_port) < 0)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid audio RTCP port...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tgboolean doaudiortcp = (artcpport != NULL && artcpport->value != NULL);\n\t\t\t\t\tif(doaudio && aiface) {\n\t\t\t\t\t\tif(!ifas) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Skipping 'rtp' mountpoint '%s', it relies on network configuration but network device information is unavailable...\\n\", cat->name);\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, aiface->value, &audio_iface) != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid network interface configuration for audio...\\n\", cat->name);\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tuint16_t video_port = 0, video_port2 = 0, video_port3 = 0, video_rtcp_port = 0;\n\t\t\t\t\tconst char *videocodec = (vcodec && vcodec->value ? vcodec->value : NULL);\n\t\t\t\t\tif(videocodec == NULL) {\n\t\t\t\t\t\t/* No videocodec property, check the deprecated videortpmap */\n\t\t\t\t\t\tif(vrtpmap && vrtpmap->value)\n\t\t\t\t\t\t\tvideocodec = janus_sdp_get_rtpmap_codec(vrtpmap->value);\n\t\t\t\t\t}\n\t\t\t\t\tif(dovideo &&\n\t\t\t\t\t\t\t(vport == NULL || vport->value == NULL ||\n\t\t\t\t\t\t\tjanus_string_to_uint16(vport->value, &video_port) < 0 ||\n\t\t\t\t\t\t\tvpt == NULL || vpt->value == NULL || videocodec == NULL)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', missing mandatory information for video...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(dovideo && vrtcpport != NULL && vrtcpport->value != NULL &&\n\t\t\t\t\t\t\t(janus_string_to_uint16(vrtcpport->value, &video_rtcp_port) < 0)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid video RTCP port...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tgboolean dovideortcp = (vrtcpport != NULL && vrtcpport->value != NULL);\n\t\t\t\t\tif(dovideo && vport2 != NULL && vport2->value != NULL &&\n\t\t\t\t\t\t\t(janus_string_to_uint16(vport2->value, &video_port2) < 0)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid simulcast port...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(dovideo && vport3 != NULL && vport3->value != NULL &&\n\t\t\t\t\t\t\t(janus_string_to_uint16(vport3->value, &video_port3) < 0)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid simulcast port...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(dovideo && viface) {\n\t\t\t\t\t\tif(!ifas) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Skipping 'rtp' mountpoint '%s', it relies on network configuration but network device information is unavailable...\\n\", cat->name);\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, viface->value, &video_iface) != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid network interface configuration for video...\\n\", cat->name);\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tuint16_t data_port = 0;\n\t\t\t\t\tif(dodata && (dport == NULL || dport->value == NULL ||\n\t\t\t\t\t\t\tjanus_string_to_uint16(dport->value, &data_port) < 0)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', missing mandatory information for data...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t#ifndef HAVE_SCTP\n\t\t\t\t\tif(dodata) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s': no datachannels support......\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t#endif\n\t\t\t\t\tif(dodata && diface) {\n\t\t\t\t\t\tif(!ifas) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Skipping 'rtp' mountpoint '%s', it relies on network configuration but network device information is unavailable...\\n\", cat->name);\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, diface->value, &data_iface) != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint '%s', invalid network interface configuration for data...\\n\", cat->name);\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* Create the individual streams */\n\t\t\t\t\tif(doaudio) {\n\t\t\t\t\t\t/* Create the audio source stream */\n\t\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\t\tcat->name, g_list_length(streams),\n\t\t\t\t\t\t\t\"audio\", \"a\", \"audio\", NULL,\n\t\t\t\t\t\t\tamcast ? (char *)amcast->value : NULL,\n\t\t\t\t\t\t\taiface && aiface->value ? (char *)aiface->value : NULL,\n\t\t\t\t\t\t\taiface && aiface->value ? &audio_iface : NULL,\n\t\t\t\t\t\t\t(aport && aport->value) ? atoi(aport->value) : 0, 0, 0,\n\t\t\t\t\t\t\tdoaudiortcp, (artcpport && artcpport->value) ? atoi(artcpport->value) : 0,\n\t\t\t\t\t\t\t(apt && apt->value) ? atoi(apt->value) : 0,\n\t\t\t\t\t\t\t(char *)audiocodec,\n\t\t\t\t\t\t\tafmtp ? (char *)afmtp->value : NULL, NULL,\n\t\t\t\t\t\t\tdoaskew, 0, 0, FALSE, FALSE, FALSE, FALSE);\n\t\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Skipping 'audio' stream '%s', error creating source stream...\\n\", cat->name);\n\t\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\t\tstreams = g_list_append(streams, stream);\n\t\t\t\t\t}\n\t\t\t\t\tif(dovideo) {\n\t\t\t\t\t\t/* Create the video source stream */\n\t\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\t\tcat->name, g_list_length(streams),\n\t\t\t\t\t\t\t\"video\", \"v\", \"video\", NULL,\n\t\t\t\t\t\t\tvmcast ? (char *)vmcast->value : NULL,\n\t\t\t\t\t\t\tviface && viface->value ? (char *)viface->value : NULL,\n\t\t\t\t\t\t\tviface && viface->value ? &video_iface : NULL,\n\t\t\t\t\t\t\t(vport && vport->value) ? atoi(vport->value) : 0,\n\t\t\t\t\t\t\t(vport2 && vport2->value) ? atoi(vport2->value) : 0,\n\t\t\t\t\t\t\t(vport3 && vport3->value) ? atoi(vport3->value) : 0,\n\t\t\t\t\t\t\tdovideortcp, (vrtcpport && vrtcpport->value) ? atoi(vrtcpport->value) : 0,\n\t\t\t\t\t\t\t(vpt && vpt->value) ? atoi(vpt->value) : 0,\n\t\t\t\t\t\t\t(char *)videocodec,\n\t\t\t\t\t\t\tvfmtp ? (char *)vfmtp->value : NULL,\n\t\t\t\t\t\t\tvsps ? (char *)vsps->value : NULL,\n\t\t\t\t\t\t\tdovskew, bufferkf_ms, bufferkf_bytes,\n\t\t\t\t\t\t\tsimulcast, dosvc, FALSE, FALSE);\n\t\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Skipping 'video' stream '%s', error creating source stream...\\n\", cat->name);\n\t\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\t\tstreams = g_list_append(streams, stream);\n\t\t\t\t\t}\n\t\t\t\t\tif(dodata) {\n\t\t\t\t\t\t/* Create the data source stream */\n\t\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\t\tcat->name, g_list_length(streams),\n\t\t\t\t\t\t\t\"data\", \"d\", \"data\", NULL,\n\t\t\t\t\t\t\tdmcast ? (char *)dmcast->value : NULL,\n\t\t\t\t\t\t\tdiface && diface->value ? (char *)diface->value : NULL,\n\t\t\t\t\t\t\tdiface && diface->value ? &data_iface : NULL,\n\t\t\t\t\t\t\t(dport && dport->value) ? atoi(dport->value) : 0,\n\t\t\t\t\t\t\t0, 0, FALSE, 0,\n\t\t\t\t\t\t\t0, NULL, NULL, NULL,\n\t\t\t\t\t\t\tFALSE, 0, 0, FALSE, FALSE, textdata, buffermsg);\n\t\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Skipping 'data' stream '%s', error creating source stream...\\n\", cat->name);\n\t\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\t\tstreams = g_list_append(streams, stream);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Streams created, create the actual mountpoint now */\n\t\t\t\tjanus_streaming_mountpoint *mp = NULL;\n\t\t\t\tif((mp = janus_streaming_create_rtp_source(\n\t\t\t\t\t\tmpid, (char *)(id ? id->value : NULL),\n\t\t\t\t\t\t(char *)cat->name,\n\t\t\t\t\t\tdesc ? (char *)desc->value : NULL,\n\t\t\t\t\t\tmd ? (char *)md->value : NULL,\n\t\t\t\t\t\tstreams,\n\t\t\t\t\t\tssuite && ssuite->value ? atoi(ssuite->value) : 0,\n\t\t\t\t\t\tscrypto && scrypto->value ? (char *)scrypto->value : NULL,\n\t\t\t\t\t\t(threads && threads->value) ? atoi(threads->value) : 0,\n\t\t\t\t\t\t(rtpcollision && rtpcollision->value) ?  atoi(rtpcollision->value) : 0,\n\t\t\t\t\t\tbufferkf_ms, bufferkf_bytes,\n\t\t\t\t\t\t(e2ee && e2ee->value) ? janus_is_true(e2ee->value) : FALSE,\n\t\t\t\t\t\t(pd && pd->value) ? janus_is_true(pd->value) : FALSE,\n\t\t\t\t\t\tabscaptime_src_id_int)) == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'rtp' mountpoint '%s'...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tmp->is_private = is_private;\n\t\t\t\tif(secret && secret->value)\n\t\t\t\t\tmp->secret = g_strdup(secret->value);\n\t\t\t\tif(pin && pin->value)\n\t\t\t\t\tmp->pin = g_strdup(pin->value);\n\t\t\t} else if(!strcasecmp(type->value, \"live\")) {\n\t\t\t\t/* File-based live source */\n\t\t\t\tjanus_config_item *desc = janus_config_get(config, cat, janus_config_type_item, \"description\");\n\t\t\t\tjanus_config_item *md = janus_config_get(config, cat, janus_config_type_item, \"metadata\");\n\t\t\t\tjanus_config_item *priv = janus_config_get(config, cat, janus_config_type_item, \"is_private\");\n\t\t\t\tjanus_config_item *secret = janus_config_get(config, cat, janus_config_type_item, \"secret\");\n\t\t\t\tjanus_config_item *pin = janus_config_get(config, cat, janus_config_type_item, \"pin\");\n\t\t\t\tjanus_config_item *file = janus_config_get(config, cat, janus_config_type_item, \"filename\");\n\t\t\t\tjanus_config_item *audio = janus_config_get(config, cat, janus_config_type_item, \"audio\");\n\t\t\t\tjanus_config_item *apt = janus_config_get(config, cat, janus_config_type_item, \"audiopt\");\n\t\t\t\tjanus_config_item *acodec = janus_config_get(config, cat, janus_config_type_item, \"audiocodec\");\n\t\t\t\tjanus_config_item *artpmap = janus_config_get(config, cat, janus_config_type_item, \"audiortpmap\");\n\t\t\t\tjanus_config_item *afmtp = janus_config_get(config, cat, janus_config_type_item, \"audiofmtp\");\n\t\t\t\tjanus_config_item *video = janus_config_get(config, cat, janus_config_type_item, \"video\");\n\t\t\t\tif(file == NULL || file->value == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' mountpoint '%s', missing mandatory information...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tgboolean is_private = priv && priv->value && janus_is_true(priv->value);\n\t\t\t\tgboolean doaudio = audio && audio->value && janus_is_true(audio->value);\n\t\t\t\tgboolean dovideo = video && video->value && janus_is_true(video->value);\n\t\t\t\t/* We only support audio for file-based streaming at the moment: for streaming\n\t\t\t\t * files using other codecs/formats an external tools should feed us RTP instead */\n\t\t\t\tif(!doaudio || dovideo) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' mountpoint '%s', we only support audio file streaming right now...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n#ifdef HAVE_LIBOGG\n\t\t\t\tif(!strstr(file->value, \".opus\") && !strstr(file->value, \".alaw\") && !strstr(file->value, \".mulaw\")) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' mountpoint '%s', unsupported format (we only support Opus and raw mu-Law/a-Law files right now)\\n\", cat->name);\n#else\n\t\t\t\tif(!strstr(file->value, \".alaw\") && !strstr(file->value, \".mulaw\")) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' mountpoint '%s', unsupported format (we only support raw mu-Law and a-Law files right now)\\n\", cat->name);\n#endif\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tFILE *audiofile = fopen(file->value, \"rb\");\n\t\t\t\tif(!audiofile) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' mountpoint, no such file '%s'...\\n\", file->value);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfclose(audiofile);\n\t\t\t\tconst char *audiocodec = (acodec && acodec->value ? acodec->value : NULL);\n\t\t\t\tif(audiocodec == NULL) {\n\t\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\t\tif(artpmap && artpmap->value)\n\t\t\t\t\t\taudiocodec = janus_sdp_get_rtpmap_codec(artpmap->value);\n\t\t\t\t}\n\n\t\t\t\tjanus_streaming_mountpoint *mp = NULL;\n\t\t\t\tif((mp = janus_streaming_create_file_source(\n\t\t\t\t\t\tmpid, (char *)(id ? id->value : NULL),\n\t\t\t\t\t\t(char *)cat->name,\n\t\t\t\t\t\tdesc ? (char *)desc->value : NULL,\n\t\t\t\t\t\tmd ? (char *)md->value : NULL,\n\t\t\t\t\t\t(char *)file->value, TRUE,\n\t\t\t\t\t\tdoaudio,\n\t\t\t\t\t\t(apt && apt->value) ? atoi(apt->value) : 0,\n\t\t\t\t\t\t(char *)audiocodec,\n\t\t\t\t\t\tafmtp ? (char *)afmtp->value : NULL,\n\t\t\t\t\t\tdovideo)) == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'live' mountpoint '%s'...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tmp->is_private = is_private;\n\t\t\t\tif(secret && secret->value)\n\t\t\t\t\tmp->secret = g_strdup(secret->value);\n\t\t\t\tif(pin && pin->value)\n\t\t\t\t\tmp->pin = g_strdup(pin->value);\n\t\t\t} else if(!strcasecmp(type->value, \"ondemand\")) {\n\t\t\t\t/* File-based on demand source */\n\t\t\t\tjanus_config_item *desc = janus_config_get(config, cat, janus_config_type_item, \"description\");\n\t\t\t\tjanus_config_item *md = janus_config_get(config, cat, janus_config_type_item, \"metadata\");\n\t\t\t\tjanus_config_item *priv = janus_config_get(config, cat, janus_config_type_item, \"is_private\");\n\t\t\t\tjanus_config_item *secret = janus_config_get(config, cat, janus_config_type_item, \"secret\");\n\t\t\t\tjanus_config_item *pin = janus_config_get(config, cat, janus_config_type_item, \"pin\");\n\t\t\t\tjanus_config_item *file = janus_config_get(config, cat, janus_config_type_item, \"filename\");\n\t\t\t\tjanus_config_item *audio = janus_config_get(config, cat, janus_config_type_item, \"audio\");\n\t\t\t\tjanus_config_item *apt = janus_config_get(config, cat, janus_config_type_item, \"audiopt\");\n\t\t\t\tjanus_config_item *acodec = janus_config_get(config, cat, janus_config_type_item, \"audiocodec\");\n\t\t\t\tjanus_config_item *artpmap = janus_config_get(config, cat, janus_config_type_item, \"audiortpmap\");\n\t\t\t\tjanus_config_item *afmtp = janus_config_get(config, cat, janus_config_type_item, \"audiofmtp\");\n\t\t\t\tjanus_config_item *video = janus_config_get(config, cat, janus_config_type_item, \"video\");\n\t\t\t\tif(file == NULL || file->value == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'ondemand' mountpoint '%s', missing mandatory information...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tgboolean is_private = priv && priv->value && janus_is_true(priv->value);\n\t\t\t\tgboolean doaudio = audio && audio->value && janus_is_true(audio->value);\n\t\t\t\tgboolean dovideo = video && video->value && janus_is_true(video->value);\n\t\t\t\t/* We only support audio for file-based streaming at the moment: for streaming\n\t\t\t\t * files using other codecs/formats an external tools should feed us RTP instead */\n\t\t\t\tif(!doaudio || dovideo) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'ondemand' mountpoint '%s', we only support audio file streaming right now...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n#ifdef HAVE_LIBOGG\n\t\t\t\tif(!strstr(file->value, \".opus\") && !strstr(file->value, \".alaw\") && !strstr(file->value, \".mulaw\")) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' mountpoint '%s', unsupported format (we only support Opus and raw mu-Law/a-Law files right now)\\n\", cat->name);\n#else\n\t\t\t\tif(!strstr(file->value, \".alaw\") && !strstr(file->value, \".mulaw\")) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'ondemand' mountpoint '%s', unsupported format (we only support raw mu-Law and a-Law files right now)\\n\", cat->name);\n#endif\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tFILE *audiofile = fopen(file->value, \"rb\");\n\t\t\t\tif(!audiofile) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'ondemand' mountpoint, no such file '%s'...\\n\", file->value);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfclose(audiofile);\n\t\t\t\tconst char *audiocodec = (acodec && acodec->value ? acodec->value : NULL);\n\t\t\t\tif(audiocodec == NULL) {\n\t\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\t\tif(artpmap && artpmap->value)\n\t\t\t\t\t\taudiocodec = janus_sdp_get_rtpmap_codec(artpmap->value);\n\t\t\t\t}\n\n\t\t\t\tjanus_streaming_mountpoint *mp = NULL;\n\t\t\t\tif((mp = janus_streaming_create_file_source(\n\t\t\t\t\t\tmpid, (char *)(id ? id->value : NULL),\n\t\t\t\t\t\t(char *)cat->name,\n\t\t\t\t\t\tdesc ? (char *)desc->value : NULL,\n\t\t\t\t\t\tmd ? (char *)md->value : NULL,\n\t\t\t\t\t\t(char *)file->value, FALSE,\n\t\t\t\t\t\tdoaudio,\n\t\t\t\t\t\t(apt && apt->value) ? atoi(apt->value) : 0,\n\t\t\t\t\t\t(char *)audiocodec,\n\t\t\t\t\t\tafmtp ? (char *)afmtp->value : NULL,\n\t\t\t\t\t\tdovideo)) == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'ondemand' mountpoint '%s'...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tmp->is_private = is_private;\n\t\t\t\tif(secret && secret->value)\n\t\t\t\t\tmp->secret = g_strdup(secret->value);\n\t\t\t\tif(pin && pin->value)\n\t\t\t\t\tmp->pin = g_strdup(pin->value);\n\t\t\t} else if(!strcasecmp(type->value, \"rtsp\")) {\n#ifndef HAVE_LIBCURL\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' mountpoint '%s', libcurl support not compiled...\\n\", cat->name);\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n#else\n\t\t\t\tjanus_config_item *desc = janus_config_get(config, cat, janus_config_type_item, \"description\");\n\t\t\t\tjanus_config_item *md = janus_config_get(config, cat, janus_config_type_item, \"metadata\");\n\t\t\t\tjanus_config_item *priv = janus_config_get(config, cat, janus_config_type_item, \"is_private\");\n\t\t\t\tjanus_config_item *secret = janus_config_get(config, cat, janus_config_type_item, \"secret\");\n\t\t\t\tjanus_config_item *pin = janus_config_get(config, cat, janus_config_type_item, \"pin\");\n\t\t\t\tjanus_config_item *file = janus_config_get(config, cat, janus_config_type_item, \"url\");\n\t\t\t\tjanus_config_item *username = janus_config_get(config, cat, janus_config_type_item, \"rtsp_user\");\n\t\t\t\tjanus_config_item *password = janus_config_get(config, cat, janus_config_type_item, \"rtsp_pwd\");\n\t\t\t\tjanus_config_item *quirk = janus_config_get(config, cat, janus_config_type_item, \"rtsp_quirk\");\n\t\t\t\tjanus_config_item *audio = janus_config_get(config, cat, janus_config_type_item, \"audio\");\n\t\t\t\tjanus_config_item *acodec = janus_config_get(config, cat, janus_config_type_item, \"audiocodec\");\n\t\t\t\tjanus_config_item *artpmap = janus_config_get(config, cat, janus_config_type_item, \"audiortpmap\");\n\t\t\t\tjanus_config_item *apt = janus_config_get(config, cat, janus_config_type_item, \"audiopt\");\n\t\t\t\tjanus_config_item *afmtp = janus_config_get(config, cat, janus_config_type_item, \"audiofmtp\");\n\t\t\t\tjanus_config_item *video = janus_config_get(config, cat, janus_config_type_item, \"video\");\n\t\t\t\tjanus_config_item *vpt = janus_config_get(config, cat, janus_config_type_item, \"videopt\");\n\t\t\t\tjanus_config_item *vcodec = janus_config_get(config, cat, janus_config_type_item, \"videocodec\");\n\t\t\t\tjanus_config_item *vrtpmap = janus_config_get(config, cat, janus_config_type_item, \"videortpmap\");\n\t\t\t\tjanus_config_item *vfmtp = janus_config_get(config, cat, janus_config_type_item, \"videofmtp\");\n\t\t\t\tjanus_config_item *vkf = janus_config_get(config, cat, janus_config_type_item, \"videobufferkf\");\n\t\t\t\tjanus_config_item *vkf_ms = janus_config_get(config, cat, janus_config_type_item, \"bufferkf_ms\");\n\t\t\t\tjanus_config_item *vkf_bytes = janus_config_get(config, cat, janus_config_type_item, \"bufferkf_bytes\");\n\t\t\t\tjanus_config_item *iface = janus_config_get(config, cat, janus_config_type_item, \"rtspiface\");\n\t\t\t\tjanus_config_item *failerr = janus_config_get(config, cat, janus_config_type_item, \"rtsp_failcheck\");\n\t\t\t\tjanus_config_item *threads = janus_config_get(config, cat, janus_config_type_item, \"threads\");\n\t\t\t\tjanus_config_item *reconnect_delay = janus_config_get(config, cat, janus_config_type_item, \"rtsp_reconnect_delay\");\n\t\t\t\tjanus_config_item *session_timeout = janus_config_get(config, cat, janus_config_type_item, \"rtsp_session_timeout\");\n\t\t\t\tjanus_config_item *rtsp_timeout = janus_config_get(config, cat, janus_config_type_item, \"rtsp_timeout\");\n\t\t\t\tjanus_config_item *rtsp_conn_timeout = janus_config_get(config, cat, janus_config_type_item, \"rtsp_conn_timeout\");\n\t\t\t\tjanus_config_item *rtsp_notify_changes = janus_config_get(config, cat, janus_config_type_item, \"rtsp_notify_changes\");\n\t\t\t\tjanus_network_address iface_value;\n\t\t\t\tif(file == NULL || file->value == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' mountpoint '%s', missing mandatory information...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tgboolean is_private = priv && priv->value && janus_is_true(priv->value);\n\t\t\t\tgboolean rtsp_quirk = quirk && quirk->value && janus_is_true(quirk->value);\n\t\t\t\tgboolean notify_changes = rtsp_notify_changes && rtsp_notify_changes->value && janus_is_true(rtsp_notify_changes->value);\n\t\t\t\tgboolean doaudio = audio && audio->value && janus_is_true(audio->value);\n\t\t\t\tgboolean dovideo = video && video->value && janus_is_true(video->value);\n\t\t\t\tif(video && vkf && vkf->value && janus_is_true(vkf->value)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The videobufferkf property has been deprecated, please refer to bufferkf_ms and/or bufferkf_bytes\\n\");\n\t\t\t\t}\n\t\t\t\tuint16_t bufferkf_ms = 0;\n\t\t\t\tif(vkf_ms && vkf_ms->value && janus_string_to_uint16(vkf_ms->value, &bufferkf_ms) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' mountpoint '%s', invalid bufferkf_ms configuration...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tuint32_t bufferkf_bytes = 0;\n\t\t\t\tif(vkf_bytes && vkf_bytes->value && janus_string_to_uint32(vkf_bytes->value, &bufferkf_bytes) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' mountpoint '%s', invalid bufferkf_bytes configuration...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tgboolean error_on_failure = TRUE;\n\t\t\t\tif(failerr && failerr->value)\n\t\t\t\t\terror_on_failure = janus_is_true(failerr->value);\n\t\t\t\tif(threads && threads->value && atoi(threads->value) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' mountpoint '%s', invalid threads configuration...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif((doaudio || dovideo) && iface && iface->value) {\n\t\t\t\t\tif(!ifas) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Skipping 'rtsp' mountpoint '%s', it relies on network configuration but network device information is unavailable...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(janus_network_lookup_interface(ifas, iface->value, &iface_value) != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' mountpoint '%s', invalid network interface configuration for stream...\\n\", cat->name);\n\t\t\t\t\t\tcl = cl->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst char *audiocodec = (acodec && acodec->value ? acodec->value : NULL);\n\t\t\t\tif(audiocodec == NULL) {\n\t\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\t\tif(artpmap && artpmap->value)\n\t\t\t\t\t\taudiocodec = janus_sdp_get_rtpmap_codec(artpmap->value);\n\t\t\t\t}\n\t\t\t\tconst char *videocodec = (vcodec && vcodec->value ? vcodec->value : NULL);\n\t\t\t\tif(videocodec == NULL) {\n\t\t\t\t\t/* No videocodec property, check the deprecated videortpmap */\n\t\t\t\t\tif(vrtpmap && vrtpmap->value)\n\t\t\t\t\t\tvideocodec = janus_sdp_get_rtpmap_codec(vrtpmap->value);\n\t\t\t\t}\n\n\t\t\t\tjanus_streaming_mountpoint *mp = NULL;\n\t\t\t\tif((mp = janus_streaming_create_rtsp_source(\n\t\t\t\t\t\tmpid, (char *)(id ? id->value : NULL),\n\t\t\t\t\t\t(char *)cat->name,\n\t\t\t\t\t\tdesc ? (char *)desc->value : NULL,\n\t\t\t\t\t\tmd ? (char *)md->value : NULL,\n\t\t\t\t\t\t(char *)file->value,\n\t\t\t\t\t\tusername ? (char *)username->value : NULL,\n\t\t\t\t\t\tpassword ? (char *)password->value : NULL,\n\t\t\t\t\t\trtsp_quirk, notify_changes,\n\t\t\t\t\t\tdoaudio,\n\t\t\t\t\t\t(apt && apt->value) ? atoi(apt->value) : -1,\n\t\t\t\t\t\t(char *)audiocodec,\n\t\t\t\t\t\tafmtp ? (char *)afmtp->value : NULL,\n\t\t\t\t\t\tdovideo,\n\t\t\t\t\t\t(vpt && vpt->value) ? atoi(vpt->value) : -1,\n\t\t\t\t\t\t(char *)videocodec,\n\t\t\t\t\t\tvfmtp ? (char *)vfmtp->value : NULL,\n\t\t\t\t\t\tbufferkf_ms, bufferkf_bytes,\n\t\t\t\t\t\tiface && iface->value ? &iface_value : NULL,\n\t\t\t\t\t\t(threads && threads->value) ? atoi(threads->value) : 0,\n\t\t\t\t\t\t((reconnect_delay && reconnect_delay->value) ? atoi(reconnect_delay->value) : JANUS_STREAMING_DEFAULT_RECONNECT_DELAY) * G_USEC_PER_SEC,\n\t\t\t\t\t\t((session_timeout && session_timeout->value) ? atoi(session_timeout->value) : JANUS_STREAMING_DEFAULT_SESSION_TIMEOUT) * G_USEC_PER_SEC,\n\t\t\t\t\t\t((rtsp_timeout && rtsp_timeout->value) ? atoi(rtsp_timeout->value) : JANUS_STREAMING_DEFAULT_CURL_TIMEOUT),\n\t\t\t\t\t\t((rtsp_conn_timeout && rtsp_conn_timeout->value) ? atoi(rtsp_conn_timeout->value) : JANUS_STREAMING_DEFAULT_CURL_CONNECT_TIMEOUT),\n\t\t\t\t\t\terror_on_failure)) == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'rtsp' mountpoint '%s'...\\n\", cat->name);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tmp->is_private = is_private;\n\t\t\t\tif(secret && secret->value)\n\t\t\t\t\tmp->secret = g_strdup(secret->value);\n\t\t\t\tif(pin && pin->value)\n\t\t\t\t\tmp->pin = g_strdup(pin->value);\n#endif\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring unknown mountpoint type '%s' (%s)...\\n\", type->value, cat->name);\n\t\t\t}\n\t\t\tcl = cl->next;\n\t\t}\n\t\tg_list_free(clist);\n\t\t/* Done: we keep the configuration file open in case we get a \"create\" or \"destroy\" with permanent=true */\n\t}\n\tif(ifas) {\n\t\tfreeifaddrs(ifas);\n\t}\n\n\t/* Show available mountpoints */\n\tjanus_mutex_lock(&mountpoints_mutex);\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, mountpoints);\n\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_streaming_mountpoint *mp = value;\n\t\tJANUS_LOG(LOG_VERB, \"  ::: [%s][%s] %s (%s, %s, %s, pin: %s)\\n\", mp->id_str, mp->name, mp->description,\n\t\t\tmp->streaming_type == janus_streaming_type_live ? \"live\" : \"on demand\",\n\t\t\tmp->streaming_source == janus_streaming_source_rtp ? \"RTP source\" : \"file source\",\n\t\t\tmp->is_private ? \"private\" : \"public\",\n\t\t\tmp->pin ? mp->pin : \"no pin\");\n\t}\n\tjanus_mutex_unlock(&mountpoints_mutex);\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_streaming_session_destroy);\n\tmessages = g_async_queue_new_full((GDestroyNotify)janus_streaming_message_free);\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Launch the thread that will handle incoming messages */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"streaming handler\", janus_streaming_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Streaming handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_config_destroy(config);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_STREAMING_NAME);\n\treturn 0;\n}\n\nvoid janus_streaming_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\t/* Remove all mountpoints */\n\tjanus_mutex_lock(&mountpoints_mutex);\n\tg_hash_table_destroy(mountpoints);\n\tmountpoints = NULL;\n\tg_hash_table_destroy(mountpoints_temp);\n\tmountpoints_temp = NULL;\n\tjanus_mutex_unlock(&mountpoints_mutex);\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\n\tjanus_config_destroy(config);\n\tg_free(admin_key);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_STREAMING_NAME);\n}\n\nint janus_streaming_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_streaming_get_version(void) {\n\treturn JANUS_STREAMING_VERSION;\n}\n\nconst char *janus_streaming_get_version_string(void) {\n\treturn JANUS_STREAMING_VERSION_STRING;\n}\n\nconst char *janus_streaming_get_description(void) {\n\treturn JANUS_STREAMING_DESCRIPTION;\n}\n\nconst char *janus_streaming_get_name(void) {\n\treturn JANUS_STREAMING_NAME;\n}\n\nconst char *janus_streaming_get_author(void) {\n\treturn JANUS_STREAMING_AUTHOR;\n}\n\nconst char *janus_streaming_get_package(void) {\n\treturn JANUS_STREAMING_PACKAGE;\n}\n\nstatic janus_streaming_session *janus_streaming_lookup_session(janus_plugin_session *handle) {\n\tjanus_streaming_session *session = NULL;\n\tif(g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_streaming_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_streaming_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_streaming_session *session = g_malloc0(sizeof(janus_streaming_session));\n\tsession->handle = handle;\n\tsession->mountpoint = NULL;\t/* This will happen later */\n\tjanus_mutex_init(&session->mutex);\n\tg_atomic_int_set(&session->started, 0);\n\tg_atomic_int_set(&session->paused, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n\thandle->plugin_handle = session;\n\tjanus_refcount_init(&session->ref, janus_streaming_session_free);\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_streaming_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_streaming_session *session = janus_streaming_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Removing streaming session...\\n\");\n\tjanus_streaming_hangup_media_internal(handle);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn;\n}\n\njson_t *janus_streaming_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_streaming_session *session = janus_streaming_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* What is this user watching, if anything? */\n\tjson_t *info = json_object();\n\tjanus_streaming_mountpoint *mp = session->mountpoint;\n\tjson_object_set_new(info, \"state\", json_string(mp ? \"watching\" : \"idle\"));\n\tif(mp) {\n\t\tjanus_refcount_increase(&mp->ref);\n\t\tjson_object_set_new(info, \"mountpoint_id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\tjson_object_set_new(info, \"mountpoint_name\", mp->name ? json_string(mp->name) : NULL);\n\t\tjanus_mutex_lock(&mp->mutex);\n\t\tjson_object_set_new(info, \"mountpoint_viewers\", json_integer(mp->viewers ? g_list_length(mp->viewers) : 0));\n\t\tjanus_mutex_unlock(&mp->mutex);\n\t\tif(mp->streaming_source == janus_streaming_source_file) {\n\t\t\tjanus_streaming_file_source *source = mp->source;\n\t\t\tjson_t *media = json_object();\n\t\t\tjson_object_set_new(media, \"type\", json_string(\"audio\"));\n\t\t\tjson_object_set_new(media, \"filename\", json_string(source->filename));\n\t\t\tjson_object_set_new(info, \"media\", media);\n\t\t} else if(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\tjson_t *media = json_array();\n\t\t\tGList *temp = session->streams;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_streaming_session_stream *s = (janus_streaming_session_stream *)temp->data;\n\t\t\t\tjanus_streaming_rtp_source_stream *stream = s->stream;\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"type\", json_string(janus_streaming_media_str(stream->type)));\n\t\t\t\tjson_object_set_new(info, \"mindex\", json_integer(s->mindex));\n\t\t\t\tjson_object_set_new(info, \"mid\", json_string(stream->mid));\n\t\t\t\tif(stream->simulcast) {\n\t\t\t\t\tjson_t *simulcast = json_object();\n\t\t\t\t\tjson_object_set_new(simulcast, \"substream\", json_integer(s->sim_context.substream));\n\t\t\t\t\tjson_object_set_new(simulcast, \"substream-target\", json_integer(s->sim_context.substream_target));\n\t\t\t\t\tjson_object_set_new(simulcast, \"temporal-layer\", json_integer(s->sim_context.templayer));\n\t\t\t\t\tjson_object_set_new(simulcast, \"temporal-layer-target\", json_integer(s->sim_context.templayer_target));\n\t\t\t\t\tif(s->sim_context.drop_trigger > 0)\n\t\t\t\t\t\tjson_object_set_new(simulcast, \"fallback\", json_integer(s->sim_context.drop_trigger));\n\t\t\t\t\tjson_object_set_new(info, \"simulcast\", simulcast);\n\t\t\t\t}\n\t\t\t\tif(stream->svc) {\n\t\t\t\t\tjson_t *svc = json_object();\n\t\t\t\t\tjson_object_set_new(svc, \"spatial-layer\", json_integer(s->spatial_layer));\n\t\t\t\t\tjson_object_set_new(svc, \"target-spatial-layer\", json_integer(s->target_spatial_layer));\n\t\t\t\t\tjson_object_set_new(svc, \"temporal-layer\", json_integer(s->temporal_layer));\n\t\t\t\t\tjson_object_set_new(svc, \"target-temporal-layer\", json_integer(s->target_temporal_layer));\n\t\t\t\t\tjson_object_set_new(info, \"svc\", svc);\n\t\t\t\t}\n\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_VIDEO && session->playoutdelay_ext) {\n\t\t\t\t\tjson_t *pd = json_object();\n\t\t\t\t\tjson_object_set_new(pd, \"min-delay\", json_integer(s->min_delay));\n\t\t\t\t\tjson_object_set_new(pd, \"max-delay\", json_integer(s->max_delay));\n\t\t\t\t\tjson_object_set_new(info, \"playout-delay\", pd);\n\t\t\t\t}\n\t\t\t\tif(session->abscapturetime_src_ext_id > 0)\n\t\t\t\t\tjson_object_set_new(info, \"abs-capture-time-src-ext-id\", json_integer(session->abscapturetime_src_ext_id));\n\t\t\t\tjson_array_append_new(media, info);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tjson_object_set_new(info, \"media\", media);\n\t\t}\n\t\tjanus_refcount_decrease(&mp->ref);\n\t}\n\tif(session->e2ee)\n\t\tjson_object_set_new(info, \"e2ee\", json_true());\n\tjson_object_set_new(info, \"hangingup\", json_integer(g_atomic_int_get(&session->hangingup)));\n\tjson_object_set_new(info, \"started\", json_integer(g_atomic_int_get(&session->started)));\n\tjson_object_set_new(info, \"dataready\", json_integer(g_atomic_int_get(&session->dataready)));\n\tjson_object_set_new(info, \"paused\", json_integer(g_atomic_int_get(&session->paused)));\n\tjson_object_set_new(info, \"stopping\", json_integer(g_atomic_int_get(&session->stopping)));\n\tjson_object_set_new(info, \"destroyed\", json_integer(g_atomic_int_get(&session->destroyed)));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\n/* Helper method to process synchronous requests */\nstatic json_t *janus_streaming_process_synchronous_request(janus_streaming_session *session, json_t *message) {\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\n\t/* Parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\tstruct ifaddrs *ifas = NULL;\n\n\tif(!strcasecmp(request_text, \"list\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Request for the list of mountpoints\\n\");\n\t\tgboolean lock_mp_list = TRUE;\n\t\tif(admin_key != NULL) {\n\t\t\tjson_t *admin_key_json = json_object_get(root, \"admin_key\");\n\t\t\t/* Verify admin_key if it was provided */\n\t\t\tif(admin_key_json != NULL && json_is_string(admin_key_json) && strlen(json_string_value(admin_key_json)) > 0) {\n\t\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t} else {\n\t\t\t\t\tlock_mp_list = FALSE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjson_t *list = json_array();\n\t\t/* Return a list of all available mountpoints */\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, mountpoints);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_streaming_mountpoint *mp = value;\n\t\t\tif(mp->is_private && lock_mp_list) {\n\t\t\t\t/* Skip private stream if no valid admin_key was provided */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping private mountpoint '%s'\\n\", mp->description);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&mp->ref);\n\t\t\tjson_t *ml = json_object();\n\t\t\tjson_object_set_new(ml, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\t\tjson_object_set_new(ml, \"type\", json_string(mp->streaming_type == janus_streaming_type_live ? \"live\" : \"on demand\"));\n\t\t\tjson_object_set_new(ml, \"description\", json_string(mp->description));\n\t\t\tif(mp->metadata) {\n\t\t\t\tjson_object_set_new(ml, \"metadata\", json_string(mp->metadata));\n\t\t\t}\n\t\t\tjson_object_set_new(ml, \"enabled\", mp->enabled ? json_true() : json_false());\n\t\t\tif(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\tjson_t *media = json_array();\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\tjson_object_set_new(info, \"mid\", json_string(stream->mid));\n\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(janus_streaming_media_str(stream->type)));\n\t\t\t\t\tjson_object_set_new(info, \"label\", json_string(stream->label));\n\t\t\t\t\tif(stream->msid && stream->mstid) {\n\t\t\t\t\t\tchar msid[150];\n\t\t\t\t\t\tg_snprintf(msid, sizeof(msid), \"%s %s\", stream->msid, stream->mstid);\n\t\t\t\t\t\tjson_object_set_new(info, \"msid\", json_string(msid));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->fd[0] != -1 || stream->fd[1] != -1 || stream->fd[2] != -1)\n\t\t\t\t\t\tjson_object_set_new(info, \"age_ms\", json_integer((now - stream->last_received[0]) / 1000));\n\t\t\t\t\tjson_array_append_new(media, info);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(ml, \"media\", media);\n\t\t\t}\n\t\t\tjson_array_append_new(list, ml);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t}\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"streaming\", json_string(\"list\"));\n\t\tjson_object_set_new(response, \"list\", list);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"info\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Request info on a specific mountpoint\\n\");\n\t\t/* Return info on a specific mountpoint */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 id_value = 0;\n\t\tchar id_num[30], *id_value_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tid_value = json_integer_value(id);\n\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\tid_value_str = id_num;\n\t\t} else {\n\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\tif(mp == NULL) {\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&mp->ref);\n\t\t/* Return more info if the right secret is provided */\n\t\tgboolean admin = FALSE;\n\t\tif(mp->secret) {\n\t\t\tjson_t *secret = json_object_get(root, \"secret\");\n\t\t\tif(secret && json_string_value(secret) && janus_strcmp_const_time(mp->secret, json_string_value(secret)))\n\t\t\t\tadmin = TRUE;\n\t\t} else {\n\t\t\tadmin = TRUE;\n\t\t}\n\t\tjson_t *ml = json_object();\n\t\tjson_object_set_new(ml, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\tif(admin && mp->name)\n\t\t\tjson_object_set_new(ml, \"name\", json_string(mp->name));\n\t\tif(mp->description)\n\t\t\tjson_object_set_new(ml, \"description\", json_string(mp->description));\n\t\tif(mp->metadata)\n\t\t\tjson_object_set_new(ml, \"metadata\", json_string(mp->metadata));\n\t\tif(admin && mp->secret)\n\t\t\tjson_object_set_new(ml, \"secret\", json_string(mp->secret));\n\t\tif(admin && mp->pin)\n\t\t\tjson_object_set_new(ml, \"pin\", json_string(mp->pin));\n\t\tif(admin && mp->is_private)\n\t\t\tjson_object_set_new(ml, \"is_private\", json_true());\n\t\tjson_object_set_new(ml, \"enabled\", mp->enabled ? json_true() : json_false());\n\t\tif(admin)\n\t\t\tjson_object_set_new(ml, \"viewers\", json_integer(mp->viewers ? g_list_length(mp->viewers) : 0));\n\t\tjson_object_set_new(ml, \"type\", json_string(mp->streaming_type == janus_streaming_type_live ? \"live\" : \"on demand\"));\n\t\t/* Add details on all the media streams in this mountpoint */\n\t\tjson_t *media = json_array();\n\t\tjson_object_set_new(ml, \"media\", media);\n\t\tif(mp->streaming_source == janus_streaming_source_file) {\n\t\t\tjanus_streaming_file_source *source = mp->source;\n\t\t\tif(admin && source->filename)\n\t\t\t\tjson_object_set_new(ml, \"filename\", json_string(source->filename));\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"type\", json_string(\"audio\"));\n\t\t\tif(source->codecs.pt != -1)\n\t\t\t\tjson_object_set_new(info, \"pt\", json_integer(source->codecs.pt));\n\t\t\tif(source->codecs.audio_codec != JANUS_AUDIOCODEC_NONE) {\n\t\t\t\tconst char *codec = janus_audiocodec_name(source->codecs.audio_codec);\n\t\t\t\tif(codec != NULL) {\n\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(codec));\n\t\t\t\t\tjson_object_set_new(info, \"rtpmap\", json_string(janus_sdp_get_codec_rtpmap(codec)));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(source->codecs.fmtp)\n\t\t\t\tjson_object_set_new(info, \"fmtp\", json_string(source->codecs.fmtp));\n\t\t\tjson_array_append_new(media, info);\n\t\t} else if(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\t\t/* Global stuff first */\n\t\t\tgint64 now = janus_get_monotonic_time();\n#ifdef HAVE_LIBCURL\n\t\t\tif(source->rtsp) {\n\t\t\t\tjson_object_set_new(ml, \"rtsp\", json_true());\n\t\t\t\tif(admin) {\n\t\t\t\t\tif(source->rtsp_url)\n\t\t\t\t\t\tjson_object_set_new(ml, \"url\", json_string(source->rtsp_url));\n\t\t\t\t\tif(source->rtsp_username)\n\t\t\t\t\t\tjson_object_set_new(ml, \"rtsp_user\", json_string(source->rtsp_username));\n\t\t\t\t\tif(source->rtsp_password)\n\t\t\t\t\t\tjson_object_set_new(ml, \"rtsp_pwd\", json_string(source->rtsp_password));\n\t\t\t\t\tif(source->rtsp_quirk)\n\t\t\t\t\t\tjson_object_set_new(ml, \"rtsp_quirk\", json_true());\n\t\t\t\t}\n\t\t\t}\n#endif\n\t\t\tif(source->is_srtp) {\n\t\t\t\tjson_object_set_new(ml, \"srtp\", json_true());\n\t\t\t}\n\t\t\tif(source->rtp_collision > 0)\n\t\t\t\tjson_object_set_new(ml, \"collision\", json_integer(source->rtp_collision));\n\t\t\tif(source->bufferkf_ms > 0) {\n\t\t\t\tjson_object_set_new(ml, \"bufferkf_ms\", json_integer(source->bufferkf_ms));\n\t\t\t}\n\t\t\tif(source->bufferkf_bytes > 0) {\n\t\t\t\tjson_object_set_new(ml, \"bufferkf_bytes\", json_integer(source->bufferkf_bytes));\n\t\t\t}\n\t\t\tif(mp->helper_threads > 0)\n\t\t\t\tjson_object_set_new(ml, \"threads\", json_integer(mp->helper_threads));\n\t\t\t/* Iterate on media now */\n\t\t\tGList *temp = source->media;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"mindex\", json_integer(stream->mindex));\n\t\t\t\tjson_object_set_new(info, \"type\", json_string(janus_streaming_media_str(stream->type)));\n\t\t\t\tjson_object_set_new(info, \"mid\", json_string(stream->mid));\n\t\t\t\tjson_object_set_new(info, \"label\", json_string(stream->label));\n\t\t\t\tif(stream->msid && stream->mstid) {\n\t\t\t\t\tchar msid[150];\n\t\t\t\t\tg_snprintf(msid, sizeof(msid), \"%s %s\", stream->msid, stream->mstid);\n\t\t\t\t\tjson_object_set_new(info, \"msid\", json_string(msid));\n\t\t\t\t}\n\t\t\t\tif(stream->codecs.pt != -1)\n\t\t\t\t\tjson_object_set_new(info, \"pt\", json_integer(stream->codecs.pt));\n\t\t\t\tif(stream->codecs.audio_codec != JANUS_AUDIOCODEC_NONE) {\n\t\t\t\t\tconst char *codec = janus_audiocodec_name(stream->codecs.audio_codec);\n\t\t\t\t\tif(codec != NULL) {\n\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(codec));\n\t\t\t\t\t\tjson_object_set_new(info, \"rtpmap\", json_string(janus_sdp_get_codec_rtpmap(codec)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(stream->codecs.video_codec != JANUS_VIDEOCODEC_NONE) {\n\t\t\t\t\tconst char *codec = janus_videocodec_name(stream->codecs.video_codec);\n\t\t\t\t\tif(codec != NULL) {\n\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(codec));\n\t\t\t\t\t\tjson_object_set_new(info, \"rtpmap\", json_string(janus_sdp_get_codec_rtpmap(codec)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(stream->codecs.fmtp)\n\t\t\t\t\tjson_object_set_new(info, \"fmtp\", json_string(stream->codecs.fmtp));\n\t\t\t\tif(stream->simulcast) {\n\t\t\t\t\tjson_object_set_new(info, \"videosimulcast\", json_true());\n\t\t\t\t}\n\t\t\t\tif(stream->svc) {\n\t\t\t\t\tjson_object_set_new(info, \"videosvc\", json_true());\n\t\t\t\t}\n\t\t\t\tif(stream->skew)\n\t\t\t\t\tjson_object_set_new(info, \"skew_compensation\", json_true());\n\t\t\t\tif(admin) {\n\t\t\t\t\tif(stream->host)\n\t\t\t\t\t\tjson_object_set_new(ml, \"host\", json_string(stream->host));\n\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(stream->port[0]));\n\t\t\t\t\tif(stream->rtcp_port > -1)\n\t\t\t\t\t\tjson_object_set_new(info, \"rtcpport\", json_integer(stream->rtcp_port));\n\t\t\t\t\tif(stream->port[1] > -1)\n\t\t\t\t\t\tjson_object_set_new(info, \"port2\", json_integer(stream->port[1]));\n\t\t\t\t\tif(stream->port[2] > -1)\n\t\t\t\t\t\tjson_object_set_new(info, \"port3\", json_integer(stream->port[2]));\n\t\t\t\t}\n\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_DATA)\n\t\t\t\t\tjson_object_set_new(info, \"datatype\", json_string(stream->textdata ? \"text\" : \"binary\"));\n\t\t\t\tif(stream->fd[0] != -1 || stream->fd[1] != -1 || stream->fd[2] != -1)\n\t\t\t\t\tjson_object_set_new(info, \"age_ms\", json_integer((now - stream->last_received[0]) / 1000));\n\t\t\t\tjanus_mutex_lock(&source->rec_mutex);\n\t\t\t\tif(admin && stream->rc && stream->rc->filename)\n\t\t\t\t\tjson_object_set_new(info, \"recording\", json_string(stream->rc->filename));\n\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\tjson_array_append_new(media, info);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t}\n\t\tjanus_refcount_decrease(&mp->ref);\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"streaming\", json_string(\"info\"));\n\t\tjson_object_set_new(response, \"info\", ml);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"create\")) {\n\t\t/* Create a new stream */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, create_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idopt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstropt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tif(getifaddrs(&ifas) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unable to acquire list of network devices/interfaces; some configurations may not work as expected... %d (%s)\\n\",\n\t\t\t\terrno, g_strerror(errno));\n\t\t}\n\n\t\tjson_t *type = json_object_get(root, \"type\");\n\t\tconst char *type_text = json_string_value(type);\n\t\tjson_t *secret = json_object_get(root, \"secret\");\n\t\tjson_t *pin = json_object_get(root, \"pin\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't create permanent mountpoint\\n\");\n\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't create permanent mountpoint\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t/* Check if an ID has been provided, or if we need to generate one ourselves */\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tguint64 mpid = string_ids ? 0 : json_integer_value(id);\n\t\tchar *mpid_str = (char *)(string_ids ? json_string_value(id) : NULL);\n\t\tif((!string_ids && mpid > 0) || (string_ids && mpid_str != NULL)) {\n\t\t\t/* Make sure the provided ID isn't already in use */\n\t\t\tif(g_hash_table_lookup(mountpoints, string_ids ? (gpointer)mpid_str : (gpointer)&mpid) != NULL ||\n\t\t\t\t\tg_hash_table_lookup(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid) != NULL) {\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"A stream with the provided ID already exists\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"A stream with the provided ID already exists\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t} else if(!string_ids && mpid == 0) {\n\t\t\t/* Generate a unique numeric ID */\n\t\t\tJANUS_LOG(LOG_VERB, \"Missing numeric id, will generate a random one...\\n\");\n\t\t\twhile(mpid == 0) {\n\t\t\t\tmpid = janus_random_uint64();\n\t\t\t\tif(g_hash_table_lookup(mountpoints, &mpid) != NULL ||\n\t\t\t\t\t\tg_hash_table_lookup(mountpoints_temp, &mpid) != NULL) {\n\t\t\t\t\t/* ID already in use, try another one */\n\t\t\t\t\tmpid = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(string_ids && mpid_str == NULL) {\n\t\t\t/* Generate a unique alphanumeric ID */\n\t\t\tJANUS_LOG(LOG_VERB, \"Missing alphanumeric id, will generate a random one...\\n\");\n\t\t\twhile(mpid_str == 0) {\n\t\t\t\tmpid_str = janus_random_uuid();\n\t\t\t\tif(g_hash_table_lookup(mountpoints, mpid_str) != NULL ||\n\t\t\t\t\t\tg_hash_table_lookup(mountpoints_temp, mpid_str) != NULL) {\n\t\t\t\t\t/* ID already in use, try another one */\n\t\t\t\t\tg_free(mpid_str);\n\t\t\t\t\tmpid_str = NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tg_hash_table_insert(mountpoints_temp,\n\t\t\tstring_ids ? (gpointer)g_strdup(mpid_str) : (gpointer)janus_uint64_dup(mpid),\n\t\t\tGUINT_TO_POINTER(TRUE));\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\tjanus_streaming_mountpoint *mp = NULL;\n\t\tgboolean legacy = FALSE;\n\t\tif(!strcasecmp(type_text, \"rtp\")) {\n\t\t\t/* RTP live source (e.g., from gstreamer/ffmpeg/vlc/etc.) */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tjson_t *name = json_object_get(root, \"name\");\n\t\t\tjson_t *desc = json_object_get(root, \"description\");\n\t\t\tjson_t *md = json_object_get(root, \"metadata\");\n\t\t\tjson_t *is_private = json_object_get(root, \"is_private\");\n\t\t\tjson_t *rtpcollision = json_object_get(root, \"collision\");\n\t\t\tjson_t *vkf_ms = json_object_get(root, \"bufferkf_ms\");\n\t\t\tjson_t *vkf_bytes = json_object_get(root, \"bufferkf_bytes\");\n\t\t\tjson_t *threads = json_object_get(root, \"threads\");\n\t\t\tjson_t *ssuite = json_object_get(root, \"srtpsuite\");\n\t\t\tjson_t *scrypto = json_object_get(root, \"srtpcrypto\");\n\t\t\tjson_t *e2ee = json_object_get(root, \"e2ee\");\n\t\t\tjson_t *pd = json_object_get(root, \"playoutdelay_ext\");\n\t\t\tjson_t *abscaptime_src_id = json_object_get(root, \"abscapturetime_src_ext_id\");\n\t\t\tif(abscaptime_src_id && (json_integer_value(abscaptime_src_id) < 1 || json_integer_value(abscaptime_src_id) > 14)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (abscaptime_src_id must be an integer between 1 and 14)\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (abscaptime_src_id must be an integer between 1 and 14)\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tif(vkf_ms && json_integer_value(vkf_ms) > UINT16_MAX) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream, invalid bufferkf_ms value...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, invalid bufferkf_ms value...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tuint16_t bufferkf_ms = vkf_ms ? json_integer_value(vkf_ms) : 0;\n\t\t\tif(vkf_bytes && json_integer_value(vkf_bytes) > UINT32_MAX) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream, invalid bufferkf_bytes value...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, invalid bufferkf_bytes value...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tuint32_t bufferkf_bytes = vkf_bytes ? json_integer_value(vkf_bytes) : 0;\n\t\t\tif(ssuite && json_integer_value(ssuite) != 32 && json_integer_value(ssuite) != 80) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream, invalid SRTP suite...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, invalid SRTP suite...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tjson_t *media = json_object_get(root, \"media\");\n\t\t\tGList *streams = NULL;\n\t\t\t/* How are we adding media? */\n\t\t\tif(media != NULL) {\n\t\t\t\tuint16_t s_bufferkf_ms = 0;\n\t\t\t\tuint32_t s_bufferkf_bytes = 0;\n\t\t\t\t/* We're using the new media-based configuration, iterate on all media objects */\n\t\t\t\tif(json_array_size(media) == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream, no audio, video or data have to be streamed...\\n\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, no audio or video have to be streamed...\");\n\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(media); i++) {\n\t\t\t\t\tjson_t *m = json_array_get(media, i);\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(m, rtp_media_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_network_address iface;\n\t\t\t\t\tuint16_t port = 0, port2 = 0, port3 = 0;\n\t\t\t\t\tuint16_t rtcpport = 0;\n\t\t\t\t\tuint8_t pt = 0;\n\t\t\t\t\tchar *mtype = NULL, *mid = NULL, *label = NULL, *msid = NULL, *codec = NULL, *fmtp = NULL, *sps = NULL, *mcast = NULL, *miface = NULL;\n\t\t\t\t\tgboolean doskew = FALSE, simulcast = FALSE, dosvc = FALSE, textdata = TRUE, buffermsg = FALSE;\n\t\t\t\t\tjson_t *jmtype = json_object_get(m, \"type\");\n\t\t\t\t\tmtype = (char *)json_string_value(jmtype);\n\t\t\t\t\tif(strcasecmp(mtype, \"audio\") && strcasecmp(mtype, \"video\") && strcasecmp(mtype, \"data\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', unsupported media type...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported media type in media stream\");\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\t/* We need mid and label for addressing this stream on the client side */\n\t\t\t\t\tjson_t *jmid = json_object_get(m, \"mid\");\n\t\t\t\t\tmid = (char *)json_string_value(jmid);\n\t\t\t\t\tjson_t *jlabel = json_object_get(m, \"label\");\n\t\t\t\t\tlabel = (char *)json_string_value(jlabel);\n\t\t\t\t\tjson_t *jmsid = json_object_get(m, \"msid\");\n\t\t\t\t\tmsid = (char *)json_string_value(jmsid);\n\t\t\t\t\tjson_t *jmcast = json_object_get(m, \"mcast\");\n\t\t\t\t\tmcast = (char *)json_string_value(jmcast);\n\t\t\t\t\tjson_t *jport = json_object_get(m, \"port\");\n\t\t\t\t\tport = json_integer_value(jport);\n\t\t\t\t\tjson_t *jrtcpport = json_object_get(m, \"rtcpport\");\n\t\t\t\t\tif(jrtcpport)\n\t\t\t\t\t\trtcpport = json_integer_value(jrtcpport);\n\t\t\t\t\tjson_t *jpt = json_object_get(m, \"pt\");\n\t\t\t\t\tpt = json_integer_value(jpt);\n\t\t\t\t\tjson_t *jcodec = json_object_get(m, \"codec\");\n\t\t\t\t\tcodec = (char *)json_string_value(jcodec);\n\t\t\t\t\tif(codec == NULL) {\n\t\t\t\t\t\t/* No codec property, check the deprecated rtpmap */\n\t\t\t\t\t\tjson_t *jrtpmap = json_object_get(m, \"rtpmap\");\n\t\t\t\t\t\tif(jrtpmap)\n\t\t\t\t\t\t\tcodec = (char *)janus_sdp_get_rtpmap_codec(json_string_value(jrtpmap));\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *jfmtp = json_object_get(m, \"fmtp\");\n\t\t\t\t\tfmtp = (char *)json_string_value(jfmtp);\n\t\t\t\t\tjson_t *jsps = json_object_get(m, \"h264sps\");\n\t\t\t\t\tsps = (char *)json_string_value(jsps);\n\t\t\t\t\tjson_t *jiface = json_object_get(m, \"iface\");\n\t\t\t\t\tmiface = (char *)json_string_value(jiface);\n\t\t\t\t\tif(jiface) {\n\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, miface, &iface) != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', invalid network interface configuration for media stream...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, ifas ? \"Invalid network interface configuration for media stream\" : \"Unable to query network device information\");\n\t\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_network_address_nullify(&iface);\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *jskew = json_object_get(m, \"skew\");\n\t\t\t\t\tdoskew = jskew ? json_is_true(jskew) : FALSE;\n\t\t\t\t\ts_bufferkf_ms = 0;\n\t\t\t\t\ts_bufferkf_bytes = 0;\n\t\t\t\t\tif(!strcasecmp(mtype, \"video\")) {\n\t\t\t\t\t\tjson_t *vkf = json_object_get(m, \"bufferkf\");\n\t\t\t\t\t\tif(json_is_true(vkf)) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The bufferkf property has been deprecated, please refer to bufferkf_ms and/or bufferkf_bytes\\n\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\ts_bufferkf_ms = bufferkf_ms;\n\t\t\t\t\t\ts_bufferkf_bytes = bufferkf_bytes;\n\t\t\t\t\t\tjson_t *vsc = json_object_get(m, \"simulcast\");\n\t\t\t\t\t\tsimulcast = vsc ? json_is_true(vsc) : FALSE;\n\t\t\t\t\t\tif(simulcast && (s_bufferkf_ms > 0 || s_bufferkf_bytes > 0)) {\n\t\t\t\t\t\t\t/* FIXME We'll need to take care of this */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Simulcasting enabled, so disabling buffering of keyframes for this stream\\n\");\n\t\t\t\t\t\t\ts_bufferkf_ms = 0;\n\t\t\t\t\t\t\ts_bufferkf_bytes = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *videoport2 = json_object_get(m, \"port2\");\n\t\t\t\t\t\tport2 = json_integer_value(videoport2);\n\t\t\t\t\t\tjson_t *videoport3 = json_object_get(m, \"port3\");\n\t\t\t\t\t\tport3 = json_integer_value(videoport3);\n\t\t\t\t\t\tjson_t *vsvc = json_object_get(m, \"svc\");\n\t\t\t\t\t\tdosvc = vsvc ? json_is_true(vsvc) : FALSE;\n\t\t\t\t\t} else if(!strcasecmp(mtype, \"data\")) {\n\t\t\t\t\t\tjson_t *dbm = json_object_get(root, \"buffermsg\");\n\t\t\t\t\t\tbuffermsg = dbm ? json_is_true(dbm) : FALSE;\n\t\t\t\t\t\tjson_t *dt = json_object_get(root, \"datatype\");\n\t\t\t\t\t\tif(dt) {\n\t\t\t\t\t\t\tconst char *datatype = (const char *)json_string_value(dt);\n\t\t\t\t\t\t\tif(!strcasecmp(datatype, \"text\"))\n\t\t\t\t\t\t\t\ttextdata = TRUE;\n\t\t\t\t\t\t\telse if(!strcasecmp(datatype, \"binary\"))\n\t\t\t\t\t\t\t\ttextdata = FALSE;\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (datatype can only be text or binary)\\n\");\n\t\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (datatype can only be text or binary)\");\n\t\t\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* Create the source stream */\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\tname ? (char *)json_string_value(name) : NULL, g_list_length(streams),\n\t\t\t\t\t\tmtype, mid, label ? label : mtype, msid, mcast, miface, &iface,\n\t\t\t\t\t\tport, port2, port3, jrtcpport != NULL, rtcpport,\n\t\t\t\t\t\tpt, codec, fmtp, sps,\n\t\t\t\t\t\tdoskew, bufferkf_ms, bufferkf_bytes, simulcast, dosvc, textdata, buffermsg);\n\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', error creating data source stream...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, error creating data source stream\");\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\tstreams = g_list_append(streams, stream);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* If we got here, we create a mountpoint the \"old\" way */\n\t\t\t\tlegacy = TRUE;\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Deprecated mountpoint 'create' API: please start looking into the new one for the future\\n\");\n\t\t\t\tjanus_network_address audio_iface, video_iface, data_iface;\n\t\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\t\tjson_t *data = json_object_get(root, \"data\");\n\t\t\t\tgboolean doaudio = audio ? json_is_true(audio) : FALSE;\n\t\t\t\tgboolean dovideo = video ? json_is_true(video) : FALSE;\n\t\t\t\tgboolean dodata = data ? json_is_true(data) : FALSE;\n\t\t\t\tgboolean doaskew = FALSE, dovskew = FALSE, dosvc = FALSE;\n\t\t\t\tif(!doaudio && !dovideo && !dodata) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream, no audio, video or data have to be streamed...\\n\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, no audio or video have to be streamed...\");\n\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\tuint16_t aport = 0;\n\t\t\t\tuint16_t artcpport = 0;\n\t\t\t\tuint8_t apt = 0;\n\t\t\t\tchar *acodec = NULL, *afmtp = NULL, *amcast = NULL, *amiface = NULL;\n\t\t\t\tif(doaudio) {\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_audio_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *audiomcast = json_object_get(root, \"audiomcast\");\n\t\t\t\t\tamcast = (char *)json_string_value(audiomcast);\n\t\t\t\t\tjson_t *audioport = json_object_get(root, \"audioport\");\n\t\t\t\t\taport = json_integer_value(audioport);\n\t\t\t\t\tjson_t *audiortcpport = json_object_get(root, \"audiortcpport\");\n\t\t\t\t\tif(audiortcpport)\n\t\t\t\t\t\tartcpport = json_integer_value(audiortcpport);\n\t\t\t\t\tjson_t *audiopt = json_object_get(root, \"audiopt\");\n\t\t\t\t\tapt = json_integer_value(audiopt);\n\t\t\t\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\t\t\t\tacodec = (char *)json_string_value(audiocodec);\n\t\t\t\t\tif(acodec == NULL) {\n\t\t\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\t\t\tjson_t *audiortpmap = json_object_get(root, \"audiortpmap\");\n\t\t\t\t\t\tif(audiortpmap)\n\t\t\t\t\t\t\tacodec = (char *)janus_sdp_get_rtpmap_codec(json_string_value(audiortpmap));\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *audiofmtp = json_object_get(root, \"audiofmtp\");\n\t\t\t\t\tafmtp = (char *)json_string_value(audiofmtp);\n\t\t\t\t\tjson_t *aiface = json_object_get(root, \"audioiface\");\n\t\t\t\t\tamiface = (char *)json_string_value(aiface);\n\t\t\t\t\tif(aiface) {\n\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, amiface, &audio_iface) != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', invalid network interface configuration for audio...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, ifas ? \"Invalid network interface configuration for audio\" : \"Unable to query network device information\");\n\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_network_address_nullify(&audio_iface);\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *askew = json_object_get(root, \"audioskew\");\n\t\t\t\t\tdoaskew = askew ? json_is_true(askew) : FALSE;\n\t\t\t\t\t/* Create the audio source stream */\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\tname ? (char *)json_string_value(name) : NULL, g_list_length(streams),\n\t\t\t\t\t\t\"audio\", \"a\", \"audio\", NULL,\n\t\t\t\t\t\tamcast, amiface, &audio_iface,\n\t\t\t\t\t\taport, 0, 0, audiortcpport != NULL, artcpport,\n\t\t\t\t\t\tapt, acodec, afmtp, NULL, doaskew, 0, 0, FALSE, FALSE, FALSE, FALSE);\n\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', error creating audio source stream...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, error creating audio source stream\");\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\tstreams = g_list_append(streams, stream);\n\t\t\t\t}\n\t\t\t\tuint16_t vport = 0, vport2 = 0, vport3 = 0;\n\t\t\t\tuint16_t vrtcpport = 0;\n\t\t\t\tuint8_t vpt = 0;\n\t\t\t\tchar *vcodec = NULL, *vfmtp = NULL, *vsps = NULL, *vmcast = NULL, *vmiface = NULL;\n\t\t\t\tgboolean simulcast = FALSE;\n\t\t\t\tif(dovideo) {\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_video_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *videomcast = json_object_get(root, \"videomcast\");\n\t\t\t\t\tvmcast = (char *)json_string_value(videomcast);\n\t\t\t\t\tjson_t *videoport = json_object_get(root, \"videoport\");\n\t\t\t\t\tvport = json_integer_value(videoport);\n\t\t\t\t\tjson_t *videortcpport = json_object_get(root, \"videortcpport\");\n\t\t\t\t\tif(videortcpport)\n\t\t\t\t\t\tvrtcpport = json_integer_value(videortcpport);\n\t\t\t\t\tjson_t *videopt = json_object_get(root, \"videopt\");\n\t\t\t\t\tvpt = json_integer_value(videopt);\n\t\t\t\t\tjson_t *videocodec = json_object_get(root, \"videocodec\");\n\t\t\t\t\tvcodec = (char *)json_string_value(videocodec);\n\t\t\t\t\tif(vcodec == NULL) {\n\t\t\t\t\t\t/* No videocodec property, check the deprecated videortpmap */\n\t\t\t\t\t\tjson_t *videortpmap = json_object_get(root, \"videortpmap\");\n\t\t\t\t\t\tif(videortpmap)\n\t\t\t\t\t\t\tvcodec = (char *)janus_sdp_get_rtpmap_codec(json_string_value(videortpmap));\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *videofmtp = json_object_get(root, \"videofmtp\");\n\t\t\t\t\tvfmtp = (char *)json_string_value(videofmtp);\n\t\t\t\t\tjson_t *h264sps = json_object_get(root, \"h264sps\");\n\t\t\t\t\tvsps = (char *)json_string_value(h264sps);\n\t\t\t\t\tjson_t *vkf = json_object_get(root, \"videobufferkf\");\n\t\t\t\t\tif(json_is_true(vkf)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The videobufferkf property has been deprecated, please refer to bufferkf_ms and/or bufferkf_bytes\\n\");\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *vsc = json_object_get(root, \"videosimulcast\");\n\t\t\t\t\tsimulcast = vsc ? json_is_true(vsc) : FALSE;\n\t\t\t\t\tif(simulcast && (bufferkf_ms > 0 || bufferkf_bytes > 0)) {\n\t\t\t\t\t\t/* FIXME We'll need to take care of this */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Simulcasting enabled, so disabling buffering of keyframes for this stream\\n\");\n\t\t\t\t\t\tbufferkf_ms = 0;\n\t\t\t\t\t\tbufferkf_bytes = 0;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *videoport2 = json_object_get(root, \"videoport2\");\n\t\t\t\t\tvport2 = json_integer_value(videoport2);\n\t\t\t\t\tjson_t *videoport3 = json_object_get(root, \"videoport3\");\n\t\t\t\t\tvport3 = json_integer_value(videoport3);\n\t\t\t\t\tjson_t *viface = json_object_get(root, \"videoiface\");\n\t\t\t\t\tvmiface = (char *)json_string_value(viface);\n\t\t\t\t\tif(viface) {\n\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, vmiface, &video_iface) != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', invalid network interface configuration for video...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, ifas ? \"Invalid network interface configuration for video\" : \"Unable to query network device information\");\n\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_network_address_nullify(&video_iface);\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *vskew = json_object_get(root, \"videoskew\");\n\t\t\t\t\tdovskew = vskew ? json_is_true(vskew) : FALSE;\n\t\t\t\t\tjson_t *vsvc = json_object_get(root, \"videosvc\");\n\t\t\t\t\tdosvc = vsvc ? json_is_true(vsvc) : FALSE;\n\t\t\t\t\t/* Create the video source stream */\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\tname ? (char *)json_string_value(name) : NULL,\n\t\t\t\t\t\tg_list_length(streams),\n\t\t\t\t\t\t\"video\", \"v\", \"video\", NULL,\n\t\t\t\t\t\tvmcast, vmiface, &video_iface,\n\t\t\t\t\t\tvport, vport2, vport3, videortcpport != NULL, vrtcpport,\n\t\t\t\t\t\tvpt, vcodec, vfmtp, vsps,\n\t\t\t\t\t\tdovskew, bufferkf_ms, bufferkf_bytes, simulcast, dosvc, FALSE, FALSE);\n\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', error creating video source stream...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, error creating video source stream\");\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\tstreams = g_list_append(streams, stream);\n\t\t\t\t}\n\t\t\t\tuint16_t dport = 0;\n\t\t\t\tgboolean textdata = TRUE, buffermsg = FALSE;\n\t\t\t\tchar *dmcast = NULL, *dmiface = NULL;\n\t\t\t\tif(dodata) {\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_data_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n#ifdef HAVE_SCTP\n\t\t\t\t\tjson_t *datamcast = json_object_get(root, \"datamcast\");\n\t\t\t\t\tdmcast = (char *)json_string_value(datamcast);\n\t\t\t\t\tjson_t *dataport = json_object_get(root, \"dataport\");\n\t\t\t\t\tdport = json_integer_value(dataport);\n\t\t\t\t\tjson_t *dbm = json_object_get(root, \"databuffermsg\");\n\t\t\t\t\tbuffermsg = dbm ? json_is_true(dbm) : FALSE;\n\t\t\t\t\tjson_t *dt = json_object_get(root, \"datatype\");\n\t\t\t\t\tif(dt) {\n\t\t\t\t\t\tconst char *datatype = (const char *)json_string_value(dt);\n\t\t\t\t\t\tif(!strcasecmp(datatype, \"text\"))\n\t\t\t\t\t\t\ttextdata = TRUE;\n\t\t\t\t\t\telse if(!strcasecmp(datatype, \"binary\"))\n\t\t\t\t\t\t\ttextdata = FALSE;\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (datatype can only be text or binary)\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (datatype can only be text or binary)\");\n\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *diface = json_object_get(root, \"dataiface\");\n\t\t\t\t\tdmiface = (char *)json_string_value(diface);\n\t\t\t\t\tif(diface) {\n\t\t\t\t\t\tif(janus_network_lookup_interface(ifas, dmiface, &data_iface) != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', invalid network interface configuration for data...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, ifas ? \"Invalid network interface configuration for data\" : \"Unable to query network device information\");\n\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_network_address_nullify(&data_iface);\n\t\t\t\t\t}\n\t\t\t\t\t/* Create the data source stream */\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = janus_streaming_create_rtp_source_stream(\n\t\t\t\t\t\tname ? (char *)json_string_value(name) : NULL,\n\t\t\t\t\t\tg_list_length(streams),\n\t\t\t\t\t\t\"data\", \"d\", \"data\", NULL,\n\t\t\t\t\t\tdmcast, dmiface, &data_iface,\n\t\t\t\t\t\tdport, 0, 0, FALSE, 0,\n\t\t\t\t\t\t0, NULL, NULL, NULL, FALSE, 0, 0, FALSE, FALSE, textdata, buffermsg);\n\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream '%s', error creating data source stream...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream, error creating data source stream\");\n\t\t\t\t\t\tg_list_free_full(streams, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\t/* Add to the list of streams */\n\t\t\t\t\tstreams = g_list_append(streams, stream);\n#else\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' stream: no datachannels support...\\n\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtp' stream: no datachannels support...\");\n\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\tgoto prepare_response;\n#endif\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Audio %s, Video %s\\n\", doaudio ? \"enabled\" : \"NOT enabled\", dovideo ? \"enabled\" : \"NOT enabled\");\n\t\t\t}\n\t\t\tmp = janus_streaming_create_rtp_source(\n\t\t\t\t\tmpid, mpid_str,\n\t\t\t\t\tname ? (char *)json_string_value(name) : NULL,\n\t\t\t\t\tdesc ? (char *)json_string_value(desc) : NULL,\n\t\t\t\t\tmd ? (char *)json_string_value(md) : NULL,\n\t\t\t\t\tstreams,\n\t\t\t\t\tssuite ? json_integer_value(ssuite) : 0,\n\t\t\t\t\tscrypto ? (char *)json_string_value(scrypto) : NULL,\n\t\t\t\t\tthreads ? json_integer_value(threads) : 0,\n\t\t\t\t\trtpcollision ? json_integer_value(rtpcollision) : 0,\n\t\t\t\t\tbufferkf_ms, bufferkf_bytes,\n\t\t\t\t\te2ee ? json_is_true(e2ee) : FALSE,\n\t\t\t\t\tpd ? json_is_true(pd) : FALSE,\n\t\t\t\t\tabscaptime_src_id ? json_integer_value(abscaptime_src_id) : 0);\n\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tif(mp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'rtp' stream...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error creating 'rtp' stream\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tmp->is_private = is_private ? json_is_true(is_private) : FALSE;\n\t\t} else if(!strcasecmp(type_text, \"live\")) {\n\t\t\t/* File-based live source */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, live_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tjson_t *name = json_object_get(root, \"name\");\n\t\t\tjson_t *desc = json_object_get(root, \"description\");\n\t\t\tjson_t *md = json_object_get(root, \"metadata\");\n\t\t\tjson_t *is_private = json_object_get(root, \"is_private\");\n\t\t\tjson_t *file = json_object_get(root, \"filename\");\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\tgboolean doaudio = audio ? json_is_true(audio) : FALSE;\n\t\t\tuint8_t apt = 0;\n\t\t\tchar *acodec = NULL, *afmtp = NULL;\n\t\t\tif(doaudio) {\n\t\t\t\tjson_t *audiopt = json_object_get(root, \"audiopt\");\n\t\t\t\tapt = json_integer_value(audiopt);\n\t\t\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\t\t\tacodec = (char *)json_string_value(audiocodec);\n\t\t\t\tif(acodec == NULL) {\n\t\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\t\tjson_t *audiortpmap = json_object_get(root, \"audiortpmap\");\n\t\t\t\t\tif(audiortpmap)\n\t\t\t\t\t\tacodec = (char *)janus_sdp_get_rtpmap_codec(json_string_value(audiortpmap));\n\t\t\t\t}\n\t\t\t\tjson_t *audiofmtp = json_object_get(root, \"audiofmtp\");\n\t\t\t\tafmtp = (char *)json_string_value(audiofmtp);\n\t\t\t}\n\t\t\tgboolean dovideo = video ? json_is_true(video) : FALSE;\n\t\t\t/* We only support audio for file-based streaming at the moment: for streaming\n\t\t\t * files using other codecs/formats an external tools should feed us RTP instead */\n\t\t\tif(!doaudio || dovideo) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' stream, we only support audio file streaming right now...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'live' stream, we only support audio file streaming right now...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tchar *filename = (char *)json_string_value(file);\n#ifdef HAVE_LIBOGG\n\t\t\tif(!strstr(filename, \".opus\") && !strstr(filename, \".alaw\") && !strstr(filename, \".mulaw\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' stream, unsupported format (we only support Opus and raw mu-Law/a-Law files right now)\\n\");\n#else\n\t\t\tif(!strstr(filename, \".alaw\") && !strstr(filename, \".mulaw\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' stream, unsupported format (we only support raw mu-Law and a-Law files right now)\\n\");\n#endif\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'live' stream, unsupported format (we only support raw mu-Law and a-Law files right now)\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tFILE *audiofile = fopen(filename, \"rb\");\n\t\t\tif(!audiofile) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' stream, no such file '%s'...\\n\", filename);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'live' stream, no such file '%s'\\n\", filename);\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tfclose(audiofile);\n\t\t\tmp = janus_streaming_create_file_source(\n\t\t\t\t\tmpid, mpid_str,\n\t\t\t\t\tname ? (char *)json_string_value(name) : NULL,\n\t\t\t\t\tdesc ? (char *)json_string_value(desc) : NULL,\n\t\t\t\t\tmd ? (char *)json_string_value(md) : NULL,\n\t\t\t\t\tfilename, TRUE,\n\t\t\t\t\tdoaudio, apt, acodec, afmtp, dovideo);\n\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tif(mp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'live' stream...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error creating 'live' stream\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tmp->is_private = is_private ? json_is_true(is_private) : FALSE;\n\t\t} else if(!strcasecmp(type_text, \"ondemand\")) {\n\t\t\t/* File-based on demand source */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, ondemand_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tjson_t *name = json_object_get(root, \"name\");\n\t\t\tjson_t *desc = json_object_get(root, \"description\");\n\t\t\tjson_t *md = json_object_get(root, \"metadata\");\n\t\t\tjson_t *is_private = json_object_get(root, \"is_private\");\n\t\t\tjson_t *file = json_object_get(root, \"filename\");\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\tgboolean doaudio = audio ? json_is_true(audio) : FALSE;\n\t\t\tuint8_t apt = 0;\n\t\t\tchar *acodec = NULL, *afmtp = NULL;\n\t\t\tif(doaudio) {\n\t\t\t\tjson_t *audiopt = json_object_get(root, \"audiopt\");\n\t\t\t\tapt = json_integer_value(audiopt);\n\t\t\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\t\t\tacodec = (char *)json_string_value(audiocodec);\n\t\t\t\tif(acodec == NULL) {\n\t\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\t\tjson_t *audiortpmap = json_object_get(root, \"audiortpmap\");\n\t\t\t\t\tif(audiortpmap)\n\t\t\t\t\t\tacodec = (char *)janus_sdp_get_rtpmap_codec(json_string_value(audiortpmap));\n\t\t\t\t}\n\t\t\t\tjson_t *audiofmtp = json_object_get(root, \"audiofmtp\");\n\t\t\t\tafmtp = (char *)json_string_value(audiofmtp);\n\t\t\t}\n\t\t\tgboolean dovideo = video ? json_is_true(video) : FALSE;\n\t\t\t/* We only support audio for file-based streaming at the moment: for streaming\n\t\t\t * files using other codecs/formats an external tools should feed us RTP instead */\n\t\t\tif(!doaudio || dovideo) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'ondemand' stream, we only support audio file streaming right now...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'ondemand' stream, we only support audio file streaming right now...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tchar *filename = (char *)json_string_value(file);\n#ifdef HAVE_LIBOGG\n\t\t\tif(!strstr(filename, \".opus\") && !strstr(filename, \".alaw\") && !strstr(filename, \".mulaw\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' stream, unsupported format (we only support Opus and raw mu-Law/a-Law files right now)\\n\");\n#else\n\t\t\tif(!strstr(filename, \".alaw\") && !strstr(filename, \".mulaw\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' stream, unsupported format (we only support raw mu-Law and a-Law files right now)\\n\");\n#endif\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'ondemand' stream, unsupported format (we only support raw mu-Law and a-Law files right now)\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'ondemand' stream, unsupported format (we only support raw mu-Law and a-Law files right now)\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tFILE *audiofile = fopen(filename, \"rb\");\n\t\t\tif(!audiofile) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'ondemand' stream, no such file '%s'...\\n\", filename);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'ondemand' stream, no such file '%s'\\n\", filename);\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tfclose(audiofile);\n\t\t\tmp = janus_streaming_create_file_source(\n\t\t\t\t\tmpid, mpid_str,\n\t\t\t\t\tname ? (char *)json_string_value(name) : NULL,\n\t\t\t\t\tdesc ? (char *)json_string_value(desc) : NULL,\n\t\t\t\t\tmd ? (char *)json_string_value(md) : NULL,\n\t\t\t\t\tfilename, FALSE,\n\t\t\t\t\tdoaudio, apt, acodec, afmtp, dovideo);\n\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tif(mp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'ondemand' stream...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error creating 'ondemand' stream\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tmp->is_private = is_private ? json_is_true(is_private) : FALSE;\n\t\t} else if(!strcasecmp(type_text, \"rtsp\")) {\n#ifndef HAVE_LIBCURL\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't create 'rtsp' mountpoint, libcurl support not compiled...\\n\");\n\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Can't create 'rtsp' mountpoint, libcurl support not compiled...\\n\");\n\t\t\tgoto prepare_response;\n#else\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtsp_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\t/* RTSP source*/\n\t\t\tjanus_network_address multicast_iface;\n\t\t\tjson_t *name = json_object_get(root, \"name\");\n\t\t\tjson_t *desc = json_object_get(root, \"description\");\n\t\t\tjson_t *md = json_object_get(root, \"metadata\");\n\t\t\tjson_t *is_private = json_object_get(root, \"is_private\");\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\tjson_t *audiopt = json_object_get(root, \"audiopt\");\n\t\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\t\tjson_t *audiortpmap = json_object_get(root, \"audiortpmap\");\n\t\t\tjson_t *audiofmtp = json_object_get(root, \"audiofmtp\");\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\tjson_t *videopt = json_object_get(root, \"videopt\");\n\t\t\tjson_t *videocodec = json_object_get(root, \"videocodec\");\n\t\t\tjson_t *videortpmap = json_object_get(root, \"videortpmap\");\n\t\t\tjson_t *videofmtp = json_object_get(root, \"videofmtp\");\n\t\t\tjson_t *vkf = json_object_get(root, \"videobufferkf\");\n\t\t\tjson_t *vkf_ms = json_object_get(root, \"bufferkf_ms\");\n\t\t\tjson_t *vkf_bytes = json_object_get(root, \"bufferkf_bytes\");\n\t\t\tjson_t *url = json_object_get(root, \"url\");\n\t\t\tjson_t *username = json_object_get(root, \"rtsp_user\");\n\t\t\tjson_t *password = json_object_get(root, \"rtsp_pwd\");\n\t\t\tjson_t *quirk = json_object_get(root, \"rtsp_quirk\");\n\t\t\tjson_t *iface = json_object_get(root, \"rtspiface\");\n\t\t\tjson_t *threads = json_object_get(root, \"threads\");\n\t\t\tjson_t *failerr = json_object_get(root, \"rtsp_failcheck\");\n\t\t\tjson_t *reconnect_delay = json_object_get(root, \"rtsp_reconnect_delay\");\n\t\t\tjson_t *session_timeout = json_object_get(root, \"rtsp_session_timeout\");\n\t\t\tjson_t *rtsp_timeout = json_object_get(root, \"rtsp_timeout\");\n\t\t\tjson_t *rtsp_conn_timeout = json_object_get(root, \"rtsp_conn_timeout\");\n\t\t\tjson_t *rtsp_notify_changes = json_object_get(root, \"rtsp_notify_changes\");\n\t\t\tif(failerr == NULL)\t/* For an old typo, we support the legacy syntax too */\n\t\t\t\tfailerr = json_object_get(root, \"rtsp_check\");\n\t\t\tgboolean doaudio = audio ? json_is_true(audio) : FALSE;\n\t\t\tgboolean dovideo = video ? json_is_true(video) : FALSE;\n\t\t\tgboolean doquirk = quirk ? json_is_true(quirk) : FALSE;\n\t\t\tgboolean notify_changes = rtsp_notify_changes ? json_is_true(rtsp_notify_changes) : FALSE;\n\t\t\tgboolean error_on_failure = failerr ? json_is_true(failerr) : TRUE;\n\t\t\tif(json_is_true(vkf)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"The videobufferkf property has been deprecated, please refer to bufferkf_ms and/or bufferkf_bytes\\n\");\n\t\t\t}\n\t\t\tif(vkf_ms && json_integer_value(vkf_ms) > UINT16_MAX) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' stream, invalid bufferkf_ms value...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtsp' stream, invalid bufferkf_ms value...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tuint16_t bufferkf_ms = vkf_ms ? json_integer_value(vkf_ms) : 0;\n\t\t\tif(vkf_bytes && json_integer_value(vkf_bytes) > UINT32_MAX) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' stream, invalid bufferkf_bytes value...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtsp' stream, invalid bufferkf_bytes value...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tuint32_t bufferkf_bytes = vkf_bytes ? json_integer_value(vkf_bytes) : 0;\n\t\t\tif(!doaudio && !dovideo) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' stream, no audio or video have to be streamed...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't add 'rtsp' stream, no audio or video have to be streamed...\");\n\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t} else {\n\t\t\t\tif(iface) {\n\t\t\t\t\tconst char *miface = (const char *)json_string_value(iface);\n\t\t\t\t\tif(janus_network_lookup_interface(ifas, miface, &multicast_iface) != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' stream '%s', invalid network interface configuration for stream...\\n\", (const char *)json_string_value(name));\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, ifas ? \"Invalid network interface configuration for stream\" : \"Unable to query network device information\");\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\t\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tjanus_network_address_nullify(&multicast_iface);\n\t\t\t\t}\n\t\t\t}\n\t\t\tchar *acodec = (char *)json_string_value(audiocodec);\n\t\t\tif(acodec == NULL && audiortpmap) {\n\t\t\t\t/* No audiocodec property, check the deprecated audiortpmap */\n\t\t\t\tacodec = (char *)janus_sdp_get_rtpmap_codec(json_string_value(audiortpmap));\n\t\t\t}\n\t\t\tchar *vcodec = (char *)json_string_value(videocodec);\n\t\t\tif(vcodec == NULL && videortpmap) {\n\t\t\t\t/* No videocodec property, check the deprecated videortpmap */\n\t\t\t\tvcodec = (char *)janus_sdp_get_rtpmap_codec(json_string_value(videortpmap));\n\t\t\t}\n\t\t\tmp = janus_streaming_create_rtsp_source(\n\t\t\t\t\tmpid, mpid_str,\n\t\t\t\t\tname ? (char *)json_string_value(name) : NULL,\n\t\t\t\t\tdesc ? (char *)json_string_value(desc) : NULL,\n\t\t\t\t\tmd ? (char *)json_string_value(md) : NULL,\n\t\t\t\t\t(char *)json_string_value(url),\n\t\t\t\t\tusername ? (char *)json_string_value(username) : NULL,\n\t\t\t\t\tpassword ? (char *)json_string_value(password) : NULL,\n\t\t\t\t\tdoquirk, notify_changes,\n\t\t\t\t\tdoaudio, (audiopt ? json_integer_value(audiopt) : -1), acodec, (char *)json_string_value(audiofmtp),\n\t\t\t\t\tdovideo, (videopt ? json_integer_value(videopt) : -1), vcodec, (char *)json_string_value(videofmtp),\n\t\t\t\t\tbufferkf_ms, bufferkf_bytes,\n\t\t\t\t\t&multicast_iface, (threads ? json_integer_value(threads) : 0),\n\t\t\t\t\t((reconnect_delay ? json_integer_value(reconnect_delay) : JANUS_STREAMING_DEFAULT_RECONNECT_DELAY) * G_USEC_PER_SEC),\n\t\t\t\t\t((session_timeout ? json_integer_value(session_timeout) : JANUS_STREAMING_DEFAULT_SESSION_TIMEOUT) * G_USEC_PER_SEC),\n\t\t\t\t\t(rtsp_timeout ? json_integer_value(rtsp_timeout) : JANUS_STREAMING_DEFAULT_CURL_TIMEOUT),\n\t\t\t\t\t(rtsp_conn_timeout ? json_integer_value(rtsp_conn_timeout) : JANUS_STREAMING_DEFAULT_CURL_CONNECT_TIMEOUT),\n\t\t\t\t\terror_on_failure);\n\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)mpid_str : (gpointer)&mpid);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tif(mp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating 'rtsp' stream...\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_CREATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error creating 'RTSP' stream\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tmp->is_private = is_private ? json_is_true(is_private) : FALSE;\n#endif\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unknown stream type '%s'...\\n\", type_text);\n\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown stream type '%s'...\\n\", type_text);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Any secret? */\n\t\tif(secret)\n\t\t\tmp->secret = g_strdup(json_string_value(secret));\n\t\t/* Any PIN? */\n\t\tif(pin)\n\t\t\tmp->pin = g_strdup(json_string_value(pin));\n\t\tif(save) {\n\t\t\t/* This mountpoint is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Saving mountpoint %s permanently in config file\\n\", mp->id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar value[BUFSIZ];\n\t\t\t/* The category to add is the mountpoint name */\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, mp->name);\n\t\t\t/* Now for the common values */\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"type\", type_text));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"id\", mp->id_str));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", mp->description));\n\t\t\tif(mp->metadata)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"metadata\", mp->metadata));\n\t\t\tif(mp->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tif(mp->secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", mp->secret));\n\t\t\tif(mp->pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", mp->pin));\n\t\t\t/* Per type values */\n\t\t\tif(!strcasecmp(type_text, \"rtp\")) {\n\t\t\t\t/* We save using the new format, not the old deprecated one */\n\t\t\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\t\t\tif(source->rtp_collision > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", source->rtp_collision);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"collision\", value));\n\t\t\t\t}\n\t\t\t\tif(source->bufferkf_ms > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu16, source->bufferkf_ms);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_ms\", value));\n\t\t\t\t}\n\t\t\t\tif(source->bufferkf_bytes > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, source->bufferkf_bytes);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_bytes\", value));\n\t\t\t\t}\n\t\t\t\tif(source->srtpsuite > 0 && source->srtpcrypto) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", source->srtpsuite);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"srtpsuite\", value));\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"srtpcrypto\", source->srtpcrypto));\n\t\t\t\t}\n\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", mp->helper_threads);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"threads\", value));\n\t\t\t\t}\n\t\t\t\tif(source->e2ee)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"e2ee\", \"true\"));\n\t\t\t\tif(source->playoutdelay_ext)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"playoutdelay_ext\", \"true\"));\n\t\t\t\tif(source->abscapturetime_src_ext_id > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", source->abscapturetime_src_ext_id);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"abscapturetime_src_ext_id\", value));\n\t\t\t\t}\n\t\t\t\t/* Iterate on all media streams */\n\t\t\t\tjanus_config_array *media = janus_config_array_create(\"media\");\n\t\t\t\tjanus_config_add(config, c, media);\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\tjanus_config_category *m = janus_config_category_create(NULL);\n\t\t\t\t\tjanus_config_add(config, media, m);\n\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"type\", janus_streaming_media_str(stream->type)));\n\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"mid\", stream->mid));\n\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"label\", stream->label));\n\t\t\t\t\tif(stream->msid && stream->mstid) {\n\t\t\t\t\t\tchar msid[150];\n\t\t\t\t\t\tg_snprintf(msid, sizeof(msid), \"%s %s\", stream->msid, stream->mstid);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"msid\", msid));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->port[0] > 0) {\n\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->port[0]);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"port\", value));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->rtcp_port > 0) {\n\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->rtcp_port);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"rtcpport\", value));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->codecs.pt >= 0) {\n\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->codecs.pt);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"pt\", value));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->codecs.audio_codec != JANUS_AUDIOCODEC_NONE || stream->codecs.video_codec != JANUS_VIDEOCODEC_NONE) {\n\t\t\t\t\t\tif(stream->codecs.audio_codec != JANUS_AUDIOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"codec\", janus_audiocodec_name(stream->codecs.audio_codec)));\n\t\t\t\t\t\telse if(stream->codecs.video_codec != JANUS_VIDEOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"codec\", janus_videocodec_name(stream->codecs.video_codec)));\n\t\t\t\t\t\tif(stream->codecs.fmtp)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"fmtp\", stream->codecs.fmtp));\n\t\t\t\t\t\tif(stream->skew)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"skew\", \"true\"));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->simulcast) {\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"videosimulcast\", \"true\"));\n\t\t\t\t\t\tif(stream->port[1]) {\n\t\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->port[1]);\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"port2\", value));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream->port[2]) {\n\t\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->port[2]);\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"port3\", value));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->svc)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"videosvc\", \"true\"));\n\t\t\t\t\tif(stream->skew)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"skew\", \"true\"));\n\t\t\t\t\tif(stream->mcast_str)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"mcast\", stream->mcast_str));\n\t\t\t\t\tif(stream->iface_str)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"iface\", stream->iface_str));\n\t\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_DATA)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"datatype\", stream->textdata ? \"text\" : \"binary\"));\n\t\t\t\t\tif(stream->buffermsg)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"databuffermsg\", \"true\"));\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(type_text, \"live\") || !strcasecmp(type_text, \"ondemand\")) {\n\t\t\t\tjanus_streaming_file_source *source = mp->source;\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"filename\", source->filename));\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio\", \"true\"));\n\t\t\t} else if(!strcasecmp(type_text, \"rtsp\")) {\n\t\t\t\tjanus_streaming_rtp_source *source = mp->source;\n#ifdef HAVE_LIBCURL\n\t\t\t\tif(source->rtsp_url)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"url\", source->rtsp_url));\n\t\t\t\tif(source->rtsp_username)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtsp_user\", source->rtsp_username));\n\t\t\t\tif(source->rtsp_password)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtsp_pwd\", source->rtsp_password));\n\t\t\t\tif(source->rtsp_quirk)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtsp_quirk\", \"true\"));\n#endif\n\t\t\t\tif(source->bufferkf_ms > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu16, source->bufferkf_ms);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_ms\", value));\n\t\t\t\t}\n\t\t\t\tif(source->bufferkf_bytes > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, source->bufferkf_bytes);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_bytes\", value));\n\t\t\t\t}\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\t/* FIXME Should we support RTSP streams with multiple media? */\n\t\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO) {\n\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio\", \"true\"));\n\t\t\t\t\t\tif(stream->codecs.audio_codec != JANUS_AUDIOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiocodec\", janus_audiocodec_name(stream->codecs.audio_codec)));\n\t\t\t\t\t\tif(stream->codecs.fmtp)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiofmtp\", stream->codecs.fmtp));\n\t\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"video\", \"true\"));\n\t\t\t\t\t\tif(stream->codecs.video_codec != JANUS_VIDEOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videocodec\", janus_videocodec_name(stream->codecs.video_codec)));\n\t\t\t\t\t\tif(stream->codecs.fmtp)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videofmtp\", stream->codecs.fmtp));\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjson_t *iface = json_object_get(root, \"rtspiface\");\n\t\t\t\tif(iface)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtspiface\", json_string_value(iface)));\n\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", mp->helper_threads);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"threads\", value));\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_STREAMING_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the mountpoint is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"streaming\", json_string(\"created\"));\n\t\tjson_object_set_new(response, \"created\", json_string(mp->name));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\tjson_t *ml = json_object();\n\t\tjson_object_set_new(ml, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\tjson_object_set_new(ml, \"type\", json_string(mp->streaming_type == janus_streaming_type_live ? \"live\" : \"on demand\"));\n\t\tjson_object_set_new(ml, \"description\", json_string(mp->description));\n\t\tjson_object_set_new(ml, \"is_private\", mp->is_private ? json_true() : json_false());\n\t\tif(!strcasecmp(type_text, \"rtp\")) {\n\t\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\t\tjson_t *media = legacy ? NULL : json_array();\n\t\t\tGList *temp = source->media;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\tif(!legacy) {\n\t\t\t\t\t/* Return the new format */\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(janus_streaming_media_str(stream->type)));\n\t\t\t\t\tjson_object_set_new(info, \"mid\", json_string(stream->mid));\n\t\t\t\t\tif(stream->msid && stream->mstid) {\n\t\t\t\t\t\tchar msid[150];\n\t\t\t\t\t\tg_snprintf(msid, sizeof(msid), \"%s %s\", stream->msid, stream->mstid);\n\t\t\t\t\t\tjson_object_set_new(info, \"msid\", json_string(msid));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->fd[0] != -1) {\n\t\t\t\t\t\tif(stream->host)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(stream->host));\n\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(stream->port[0]));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->rtcp_fd != -1) {\n\t\t\t\t\t\tjson_object_set_new(info, \"rtcp_port\", json_integer(stream->rtcp_port));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->fd[1] != -1) {\n\t\t\t\t\t\tjson_object_set_new(info, \"port_2\", json_integer(stream->port[1]));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->fd[2] != -1) {\n\t\t\t\t\t\tjson_object_set_new(info, \"port_3\", json_integer(stream->port[2]));\n\t\t\t\t\t}\n\t\t\t\t\tjson_array_append_new(media, info);\n\t\t\t\t} else {\n\t\t\t\t\t/* Return the old format */\n\t\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO) {\n\t\t\t\t\t\tif(stream->fd[0] != -1) {\n\t\t\t\t\t\t\tif(stream->host)\n\t\t\t\t\t\t\t\tjson_object_set_new(ml, \"audio_host\", json_string(stream->host));\n\t\t\t\t\t\t\tjson_object_set_new(ml, \"audio_port\", json_integer(stream->port[0]));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream->rtcp_fd != -1) {\n\t\t\t\t\t\t\tjson_object_set_new(ml, \"audio_rtcp_port\", json_integer(stream->rtcp_port));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\t\t\t\t\tif(stream->fd[0] != -1) {\n\t\t\t\t\t\t\tif(stream->host)\n\t\t\t\t\t\t\t\tjson_object_set_new(ml, \"video_host\", json_string(stream->host));\n\t\t\t\t\t\t\tjson_object_set_new(ml, \"video_port\", json_integer(stream->port[0]));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream->rtcp_fd != -1) {\n\t\t\t\t\t\t\tjson_object_set_new(ml, \"video_rtcp_port\", json_integer(stream->rtcp_port));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream->fd[1] != -1) {\n\t\t\t\t\t\t\tjson_object_set_new(ml, \"video_port_2\", json_integer(stream->port[1]));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream->fd[2] != -1) {\n\t\t\t\t\t\t\tjson_object_set_new(ml, \"video_port_3\", json_integer(stream->port[2]));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_DATA) {\n\t\t\t\t\t\tif(stream->fd[0] != -1) {\n\t\t\t\t\t\t\tif(stream->host)\n\t\t\t\t\t\t\t\tjson_object_set_new(ml, \"data_host\", json_string(stream->host));\n\t\t\t\t\t\t\tjson_object_set_new(ml, \"data_port\", json_integer(stream->port[0]));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tif(!legacy)\n\t\t\t\tjson_object_set_new(ml, \"ports\", media);\n\t\t}\n\t\tjson_object_set_new(response, \"stream\", ml);\n\t\tif(legacy)\n\t\t\tjson_object_set_new(response, \"warning\", json_string(\"deprecated_api\"));\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"created\"));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\t\tjson_object_set_new(info, \"type\", json_string(mp->streaming_type == janus_streaming_type_live ? \"live\" : \"on demand\"));\n\t\t\tgateway->notify_event(&janus_streaming_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"edit\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to edit an existing streaming mountpoint\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, edit_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\t/* We only allow for a limited set of properties to be edited */\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tjson_t *desc = json_object_get(root, \"new_description\");\n\t\tjson_t *md = json_object_get(root, \"new_metadata\");\n\t\tjson_t *secret = json_object_get(root, \"new_secret\");\n\t\tjson_t *pin = json_object_get(root, \"new_pin\");\n\t\tjson_t *is_private = json_object_get(root, \"new_is_private\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tjson_t *edited_event = json_object_get(root, \"edited_event\");\n\t\tgboolean send_edited_event = edited_event ? json_is_true(edited_event) : FALSE;\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't edit mountpoint permanently\\n\");\n\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't edit mountpoint permanently\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 id_value = 0;\n\t\tchar id_num[30], *id_value_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tid_value = json_integer_value(id);\n\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\tid_value_str = id_num;\n\t\t} else {\n\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\tif(mp == NULL) {\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such mountpoint (%s)\\n\", id_value_str);\n\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint (%s)\", id_value_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&mp->ref);\n\t\tjanus_mutex_lock(&mp->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(mp->secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Edit the mountpoint properties that were provided */\n\t\tif(desc != NULL && strlen(json_string_value(desc)) > 0) {\n\t\t\tchar *old_description = mp->description;\n\t\t\tchar *new_description = g_strdup(json_string_value(desc));\n\t\t\tmp->description = new_description;\n\t\t\tg_free(old_description);\n\t\t}\n\t\tif(md != NULL) {\n\t\t\tchar *old_metadata = mp->metadata;\n\t\t\tchar *new_metadata = g_strdup(json_string_value(md));\n\t\t\tmp->metadata = new_metadata;\n\t\t\tif(send_edited_event == TRUE && ((old_metadata == NULL && new_metadata == NULL) || (old_metadata != NULL && new_metadata != NULL && !strcmp(old_metadata, new_metadata))))\n\t\t\t\tsend_edited_event = FALSE;\n\t\t\tg_free(old_metadata);\n\t\t}\n\t\tif(is_private)\n\t\t\tmp->is_private = json_is_true(is_private);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(mp->secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(secret && strlen(json_string_value(secret)) > 0) {\n\t\t\tchar *old_secret = mp->secret;\n\t\t\tchar *new_secret = g_strdup(json_string_value(secret));\n\t\t\tmp->secret = new_secret;\n\t\t\tg_free(old_secret);\n\t\t}\n\t\tif(pin) {\n\t\t\tchar *old_pin = mp->pin;\n\t\t\tif(strlen(json_string_value(pin)) > 0) {\n\t\t\t\tchar *new_pin = g_strdup(json_string_value(pin));\n\t\t\t\tmp->pin = new_pin;\n\t\t\t} else {\n\t\t\t\tmp->pin = NULL;\n\t\t\t}\n\t\t\tg_free(old_pin);\n\t\t}\n\t\tif(save) {\n\t\t\t/* This changes are permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Saving edited mountpoint %s permanently in config file\\n\", mp->id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar value[BUFSIZ];\n\t\t\t/* The category to add is the mountpoint name */\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, mp->name);\n\t\t\t/* Now for the common values */\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"id\", mp->id_str));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", mp->description));\n\t\t\tif(mp->metadata)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"metadata\", mp->metadata));\n\t\t\tif(mp->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tif(mp->secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", mp->secret));\n\t\t\tif(mp->pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", mp->pin));\n\t\t\t/* Per type values */\n\t\t\tif(mp->streaming_source == janus_streaming_source_rtp && !((janus_streaming_rtp_source *)mp->source)->rtsp) {\n\t\t\t\t/* We save using the new format, not the old deprecated one */\n\t\t\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\t\t\tif(source->rtp_collision > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", source->rtp_collision);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"collision\", value));\n\t\t\t\t}\n\t\t\t\tif(source->bufferkf_ms > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu16, source->bufferkf_ms);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_ms\", value));\n\t\t\t\t}\n\t\t\t\tif(source->bufferkf_bytes > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, source->bufferkf_bytes);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_bytes\", value));\n\t\t\t\t}\n\t\t\t\tif(source->srtpsuite > 0 && source->srtpcrypto) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", source->srtpsuite);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"srtpsuite\", value));\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"srtpcrypto\", source->srtpcrypto));\n\t\t\t\t}\n\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", mp->helper_threads);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"threads\", value));\n\t\t\t\t}\n\t\t\t\tif(source->e2ee)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"e2ee\", \"true\"));\n\t\t\t\tif(source->playoutdelay_ext)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"playoutdelay_ext\", \"true\"));\n\t\t\t\tif(source->abscapturetime_src_ext_id > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", source->abscapturetime_src_ext_id);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"abscapturetime_src_ext_id\", value));\n\t\t\t\t}\n\t\t\t\t/* Iterate on all media streams */\n\t\t\t\tjanus_config_array *media = janus_config_array_create(\"media\");\n\t\t\t\tjanus_config_add(config, c, media);\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\tjanus_config_category *m = janus_config_category_create(NULL);\n\t\t\t\t\tjanus_config_add(config, media, m);\n\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"type\", janus_streaming_media_str(stream->type)));\n\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"mid\", stream->mid));\n\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"label\", stream->label));\n\t\t\t\t\tif(stream->msid && stream->mstid) {\n\t\t\t\t\t\tchar msid[150];\n\t\t\t\t\t\tg_snprintf(msid, sizeof(msid), \"%s %s\", stream->msid, stream->mstid);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"msid\", msid));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->port[0] > 0) {\n\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->port[0]);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"port\", value));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->rtcp_port > 0) {\n\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->rtcp_port);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"rtcpport\", value));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->codecs.pt >= 0) {\n\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->codecs.pt);\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"pt\", value));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->codecs.audio_codec != JANUS_AUDIOCODEC_NONE || stream->codecs.video_codec != JANUS_VIDEOCODEC_NONE) {\n\t\t\t\t\t\tif(stream->codecs.audio_codec != JANUS_AUDIOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"codec\", janus_audiocodec_name(stream->codecs.audio_codec)));\n\t\t\t\t\t\telse if(stream->codecs.video_codec != JANUS_VIDEOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"codec\", janus_videocodec_name(stream->codecs.video_codec)));\n\t\t\t\t\t\tif(stream->codecs.fmtp)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"fmtp\", stream->codecs.fmtp));\n\t\t\t\t\t\tif(stream->skew)\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"skew\", \"true\"));\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->simulcast) {\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"videosimulcast\", \"true\"));\n\t\t\t\t\t\tif(stream->port[1]) {\n\t\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->port[1]);\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"port2\", value));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream->port[2]) {\n\t\t\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", stream->port[2]);\n\t\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"port3\", value));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->svc)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"videosvc\", \"true\"));\n\t\t\t\t\tif(stream->skew)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"skew\", \"true\"));\n\t\t\t\t\tif(stream->mcast_str)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"mcast\", stream->mcast_str));\n\t\t\t\t\tif(stream->iface_str)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"iface\", stream->iface_str));\n\t\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_DATA)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"datatype\", stream->textdata ? \"text\" : \"binary\"));\n\t\t\t\t\tif(stream->buffermsg)\n\t\t\t\t\t\tjanus_config_add(config, m, janus_config_item_create(\"databuffermsg\", \"true\"));\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t} else if(mp->streaming_source == janus_streaming_source_file) {\n\t\t\t\tjanus_streaming_file_source *source = mp->source;\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"filename\", source->filename));\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio\", \"true\"));\n\t\t\t} else if(mp->streaming_source == janus_streaming_source_rtp && ((janus_streaming_rtp_source *)mp->source)->rtsp) {\n\t\t\t\tjanus_streaming_rtp_source *source = mp->source;\n#ifdef HAVE_LIBCURL\n\t\t\t\tif(source->rtsp_url)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"url\", source->rtsp_url));\n\t\t\t\tif(source->rtsp_username)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtsp_user\", source->rtsp_username));\n\t\t\t\tif(source->rtsp_password)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtsp_pwd\", source->rtsp_password));\n\t\t\t\tif(source->rtsp_quirk)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtsp_quirk\", \"true\"));\n#endif\n\t\t\t\tif(source->bufferkf_ms > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu16, source->bufferkf_ms);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_ms\", value));\n\t\t\t\t}\n\t\t\t\tif(source->bufferkf_bytes > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, source->bufferkf_bytes);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bufferkf_bytes\", value));\n\t\t\t\t}\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\t/* FIXME Should we support RTSP streams with multiple media? */\n\t\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO) {\n\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio\", \"true\"));\n\t\t\t\t\t\tif(stream->codecs.audio_codec != JANUS_AUDIOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiocodec\", janus_audiocodec_name(stream->codecs.audio_codec)));\n\t\t\t\t\t\tif(stream->codecs.fmtp)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiofmtp\", stream->codecs.fmtp));\n\t\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"video\", \"true\"));\n\t\t\t\t\t\tif(stream->codecs.video_codec != JANUS_VIDEOCODEC_NONE)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videocodec\", janus_videocodec_name(stream->codecs.video_codec)));\n\t\t\t\t\t\tif(stream->codecs.fmtp)\n\t\t\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videofmtp\", stream->codecs.fmtp));\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjson_t *iface = json_object_get(root, \"rtspiface\");\n\t\t\t\tif(iface)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rtspiface\", json_string_value(iface)));\n\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", mp->helper_threads);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"threads\", value));\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_STREAMING_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the mountpoint is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\t/* Prepare response/notification */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"streaming\", json_string(\"edited\"));\n\t\tjson_object_set_new(response, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"edited\"));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\t\tgateway->notify_event(&janus_streaming_plugin, session ? session->handle : NULL, info);\n\t\t}\n\n\t\t/* Also notify viewers */\n\t\tif(send_edited_event) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"edited\"));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\t\tif(mp->metadata)\n\t\t\t\tjson_object_set_new(info, \"metadata\", json_string(mp->metadata));\n\t\t\tjanus_streaming_notify_subscribers(mp, info);\n\t\t\tjson_decref(info);\n\t\t}\n\n\t\tjanus_mutex_unlock(&mp->mutex);\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\tjanus_refcount_decrease(&mp->ref);\n\t\t/* Done */\n\t\tJANUS_LOG(LOG_VERB, \"Streaming mountpoint edited\\n\");\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"kick_all\")) {\n\t\t/* Note the kick_all request works with all mountpoint types except for on-demand streaming,\n\t\t * because each on-demand viewer has their own thread and their own playback context. */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 id_value = 0;\n\t\tchar id_num[30], *id_value_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tid_value = json_integer_value(id);\n\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\tid_value_str = id_num;\n\t\t} else {\n\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\tif(mp == NULL) {\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&mp->ref);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(mp->secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Request to kick all viewers from mountpoint/stream %s\\n\", id_value_str);\n\t\tjanus_mutex_lock(&mp->mutex);\n\t\tGList *viewer = g_list_first(mp->viewers);\n\t\t/* Prepare JSON event */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\tjson_t *result = json_object();\n\t\tjson_object_set_new(result, \"status\", json_string(\"kicked_all\"));\n\t\tjson_object_set_new(event, \"result\", result);\n\t\twhile(viewer) {\n\t\t\tjanus_streaming_session *s = (janus_streaming_session *)viewer->data;\n\t\t\tif(s == NULL) {\n\t\t\t\tmp->viewers = g_list_remove_all(mp->viewers, s);\n\t\t\t\tviewer = g_list_first(mp->viewers);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&s->mutex);\n\t\t\tif(s->mountpoint != mp) {\n\t\t\t\tmp->viewers = g_list_remove_all(mp->viewers, s);\n\t\t\t\tviewer = g_list_first(mp->viewers);\n\t\t\t\tjanus_mutex_unlock(&s->mutex);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tg_atomic_int_set(&s->stopping, 1);\n\t\t\tg_atomic_int_set(&s->started, 0);\n\t\t\tg_atomic_int_set(&s->paused, 0);\n\t\t\ts->mountpoint = NULL;\n\t\t\tjanus_mutex_unlock(&s->mutex);\n\t\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\t\tgateway->push_event(s->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\tgateway->close_pc(s->handle);\n\t\t\tif(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t\t/* Remove the viewer from the helper threads too, if any */\n\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\tGList *l = mp->threads;\n\t\t\t\t\twhile(l) {\n\t\t\t\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\t\t\t\tjanus_mutex_lock(&ht->mutex);\n\t\t\t\t\t\tif(g_list_find(ht->viewers, s) != NULL) {\n\t\t\t\t\t\t\tht->num_viewers--;\n\t\t\t\t\t\t\tht->viewers = g_list_remove_all(ht->viewers, s);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Removing viewer from helper thread #%d (destroy)\\n\", ht->id);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tmp->viewers = g_list_remove_all(mp->viewers, s);\n\t\t\tviewer = g_list_first(mp->viewers);\n\t\t\tjanus_refcount_decrease(&s->ref);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t}\n\t\tjson_decref(event);\n\t\tjanus_mutex_unlock(&mp->mutex);\n\t\tjanus_refcount_decrease(&mp->ref);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"kicked_all\"));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(id_value_str) : json_integer(id_value));\n\t\t\tgateway->notify_event(&janus_streaming_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"streaming\", json_string(\"kicked_all\"));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"destroy\")) {\n\t\t/* Get rid of an existing stream (notice this doesn't remove it from the config file, though) */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, destroy_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 id_value = 0;\n\t\tchar id_num[30], *id_value_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tid_value = json_integer_value(id);\n\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\tid_value_str = id_num;\n\t\t} else {\n\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t}\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't destroy mountpoint permanently\\n\");\n\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't destroy mountpoint permanently\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\tif(mp == NULL) {\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&mp->ref);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(mp->secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Request to unmount mountpoint/stream %s\\n\", id_value_str);\n\t\t/* Remove mountpoint from the hashtable: this will get it destroyed eventually */\n\t\tg_hash_table_remove(mountpoints,\n\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\t/* FIXME Should we kick the current viewers as well? */\n\t\tjanus_mutex_lock(&mp->mutex);\n\t\tGList *viewer = g_list_first(mp->viewers);\n\t\t/* Prepare JSON event */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\tjson_t *result = json_object();\n\t\tjson_object_set_new(result, \"status\", json_string(\"stopped\"));\n\t\tjson_object_set_new(event, \"result\", result);\n\t\twhile(viewer) {\n\t\t\tjanus_streaming_session *s = (janus_streaming_session *)viewer->data;\n\t\t\tif(s == NULL) {\n\t\t\t\tmp->viewers = g_list_remove_all(mp->viewers, s);\n\t\t\t\tviewer = g_list_first(mp->viewers);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&s->mutex);\n\t\t\tif(s->mountpoint != mp) {\n\t\t\t\tmp->viewers = g_list_remove_all(mp->viewers, s);\n\t\t\t\tviewer = g_list_first(mp->viewers);\n\t\t\t\tjanus_mutex_unlock(&s->mutex);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tg_atomic_int_set(&s->stopping, 1);\n\t\t\tg_atomic_int_set(&s->started, 0);\n\t\t\tg_atomic_int_set(&s->paused, 0);\n\t\t\ts->mountpoint = NULL;\n\t\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\t\tgateway->push_event(s->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\tgateway->close_pc(s->handle);\n\t\t\tif(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t\t/* Remove the viewer from the helper threads too, if any */\n\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\tGList *l = mp->threads;\n\t\t\t\t\twhile(l) {\n\t\t\t\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\t\t\t\tjanus_mutex_lock(&ht->mutex);\n\t\t\t\t\t\tif(g_list_find(ht->viewers, s) != NULL) {\n\t\t\t\t\t\t\tht->num_viewers--;\n\t\t\t\t\t\t\tht->viewers = g_list_remove_all(ht->viewers, s);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Removing viewer from helper thread #%d (destroy)\\n\", ht->id);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tmp->viewers = g_list_remove_all(mp->viewers, s);\n\t\t\tviewer = g_list_first(mp->viewers);\n\t\t\tjanus_mutex_unlock(&s->mutex);\n\t\t\tjanus_refcount_decrease(&s->ref);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t}\n\t\tjson_decref(event);\n\t\tjanus_mutex_unlock(&mp->mutex);\n\t\tif(save) {\n\t\t\t/* This change is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Destroying mountpoint %s (%s) permanently in config file\\n\", mp->id_str, mp->name);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\t/* The category to remove is the mountpoint name */\n\t\t\tjanus_config_remove(config, NULL, mp->name);\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_STREAMING_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the mountpoint is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\tjanus_refcount_decrease(&mp->ref);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"destroyed\"));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(id_value_str) : json_integer(id_value));\n\t\t\tgateway->notify_event(&janus_streaming_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"streaming\", json_string(\"destroyed\"));\n\t\tjson_object_set_new(response, \"destroyed\", string_ids ? json_string(id_value_str) : json_integer(id_value));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"recording\")) {\n\t\t/* We can start/stop recording a live, RTP-based stream */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, recording_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *action = json_object_get(root, \"action\");\n\t\tconst char *action_text = json_string_value(action);\n\t\tif(strcasecmp(action_text, \"start\") && strcasecmp(action_text, \"stop\")) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid action (should be start|stop)\\n\");\n\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Invalid action (should be start|stop)\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 id_value = 0;\n\t\tchar id_num[30], *id_value_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tid_value = json_integer_value(id);\n\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\tid_value_str = id_num;\n\t\t} else {\n\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\tif(mp == NULL) {\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&mp->ref);\n\t\tif(mp->streaming_type != janus_streaming_type_live || mp->streaming_source != janus_streaming_source_rtp) {\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"Recording is only available on RTP-based live streams\\n\");\n\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Recording is only available on RTP-based live streams\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(mp->secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\tif(!strcasecmp(action_text, \"start\")) {\n\t\t\t/* Start a recording for audio and/or video */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, recording_start_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tjson_t *media = json_object_get(root, \"media\");\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\tjson_t *data = json_object_get(root, \"data\");\n\t\t\tjanus_mutex_lock(&source->rec_mutex);\n\t\t\tif(media) {\n\t\t\t\t/* Iterate on all media to start */\n\t\t\t\tif(json_array_size(media) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(media); i++) {\n\t\t\t\t\t\tjson_t *m = json_array_get(media, i);\n\t\t\t\t\t\tif(!json_is_object(m))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\tconst char *mid = json_string_value(json_object_get(m, \"mid\"));\n\t\t\t\t\t\tconst char *filename = json_string_value(json_object_get(m, \"filename\"));\n\t\t\t\t\t\tif(mid == NULL || filename == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No mid/filename, can't start recording for this stream...\\n\");\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tGList *temp = source->media;\n\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\t\t\tif(strcasecmp(stream->mid, mid)) {\n\t\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(stream->rc) {\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Recording already started for this stream\\n\");\n\t\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Recording for audio, video and/or data already started for this stream\");\n\t\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst char *codec = NULL;\n\t\t\t\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO) {\n\t\t\t\t\t\t\t\tcodec = janus_audiocodec_name(stream->codecs.audio_codec);\n\t\t\t\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\tcodec = janus_videocodec_name(stream->codecs.video_codec);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcodec = \"text\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_recorder *rc = janus_recorder_create(NULL, codec, (char *)filename);\n\t\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Error starting recorder for %s\\n\",\n\t\t\t\t\t\t\t\t\tmp->name, janus_streaming_media_str(stream->type));\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_RECORD;\n\t\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Error starting recorder for %s\", janus_streaming_media_str(stream->type));\n\t\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\t\t\t\tif(source->e2ee)\n\t\t\t\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\t\t\t\tstream->rc = rc;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] %s recording started (%s)\\n\",\n\t\t\t\t\t\t\t\tmp->name, janus_streaming_media_str(stream->type), stream->mid);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t/* Send a success response back */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"streaming\", json_string(\"ok\"));\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\t/* If we're here, we're using the old method to record */\n\t\t\tif(!audio && !video && !data) {\n\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing audio, video and/or data\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing audio, video and/or data\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tGList *temp = source->media;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\tif((stream->type == JANUS_STREAMING_MEDIA_AUDIO && !audio) ||\n\t\t\t\t\t\t(stream->type == JANUS_STREAMING_MEDIA_VIDEO && !video) ||\n\t\t\t\t\t\t(stream->type == JANUS_STREAMING_MEDIA_DATA && !data)) {\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(stream->rc) {\n\t\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Recording already started for this %s stream\\n\", janus_streaming_media_str(stream->type));\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Recording started for this stream\");\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\tconst char *codec = NULL, *file = NULL;\n\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO) {\n\t\t\t\t\tcodec = janus_audiocodec_name(stream->codecs.audio_codec);\n\t\t\t\t\tfile = json_string_value(audio);\n\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\t\t\t\tcodec = janus_videocodec_name(stream->codecs.video_codec);\n\t\t\t\t\tfile = json_string_value(video);\n\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_DATA) {\n\t\t\t\t\tcodec = \"text\";\n\t\t\t\t\tfile = json_string_value(data);\n\t\t\t\t}\n\t\t\t\tjanus_recorder *rc = janus_recorder_create(NULL, codec, (char *)file);\n\t\t\t\tif(rc == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Error starting recorder for %s\\n\", mp->name, janus_streaming_media_str(stream->type));\n\t\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_RECORD;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error starting recorder\");\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\tif(source->e2ee)\n\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Recording for %s started\\n\", mp->name, janus_streaming_media_str(stream->type));\n\t\t\t\tstream->rc = rc;\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t/* Send a success response back */\n\t\t\tresponse = json_object();\n\t\t\tjson_object_set_new(response, \"streaming\", json_string(\"ok\"));\n\t\t\tgoto prepare_response;\n\t\t} else if(!strcasecmp(action_text, \"stop\")) {\n\t\t\t/* Stop the recording */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, recording_stop_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tjson_t *media = json_object_get(root, \"media\");\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\tjson_t *data = json_object_get(root, \"data\");\n\t\t\tjanus_mutex_lock(&source->rec_mutex);\n\t\t\tif(media) {\n\t\t\t\t/* Iterate on all media to stop */\n\t\t\t\tif(json_array_size(media) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(media); i++) {\n\t\t\t\t\t\tjson_t *m = json_array_get(media, i);\n\t\t\t\t\t\tif(!json_is_object(m))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\tconst char *mid = json_string_value(json_object_get(m, \"mid\"));\n\t\t\t\t\t\tif(mid == NULL)\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\tGList *temp = source->media;\n\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\t\t\tif(strcasecmp(stream->mid, mid) || stream->rc == NULL) {\n\t\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_recorder_close(stream->rc);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Closed %s recording %s (%s)\\n\", mp->name,\n\t\t\t\t\t\t\t\tjanus_streaming_media_str(stream->type), stream->rc->filename, stream->mid);\n\t\t\t\t\t\t\tjanus_recorder *tmp = stream->rc;\n\t\t\t\t\t\t\tstream->rc = NULL;\n\t\t\t\t\t\t\tjanus_recorder_destroy(tmp);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\t/* Send a success response back */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"streaming\", json_string(\"ok\"));\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\t/* If we're here, we're using the old method to stop recording */\n\t\t\tif(!audio && !video && !data) {\n\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing audio, video and or data\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing audio, video and or data\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tGList *temp = source->media;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\tif((stream->type == JANUS_STREAMING_MEDIA_AUDIO && !json_is_true(audio)) ||\n\t\t\t\t\t\t(stream->type == JANUS_STREAMING_MEDIA_VIDEO && !json_is_true(video)) ||\n\t\t\t\t\t\t(stream->type == JANUS_STREAMING_MEDIA_DATA && !json_is_true(data)) || stream->rc == NULL) {\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Close the recording */\n\t\t\t\tjanus_recorder_close(stream->rc);\n\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Closed %s recording %s (%s)\\n\", mp->name,\n\t\t\t\t\tjanus_streaming_media_str(stream->type), stream->rc->filename, stream->mid);\n\t\t\t\tjanus_recorder *tmp = stream->rc;\n\t\t\t\tstream->rc = NULL;\n\t\t\t\tjanus_recorder_destroy(tmp);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t/* Send a success response back */\n\t\t\tresponse = json_object();\n\t\t\tjson_object_set_new(response, \"streaming\", json_string(\"ok\"));\n\t\t\tgoto prepare_response;\n\t\t}\n\t} else if(!strcasecmp(request_text, \"enable\") || !strcasecmp(request_text, \"disable\")) {\n\t\t/* A request to enable/disable a mountpoint */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 id_value = 0;\n\t\tchar id_num[30], *id_value_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tid_value = json_integer_value(id);\n\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\tid_value_str = id_num;\n\t\t} else {\n\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\tif(mp == NULL) {\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&mp->ref);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(mp->secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(!strcasecmp(request_text, \"enable\")) {\n\t\t\t/* Enable a previously disabled mountpoint */\n\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Stream enabled\\n\", mp->name);\n\t\t\tmp->enabled = TRUE;\n\t\t\t/* FIXME: Should we notify the viewers, or is this up to the controller application? */\n\t\t} else {\n\t\t\t/* Disable a previously enabled mountpoint */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, disable_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tmp->enabled = FALSE;\n\t\t\tgboolean stop_recording = TRUE;\n\t\t\tjson_t *stop_rec = json_object_get(root, \"stop_recording\");\n\t\t\tif (stop_rec) {\n\t\t\t\tstop_recording = json_is_true(stop_rec);\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Stream disabled (stop_recording=%s)\\n\", mp->name, stop_recording ? \"true\" : \"false\");\n\t\t\t/* Any recording to close? */\n\t\t\tif(mp->streaming_source == janus_streaming_source_rtp && stop_recording) {\n\t\t\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\t\t\tjanus_mutex_lock(&source->rec_mutex);\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\t/* Close the recording */\n\t\t\t\t\tjanus_recorder_close(stream->rc);\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Closed %s recording %s (%s)\\n\", mp->name,\n\t\t\t\t\t\tjanus_streaming_media_str(stream->type), stream->rc->filename, stream->mid);\n\t\t\t\t\tjanus_recorder *tmp = stream->rc;\n\t\t\t\t\tstream->rc = NULL;\n\t\t\t\t\tjanus_recorder_destroy(tmp);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t}\n\t\t\t/* FIXME: Should we notify the viewers, or is this up to the controller application? */\n\t\t}\n\t\tjanus_refcount_decrease(&mp->ref);\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t/* Send a success response back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"streaming\", json_string(\"ok\"));\n\t\tgoto prepare_response;\n\t} else {\n\t\t/* Not a request we recognize, don't do anything */\n\t\treturn NULL;\n\t}\n\nprepare_response:\n\t\t{\n\t\t\tif(ifas) {\n\t\t\t\tfreeifaddrs(ifas);\n\t\t\t}\n\n\t\t\tif(error_code == 0 && !response) {\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"streaming\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nstruct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\t/* Pre-parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_streaming_session *session = janus_streaming_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No session associated with this handle...\");\n\t\tgoto plugin_response;\n\t}\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tJANUS_LOG(LOG_ERR, \"Session has already been destroyed...\\n\");\n\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"Session has already been destroyed...\");\n\t\tgoto plugin_response;\n\t}\n\n\tif(message == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\terror_code = JANUS_STREAMING_ERROR_NO_MESSAGE;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\tgoto plugin_response;\n\t}\n\tif(!json_is_object(root)) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\terror_code = JANUS_STREAMING_ERROR_INVALID_JSON;\n\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\tgoto plugin_response;\n\t}\n\t/* Get the request first */\n\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\tjson_t *request = json_object_get(root, \"request\");\n\t/* Some requests ('create' and 'destroy') can be handled synchronously */\n\tconst char *request_text = json_string_value(request);\n\t/* We have a separate method to process synchronous requests, as those may\n\t * arrive from the Admin API as well, and so we handle them the same way */\n\tresponse = janus_streaming_process_synchronous_request(session, root);\n\tif(response != NULL) {\n\t\t/* We got a response, send it back */\n\t\tgoto plugin_response;\n\t} else if(!strcasecmp(request_text, \"watch\") || !strcasecmp(request_text, \"start\")\n\t\t\t|| !strcasecmp(request_text, \"pause\") || !strcasecmp(request_text, \"stop\")\n\t\t\t|| !strcasecmp(request_text, \"configure\") || !strcasecmp(request_text, \"switch\")) {\n\t\t/* These messages are handled asynchronously */\n\t\tjanus_streaming_message *msg = g_malloc(sizeof(janus_streaming_message));\n\t\tmsg->handle = handle;\n\t\tmsg->transaction = transaction;\n\t\tmsg->message = root;\n\t\tmsg->jsep = jsep;\n\n\t\tg_async_queue_push(messages, msg);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code == 0 && !response) {\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\t\tresponse = event;\n\t\t\t}\n\t\t\tif(root != NULL)\n\t\t\t\tjson_decref(root);\n\t\t\tif(jsep != NULL)\n\t\t\t\tjson_decref(jsep);\n\t\t\tg_free(transaction);\n\n\t\t\tif(session != NULL)\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, response);\n\t\t}\n\n}\n\njson_t *janus_streaming_handle_admin_message(json_t *message) {\n\t/* Some requests (e.g., 'create' and 'destroy') can be handled via Admin API */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *response = NULL;\n\n\tJANUS_VALIDATE_JSON_OBJECT(message, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto admin_response;\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\tif((response = janus_streaming_process_synchronous_request(NULL, message)) != NULL) {\n\t\t/* We got a response, send it back */\n\t\tgoto admin_response;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nadmin_response:\n\t\t{\n\t\t\tif(!response) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"streaming\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nvoid janus_streaming_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_STREAMING_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_streaming_session *session = janus_streaming_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_atomic_int_set(&session->hangingup, 0);\n\t/* If this is related to a live RTP mountpoint, any keyframe we can shoot already? */\n\tjanus_streaming_mountpoint *mountpoint = session->mountpoint;\n\tif (!mountpoint) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tJANUS_LOG(LOG_ERR, \"No mountpoint associated with this session...\\n\");\n\t\treturn;\n\t}\n\tif(mountpoint->streaming_source == janus_streaming_source_rtp) {\n\t\tjanus_streaming_rtp_source *source = mountpoint->source;\n\t\tGList *temp = source->media;\n\t\twhile(temp) {\n\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\tif(stream->keyframe.enabled) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Any keyframe to send? (%s)\\n\", stream->mid);\n\t\t\t\tjanus_mutex_lock(&stream->keyframe.mutex);\n\t\t\t\tif(stream->keyframe.latest_keyframe != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Yep! %d packets\\n\", g_list_length(stream->keyframe.latest_keyframe));\n\t\t\t\t\tGList *packets = g_list_reverse(g_list_copy(stream->keyframe.latest_keyframe)), *temp = packets;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_streaming_relay_rtp_packet(session, temp->data);\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t\tg_list_free(packets);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&stream->keyframe.mutex);\n\t\t\t}\n\t\t\t/* If this mountpoint has RTCP support, send a PLI */\n\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_VIDEO)\n\t\t\t\tjanus_streaming_rtcp_pli_send(stream);\n\t\t\ttemp = temp->next;\n\t\t}\n\t}\n\tg_atomic_int_set(&session->started, 1);\n\t/* Prepare JSON event */\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\tjson_t *result = json_object();\n\tjson_object_set_new(result, \"status\", json_string(\"started\"));\n\tjson_object_set_new(event, \"result\", result);\n\tint ret = gateway->push_event(handle, &janus_streaming_plugin, NULL, event, NULL);\n\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\tjson_decref(event);\n\t/* Also notify event handlers */\n\tif(notify_events && gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"status\", json_string(\"started\"));\n\t\tif(session->mountpoint != NULL)\n\t\t\tjson_object_set_new(info, \"id\", string_ids ?\n\t\t\t\tjson_string(session->mountpoint->id_str) :json_integer(session->mountpoint->id));\n\t\tgateway->notify_event(&janus_streaming_plugin, session->handle, info);\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_streaming_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\t/* FIXME We don't care about what the browser sends us, we're sendonly */\n}\n\nvoid janus_streaming_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_streaming_session *session = (janus_streaming_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->stopping) ||\n\t\t\t!g_atomic_int_get(&session->started) || g_atomic_int_get(&session->paused))\n\t\treturn;\n\tjanus_streaming_mountpoint *mp = (janus_streaming_mountpoint *)session->mountpoint;\n\tif(mp->streaming_source != janus_streaming_source_rtp)\n\t\treturn;\n\tjanus_streaming_rtp_source *source = (janus_streaming_rtp_source *)mp->source;\n\t/* Check which stream this feedback refers to */\n\tjanus_streaming_rtp_source_stream *stream = g_hash_table_lookup(source->media_byid, GINT_TO_POINTER(packet->mindex));\n\tif(stream == NULL)\n\t\treturn;\n\tgboolean video = packet->video;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\tif(!video && (stream->rtcp_fd > -1) && (stream->rtcp_addr.ss_family != 0)) {\n\t\tJANUS_LOG(LOG_HUGE, \"Got audio RTCP feedback from a viewer: SSRC %\"SCNu32\"\\n\",\n\t\t\tjanus_rtcp_get_sender_ssrc(buf, len));\n\t\t/* FIXME We don't forward RR packets, so what should we check here? */\n\t} else if(video && (stream->rtcp_fd > -1) && (stream->rtcp_addr.ss_family != 0)) {\n\t\tJANUS_LOG(LOG_HUGE, \"Got video RTCP feedback from a viewer: SSRC %\"SCNu32\"\\n\",\n\t\t\tjanus_rtcp_get_sender_ssrc(buf, len));\n\t\t/* We only relay PLI/FIR and REMB packets, but in a selective way */\n\t\tif(janus_rtcp_has_fir(buf, len) || janus_rtcp_has_pli(buf, len)) {\n\t\t\t/* We got a PLI/FIR, pass it along unless we just sent one */\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Keyframe request\\n\");\n\t\t\tjanus_streaming_rtcp_pli_send(stream);\n\t\t}\n\t\tuint64_t bw = janus_rtcp_get_remb(buf, len);\n\t\tif(bw > 0) {\n\t\t\t/* Keep track of this value, if this is the lowest right now */\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- REMB for this PeerConnection: %\"SCNu64\"\\n\", bw);\n\t\t\tif((0 == source->lowest_bitrate) || (source->lowest_bitrate > bw))\n\t\t\t\tsource->lowest_bitrate = bw;\n\t\t}\n\t}\n}\n\nvoid janus_streaming_data_ready(janus_plugin_session *handle) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) ||\n\t\t\tg_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized) || !gateway)\n\t\treturn;\n\t/* Data channels are writable: we shouldn't send any datachannel message before this happens */\n\tjanus_streaming_session *session = (janus_streaming_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tjanus_refcount_increase(&session->ref);\n\tif(g_atomic_int_compare_and_exchange(&session->dataready, 0, 1)) {\n\t\tJANUS_LOG(LOG_INFO, \"[%s-%p] Data channel available\\n\", JANUS_STREAMING_PACKAGE, handle);\n\t\t/* Try to send a buffered datachannel message when datachannel is ready */\n\t\tGList *temp = session->streams;\n\t\tjanus_streaming_session_stream *s;\n\t\tjanus_streaming_rtp_source_stream *stream;\n\t\twhile(temp) {\n\t\t\ts = (janus_streaming_session_stream *)temp->data;\n\t\t\tstream = s->stream;\n\t\t\tif(stream->buffermsg) {\n\t\t\t\tjanus_refcount_increase(&stream->ref);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p] Trying to send the most recent message (%s)\\n\", JANUS_STREAMING_PACKAGE, handle, stream->mid);\n\t\t\t\tjanus_mutex_lock(&stream->buffermsg_mutex);\n\t\t\t\tif(stream->last_msg != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Buffered datachannel message found!\\n\");\n\t\t\t\t\tjanus_streaming_relay_rtp_packet(session, stream->last_msg);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&stream->buffermsg_mutex);\n\t\t\t\tjanus_refcount_decrease(&stream->ref);\n\t\t\t}\n\t\t\ttemp = temp->next;\n\t\t}\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_streaming_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_STREAMING_PACKAGE, handle);\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_streaming_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_streaming_hangup_media_internal(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_streaming_session *session = janus_streaming_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\tg_atomic_int_set(&session->dataready, 0);\n\tg_atomic_int_set(&session->stopping, 1);\n\tg_atomic_int_set(&session->started, 0);\n\tg_atomic_int_set(&session->paused, 0);\n\tsession->e2ee = FALSE;\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_streaming_mountpoint *mp = session->mountpoint;\n\tsession->mountpoint = NULL;\n\tjanus_mutex_unlock(&session->mutex);\n\tif(mp) {\n\t\tjanus_mutex_lock(&mp->mutex);\n\t\tJANUS_LOG(LOG_VERB, \"  -- Removing the session from the mountpoint viewers\\n\");\n\t\tif(g_list_find(mp->viewers, session) != NULL) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- -- Found!\\n\");\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t}\n\t\tmp->viewers = g_list_remove_all(mp->viewers, session);\n\t\tif(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t/* Remove the viewer from the helper threads too, if any */\n\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\tGList *l = mp->threads;\n\t\t\t\twhile(l) {\n\t\t\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\t\t\tjanus_mutex_lock(&ht->mutex);\n\t\t\t\t\tif(g_list_find(ht->viewers, session) != NULL) {\n\t\t\t\t\t\tht->num_viewers--;\n\t\t\t\t\t\tht->viewers = g_list_remove_all(ht->viewers, session);\n\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Removing viewer from helper thread #%d\\n\", ht->id);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\tl = l->next;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Get rid of streams and streams_byid while holding the mountpoint mutex */\n\t\tg_list_free_full(session->streams, (GDestroyNotify)(janus_streaming_session_stream_free));\n\t\tsession->streams = NULL;\n\t\tif(session->streams_byid != NULL)\n\t\t\tg_hash_table_unref(session->streams_byid);\n\t\tsession->streams_byid = NULL;\n\t\tjanus_mutex_unlock(&mp->mutex);\n\t} else {\n\t\t/* Get rid of streams and streams_byid */\n\t\tg_list_free_full(session->streams, (GDestroyNotify)(janus_streaming_session_stream_free));\n\t\tsession->streams = NULL;\n\t\tif(session->streams_byid != NULL)\n\t\t\tg_hash_table_unref(session->streams_byid);\n\t\tsession->streams_byid = NULL;\n\t}\n\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_streaming_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining Streaming handler thread\\n\");\n\tjanus_streaming_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_streaming_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_streaming_session *session = janus_streaming_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_streaming_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_streaming_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = NULL;\n\t\tif(msg->message == NULL) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_STREAMING_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\troot = msg->message;\n\t\t/* Get the request first */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tgoto error;\n\t\t}\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tjson_t *result = NULL;\n\t\tconst char *sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\tconst char *jsep_sdp = (char *)json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\tchar *sdp = NULL;\n\t\tgboolean do_restart = FALSE;\n\t\t/* All these requests can only be handled asynchronously */\n\t\tif(!strcasecmp(request_text, \"watch\") && jsep_sdp == NULL) {\n\t\t\t/* New subscriber, plugin will generate an offer */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, watch_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!string_ids) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t\tguint64 id_value = 0;\n\t\t\tchar id_num[30], *id_value_str = NULL;\n\t\t\tif(!string_ids) {\n\t\t\t\tid_value = json_integer_value(id);\n\t\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\t\tid_value_str = id_num;\n\t\t\t} else {\n\t\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t\t}\n\t\t\t/* The proper way is listing the mids of the streams we want to\n\t\t\t * receive: a missing or empty list will subscribe to them all */\n\t\t\tjson_t *mids = json_object_get(root, \"media\");\n\t\t\tif(mids != NULL && json_array_size(mids) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(mids); i++) {\n\t\t\t\t\tjson_t *s = json_array_get(mids, i);\n\t\t\t\t\tif(!json_is_string(s)) {\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"The media array must only contain strings (mid values)\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* The offer_audio/video/data attribute are deprecated,\n\t\t\t * but we keep them there for backwards compatibility */\n\t\t\tjson_t *offer_audio = json_object_get(root, \"offer_audio\");\n\t\t\tjson_t *offer_video = json_object_get(root, \"offer_video\");\n\t\t\tjson_t *offer_data = json_object_get(root, \"offer_data\");\n\t\t\t/* There may be an ICE restart request involved */\n\t\t\tjson_t *restart = json_object_get(root, \"restart\");\n\t\t\tdo_restart = restart ? json_is_true(restart) : FALSE;\n\t\t\t/* Find the mountpoint and go on */\n\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\t\tif(mp == NULL) {\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&mp->ref);\n\t\t\t/* A secret may be required for this action */\n\t\t\tJANUS_CHECK_SECRET(mp->pin, root, \"pin\", error_code, error_cause,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&mp->mutex);\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t/* Check if this is a new viewer, or if an update is taking place (i.e., ICE restart) */\n\t\t\tgboolean audio = TRUE, video = TRUE, data = TRUE;\n\t\t\tif(do_restart) {\n\t\t\t\tif(session->mountpoint == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't perform ICE restart: no mountpoint set\\n\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't perform ICE restart: no mountpoint set\");\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(session->mountpoint != mp) {\n\t\t\t\t\t/* Already watching something else */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Already watching mountpoint %s\\n\", session->mountpoint->id_str);\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Already watching mountpoint %s\", session->mountpoint->id_str);\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* User asked for an ICE restart: provide a new offer */\n\t\t\t\tif(!g_atomic_int_compare_and_exchange(&session->renegotiating, 0, 1)) {\n\t\t\t\t\t/* Already triggered a renegotiation, and still waiting for an answer */\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Already renegotiating mountpoint %s\\n\", session->mountpoint->id_str);\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Already renegotiating mountpoint %s\", session->mountpoint->id_str);\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Request to perform an ICE restart on mountpoint/stream %s subscription\\n\", id_value_str);\n\t\t\t\tsession->sdp_version++;\t/* This needs to be increased when it changes */\n\t\t\t\tgoto done;\n\t\t\t}\n\t\t\tif(session->mountpoint != NULL) {\n\t\t\t\tif(session->mountpoint != mp) {\n\t\t\t\t\t/* Already watching something else */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Already watching mountpoint %s\\n\", session->mountpoint->id_str);\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_STATE;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Already watching mountpoint %s\", session->mountpoint->id_str);\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t} else {\n\t\t\t\t\t/* Make sure it's not an API error */\n\t\t\t\t\tif(!g_atomic_int_get(&session->started)) {\n\t\t\t\t\t\t/* Can't be a renegotiation, PeerConnection isn't up yet */\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Already watching mountpoint %s\\n\", session->mountpoint->id_str);\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_STATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Already watching mountpoint %s\", session->mountpoint->id_str);\n\t\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tif(!g_atomic_int_compare_and_exchange(&session->renegotiating, 0, 1)) {\n\t\t\t\t\t\t/* Already triggered a renegotiation, and still waiting for an answer */\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Already renegotiating mountpoint %s\\n\", session->mountpoint->id_str);\n\t\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_STATE;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Already renegotiating mountpoint %s\", session->mountpoint->id_str);\n\t\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\t/* Simple renegotiation, remove the extra unneeded reference */\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Request to update mountpoint/stream %s subscription (no restart)\\n\", id_value_str);\n\t\t\t\t\tsession->sdp_version++;\t/* This needs to be increased when it changes */\n\t\t\t\t\tgoto done;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* New viewer: we send an offer ourselves */\n\t\t\tJANUS_LOG(LOG_VERB, \"Request to watch mountpoint/stream %s\\n\", id_value_str);\n\t\t\tif(g_list_find(mp->viewers, session) != NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already watching a stream (found %p in %s's viewers)...\\n\", session, id_value_str);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already watching a stream\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tg_atomic_int_set(&session->stopping, 0);\n\t\t\tsession->mountpoint = mp;\n\t\t\tsession->sdp_version = 1;\t/* This needs to be increased when it changes */\n\t\t\tsession->sdp_sessid = janus_get_real_time();\n\t\t\t/* Check what we should offer: the new way of subscribing is\n\t\t\t * specifying the streams we're interested in by mid */\n\t\t\tgboolean legacy = FALSE;\n\t\t\tif(mids == NULL || json_array_size(mids) == 0) {\n\t\t\t\t/* We'll treat this as a legacy request, even though\n\t\t\t\t * it may simply be a \"subscribe to all streams\" */\n\t\t\t\tlegacy = TRUE;\n\t\t\t}\n\t\t\t/* These are deprecated attributes, that we handle for backwards compatibility */\n\t\t\taudio = offer_audio ? json_is_true(offer_audio) : TRUE;\t/* True by default */\n\t\t\tif(!mp->audio)\n\t\t\t\taudio = FALSE;\t/* ... unless the mountpoint isn't sending any audio */\n\t\t\tvideo = offer_video ? json_is_true(offer_video) : TRUE;\t/* True by default */\n\t\t\tif(!mp->video)\n\t\t\t\tvideo = FALSE;\t/* ... unless the mountpoint isn't sending any video */\n\t\t\tdata = offer_data ? json_is_true(offer_data) : TRUE;\t/* True by default */\n\t\t\tif(!mp->data)\n\t\t\t\tdata = FALSE;\t/* ... unless the mountpoint isn't sending any data */\n\t\t\tif((!mp->audio || !audio) && (!mp->video || !video) && (!mp->data || !data)) {\n\t\t\t\tsession->mountpoint = NULL;\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't offer an SDP with no audio, video or data for this mountpoint\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't offer an SDP with no audio, video or data for this mountpoint\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(mp->streaming_source == janus_streaming_source_file) {\n\t\t\t\t/* Create a session stream */\n\t\t\t\tjanus_streaming_session_stream *s = g_malloc0(sizeof(janus_streaming_session_stream));\n\t\t\t\ts->mindex = -1;\n\t\t\t\ts->send = TRUE;\n\t\t\t\ts->pt = -1;\n\t\t\t\tjanus_rtp_switching_context_reset(&s->context);\n\t\t\t\ts->min_delay = -1;\n\t\t\t\ts->max_delay = -1;\n\t\t\t\tsession->streams = g_list_append(session->streams, s);\n\t\t\t\tif(session->streams_byid == NULL)\n\t\t\t\t\tsession->streams_byid = g_hash_table_new(NULL, NULL);\n\t\t\t\tg_hash_table_insert(session->streams_byid, GINT_TO_POINTER(s->mindex), s);\n\t\t\t}\n\t\t\tif(mp->streaming_type == janus_streaming_type_on_demand) {\n\t\t\t\t/* Spawn a thread */\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"mp %s\", mp->id_str);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tjanus_refcount_increase(&mp->ref);\n\t\t\t\tg_thread_try_new(tname, &janus_streaming_ondemand_thread, session, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tsession->mountpoint = NULL;\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\t/* This is for the failed thread */\n\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\t\t/* This is for the failed thread */\n\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the on-demand thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Got error %d (%s) trying to launch the on-demand thread\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t} else if(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t\t/* Create a session stream for each source stream we're subscribing to */\n\t\t\t\tjanus_streaming_rtp_source *source = (janus_streaming_rtp_source *)mp->source;\n\t\t\t\tjanus_streaming_session_stream *s = NULL;\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\t/* If we're using the legacy API, make sure we honor the deprecated attributes */\n\t\t\t\t\tif(legacy && ((stream->type == JANUS_STREAMING_MEDIA_AUDIO && !audio) ||\n\t\t\t\t\t\t\t(stream->type == JANUS_STREAMING_MEDIA_VIDEO && !video) ||\n\t\t\t\t\t\t\t(stream->type == JANUS_STREAMING_MEDIA_DATA && !data))) {\n\t\t\t\t\t\t/* FIXME Don't subscribe to this stream */\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Check if this stream is among the mids the user subscribed to */\n\t\t\t\t\tif(mids != NULL && json_array_size(mids) > 0) {\n\t\t\t\t\t\tgboolean found = FALSE;\n\t\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\t\tfor(i=0; i<json_array_size(mids); i++) {\n\t\t\t\t\t\t\tjson_t *s = json_array_get(mids, i);\n\t\t\t\t\t\t\tconst char *mid = json_string_value(s);\n\t\t\t\t\t\t\tif(mid && stream->mid && !strcasecmp(stream->mid, mid)) {\n\t\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(!found) {\n\t\t\t\t\t\t\t/* Not in the mids list, don't subscribe to this stream */\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* Create a new session stream and add a reference to the source stream */\n\t\t\t\t\ts = g_malloc0(sizeof(janus_streaming_session_stream));\n\t\t\t\t\ts->send = TRUE;\n\t\t\t\t\ts->pt = stream->codecs.pt;\n\t\t\t\t\tjanus_rtp_switching_context_reset(&s->context);\n\t\t\t\t\ts->min_delay = -1;\n\t\t\t\t\ts->max_delay = -1;\n\t\t\t\t\tif(stream && stream->simulcast) {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, simulcast_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\tg_free(s);\n\t\t\t\t\t\t\tsession->mountpoint = NULL;\n\t\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* In case this mountpoint is simulcasting, let's aim high by default */\n\t\t\t\t\t\tjanus_rtp_switching_context_reset(&s->context);\n\t\t\t\t\t\tjanus_rtp_simulcasting_context_reset(&s->sim_context);\n\t\t\t\t\t\ts->sim_context.substream_target = 2;\n\t\t\t\t\t\ts->sim_context.templayer_target = 2;\n\t\t\t\t\t\tjanus_vp8_simulcast_context_reset(&s->vp8_context);\n\t\t\t\t\t\t/* Unless the request contains a target for either layer */\n\t\t\t\t\t\tjson_t *substream = json_object_get(root, \"substream\");\n\t\t\t\t\t\tif(substream) {\n\t\t\t\t\t\t\ts->sim_context.substream_target = json_integer_value(substream);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video substream to let through (simulcast): %d (was %d)\\n\",\n\t\t\t\t\t\t\t\ts->sim_context.substream_target, s->sim_context.substream);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *temporal = json_object_get(root, \"temporal\");\n\t\t\t\t\t\tif(temporal) {\n\t\t\t\t\t\t\ts->sim_context.templayer_target = json_integer_value(temporal);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video temporal layer to let through (simulcast): %d (was %d)\\n\",\n\t\t\t\t\t\t\t\ts->sim_context.templayer_target, s->sim_context.templayer);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Check if we need a custom fallback timer for the substream */\n\t\t\t\t\t\tjson_t *fallback = json_object_get(root, \"fallback\");\n\t\t\t\t\t\tif(fallback) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting fallback timer (simulcast): %lld (was %\"SCNu32\")\\n\",\n\t\t\t\t\t\t\t\tjson_integer_value(fallback) ? json_integer_value(fallback) : 250000,\n\t\t\t\t\t\t\t\ts->sim_context.drop_trigger ? s->sim_context.drop_trigger : 250000);\n\t\t\t\t\t\t\ts->sim_context.drop_trigger = json_integer_value(fallback);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(stream && stream->svc) {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, svc_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\tg_free(s);\n\t\t\t\t\t\t\tsession->mountpoint = NULL;\n\t\t\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* In case this mountpoint is doing VP9-SVC, let's aim high by default */\n\t\t\t\t\t\ts->spatial_layer = -1;\n\t\t\t\t\t\ts->target_spatial_layer = 2;\t/* FIXME Chrome sends 0, 1 and 2 (if using EnabledByFlag_3SL3TL) */\n\t\t\t\t\t\ts->temporal_layer = -1;\n\t\t\t\t\t\ts->target_temporal_layer = 2;\t/* FIXME Chrome sends 0, 1 and 2 */\n\t\t\t\t\t\t/* Unless the request contains a target for either layer */\n\t\t\t\t\t\tjson_t *spatial = json_object_get(root, \"spatial_layer\");\n\t\t\t\t\t\tif(spatial) {\n\t\t\t\t\t\t\ts->target_spatial_layer = json_integer_value(spatial);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video spatial layer to let through (SVC): %d (was %d)\\n\",\n\t\t\t\t\t\t\t\ts->target_spatial_layer, s->spatial_layer);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *temporal = json_object_get(root, \"temporal_layer\");\n\t\t\t\t\t\tif(temporal) {\n\t\t\t\t\t\t\ts->target_temporal_layer = json_integer_value(temporal);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video temporal layer to let through (SVC): %d (was %d)\\n\",\n\t\t\t\t\t\t\t\ts->target_temporal_layer, s->temporal_layer);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ts->mindex = g_list_length(session->streams);\n\t\t\t\t\ts->stream = stream;\n\t\t\t\t\tjanus_refcount_increase(&stream->ref);\n\t\t\t\t\tsession->streams = g_list_append(session->streams, s);\n\t\t\t\t\tif(session->streams_byid == NULL)\n\t\t\t\t\t\tsession->streams_byid = g_hash_table_new(NULL, NULL);\n\t\t\t\t\tg_hash_table_insert(session->streams_byid, GINT_TO_POINTER(stream->mindex), s);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\t/* If this mountpoint is broadcasting end-to-end encrypted media,\n\t\t\t\t * add the info to the JSEP offer we'll be sending them */\n\t\t\t\tsession->e2ee = source->e2ee;\n\t\t\t\t/* Also check if we have to offer the playout-delay extension */\n\t\t\t\tsession->playoutdelay_ext = source->playoutdelay_ext;\n\t\t\t\t/* Also check if we have to offer the abs-capture-time extension */\n\t\t\t\tsession->abscapturetime_src_ext_id = source->abscapturetime_src_ext_id;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&session->ref);\ndone:\n\t\t\t/* Let's prepare an offer now, but let's also check if there's something we need to skip */\n\t\t\tsdp_type = \"offer\";\t/* We're always going to do the offer ourselves, never answer */\n\t\t\tchar s_name[100];\n\t\t\tg_snprintf(s_name, sizeof(s_name), \"Mountpoint %s\", mp->id_str);\n\t\t\tjanus_sdp *offer = janus_sdp_generate_offer(s_name, \"0.0.0.0\",\n\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\toffer->o_version = session->sdp_version;\n\t\t\tif(mp->streaming_source == janus_streaming_source_file) {\n\t\t\t\tjanus_streaming_file_source *source = mp->source;\n\t\t\t\t/* Add audio line */\n\t\t\t\tjanus_sdp_generate_offer_mline(offer,\n\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\tJANUS_SDP_OA_PT, source->codecs.pt,\n\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(source->codecs.audio_codec),\n\t\t\t\t\tJANUS_SDP_OA_FMTP, source->codecs.fmtp,\n\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_MID, janus_rtp_extension_id(JANUS_RTP_EXTMAP_MID),\n\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_ABS_SEND_TIME, janus_rtp_extension_id(JANUS_RTP_EXTMAP_ABS_SEND_TIME),\n\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME,\n\t\t\t\t\t\t(session->abscapturetime_src_ext_id > 0 ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME) : 0),\n\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_PLAYOUT_DELAY,\n\t\t\t\t\t\t(session->playoutdelay_ext ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_PLAYOUT_DELAY) : 0),\n\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t} else {\n\t\t\t\t/* Iterate on all media streams */\n\t\t\t\tGList *temp = session->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_session_stream *s = (janus_streaming_session_stream *)temp->data;\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = s->stream;\n\t\t\t\t\tint pt = s->pt > 0 ? s->pt : stream->codecs.pt;\n\t\t\t\t\tgboolean add_msid = (stream->msid && stream->mstid);\n\t\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO && audio) {\n\t\t\t\t\t\t/* Add audio line */\n\t\t\t\t\t\tjanus_sdp_generate_offer_mline(offer,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MID, stream->mid,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MSID, add_msid ? stream->msid : NULL, add_msid ? stream->mstid : NULL,\n\t\t\t\t\t\t\tJANUS_SDP_OA_PT, pt,\n\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(stream->codecs.audio_codec),\n\t\t\t\t\t\t\tJANUS_SDP_OA_FMTP, stream->codecs.fmtp,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_MID, janus_rtp_extension_id(JANUS_RTP_EXTMAP_MID),\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_ABS_SEND_TIME, janus_rtp_extension_id(JANUS_RTP_EXTMAP_ABS_SEND_TIME),\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME,\n\t\t\t\t\t\t\t\t(session->abscapturetime_src_ext_id > 0 ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME) : 0),\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_PLAYOUT_DELAY,\n\t\t\t\t\t\t\t\t(session->playoutdelay_ext ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_PLAYOUT_DELAY) : 0),\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_VIDEO && video) {\n\t\t\t\t\t\t/* Add video line */\n\t\t\t\t\t\tjanus_sdp_generate_offer_mline(offer,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_VIDEO,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MID, stream->mid,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MSID, add_msid ? stream->msid : NULL, add_msid ? stream->mstid : NULL,\n\t\t\t\t\t\t\tJANUS_SDP_OA_PT, pt,\n\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_videocodec_name(stream->codecs.video_codec),\n\t\t\t\t\t\t\tJANUS_SDP_OA_FMTP, stream->codecs.fmtp,\n\t\t\t\t\t\t\tJANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS, TRUE,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_MID, janus_rtp_extension_id(JANUS_RTP_EXTMAP_MID),\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_ABS_SEND_TIME, janus_rtp_extension_id(JANUS_RTP_EXTMAP_ABS_SEND_TIME),\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME,\n\t\t\t\t\t\t\t\t(session->abscapturetime_src_ext_id > 0 ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME) : 0),\n\t\t\t\t\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_PLAYOUT_DELAY,\n\t\t\t\t\t\t\t\t(session->playoutdelay_ext ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_PLAYOUT_DELAY) : 0),\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t}\n#ifdef HAVE_SCTP\n\t\t\t\t\telse if(stream->type == JANUS_STREAMING_MEDIA_DATA && data) {\n\t\t\t\t\t\t/* Add data line */\n\t\t\t\t\t\tjanus_sdp_generate_offer_mline(offer,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_APPLICATION,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MID, stream->mid,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t}\n#endif\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t}\n\t\t\tsdp = janus_sdp_write(offer);\n\t\t\tjanus_sdp_destroy(offer);\n\t\t\tJANUS_LOG(LOG_VERB, \"Going to %s this SDP:\\n%s\\n\", sdp_type, sdp);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(do_restart ? \"updating\" : \"preparing\"));\n\t\t\t/* Add the user to the list of watchers and we're done */\n\t\t\tif(g_list_find(mp->viewers, session) == NULL) {\n\t\t\t\tmp->viewers = g_list_append(mp->viewers, session);\n\t\t\t\tif(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t\t\t/* If we're using helper threads, add the viewer to one of those */\n\t\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\t\tint viewers = -1;\n\t\t\t\t\t\tjanus_streaming_helper *helper = NULL;\n\t\t\t\t\t\tGList *l = mp->threads;\n\t\t\t\t\t\twhile(l) {\n\t\t\t\t\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\t\t\t\t\tif(viewers == -1 || (helper == NULL && ht->num_viewers == 0) || ht->num_viewers < viewers) {\n\t\t\t\t\t\t\t\tviewers = ht->num_viewers;\n\t\t\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\t\t\thelper->viewers = g_list_append(helper->viewers, session);\n\t\t\t\t\t\thelper->num_viewers++;\n\t\t\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Added viewer to helper thread #%d (%d viewers)\\n\",\n\t\t\t\t\t\t\thelper->id, helper->num_viewers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t} else if(!strcasecmp(request_text, \"watch\") && jsep_sdp != NULL) {\n\t\t\t/* New subscriber provided an offer, plugin will answer */\n\t\t\tif(sdp_type == NULL || strcasecmp(sdp_type, \"offer\")) {\n\t\t\t\t/* This isn't an offer, respond with an error */\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"User provided SDP for a watch request must be an offer\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"User provided SDP for a watch request must be an offer\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tchar error_str[512];\n\t\t\tjanus_sdp *parsed_sdp = janus_sdp_parse(jsep_sdp, error_str, sizeof(error_str));\n\t\t\tif(parsed_sdp == NULL) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing SDP: %s\\n\", error_str);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing SDP: %s\", error_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* When users provide an offer for a \"watch\", we ignore the media object, as\n\t\t\t * we'll just match offered m-lines with available streams in the mountpoint;\n\t\t\t * that said, we still validate the JSON request as for a generic \"watch\" */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, watch_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!string_ids) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t\tguint64 id_value = 0;\n\t\t\tchar id_num[30], *id_value_str = NULL;\n\t\t\tif(!string_ids) {\n\t\t\t\tid_value = json_integer_value(id);\n\t\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\t\tid_value_str = id_num;\n\t\t\t} else {\n\t\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t\t}\n\t\t\t/* Find the mountpoint and go on */\n\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\t\tif(mp == NULL) {\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&mp->ref);\n\t\t\t/* A secret may be required for this action */\n\t\t\tJANUS_CHECK_SECRET(mp->pin, root, \"pin\", error_code, error_cause,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT, JANUS_STREAMING_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&mp->mutex);\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tif(session->mountpoint) {\n\t\t\t\t/* Already watching something else */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already watching mountpoint %s\\n\", session->mountpoint->id_str);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_STATE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already watching mountpoint %s\", session->mountpoint->id_str);\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(g_list_find(mp->viewers, session) != NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already watching a stream (found %p in %s's viewers)...\\n\", session, id_value_str);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already watching a stream\");\n\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tg_atomic_int_set(&session->stopping, 0);\n\t\t\tsession->mountpoint = mp;\n\t\t\t/* Start preparing an answer */\n\t\t\tjanus_sdp *answer = janus_sdp_generate_answer(parsed_sdp);\n\t\t\t/* Iterate through the m-lines in the offer, and see if we can find a match */\n\t\t\tGList *subscribed = NULL;\n\t\t\tGList *temp = parsed_sdp->m_lines;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\tif(m->direction == JANUS_SDP_INACTIVE || m->direction == JANUS_SDP_SENDONLY) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping (%s) m-line (unsupported media direction)\\n\", janus_sdp_mtype_str(m->type));\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(mp->streaming_source == janus_streaming_source_file) {\n\t\t\t\t\t/* File based streaming, check if we already subscribed to the stream */\n\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\t\tjanus_streaming_file_source *source = mp->source;\n\t\t\t\t\t\tint pt = janus_sdp_get_codec_pt(parsed_sdp, m->index, janus_audiocodec_name(source->codecs.audio_codec));\n\t\t\t\t\t\tif(pt != -1) {\n\t\t\t\t\t\t\t/* Create a session stream */\n\t\t\t\t\t\t\tjanus_streaming_session_stream *s = g_malloc0(sizeof(janus_streaming_session_stream));\n\t\t\t\t\t\t\ts->mindex = -1;\n\t\t\t\t\t\t\ts->send = TRUE;\n\t\t\t\t\t\t\ts->pt = pt;\n\t\t\t\t\t\t\tjanus_rtp_switching_context_reset(&s->context);\n\t\t\t\t\t\t\ts->min_delay = -1;\n\t\t\t\t\t\t\ts->max_delay = -1;\n\t\t\t\t\t\t\tsession->streams = g_list_append(session->streams, s);\n\t\t\t\t\t\t\tif(session->streams_byid == NULL)\n\t\t\t\t\t\t\t\tsession->streams_byid = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\tg_hash_table_insert(session->streams_byid, GINT_TO_POINTER(s->mindex), s);\n\t\t\t\t\t\t\t/* Accept the m-line */\n\t\t\t\t\t\t\tjanus_sdp_generate_answer_mline(parsed_sdp, answer, m,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(source->codecs.audio_codec),\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_FMTP, source->codecs.fmtp,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_ABS_SEND_TIME,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_PLAYOUT_DELAY,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t\t\t/* Done */\n\t\t\t\t\t\t\tsubscribed = g_list_append(subscribed, source);\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Iterate on all media streams, to see if we can find a match */\n\t\t\t\t\tgboolean found = FALSE;\n\t\t\t\t\tjanus_streaming_rtp_source *source = (janus_streaming_rtp_source *)mp->source;\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = NULL;\n\t\t\t\t\tjanus_streaming_session_stream *s = NULL;\n\t\t\t\t\tGList *stemp = source->media;\n\t\t\t\t\twhile(stemp) {\n\t\t\t\t\t\tstream = (janus_streaming_rtp_source_stream *)stemp->data;\n\t\t\t\t\t\t/* Try matching this stream with this m-line */\n\t\t\t\t\t\tif(g_list_find(subscribed, stream)) {\n\t\t\t\t\t\t\t/* Stream already matched to a different m-line */\n\t\t\t\t\t\t\tstemp = stemp->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif((m->type == JANUS_SDP_AUDIO && stream->type != JANUS_STREAMING_MEDIA_AUDIO) ||\n\t\t\t\t\t\t\t\t(m->type == JANUS_SDP_VIDEO && stream->type != JANUS_STREAMING_MEDIA_VIDEO) ||\n\t\t\t\t\t\t\t\t(m->type == JANUS_SDP_APPLICATION && stream->type != JANUS_STREAMING_MEDIA_DATA)) {\n\t\t\t\t\t\t\t/* Stream is not the same type as the m-line, skip to the next */\n\t\t\t\t\t\t\tstemp = stemp->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tint pt = -1;\n\t\t\t\t\t\tconst char *codec = NULL;\n\t\t\t\t\t\tif(stream->type != JANUS_STREAMING_MEDIA_DATA) {\n\t\t\t\t\t\t\tcodec = (stream->type == JANUS_STREAMING_MEDIA_AUDIO ?\n\t\t\t\t\t\t\t\tjanus_audiocodec_name(stream->codecs.audio_codec) : janus_videocodec_name(stream->codecs.video_codec));\n\t\t\t\t\t\t\tpt = janus_sdp_get_codec_pt(parsed_sdp, m->index, codec);\n\t\t\t\t\t\t\tif(pt == -1) {\n\t\t\t\t\t\t\t\t/* This m-line doesn't support this stream's codec, skip to the next stream */\n\t\t\t\t\t\t\t\tstemp = stemp->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Create a new session stream and add a reference to the source stream */\n\t\t\t\t\t\ts = g_malloc0(sizeof(janus_streaming_session_stream));\n\t\t\t\t\t\ts->mindex = m->index;\n\t\t\t\t\t\ts->send = TRUE;\n\t\t\t\t\t\ts->pt = pt;\n\t\t\t\t\t\tjanus_rtp_switching_context_reset(&s->context);\n\t\t\t\t\t\ts->min_delay = -1;\n\t\t\t\t\t\ts->max_delay = -1;\n\t\t\t\t\t\tif(stream && stream->simulcast) {\n\t\t\t\t\t\t\t/* In case this mountpoint is simulcasting, let's aim high by default */\n\t\t\t\t\t\t\tjanus_rtp_switching_context_reset(&s->context);\n\t\t\t\t\t\t\tjanus_rtp_simulcasting_context_reset(&s->sim_context);\n\t\t\t\t\t\t\ts->sim_context.substream_target = 2;\n\t\t\t\t\t\t\ts->sim_context.templayer_target = 2;\n\t\t\t\t\t\t\tjanus_vp8_simulcast_context_reset(&s->vp8_context);\n\t\t\t\t\t\t} else if(stream && stream->svc) {\n\t\t\t\t\t\t\t/* In case this mountpoint is doing VP9-SVC, let's aim high by default */\n\t\t\t\t\t\t\ts->spatial_layer = -1;\n\t\t\t\t\t\t\ts->target_spatial_layer = 2;\t/* FIXME Chrome sends 0, 1 and 2 (if using EnabledByFlag_3SL3TL) */\n\t\t\t\t\t\t\ts->temporal_layer = -1;\n\t\t\t\t\t\t\ts->target_temporal_layer = 2;\t/* FIXME Chrome sends 0, 1 and 2 */\n\t\t\t\t\t\t}\n\t\t\t\t\t\ts->stream = stream;\n\t\t\t\t\t\tjanus_refcount_increase(&stream->ref);\n\t\t\t\t\t\tsession->streams = g_list_append(session->streams, s);\n\t\t\t\t\t\tif(session->streams_byid == NULL)\n\t\t\t\t\t\t\tsession->streams_byid = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\tg_hash_table_insert(session->streams_byid, GINT_TO_POINTER(stream->mindex), s);\n\t\t\t\t\t\t/* If this mountpoint is broadcasting end-to-end encrypted media,\n\t\t\t\t\t\t * add the info to the JSEP offer we'll be sending them */\n\t\t\t\t\t\tsession->e2ee = source->e2ee;\n\t\t\t\t\t\t/* Also check if we have to offer the playout-delay extension */\n\t\t\t\t\t\tsession->playoutdelay_ext = source->playoutdelay_ext;\n\t\t\t\t\t\t/* Also check if we have to offer the abs-capture-time extension */\n\t\t\t\t\t\tsession->abscapturetime_src_ext_id = source->abscapturetime_src_ext_id;\n\t\t\t\t\t\t/* Accept the m-line */\n\t\t\t\t\t\tjanus_sdp_generate_answer_mline(parsed_sdp, answer, m,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, m->type,\n\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, codec,\n\t\t\t\t\t\t\tJANUS_SDP_OA_FMTP, stream->codecs.fmtp,\n\t\t\t\t\t\t\tJANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS, (m->type == JANUS_SDP_VIDEO),\n\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_ABS_SEND_TIME,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_PLAYOUT_DELAY,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t\t/* Done */\n\t\t\t\t\t\tsubscribed = g_list_append(subscribed, stream);\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(found) {\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* If we got here, the m-line was rejected */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping %s m-line (no matching mountpoint stream)\\n\", janus_sdp_mtype_str(m->type));\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tif(subscribed == NULL) {\n\t\t\t\t/* FIXME Ended up not subscribing to any stream? */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Not subscribed to any stream (all m-lines rejected)\\n\");\n\t\t\t} else if(mp->streaming_type == janus_streaming_type_on_demand) {\n\t\t\t\t/* Spawn a thread */\n\t\t\t\tGError *error = NULL;\n\t\t\t\tchar tname[16];\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"mp %s\", mp->id_str);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tjanus_refcount_increase(&mp->ref);\n\t\t\t\tg_thread_try_new(tname, &janus_streaming_ondemand_thread, session, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the on-demand thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Got error %d (%s) trying to launch the on-demand thread\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\tg_error_free(error);\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_list_free(subscribed);\n\t\t\t/* Prepare the response */\n\t\t\tsdp_type = \"answer\";\n\t\t\tsdp = janus_sdp_write(answer);\n\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\tjanus_sdp_destroy(answer);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(\"starting\"));\n\t\t\t/* Add the user to the list of watchers and we're done */\n\t\t\tif(g_list_find(mp->viewers, session) == NULL) {\n\t\t\t\tmp->viewers = g_list_append(mp->viewers, session);\n\t\t\t\tif(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t\t\t/* If we're using helper threads, add the viewer to one of those */\n\t\t\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\t\t\tint viewers = -1;\n\t\t\t\t\t\tjanus_streaming_helper *helper = NULL;\n\t\t\t\t\t\tGList *l = mp->threads;\n\t\t\t\t\t\twhile(l) {\n\t\t\t\t\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\t\t\t\t\tif(viewers == -1 || (helper == NULL && ht->num_viewers == 0) || ht->num_viewers < viewers) {\n\t\t\t\t\t\t\t\tviewers = ht->num_viewers;\n\t\t\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\t\t\thelper->viewers = g_list_append(helper->viewers, session);\n\t\t\t\t\t\thelper->num_viewers++;\n\t\t\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Added viewer to helper thread #%d (%d viewers)\\n\",\n\t\t\t\t\t\t\thelper->id, helper->num_viewers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t} else if(!strcasecmp(request_text, \"start\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tif(session->mountpoint == NULL) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Can't start: no mountpoint set\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't start: no mountpoint set\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Starting the streaming\\n\");\n\t\t\tif(g_atomic_int_get(&session->paused) == 1) {\n\t\t\t\t/* We were paused: reset the sequence number in RTP packets */\n\t\t\t\tif(session->streams != NULL) {\n\t\t\t\t\tGList *temp = session->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_streaming_session_stream *s = (janus_streaming_session_stream *)temp->data;\n\t\t\t\t\t\tif(s != NULL)\n\t\t\t\t\t\t\ts->context.seq_reset = TRUE;\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_atomic_int_set(&session->paused, 0);\n\t\t\tresult = json_object();\n\t\t\t/* We wait for the setup_media event to start: on the other hand, it may have already arrived */\n\t\t\tjson_object_set_new(result, \"status\", json_string(g_atomic_int_get(&session->started) ? \"started\" : \"starting\"));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"status\", json_string(g_atomic_int_get(&session->started) ? \"started\" : \"starting\"));\n\t\t\t\tif(session->mountpoint != NULL)\n\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ?\n\t\t\t\t\t\tjson_string(session->mountpoint->id_str) :json_integer(session->mountpoint->id));\n\t\t\t\tgateway->notify_event(&janus_streaming_plugin, session->handle, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"pause\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tif(session->mountpoint == NULL) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Can't pause: no mountpoint set\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't start: no mountpoint set\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Pausing the streaming\\n\");\n\t\t\tg_atomic_int_set(&session->paused, 1);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(\"pausing\"));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"status\", json_string(\"pausing\"));\n\t\t\t\tif(session->mountpoint != NULL)\n\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ?\n\t\t\t\t\t\tjson_string(session->mountpoint->id_str) : json_integer(session->mountpoint->id));\n\t\t\t\tgateway->notify_event(&janus_streaming_plugin, session->handle, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"configure\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_streaming_mountpoint *mp = session->mountpoint;\n\t\t\tif(mp == NULL) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Can't configure: not on a mountpoint\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't configure: not on a mountpoint\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, configure_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t/* Audio, video and data are deprecated properties */\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\tjson_t *data = json_object_get(root, \"data\");\n\n\t\t\t/* We use an array of streams to state the changes we want to make,\n\t\t\t * were for each stream we specify the 'mid' to impact (e.g., send) */\n\t\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\t\tif(streams == NULL) {\n\t\t\t\t/* No streams object, check if the properties have been\n\t\t\t\t * provided globally, which is how we handled this\n\t\t\t\t * request before: if so, create a new fake streams\n\t\t\t\t * array, and move the parsed options there */\n\t\t\t\tstreams = json_array();\n\t\t\t\tjson_t *stream = json_object();\n\t\t\t\tconst char *mid = json_string_value(json_object_get(root, \"mid\"));\n\t\t\t\tif(mid != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"mid\", json_string(mid));\n\t\t\t\tjson_t *send = json_object_get(root, \"send\");\n\t\t\t\tif(send != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"send\", json_is_true(send) ? json_true() : json_false());\n\t\t\t\tjson_t *spatial = json_object_get(root, \"spatial_layer\");\n\t\t\t\tif(spatial != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"spatial_layer\", json_integer(json_integer_value(spatial)));\n\t\t\t\tjson_t *sc_substream = json_object_get(root, \"substream\");\n\t\t\t\tif(sc_substream != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"substream\", json_integer(json_integer_value(sc_substream)));\n\t\t\t\tjson_t *temporal = json_object_get(root, \"temporal_layer\");\n\t\t\t\tif(temporal != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"temporal_layer\", json_integer(json_integer_value(temporal)));\n\t\t\t\tjson_t *sc_temporal = json_object_get(root, \"temporal\");\n\t\t\t\tif(sc_temporal != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"temporal\", json_integer(json_integer_value(sc_temporal)));\n\t\t\t\tjson_t *sc_fallback = json_object_get(root, \"fallback\");\n\t\t\t\tif(sc_fallback != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"fallback\", json_integer(json_integer_value(sc_fallback)));\n\t\t\t\tjson_t *min_delay = json_object_get(root, \"min_delay\");\n\t\t\t\tif(min_delay != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"min_delay\", json_integer(json_integer_value(min_delay)));\n\t\t\t\tjson_t *max_delay = json_object_get(root, \"max_delay\");\n\t\t\t\tif(max_delay != NULL)\n\t\t\t\t\tjson_object_set_new(stream, \"max_delay\", json_integer(json_integer_value(max_delay)));\n\t\t\t\tjson_array_append_new(streams, stream);\n\t\t\t\tjson_object_set_new(root, \"streams\", streams);\n\t\t\t}\n\n\t\t\tsize_t i = 0;\n\t\t\tsize_t streams_size = json_array_size(streams);\n\t\t\tfor(i=0; i<streams_size; i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, configure_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tbreak;\n\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\tif(mid == NULL && streams_size > 1) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (mid can't be null in a streams array)\\n\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (mid can't be null in a streams array)\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif(mid != NULL) {\n\t\t\t\t\tjson_object_del(root, \"audio\");\n\t\t\t\t\taudio = NULL;\n\t\t\t\t\tjson_object_del(root, \"video\");\n\t\t\t\t\tvideo = NULL;\n\t\t\t\t\tjson_object_del(root, \"data\");\n\t\t\t\t\tdata = NULL;\n\t\t\t\t}\n\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\tjson_t *sc_substream = json_object_get(s, \"substream\");\n\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\tjson_t *sc_temporal = json_object_get(s, \"temporal\");\n\t\t\t\tif(json_integer_value(spatial) < 0 || json_integer_value(spatial) > 2 ||\n\t\t\t\t\t\tjson_integer_value(sc_substream) < 0 || json_integer_value(sc_substream) > 2) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (substream/spatial_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (substream/spatial_layer should be 0, 1 or 2)\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif(json_integer_value(temporal) < 0 || json_integer_value(temporal) > 2 ||\n\t\t\t\t\t\tjson_integer_value(sc_temporal) < 0 || json_integer_value(sc_temporal) > 2) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (temporal/temporal_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (temporal/temporal_layer should be 0, 1 or 2)\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\tgoto error;\n\t\t\t}\n\n\t\t\tif(mp->streaming_source == janus_streaming_source_rtp) {\n\t\t\t\t/* Enforce the requested changes */\n\t\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\t\t/* Get the stream we need to tweak */\n\t\t\t\t\tjson_t *sconf = json_array_get(streams, i);\n\t\t\t\t\t/* Check which properties we need to tweak */\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(sconf, \"mid\"));\n\t\t\t\t\tjson_t *send = json_object_get(sconf, \"send\");\n\t\t\t\t\tGList *temp = session->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_streaming_session_stream *s = (janus_streaming_session_stream *)temp->data;\n\t\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = s->stream;\n\t\t\t\t\t\t/* Check the old and deprecated approach first */\n\t\t\t\t\t\tif(audio && stream->type == JANUS_STREAMING_MEDIA_AUDIO)\n\t\t\t\t\t\t\ts->send = json_is_true(audio);\n\t\t\t\t\t\telse if(video && stream->type == JANUS_STREAMING_MEDIA_VIDEO)\n\t\t\t\t\t\t\ts->send = json_is_true(video);\n\t\t\t\t\t\telse if(data && stream->type == JANUS_STREAMING_MEDIA_DATA)\n\t\t\t\t\t\t\ts->send = json_is_true(data);\n\t\t\t\t\t\t/* Now let's see if this is the right mid */\n\t\t\t\t\t\tif(mid && strcasecmp(stream->mid, mid)) {\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(send)\n\t\t\t\t\t\t\ts->send = json_is_true(send);\n\t\t\t\t\t\tif(stream && stream->simulcast) {\n\t\t\t\t\t\t\t/* Check if the viewer is requesting a different substream/temporal layer */\n\t\t\t\t\t\t\tjson_t *substream = json_object_get(sconf, \"substream\");\n\t\t\t\t\t\t\tif(substream) {\n\t\t\t\t\t\t\t\ts->sim_context.substream_target = json_integer_value(substream);\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video substream to let through (simulcast): %d (was %d)\\n\",\n\t\t\t\t\t\t\t\t\ts->sim_context.substream_target, s->sim_context.substream);\n\t\t\t\t\t\t\t\tif(s->sim_context.substream_target == s->sim_context.substream) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right substream, so notify the viewer */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(result, \"substream\", json_integer(s->sim_context.substream));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t/* Schedule a PLI */\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the simulcast context\\n\");\n\t\t\t\t\t\t\t\t\tg_atomic_int_set(&stream->need_pli, 1);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_t *temporal = json_object_get(sconf, \"temporal\");\n\t\t\t\t\t\t\tif(temporal) {\n\t\t\t\t\t\t\t\ts->sim_context.templayer_target = json_integer_value(temporal);\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video temporal layer to let through (simulcast): %d (was %d)\\n\",\n\t\t\t\t\t\t\t\t\ts->sim_context.templayer_target, s->sim_context.templayer);\n\t\t\t\t\t\t\t\tif(stream->codecs.video_codec == JANUS_VIDEOCODEC_VP8 && s->sim_context.templayer_target == s->sim_context.templayer) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right temporal layer, so notify the viewer */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(result, \"temporal\", json_integer(s->sim_context.templayer));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/* Check if we need to change the fallback timer for the substream */\n\t\t\t\t\t\t\tjson_t *fallback = json_object_get(sconf, \"fallback\");\n\t\t\t\t\t\t\tif(fallback) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting fallback timer (simulcast): %lld (was %\"SCNu32\")\\n\",\n\t\t\t\t\t\t\t\t\tjson_integer_value(fallback) ? json_integer_value(fallback) : 250000,\n\t\t\t\t\t\t\t\t\ts->sim_context.drop_trigger ? s->sim_context.drop_trigger : 250000);\n\t\t\t\t\t\t\t\ts->sim_context.drop_trigger = json_integer_value(fallback);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream && stream->svc) {\n\t\t\t\t\t\t\t/* Check if the viewer is requesting a different SVC spatial/temporal layer */\n\t\t\t\t\t\t\tjson_t *spatial = json_object_get(sconf, \"spatial_layer\");\n\t\t\t\t\t\t\tif(spatial) {\n\t\t\t\t\t\t\t\tint spatial_layer = json_integer_value(spatial);\n\t\t\t\t\t\t\t\tif(spatial_layer > 1) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Spatial layer higher than 1, will probably be ignored\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(spatial_layer == s->spatial_layer) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right spatial layer, so notify the user */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(result, \"spatial_layer\", json_integer(s->spatial_layer));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\t\t\t\tgateway->push_event(msg->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t} else if(spatial_layer != s->target_spatial_layer) {\n\t\t\t\t\t\t\t\t\t/* Send a FIR to the source, if RTCP is enabled */\n\t\t\t\t\t\t\t\t\tg_atomic_int_set(&stream->need_pli, 1);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ts->target_spatial_layer = spatial_layer;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_t *temporal = json_object_get(sconf, \"temporal_layer\");\n\t\t\t\t\t\t\tif(temporal) {\n\t\t\t\t\t\t\t\tint temporal_layer = json_integer_value(temporal);\n\t\t\t\t\t\t\t\tif(temporal_layer > 2) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Temporal layer higher than 2, will probably be ignored\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(temporal_layer == s->temporal_layer) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right temporal layer, so notify the user */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(result, \"temporal_layer\", json_integer(s->temporal_layer));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\t\t\t\tgateway->push_event(msg->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ts->target_temporal_layer = temporal_layer;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream && stream->type == JANUS_STREAMING_MEDIA_VIDEO && session->playoutdelay_ext) {\n\t\t\t\t\t\t\t/* Check if we need to specify a custom playout delay for this stream */\n\t\t\t\t\t\t\tjson_t *min_delay = json_object_get(sconf, \"min_delay\");\n\t\t\t\t\t\t\tif(min_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\ts->min_delay = -1;\n\t\t\t\t\t\t\t\t\ts->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\ts->min_delay = md;\n\t\t\t\t\t\t\t\t\tif(s->min_delay > s->max_delay)\n\t\t\t\t\t\t\t\t\t\ts->max_delay = s->min_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_t *max_delay = json_object_get(sconf, \"max_delay\");\n\t\t\t\t\t\t\tif(max_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\ts->min_delay = -1;\n\t\t\t\t\t\t\t\t\ts->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\ts->max_delay = md;\n\t\t\t\t\t\t\t\t\tif(s->max_delay < s->min_delay)\n\t\t\t\t\t\t\t\t\t\ts->min_delay = s->max_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Done */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"configured\"));\n\t\t} else if(!strcasecmp(request_text, \"switch\")) {\n\t\t\t/* This listener wants to switch to a different mountpoint\n\t\t\t * NOTE: this only works for live RTP streams as of now: you\n\t\t\t * cannot, for instance, switch from a live RTP mountpoint to\n\t\t\t * an on demand one or viceversa (TBD.), Besides, it needs\n\t\t\t * to be mountpoints with the same media in the same order. */\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tjanus_streaming_mountpoint *oldmp = session->mountpoint;\n\t\t\tif(oldmp == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Can't switch: not on a mountpoint\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't switch: not on a mountpoint\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(oldmp->streaming_type != janus_streaming_type_live ||\n\t\t\t\t\toldmp->streaming_source != janus_streaming_source_rtp) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Can't switch: not on a live RTP mountpoint\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_SWITCH;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't switch: not on a live RTP mountpoint\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&oldmp->ref);\n\t\t\tif(!string_ids) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_STREAMING_ERROR_MISSING_ELEMENT, JANUS_STREAMING_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_refcount_decrease(&oldmp->ref);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t\tguint64 id_value = 0;\n\t\t\tchar id_num[30], *id_value_str = NULL;\n\t\t\tif(!string_ids) {\n\t\t\t\tid_value = json_integer_value(id);\n\t\t\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id_value);\n\t\t\t\tid_value_str = id_num;\n\t\t\t} else {\n\t\t\t\tid_value_str = (char *)json_string_value(id);\n\t\t\t}\n\t\t\tjanus_mutex_lock(&mountpoints_mutex);\n\t\t\tjanus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints,\n\t\t\t\tstring_ids ? (gpointer)id_value_str : (gpointer)&id_value);\n\t\t\tif(mp == NULL || g_atomic_int_get(&mp->destroyed)) {\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"No such mountpoint/stream %s\\n\", id_value_str);\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such mountpoint/stream %s\", id_value_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&mp->ref);\n\t\t\tif(mp->streaming_type != janus_streaming_type_live ||\n\t\t\t\t\tmp->streaming_source != janus_streaming_source_rtp) {\n\t\t\t\tjanus_refcount_decrease(&oldmp->ref);\n\t\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Can't switch: target is not a live RTP mountpoint\\n\");\n\t\t\t\terror_code = JANUS_STREAMING_ERROR_CANT_SWITCH;\n\t\t\t\tg_snprintf(error_cause, 512, \"Can't switch: target is not a live RTP mountpoint\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* TODO: compare the streams of the two mountpoints */\n\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"Request to switch to mountpoint/stream %s (old: %s)\\n\", mp->id_str, oldmp->id_str);\n\t\t\tg_atomic_int_set(&session->paused, 1);\n\t\t\t/* Unsubscribe from the previous mountpoint and subscribe to the new one */\n\t\t\tsession->mountpoint = NULL;\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_lock(&oldmp->mutex);\n\t\t\toldmp->viewers = g_list_remove_all(oldmp->viewers, session);\n\t\t\t/* Remove the viewer from the helper threads too, if any */\n\t\t\tif(oldmp->helper_threads > 0) {\n\t\t\t\tGList *l = oldmp->threads;\n\t\t\t\twhile(l) {\n\t\t\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\t\t\tjanus_mutex_lock(&ht->mutex);\n\t\t\t\t\tif(g_list_find(ht->viewers, session) != NULL) {\n\t\t\t\t\t\tht->num_viewers--;\n\t\t\t\t\t\tht->viewers = g_list_remove_all(ht->viewers, session);\n\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Removing viewer from helper thread #%d (switching)\\n\", ht->id);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\tl = l->next;\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&oldmp->ref);\t/* This is for the user going away */\n\t\t\tjanus_mutex_unlock(&oldmp->mutex);\n\t\t\t/* Subscribe to the new one */\n\t\t\tjanus_mutex_lock(&mp->mutex);\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tjanus_refcount_increase(&mp->ref);\n\t\t\tmp->viewers = g_list_append(mp->viewers, session);\n\t\t\t/* If we're using helper threads, add the viewer to one of those */\n\t\t\tif(mp->helper_threads > 0) {\n\t\t\t\tint viewers = -1;\n\t\t\t\tjanus_streaming_helper *helper = NULL;\n\t\t\t\tGList *l = mp->threads;\n\t\t\t\twhile(l) {\n\t\t\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\t\t\tif(viewers == -1 || (helper == NULL && ht->num_viewers == 0) || ht->num_viewers < viewers) {\n\t\t\t\t\t\tviewers = ht->num_viewers;\n\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t}\n\t\t\t\t\tl = l->next;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Adding viewer to helper thread #%d\\n\", helper->id);\n\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\thelper->viewers = g_list_append(helper->viewers, session);\n\t\t\t\thelper->num_viewers++;\n\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t}\n\t\t\tsession->mountpoint = mp;\n\t\t\t/* Send a PLI too, in case the mountpoint supports video and RTCP */\n\t\t\tjanus_streaming_rtp_source *source = mp->source;\n\t\t\tGList *temp = source->media;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\tif(stream && stream->type == JANUS_STREAMING_MEDIA_VIDEO)\n\t\t\t\t\tjanus_streaming_rtcp_pli_send(stream);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tg_atomic_int_set(&session->paused, 0);\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&mp->mutex);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t/* Done with the request, remove the references we took for that */\n\t\t\tjanus_refcount_decrease(&oldmp->ref);\n\t\t\tjanus_refcount_decrease(&mp->ref);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"switched\", json_string(\"ok\"));\n\t\t\tjson_object_set_new(result, \"id\", string_ids ? json_string(id_value_str) : json_integer(id_value));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"status\", json_string(\"switching\"));\n\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(id_value_str) : json_integer(id_value));\n\t\t\t\tgateway->notify_event(&janus_streaming_plugin, session->handle, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"stop\")) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tif(g_atomic_int_get(&session->stopping) || !g_atomic_int_get(&session->started)) {\n\t\t\t\t/* Been there, done that: ignore */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"status\", json_string(\"idle\"));\n\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\tint ret = gateway->push_event(msg->handle, &janus_streaming_plugin, msg->transaction, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(event);\n\t\t\t\tjanus_streaming_message_free(msg);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Stopping the streaming\\n\");\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"status\", json_string(\"stopping\"));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"status\", json_string(\"stopping\"));\n\t\t\t\tjanus_streaming_mountpoint *mp = session->mountpoint;\n\t\t\t\tif(mp)\n\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mp->id_str) : json_integer(mp->id));\n\t\t\t\tgateway->notify_event(&janus_streaming_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\t\tgateway->close_pc(session->handle);\n\t\t} else {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\t\terror_code = JANUS_STREAMING_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Any SDP to handle? */\n\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\tif(msg_sdp) {\n\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well (%s):\\n%s\\n\",\n\t\t\t\tdo_restart ? \"renegotiation occurring\" : \"but we really don't care\", msg_sdp_type, msg_sdp);\n\t\t}\n\t\tg_atomic_int_set(&session->renegotiating, 0);\n\n\t\t/* Prepare JSON event */\n\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", sdp_type, \"sdp\", sdp);\n\t\tif(do_restart)\n\t\t\tjson_object_set_new(jsep, \"restart\", json_true());\n\t\tif(session->e2ee)\n\t\t\tjson_object_set_new(jsep, \"e2ee\", json_true());\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\tif(result != NULL)\n\t\t\tjson_object_set_new(event, \"result\", result);\n\t\tint ret = gateway->push_event(msg->handle, &janus_streaming_plugin, msg->transaction, event, jsep);\n\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\tg_free(sdp);\n\t\tjson_decref(event);\n\t\tjson_decref(jsep);\n\t\tjanus_streaming_message_free(msg);\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_streaming_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_streaming_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving Streaming handler thread\\n\");\n\treturn NULL;\n}\n\n/* Helpers to create a listener filedescriptor */\nstatic int janus_streaming_create_fd(int port, in_addr_t mcast, const janus_network_address *iface, char *host, size_t hostlen,\n\t\tconst char *listenername, const char *medianame, const char *mountpointname, gboolean quiet) {\n\tjanus_mutex_lock(&fd_mutex);\n\tstruct sockaddr_in address = { 0 };\n\tstruct sockaddr_in6 address6 = { 0 };\n\tjanus_network_address_string_buffer address_representation;\n\n\tuint16_t rtp_port_next = rtp_range_slider; \t\t\t\t\t/* Read global slider */\n\tuint16_t rtp_port_start = rtp_port_next;\n\tgboolean use_range = (port == 0), rtp_port_wrap = FALSE;\n\n\tint fd = -1, family = 0;\n\twhile(1) {\n\t\t/* By default, we bind to both IPv4 and IPv6, unless IPv6 is disabled */\n\t\tfamily = ipv6_disabled ? AF_INET : 0;\n\t\tif(use_range && rtp_port_wrap && rtp_port_next >= rtp_port_start) {\n\t\t\t/* Full range scanned */\n\t\t\tJANUS_LOG(LOG_ERR, \"No ports available for RTP/RTCP in range: %u -- %u\\n\",\n\t\t\t\t  rtp_range_min, rtp_range_max);\n\t\t\tbreak;\n\t\t}\n\t\tif(!use_range) {\n\t\t\t/* Use the port specified in the arguments */\n\t\t\tif(IN_MULTICAST(ntohl(mcast))) {\n\t\t\t\tfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\t\tif(fd < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Cannot create socket for %s... %d (%s)\\n\",\n\t\t\t\t\t\tmountpointname, medianame, errno, g_strerror(errno));\n\t\t\t\t\tbreak;\n\t\t\t\t}\n#ifdef IP_MULTICAST_ALL\n\t\t\t\tint mc_all = 0;\n\t\t\t\tif((setsockopt(fd, IPPROTO_IP, IP_MULTICAST_ALL, (void*) &mc_all, sizeof(mc_all))) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] %s listener setsockopt IP_MULTICAST_ALL failed... %d (%s)\\n\",\n\t\t\t\t\t\tmountpointname, listenername, errno, g_strerror(errno));\n\t\t\t\t\tclose(fd);\n\t\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n#endif\n\t\t\t\tstruct ip_mreq mreq;\n\t\t\t\tmemset(&mreq, '\\0', sizeof(mreq));\n\t\t\t\tmreq.imr_multiaddr.s_addr = mcast;\n\t\t\t\tif(!janus_network_address_is_null(iface)) {\n\t\t\t\t\tfamily = AF_INET;\n\t\t\t\t\tif(iface->family == AF_INET) {\n\t\t\t\t\t\tmreq.imr_interface = iface->ipv4;\n\t\t\t\t\t\t(void) janus_network_address_to_string_buffer(iface, &address_representation); /* This is OK: if we get here iface must be non-NULL */\n\t\t\t\t\t\tchar *maddr = inet_ntoa(mreq.imr_multiaddr);\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] %s listener using interface address: %s (%s)\\n\", mountpointname, listenername,\n\t\t\t\t\t\t\tjanus_network_address_string_from_buffer(&address_representation), maddr);\n\t\t\t\t\t\tif(maddr && host && hostlen > 0)\n\t\t\t\t\t\t\tg_strlcpy(host, maddr, hostlen);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] %s listener: invalid multicast address type (only IPv4 multicast is currently supported by this plugin)\\n\", mountpointname, listenername);\n\t\t\t\t\t\tclose(fd);\n\t\t\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\t\t\treturn -1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] No multicast interface for: %s. This may not work as expected if you have multiple network devices (NICs)\\n\", mountpointname, listenername);\n\t\t\t\t}\n\t\t\t\tif(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] %s listener IP_ADD_MEMBERSHIP failed... %d (%s)\\n\",\n\t\t\t\t\t\tmountpointname, listenername, errno, g_strerror(errno));\n\t\t\t\t\tclose(fd);\n\t\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] %s listener IP_ADD_MEMBERSHIP ok\\n\", mountpointname, listenername);\n\t\t\t}\n\t\t} else {\n\t\t\t/* Pick a port in the configured range */\n\t\t\tport = rtp_port_next;\n\t\t\tif((uint32_t)(rtp_port_next) < rtp_range_max) {\n\t\t\t\trtp_port_next++;\n\t\t\t} else {\n\t\t\t\trtp_port_next = rtp_range_min;\n\t\t\t\trtp_port_wrap = TRUE;\n\t\t\t}\n\t\t}\n\t\taddress.sin_family = AF_INET;\n\t\taddress.sin_port = htons(port);\n\t\taddress.sin_addr.s_addr = INADDR_ANY;\n\t\taddress6.sin6_family = AF_INET6;\n\t\taddress6.sin6_port = htons(port);\n\t\taddress6.sin6_addr = in6addr_any;\n\t\t/* If this is multicast, allow a re-use of the same ports (different groups may be used) */\n\t\tif(!use_range && IN_MULTICAST(ntohl(mcast))) {\n\t\t\tint reuse = 1;\n\t\t\tif(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] %s listener setsockopt SO_REUSEADDR failed... %d (%s)\\n\",\n\t\t\t\t\tmountpointname, listenername, errno, g_strerror(errno));\n\t\t\t\tclose(fd);\n\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\t/* TODO IPv6 */\n\t\t\tfamily = AF_INET;\n\t\t\taddress.sin_addr.s_addr = mcast;\n\t\t} else {\n\t\t\tif(!IN_MULTICAST(ntohl(mcast)) && !janus_network_address_is_null(iface)) {\n\t\t\t\tfamily = iface->family;\n\t\t\t\tif(iface->family == AF_INET) {\n\t\t\t\t\taddress.sin_addr = iface->ipv4;\n\t\t\t\t\t(void) janus_network_address_to_string_buffer(iface, &address_representation); /* This is OK: if we get here iface must be non-NULL */\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] %s listener restricted to interface address: %s\\n\",\n\t\t\t\t\t\tmountpointname, listenername, janus_network_address_string_from_buffer(&address_representation));\n\t\t\t\t\tif(host && hostlen > 0)\n\t\t\t\t\t\tg_strlcpy(host, janus_network_address_string_from_buffer(&address_representation), hostlen);\n\t\t\t\t} else if(iface->family == AF_INET6) {\n\t\t\t\t\tif(ipv6_disabled) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't bind to IPv6 address, IPv6 is disabled\\n\", mountpointname);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tmemcpy(&address6.sin6_addr, &iface->ipv6, sizeof(iface->ipv6));\n\t\t\t\t\t(void) janus_network_address_to_string_buffer(iface, &address_representation); /* This is OK: if we get here iface must be non-NULL */\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] %s listener restricted to interface address: %s\\n\",\n\t\t\t\t\t\tmountpointname, listenername, janus_network_address_string_from_buffer(&address_representation));\n\t\t\t\t\tif(host && hostlen > 0)\n\t\t\t\t\t\tg_strlcpy(host, janus_network_address_string_from_buffer(&address_representation), hostlen);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] %s listener: invalid address/restriction type\\n\", mountpointname, listenername);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Bind to the specified port */\n\t\tif(fd == -1) {\n\t\t\tfd = socket(family == AF_INET ? AF_INET : AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\tint v6only = 0;\n\t\t\tif(fd < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Cannot create socket for %s... %d (%s)\\n\",\n\t\t\t\t\tmountpointname, medianame, errno, g_strerror(errno));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(family != AF_INET && setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] setsockopt on socket failed for %s... %d (%s)\\n\",\n\t\t\t\t\tmountpointname, medianame, errno, g_strerror(errno));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tsize_t addrlen = (family == AF_INET ? sizeof(address) : sizeof(address6));\n\t\tif(bind(fd, (family == AF_INET ? (struct sockaddr *)&address : (struct sockaddr *)&address6), addrlen) < 0) {\n\t\t\tclose(fd);\n\t\t\tfd = -1;\n\t\t\tif(!quiet) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Bind failed for %s (port %d)... %d (%s)\\n\",\n\t\t\t\t\tmountpointname, medianame, port, errno, g_strerror(errno));\n\t\t\t}\n\t\t\tif(!use_range)\t/* Asked for a specific port but it's not available, give up */\n\t\t\t\tbreak;\n\t\t} else {\n\t\t\tif(use_range)\n\t\t\t\trtp_range_slider = port;\t/* Update global slider */\n\t\t\tbreak;\n\t\t}\n\t}\n\tjanus_mutex_unlock(&fd_mutex);\n\treturn fd;\n}\n/* Helper to bind RTP/RTCP port pair (for RTSP) */\nstatic int janus_streaming_allocate_port_pair(const char *name, const char *media,\n\t\tin_addr_t mcast, const janus_network_address *iface, multiple_fds *fds, int ports[2]) {\n\t/* Start from the global slider */\n\tuint16_t rtp_port_next = rtp_range_slider;\n\tif(rtp_port_next % 2 != 0)\t/* We want an even port for RTP */\n\t\trtp_port_next++;\n\tuint16_t rtp_port_start = rtp_port_next;\n\tgboolean rtp_port_wrap = FALSE;\n\n\tint rtp_fd = -1, rtcp_fd = -1;\n\twhile(1) {\n\t\tif(rtp_port_wrap && rtp_port_next >= rtp_port_start) {\n\t\t\t/* Full range scanned */\n\t\t\tJANUS_LOG(LOG_ERR, \"No ports available for audio/video channel in range: %u -- %u\\n\",\n\t\t\t\trtp_range_min, rtp_range_max);\n\t\t\tbreak;\n\t\t}\n\t\tint rtp_port = rtp_port_next;\n\t\tint rtcp_port = rtp_port+1;\n\t\tif((uint32_t)(rtp_port_next + 2UL) < rtp_range_max) {\n\t\t\t/* Advance to next pair */\n\t\t\trtp_port_next += 2;\n\t\t} else {\n\t\t\trtp_port_next = rtp_range_min;\n\t\t\trtp_port_wrap = TRUE;\n\t\t}\n\t\trtp_fd = janus_streaming_create_fd(rtp_port, mcast, iface, NULL, 0, media, media, name, TRUE);\n\t\tif(rtp_fd != -1) {\n\t\t\trtcp_fd = janus_streaming_create_fd(rtcp_port, mcast, iface, NULL, 0, media, media, name, TRUE);\n\t\t\tif(rtcp_fd != -1) {\n\t\t\t\t/* Done */\n\t\t\t\tfds->fd = rtp_fd;\n\t\t\t\tfds->rtcp_fd = rtcp_fd;\n\t\t\t\tports[0] = rtp_port;\n\t\t\t\tports[1] = rtcp_port;\n\t\t\t\t/* Update global slider */\n\t\t\t\trtp_range_slider = rtp_port_next;\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t}\n\t\t/* If we got here, something failed: try again */\n\t\tif(rtp_fd != -1)\n\t\t\tclose(rtp_fd);\n\t}\n\treturn -1;\n}\n\n/* Helper to return fd port */\nstatic int janus_streaming_get_fd_port(int fd) {\n\tstruct sockaddr_in6 server = { 0 };\n\tsocklen_t len = sizeof(server);\n\tif(getsockname(fd, (struct sockaddr *)&server, &len) == -1) {\n\t\treturn -1;\n\t}\n\n\treturn ntohs(server.sin6_port);\n}\n\n/* Helpers to destroy a streaming mountpoint source */\nstatic void janus_streaming_rtp_source_stream_free(const janus_refcount *st_ref) {\n\tjanus_streaming_rtp_source_stream *stream = janus_refcount_containerof(st_ref, janus_streaming_rtp_source_stream, ref);\n\t/* This stream can be destroyed, free all the resources */\n\tif(stream->fd[0] > -1)\n\t\tclose(stream->fd[0]);\n\tif(stream->fd[1] > -1)\n\t\tclose(stream->fd[1]);\n\tif(stream->fd[2] > -1)\n\t\tclose(stream->fd[2]);\n\tif(stream->rtcp_fd > -1)\n\t\tclose(stream->rtcp_fd);\n\tg_free(stream->host);\n\tjanus_mutex_lock(&stream->keyframe.mutex);\n\tif(stream->keyframe.latest_keyframe != NULL)\n\t\tg_list_free_full(stream->keyframe.latest_keyframe, (GDestroyNotify)janus_streaming_rtp_relay_packet_free);\n\tjanus_mutex_unlock(&stream->keyframe.mutex);\n\tjanus_mutex_lock(&stream->buffermsg_mutex);\n\tif(stream->last_msg != NULL)\n\t\tjanus_streaming_rtp_relay_packet_free((janus_streaming_rtp_relay_packet *)stream->last_msg);\n\tstream->last_msg = NULL;\n\tjanus_mutex_unlock(&stream->buffermsg_mutex);\n\tg_free(stream->codecs.fmtp);\n\tg_free(stream->h264_spspps);\n\tg_free(stream->mid);\n\tg_free(stream->label);\n\tg_free(stream->msid);\n\tg_free(stream->mstid);\n\tg_free(stream->mcast_str);\n\tg_free(stream->iface_str);\n\tif(stream->rc != NULL) {\n\t\tjanus_recorder_close(stream->rc);\n\t\tjanus_recorder_destroy(stream->rc);\n\t}\n\tg_free(stream);\n}\n\n/* Helpers to destroy a streaming mountpoint. */\nstatic void janus_streaming_rtp_source_free(gpointer data) {\n\tjanus_streaming_rtp_source *source = (janus_streaming_rtp_source *)data;\n\tif(source->pipefd[0] > -1) {\n\t\tclose(source->pipefd[0]);\n\t}\n\tif(source->pipefd[1] > -1) {\n\t\tclose(source->pipefd[1]);\n\t}\n\tif(source->is_srtp) {\n\t\tg_free(source->srtpcrypto);\n\t\tsrtp_dealloc(source->srtp_ctx);\n\t\tg_free(source->srtp_policy.key);\n\t}\n#ifdef HAVE_LIBCURL\n\tjanus_mutex_lock(&source->rtsp_mutex);\n\tif(source->curl) {\n\t\t/* Send an RTSP TEARDOWN */\n\t\tcurl_easy_setopt(source->curl, CURLOPT_RTSP_REQUEST, (long)CURL_RTSPREQ_TEARDOWN);\n\t\tint res = curl_easy_perform(source->curl);\n\t\tif(res != CURLE_OK) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't send TEARDOWN request: %s\\n\", curl_easy_strerror(res));\n\t\t}\n\t\tcurl_easy_cleanup(source->curl);\n\t\tg_free(source->curl_errbuf);\n\t}\n\tjanus_streaming_buffer *curldata = source->curldata;\n\tif(curldata != NULL) {\n\t\tg_free(curldata->buffer);\n\t\tg_free(curldata);\n\t}\n\tg_free(source->rtsp_url);\n\tg_free(source->rtsp_username);\n\tg_free(source->rtsp_password);\n\tg_free(source->rtsp_stream_uri);\n\tg_free(source->rtsp_ahost);\n\tg_free(source->rtsp_vhost);\n\tg_free(source->rtsp_vcodecs.fmtp);\n\tjanus_mutex_unlock(&source->rtsp_mutex);\n\tjanus_mutex_destroy(&source->rtsp_mutex);\n#endif\n\tg_list_free_full(source->media, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\tg_hash_table_unref(source->media_byid);\n\tg_hash_table_unref(source->media_byfd);\n\tjanus_mutex_destroy(&source->rec_mutex);\n\tg_free(source);\n}\n\nstatic void janus_streaming_file_source_free(gpointer data) {\n\tjanus_streaming_file_source *source = (janus_streaming_file_source *)data;\n\tg_free(source->codecs.fmtp);\n\tg_free(source->filename);\n\tg_free(source);\n}\n\n/* Helper to create an RTP live source (e.g., from gstreamer/ffmpeg/vlc/etc.) */\n/* Helpers to create an RTP live source (e.g., from gstreamer/ffmpeg/vlc/etc.) */\njanus_streaming_rtp_source_stream *janus_streaming_create_rtp_source_stream(\n\t\tconst char *name, int mindex, const char *type, const char *mid, const char *label, const char *msid,\n\t\tchar *mcast, char *miface, const janus_network_address *iface,\n\t\tuint16_t port, uint16_t port2, uint16_t port3, gboolean dortcp, uint16_t rtcpport,\n\t\tuint8_t pt, char *codec, char *fmtp, char *sprop,\n\t\tgboolean doskew, uint16_t bufferkf_ms, uint32_t bufferkf_bytes, gboolean simulcast, gboolean svc,\n\t\tgboolean textdata, gboolean buffermsg) {\n\tif(type == NULL || mid == NULL || label == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't add 'rtp' stream, missing media type, mid or label...\\n\", name);\n\t\treturn NULL;\n\t}\n\tjanus_streaming_media mtype = janus_streaming_parse_media(type);\n\tif(mtype == JANUS_STREAMING_MEDIA_NONE) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't add 'rtp' stream, invalid type '%s'...\\n\", name, type);\n\t\treturn NULL;\n\t}\n#ifndef HAVE_SCTP\n\tif(mtype == JANUS_STREAMING_MEDIA_DATA) {\n\t\tJANUS_LOG(LOG_WARN, \"[%s] Mountpoint wants to do datachannel relaying, but datachannels support was not compiled...\\n\", name);\n\t\treturn NULL;\n\t}\n#endif\n\tif(mtype != JANUS_STREAMING_MEDIA_DATA && (codec == NULL)) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't add 'rtp' stream, missing mandatory information for %s...\\n\", name, type);\n\t\treturn NULL;\n\t}\n\t/* Allocate file descriptors (if video, we may be simulcasting) */\n\tint fd[3] = {-1, -1, -1};\n\tint rtcp_fd = -1;\n\tchar host[46];\n\thost[0] = '\\0';\n\tfd[0] = janus_streaming_create_fd(port, mcast ? inet_addr(mcast) : INADDR_ANY, iface,\n\t\thost, sizeof(host), type, type, name, port == 0);\n\tif(fd[0] < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't bind to port %d...\\n\", name, port);\n\t\treturn NULL;\n\t}\n\tport = janus_streaming_get_fd_port(fd[0]);\n\tif(dortcp) {\n\t\trtcp_fd = janus_streaming_create_fd(rtcpport, mcast ? inet_addr(mcast) : INADDR_ANY, iface,\n\t\t\tNULL, 0, type, type, name, rtcpport == 0);\n\t\tif(rtcp_fd < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't bind to port %d for RTCP...\\n\", name, rtcpport);\n\t\t\tif(fd[0] > -1)\n\t\t\t\tclose(fd[0]);\n\t\t\treturn NULL;\n\t\t}\n\t\trtcpport = janus_streaming_get_fd_port(rtcp_fd);\n\t}\n\tif(mtype == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\tif(simulcast) {\n\t\t\tfd[1] = janus_streaming_create_fd(port2, mcast ? inet_addr(mcast) : INADDR_ANY, iface,\n\t\t\t\tNULL, 0, \"Video\", \"video\", name, FALSE);\n\t\t\tif(fd[1] < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't bind to port %d for video (2nd port)...\\n\", name, port2);\n\t\t\t\tif(fd[0] > -1)\n\t\t\t\t\tclose(fd[0]);\n\t\t\t\tif(rtcp_fd > -1)\n\t\t\t\t\tclose(rtcp_fd);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\tport2 = janus_streaming_get_fd_port(fd[1]);\n\t\t\tfd[2] = janus_streaming_create_fd(port3, mcast ? inet_addr(mcast) : INADDR_ANY, iface,\n\t\t\t\tNULL, 0, \"Video\", \"video\", name, FALSE);\n\t\t\tif(fd[2] < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Can't bind to port %d for video (3rd port)...\\n\", name, port3);\n\t\t\t\tif(fd[0] > -1)\n\t\t\t\t\tclose(fd[0]);\n\t\t\t\tif(rtcp_fd > -1)\n\t\t\t\t\tclose(rtcp_fd);\n\t\t\t\tif(fd[1] > -1)\n\t\t\t\t\tclose(fd[1]);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\tport3 = janus_streaming_get_fd_port(fd[2]);\n\t\t}\n\t}\n\t/* Create the stream */\n\tjanus_network_address nil;\n\tjanus_network_address_nullify(&nil);\n\n\tjanus_streaming_rtp_source_stream *stream = g_malloc0(sizeof(janus_streaming_rtp_source_stream));\n\tstream->mindex = mindex;\n\tstream->type = mtype;\n\tstream->mid = g_strdup(mid);\n\tstream->label = g_strdup(label);\n\tif(msid) {\n\t\tchar s_msid[65], s_mstid[65];\n\t\ts_msid[0] = '\\0';\n\t\ts_mstid[0] = '\\0';\n\t\tif(sscanf(msid, \"%64s %64s\", s_msid, s_mstid) != 2) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Invalid msid for stream, ignoring\\n\", name);\n\t\t} else {\n\t\t\tstream->msid = g_strdup(s_msid);\n\t\t\tstream->mstid = g_strdup(s_mstid);\n\t\t}\n\t}\n\tstream->codecs.pt = (mtype != JANUS_STREAMING_MEDIA_DATA ? pt : -1);\n\tstream->codecs.fmtp = fmtp ? g_strdup(fmtp) : NULL;\n\tstream->codecs.audio_codec = JANUS_AUDIOCODEC_NONE;\n\tif(mtype == JANUS_STREAMING_MEDIA_AUDIO)\n\t\tstream->codecs.audio_codec = janus_audiocodec_from_name(codec);\n\tstream->codecs.video_codec = JANUS_VIDEOCODEC_NONE;\n\tif(mtype == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\tstream->codecs.video_codec = janus_videocodec_from_name(codec);\n\t\tif(svc) {\n\t\t\tif(stream->codecs.video_codec == JANUS_VIDEOCODEC_VP9) {\n\t\t\t\tstream->svc = TRUE;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] SVC is only supported, in an experimental way, for VP9-SVC mountpoints: disabling it...\\n\", name);\n\t\t\t}\n\t\t}\n\t\tif(sprop && stream->codecs.video_codec == JANUS_VIDEOCODEC_H264) {\n\t\t\t/* Create a fake RTP packet out of the sprop-parameter-sets */\n\t\t\tint spslen = 0;\n\t\t\tchar *sps = janus_streaming_parse_sprop(sprop, &spslen);\n\t\t\tif(sps == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Error parsing sprop-parameter-sets value, ignoring...\\n\");\n\t\t\t} else {\n\t\t\t\tstream->h264_spspps = sps;\n\t\t\t\tstream->h264_spspps_len = spslen;\n\t\t\t}\n\t\t}\n\t}\n\tstream->simulcast = (mtype == JANUS_STREAMING_MEDIA_VIDEO && simulcast);\n\tstream->mcast = mcast ? inet_addr(mcast) : INADDR_ANY;\n\tstream->mcast_str = mcast ? g_strdup(mcast) : NULL;\n\tif(strlen(host) > 0)\n\t\tstream->host = g_strdup(host);\n\tstream->port[0] = port;\n\tstream->rtcp_port = rtcpport ? rtcpport : -1;\n\tstream->port[1] = stream->simulcast ? port2 : -1;\n\tstream->port[2] = stream->simulcast ? port3 : -1;\n\tstream->iface = !janus_network_address_is_null(iface) ? *iface : nil;\n\tstream->iface_str = miface ? g_strdup(miface) : NULL;\n\tstream->skew = doskew;\n\tstream->rc = NULL;\n\tjanus_rtp_switching_context_reset(&stream->context[0]);\n\tjanus_rtp_switching_context_reset(&stream->context[1]);\n\tjanus_rtp_switching_context_reset(&stream->context[2]);\n\tstream->fd[0] = fd[0];\n\tstream->fd[1] = fd[1];\n\tstream->fd[2] = fd[2];\n\tstream->rtcp_fd = rtcp_fd;\n\tstream->last_received[0] = janus_get_monotonic_time();\n\tstream->last_received[1] = stream->last_received[0];\n\tstream->last_received[2] = stream->last_received[0];\n\tif(mtype == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\tstream->keyframe.enabled = (bufferkf_ms > 0 || bufferkf_bytes > 0);\n\t\tstream->keyframe.bufferkf_ms = bufferkf_ms;\n\t\tstream->keyframe.bufferkf_bytes = bufferkf_bytes;\n\t\tstream->keyframe.latest_keyframe = NULL;\n\t\tstream->keyframe.kf_ssrc = 0;\n\t\tstream->keyframe.kf_ts = 0;\n\t\tstream->keyframe.kf_bytes = 0;\n\t\tstream->keyframe.kf_start = 0;\n\t\tstream->keyframe.first_ts = FALSE;\n\t\tjanus_mutex_init(&stream->keyframe.mutex);\n\t} else if(mtype == JANUS_STREAMING_MEDIA_DATA) {\n\t\tstream->textdata = textdata;\n\t\tstream->buffermsg = buffermsg;\n\t\tstream->last_msg = NULL;\n\t\tjanus_mutex_init(&stream->buffermsg_mutex);\n\t}\n\tjanus_refcount_init(&stream->ref, janus_streaming_rtp_source_stream_free);\n\treturn stream;\n}\n\njanus_streaming_mountpoint *janus_streaming_create_rtp_source(\n\t\tuint64_t id, char *id_str, char *name, char *desc, char *metadata,\n\t\tGList *media, int srtpsuite, char *srtpcrypto, int threads, int rtp_collision,\n\t\tuint16_t bufferkf_ms, uint32_t bufferkf_bytes,\n\t\tgboolean e2ee, gboolean playoutdelay_ext, int abscapturetime_src_ext_id) {\n\tchar id_num[30];\n\tif(!string_ids) {\n\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id);\n\t\tid_str = id_num;\n\t}\n\tchar tempname[255];\n\tif(name == NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Missing name, will generate a random one...\\n\");\n\t\tmemset(tempname, 0, 255);\n\t\tg_snprintf(tempname, 255, \"mp-%s\", id_str);\n\t} else if(atoi(name) != 0) {\n\t\tJANUS_LOG(LOG_VERB, \"Names can't start with a number, prefixing it...\\n\");\n\t\tmemset(tempname, 0, 255);\n\t\tg_snprintf(tempname, 255, \"mp-%s\", name);\n\t\tname = NULL;\n\t}\n\tif(!media || g_list_length(media) == 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtp' mountpoint, no audio, video or data have to be streamed...\\n\");\n\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\treturn NULL;\n\t}\n\t/* Create the mountpoint */\n\tjanus_network_address nil;\n\tjanus_network_address_nullify(&nil);\n\n\tjanus_streaming_mountpoint *live_rtp = g_malloc0(sizeof(janus_streaming_mountpoint));\n\tlive_rtp->id = id;\n\tlive_rtp->id_str = g_strdup(id_str);\n\tlive_rtp->name = g_strdup(name ? name : tempname);\n\tchar *description = NULL;\n\tif(desc != NULL)\n\t\tdescription = g_strdup(desc);\n\telse\n\t\tdescription = g_strdup(name ? name : tempname);\n\tlive_rtp->description = description;\n\tlive_rtp->metadata = (metadata ? g_strdup(metadata) : NULL);\n\tlive_rtp->enabled = TRUE;\n\tlive_rtp->active = FALSE;\n\tlive_rtp->streaming_type = janus_streaming_type_live;\n\tlive_rtp->streaming_source = janus_streaming_source_rtp;\n\tjanus_streaming_rtp_source *live_rtp_source = g_malloc0(sizeof(janus_streaming_rtp_source));\n\t/* First of all, let's check if we need to setup an SRTP mountpoint */\n\tif(srtpsuite > 0 && srtpcrypto != NULL) {\n\t\t/* Base64 decode the crypto string and set it as the SRTP context */\n\t\tgsize len = 0;\n\t\tguchar *decoded = g_base64_decode(srtpcrypto, &len);\n\t\tif(len < SRTP_MASTER_LENGTH) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP crypto (%s)\\n\", srtpcrypto);\n\t\t\tg_free(decoded);\n\t\t\tg_list_free_full(media, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\tg_free(live_rtp_source);\n\t\t\tg_free(live_rtp->name);\n\t\t\tg_free(live_rtp->description);\n\t\t\tg_free(live_rtp->metadata);\n\t\t\tg_free(live_rtp);\n\t\t\treturn NULL;\n\t\t}\n\t\t/* Set SRTP policy */\n\t\tsrtp_policy_t *policy = &live_rtp_source->srtp_policy;\n\t\tsrtp_crypto_policy_set_rtp_default(&(policy->rtp));\n\t\tif(srtpsuite == 32) {\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(policy->rtp));\n\t\t} else if(srtpsuite == 80) {\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtp));\n\t\t}\n\t\tpolicy->ssrc.type = ssrc_any_inbound;\n\t\tpolicy->key = decoded;\n\t\tpolicy->next = NULL;\n\t\t/* Create SRTP context */\n\t\tsrtp_err_status_t res = srtp_create(&live_rtp_source->srtp_ctx, policy);\n\t\tif(res != srtp_err_status_ok) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating forwarder SRTP session: %d (%s)\\n\", res, janus_srtp_error_str(res));\n\t\t\tg_free(decoded);\n\t\t\tg_list_free_full(media, (GDestroyNotify)(janus_streaming_rtp_source_stream_unref));\n\t\t\tg_free(live_rtp_source);\n\t\t\tg_free(live_rtp->name);\n\t\t\tg_free(live_rtp->description);\n\t\t\tg_free(live_rtp->metadata);\n\t\t\tg_free(live_rtp);\n\t\t\treturn NULL;\n\t\t}\n\t\tlive_rtp_source->is_srtp = TRUE;\n\t\tlive_rtp_source->srtpsuite = srtpsuite;\n\t\tlive_rtp_source->srtpcrypto = g_strdup(srtpcrypto);\n\t}\n\tlive_rtp->audio = FALSE;\n\tlive_rtp->video = FALSE;\n\tlive_rtp->data = FALSE;\n\tlive_rtp_source->media = media;\n\tlive_rtp_source->media_byid = g_hash_table_new(NULL, NULL);\n\tlive_rtp_source->media_byfd = g_hash_table_new(NULL, NULL);\n\tGList *temp = media;\n\twhile(temp) {\n\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO)\n\t\t\tlive_rtp->audio = TRUE;\n\t\telse if(stream->type == JANUS_STREAMING_MEDIA_VIDEO)\n\t\t\tlive_rtp->video = TRUE;\n\t\telse if(stream->type == JANUS_STREAMING_MEDIA_DATA)\n\t\t\tlive_rtp->data = TRUE;\n\t\t/* Map this stream by mindex */\n\t\tg_hash_table_insert(live_rtp_source->media_byid, GINT_TO_POINTER(stream->mindex), stream);\n\t\t/* Map this stream by all its file descriptors */\n\t\tif(stream->fd[0] != -1)\n\t\t\tg_hash_table_insert(live_rtp_source->media_byfd, GINT_TO_POINTER(stream->fd[0]), stream);\n\t\tif(stream->fd[1] != -1)\n\t\t\tg_hash_table_insert(live_rtp_source->media_byfd, GINT_TO_POINTER(stream->fd[1]), stream);\n\t\tif(stream->fd[2] != -1)\n\t\t\tg_hash_table_insert(live_rtp_source->media_byfd, GINT_TO_POINTER(stream->fd[2]), stream);\n\t\tif(stream->rtcp_fd != -1)\n\t\t\tg_hash_table_insert(live_rtp_source->media_byfd, GINT_TO_POINTER(stream->rtcp_fd), stream);\n\t\ttemp = temp->next;\n\t}\n\tlive_rtp_source->pipefd[0] = -1;\n\tlive_rtp_source->pipefd[1] = -1;\n\tpipe(live_rtp_source->pipefd);\n\tjanus_mutex_init(&live_rtp_source->rec_mutex);\n\tlive_rtp_source->rtp_collision = rtp_collision;\n\tlive_rtp_source->bufferkf_ms = bufferkf_ms;\n\tlive_rtp_source->bufferkf_bytes = bufferkf_bytes;\n\tlive_rtp_source->e2ee = e2ee;\n\tlive_rtp_source->playoutdelay_ext = playoutdelay_ext;\n\tlive_rtp_source->abscapturetime_src_ext_id = abscapturetime_src_ext_id;\n\tlive_rtp->source = live_rtp_source;\n\tlive_rtp->source_destroy = (GDestroyNotify)janus_streaming_rtp_source_free;\n\tlive_rtp->viewers = NULL;\n\tg_atomic_int_set(&live_rtp->destroyed, 0);\n\tjanus_refcount_init(&live_rtp->ref, janus_streaming_mountpoint_free);\n\tjanus_mutex_init(&live_rtp->mutex);\n\tjanus_mutex_lock(&mountpoints_mutex);\n\tg_hash_table_insert(mountpoints,\n\t\tstring_ids ? (gpointer)g_strdup(live_rtp->id_str) : (gpointer)janus_uint64_dup(live_rtp->id),\n\t\tlive_rtp);\n\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)live_rtp->id_str : (gpointer)&live_rtp->id);\n\t/* If we need helper threads, spawn them now */\n\tGError *error = NULL;\n\tchar tname[16];\n\tif(threads > 0) {\n\t\tint i=0;\n\t\tfor(i=0; i<threads; i++) {\n\t\t\tjanus_streaming_helper *helper = g_malloc0(sizeof(janus_streaming_helper));\n\t\t\thelper->id = i+1;\n\t\t\thelper->mp = live_rtp;\n\t\t\thelper->queued_packets = g_async_queue_new_full((GDestroyNotify)janus_streaming_rtp_relay_packet_free);\n\t\t\tjanus_mutex_init(&helper->mutex);\n\t\t\tjanus_refcount_init(&helper->ref, janus_streaming_helper_free);\n\t\t\tlive_rtp->helper_threads++;\n\t\t\t/* Spawn a thread and add references */\n\t\t\tg_snprintf(tname, sizeof(tname), \"help %u-%\"SCNu64, helper->id, live_rtp->id);\n\t\t\tjanus_refcount_increase(&live_rtp->ref);\n\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\thelper->thread = g_thread_try_new(tname, &janus_streaming_helper_thread, helper, &error);\n\t\t\tif(error != NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the helper thread...\\n\",\n\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\tg_error_free(error);\n\t\t\t\tjanus_refcount_decrease(&live_rtp->ref);\t/* This is for the helper thread */\n\t\t\t\tg_async_queue_unref(helper->queued_packets);\n\t\t\t\tjanus_refcount_decrease(&helper->ref);\n\t\t\t\t/* This extra unref is for the init */\n\t\t\t\tjanus_refcount_decrease(&helper->ref);\n\t\t\t\tjanus_mutex_unlock(&mountpoints_mutex);\n\t\t\t\tjanus_streaming_mountpoint_destroy(live_rtp);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\tlive_rtp->threads = g_list_append(live_rtp->threads, helper);\n\t\t}\n\t}\n\tjanus_mutex_unlock(&mountpoints_mutex);\n\t/* Finally, create the mountpoint thread itself */\n\tg_snprintf(tname, sizeof(tname), \"mp %s\", live_rtp->id_str);\n\tjanus_refcount_increase(&live_rtp->ref);\n\tlive_rtp->thread = g_thread_try_new(tname, &janus_streaming_relay_thread, live_rtp, &error);\n\tif(error != NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the RTP thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_refcount_decrease(&live_rtp->ref);\t/* This is for the failed thread */\n\t\tjanus_streaming_mountpoint_destroy(live_rtp);\n\t\treturn NULL;\n\t}\n\treturn live_rtp;\n}\n\n/* Helper to create a file/ondemand live source */\njanus_streaming_mountpoint *janus_streaming_create_file_source(\n\t\tuint64_t id, char *id_str, char *name, char *desc, char *metadata, char *filename, gboolean live,\n\t\tgboolean doaudio, uint8_t apt, char *acodec, char *afmtp, gboolean dovideo) {\n\tchar id_num[30];\n\tif(!string_ids) {\n\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id);\n\t\tid_str = id_num;\n\t}\n\tif(filename == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'live' stream, missing filename...\\n\");\n\t\treturn NULL;\n\t}\n\tif(name == NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Missing name, will generate a random one...\\n\");\n\t}\n\tif(!doaudio && !dovideo) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'file' stream, no audio or video have to be streamed...\\n\");\n\t\treturn NULL;\n\t}\n\t/* FIXME We don't support video streaming from file yet */\n\tif(!doaudio || dovideo) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'file' stream, we only support audio file streaming right now...\\n\");\n\t\treturn NULL;\n\t}\n\t/* TODO We should support something more than raw a-Law and mu-Law streams... */\n#ifdef HAVE_LIBOGG\n\tif(!strstr(filename, \".opus\") && !strstr(filename, \".alaw\") && !strstr(filename, \".mulaw\")) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'file' stream, unsupported format (we only support Opus and raw mu-Law/a-Law files right now)\\n\");\n#else\n\tif(!strstr(filename, \".alaw\") && !strstr(filename, \".mulaw\")) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'file' stream, unsupported format (we only support raw mu-Law and a-Law files right now)\\n\");\n#endif\n\t\treturn NULL;\n\t}\n\tjanus_audiocodec audio_codec = janus_audiocodec_from_name(acodec);\n#ifdef HAVE_LIBOGG\n\tif(strstr(filename, \".opus\") && audio_codec != JANUS_AUDIOCODEC_OPUS) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'file' stream, opus file is not associated with an opus rtpmap\\n\");\n\t\treturn NULL;\n\t}\n#endif\n\tjanus_streaming_mountpoint *file_source = g_malloc0(sizeof(janus_streaming_mountpoint));\n\tfile_source->id = id;\n\tfile_source->id_str = g_strdup(id_str);\n\tchar tempname[255];\n\tif(!name) {\n\t\tmemset(tempname, 0, 255);\n\t\tg_snprintf(tempname, 255, \"mp-%s\", file_source->id_str);\n\t} else if(atoi(name) != 0) {\n\t\tmemset(tempname, 0, 255);\n\t\tg_snprintf(tempname, 255, \"mp-%s\", name);\n\t\tname = NULL;\n\t}\n\tfile_source->name = g_strdup(name ? name : tempname);\n\tchar *description = NULL;\n\tif(desc != NULL)\n\t\tdescription = g_strdup(desc);\n\telse\n\t\tdescription = g_strdup(name ? name : tempname);\n\tfile_source->description = description;\n\tfile_source->metadata = (metadata ? g_strdup(metadata) : NULL);\n\tfile_source->enabled = TRUE;\n\tfile_source->active = FALSE;\n\tfile_source->audio = TRUE;\n\tfile_source->video = FALSE;\n\tfile_source->data = FALSE;\n\tfile_source->streaming_type = live ? janus_streaming_type_live : janus_streaming_type_on_demand;\n\tfile_source->streaming_source = janus_streaming_source_file;\n\tjanus_streaming_file_source *file_source_source = g_malloc0(sizeof(janus_streaming_file_source));\n\tfile_source_source->filename = g_strdup(filename);\n\tfile_source->source = file_source_source;\n\tfile_source->source_destroy = (GDestroyNotify)janus_streaming_file_source_free;\n\tif(strstr(filename, \".opus\")) {\n\t\tfile_source_source->opus = TRUE;\n\t\tfile_source_source->codecs.pt = apt;\n\t\tfile_source_source->codecs.audio_codec = JANUS_AUDIOCODEC_OPUS;\n\t\tfile_source_source->codecs.fmtp = afmtp ? g_strdup(afmtp) : NULL;\n\t} else {\n\t\tfile_source_source->codecs.pt = strstr(filename, \".alaw\") ? 8 : 0;\n\t\tfile_source_source->codecs.audio_codec = strstr(filename, \".alaw\") ? JANUS_AUDIOCODEC_PCMA : JANUS_AUDIOCODEC_PCMU;\n\t}\n\tfile_source->viewers = NULL;\n\tg_atomic_int_set(&file_source->destroyed, 0);\n\tjanus_refcount_init(&file_source->ref, janus_streaming_mountpoint_free);\n\tjanus_mutex_init(&file_source->mutex);\n\tjanus_mutex_lock(&mountpoints_mutex);\n\tg_hash_table_insert(mountpoints,\n\t\tstring_ids ? (gpointer)g_strdup(file_source->id_str) : (gpointer)janus_uint64_dup(file_source->id),\n\t\tfile_source);\n\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)file_source->id_str : (gpointer)&file_source->id);\n\tjanus_mutex_unlock(&mountpoints_mutex);\n\tif(live) {\n\t\tGError *error = NULL;\n\t\tchar tname[16];\n\t\tg_snprintf(tname, sizeof(tname), \"mp %s\", file_source->id_str);\n\t\tjanus_refcount_increase(&file_source->ref);\n\t\tfile_source->thread = g_thread_try_new(tname, &janus_streaming_filesource_thread, file_source, &error);\n\t\tif(error != NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the live filesource thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tjanus_refcount_decrease(&file_source->ref);\t\t/* This is for the failed thread */\n\t\t\tjanus_refcount_decrease(&file_source->ref);\n\t\t\treturn NULL;\n\t\t}\n\t}\n\treturn file_source;\n}\n\n#ifdef HAVE_LIBCURL\nstatic size_t janus_streaming_rtsp_curl_callback(void *payload, size_t size, size_t nmemb, void *data) {\n\tsize_t realsize = size * nmemb;\n\tjanus_streaming_buffer *buf = (struct janus_streaming_buffer *)data;\n\t/* (Re)allocate if needed */\n\tbuf->buffer = realloc(buf->buffer, buf->size+realsize+1);\n\t/* Update the buffer */\n\tmemcpy(&(buf->buffer[buf->size]), payload, realsize);\n\tbuf->size += realsize;\n\tbuf->buffer[buf->size] = 0;\n\t/* Done! */\n\treturn realsize;\n}\n\nstatic int janus_streaming_rtsp_parse_sdp(const char *buffer, const char *name, const char *media,\n\t\tchar *base, int *pt, char *transport, char *host,\n\t\tchar *rtpmap, char *fmtp, char *control, char **sps, int *spslen,\n\t\tconst janus_network_address *iface, multiple_fds *fds) {\n\t/* Start by checking if there's any Content-Base header we should be aware of */\n\tconst char *cb = strstr(buffer, \"Content-Base:\");\n\tif(cb == NULL)\n\t\tcb = strstr(buffer, \"content-base:\");\n\tif(cb != NULL) {\n\t\tcb = strstr(cb, \"rtsp://\");\n\t\tconst char *crlf = (cb ? strstr(cb, \"\\r\\n\") : NULL);\n\t\tif(crlf != NULL && base != NULL) {\n\t\t\tgulong size = (crlf-cb)+1;\n\t\t\tif(size > 256)\n\t\t\t\tsize = 256;\n\t\t\tg_snprintf(base, size, \"%s\", cb);\n\t\t\tif(base[size-2] == '/')\n\t\t\t\tbase[size-2] = '\\0';\n\t\t}\n\t}\n\t/* Parse the SDP now */\n\tchar pattern[256];\n\tg_snprintf(pattern, sizeof(pattern), \"m=%s\", media);\n\tchar *m = strstr(buffer, pattern);\n\tif(m == NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"[%s] no media %s...\\n\", name, media);\n\t\treturn -1;\n\t}\n\tsscanf(m, \"m=%*s %*d %*s %d\", pt);\n\tchar *s = strstr(m, \"a=control:\");\n\tif(s == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] no control for %s...\\n\", name, media);\n\t\treturn -1;\n\t}\n\tsscanf(s, \"a=control:%2047s\", control);\n\tchar *r = strstr(m, \"a=rtpmap:\");\n\tif(r != NULL) {\n\t\tif(sscanf(r, \"a=rtpmap:%*d%*[ ]%2047[^\\r\\n]s\", rtpmap) != 1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] cannot parse %s rtpmap...\\n\", name, media);\n\t\t\treturn -1;\n\t\t}\n\t}\n\tchar *f = strstr(m, \"a=fmtp:\");\n\tif(f != NULL) {\n\t\tif(sscanf(f, \"a=fmtp:%*d%*[ ]%2047[^\\r\\n]s\", fmtp) != 1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] cannot parse %s fmtp...\\n\", name, media);\n\t\t\treturn -1;\n\t\t}\n\t\tchar *start = strstr(f, \"sprop-parameter-sets=\");\n\t\tif(sps != NULL && start != NULL) {\n\t\t\tstart += strlen(\"sprop-parameter-sets=\");\n\t\t\tchar *end = strstr(start, \";\");\n\t\t\tif(end == NULL)\n\t\t\t\tend = strstr(start, \"\\r\");\n\t\t\tif(end == NULL)\n\t\t\t\tend = strstr(start, \"\\n\");\n\t\t\tif(end) {\n\t\t\t\tchar c = *end;\n\t\t\t\t*end = '\\0';\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%s] Found sprop-parameter-sets: %s\\n\", name, start);\n\t\t\t\t*sps = janus_streaming_parse_sprop(start, spslen);\n\t\t\t\tif(*sps == NULL)\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Error parsing sprop-parameter-sets value, ignoring...\\n\", name);\n\t\t\t\t*end = c;\n\t\t\t}\n\t\t}\n\t}\n\tchar *c = strstr(m, \"c=IN IP4\");\n\tif(c == NULL) {\n\t\t/* No m-line c= attribute? try in the whole SDP */\n\t\tc = strstr(buffer, \"c=IN IP4\");\n\t}\n\tchar ip[256];\n\tin_addr_t mcast = INADDR_ANY;\n\tif(c != NULL) {\n\t\tif(sscanf(c, \"c=IN IP4 %255[^/]\", ip) != 0) {\n\t\t\tmemcpy(host, ip, sizeof(ip));\n\t\t\tc = strstr(host, \"\\r\\n\");\n\t\t\tif(c)\n\t\t\t\t*c = '\\0';\n\t\t\tmcast = inet_addr(ip);\n\t\t}\n\t}\n\t/* Bind two adjacent ports for RTP and RTCP */\n\tint ports[2];\n\tif(janus_streaming_allocate_port_pair(name, media, mcast, iface, fds, ports)) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Bind failed for %s...\\n\", name, media);\n\t\treturn -1;\n\t}\n\n\tif(IN_MULTICAST(ntohl(mcast))) {\n\t\tg_snprintf(transport, 1024, \"RTP/AVP/UDP;multicast;client_port=%d-%d\", ports[0], ports[1]);\n\t} else {\n\t\tg_snprintf(transport, 1024, \"RTP/AVP/UDP;unicast;client_port=%d-%d\", ports[0], ports[1]);\n\t}\n\n\treturn 0;\n}\n\n/* Helper function to calculating the minimum value if 'a' is bigger than zero */\nstatic inline gint64 janus_streaming_min_if(gint64 a, gint64 b) {\n\treturn a > 0 ? (a > b ? b : a) : b;\n}\n\n/* Static helper to connect to an RTSP server, considering we might do this either\n * when creating a new mountpoint, or when reconnecting after some failure */\nstatic int janus_streaming_rtsp_connect_to_server(janus_streaming_mountpoint *mp) {\n\tif(mp == NULL)\n\t\treturn -1;\n\tjanus_streaming_rtp_source *source = (janus_streaming_rtp_source *)mp->source;\n\tif(source == NULL)\n\t\treturn -1;\n\n\tchar *name = mp->name;\n\tgboolean doaudio = mp->audio;\n\tgboolean dovideo = mp->video;\n\n\tCURL *curl = curl_easy_init();\n\tif(curl == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't init CURL\\n\");\n\t\treturn -1;\n\t}\n\tif(janus_log_level > LOG_INFO)\n\t\tcurl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);\n\tcurl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);\n\tcurl_easy_setopt(curl, CURLOPT_URL, source->rtsp_url);\n\tcurl_easy_setopt(curl, CURLOPT_TIMEOUT, source->rtsp_timeout);\n\tcurl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, source->rtsp_conn_timeout);\n\tcurl_easy_setopt(curl, CURLOPT_NOSIGNAL, 0L);\n\tcurl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);\n#if CURL_AT_LEAST_VERSION(7, 66, 0)\n#if CURL_AT_LEAST_VERSION(7, 85, 0)\n\tcurl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, \"rtsp\");\n#else\n\tcurl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_RTSP);\n#endif\n\tcurl_easy_setopt(curl, CURLOPT_HTTP09_ALLOWED, 1L);\n#endif\n\tchar *curl_errbuf = g_malloc(CURL_ERROR_SIZE);\n\t*curl_errbuf = '\\0';\n\tcurl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);\n\t/* Any authentication to take into account? */\n\tif(source->rtsp_username && source->rtsp_password) {\n\t\t/* Point out that digest authentication is only available is libcurl >= 7.45.0 */\n\t\tif(LIBCURL_VERSION_NUM < 0x072d00) {\n\t\t\tJANUS_LOG(LOG_WARN, \"RTSP digest authentication unsupported (needs libcurl >= 7.45.0)\\n\");\n\t\t}\n\t\tcurl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);\n\t\tcurl_easy_setopt(curl, CURLOPT_USERNAME, source->rtsp_username);\n\t\tcurl_easy_setopt(curl, CURLOPT_PASSWORD, source->rtsp_password);\n\t}\n\t/* Send an RTSP DESCRIBE */\n\tjanus_streaming_buffer *curldata = g_malloc(sizeof(janus_streaming_buffer));\n\tcurldata->buffer = g_malloc0(1);\n\tcurldata->size = 0;\n\tcurl_easy_setopt(curl, CURLOPT_RTSP_STREAM_URI, source->rtsp_url);\n\tcurl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, (long)CURL_RTSPREQ_DESCRIBE);\n\tcurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, janus_streaming_rtsp_curl_callback);\n\tcurl_easy_setopt(curl, CURLOPT_WRITEDATA, curldata);\n\tcurl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, janus_streaming_rtsp_curl_callback);\n\tcurl_easy_setopt(curl, CURLOPT_HEADERDATA, curldata);\n\tint res = curl_easy_perform(curl);\n\tif(res != CURLE_OK) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't send DESCRIBE request: %s (%s)\\n\",\n\t\t\tcurl_easy_strerror(res), curl_errbuf);\n\t\tcurl_easy_cleanup(curl);\n\t\tg_free(curl_errbuf);\n\t\tg_free(curldata->buffer);\n\t\tg_free(curldata);\n\t\treturn -2;\n\t}\n\tlong code = 0;\n\tres = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);\n#if CURL_AT_LEAST_VERSION(7, 62, 0)\n\tif(source->rtsp_quirk && code == 404) {\n\t\t/* Possibly a quirk in the RTSP server, where the DESCRIBE request expects a path only. */\n\t\tCURLU *curl_u = curl_url();\n\t\tchar *path = NULL;\n\t\tif(curl_u && !(curl_url_set(curl_u, CURLUPART_URL, source->rtsp_url, 0))) {\n\t\t\tif(!(curl_url_get(curl_u, CURLUPART_PATH, &path, 0))) {\n\t\t\t\tcurl_easy_setopt(curl, CURLOPT_RTSP_STREAM_URI, path);\n\t\t\t\tres = curl_easy_perform(curl);\n\t\t\t\tif(res == CURLE_OK) {\n\t\t\t\t\tres = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);\n\t\t\t\t\tif((res == CURLE_OK) && (code != 404)) {\n\t\t\t\t\t\tsource->rtsp_stream_uri = g_strdup(path);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurl_free(path);\n\t\t\t}\n\t\t}\n\t\tcurl_url_cleanup(curl_u);\n\t}\n#endif\n\tif(res != CURLE_OK) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't get DESCRIBE answer: %s (%s)\\n\",\n\t\t\tcurl_easy_strerror(res), curl_errbuf);\n\t\tcurl_easy_cleanup(curl);\n\t\tg_free(curl_errbuf);\n\t\tg_free(curldata->buffer);\n\t\tg_free(curldata);\n\t\treturn -3;\n\t} else if(code != 200) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't get DESCRIBE code: %ld\\n\", code);\n\t\tcurl_easy_cleanup(curl);\n\t\tg_free(curl_errbuf);\n\t\tg_free(curldata->buffer);\n\t\tg_free(curldata);\n\t\treturn -4;\n\t}\n\tJANUS_LOG(LOG_VERB, \"DESCRIBE answer:%s\\n\", curldata->buffer);\n\t/* Parse the SDP we just got to figure out the negotiated media */\n\tint vpt = -1;\n\tchar vrtpmap[2048];\n\tvrtpmap[0] = '\\0';\n\tchar vfmtp[2048];\n\tvfmtp[0] = '\\0';\n\tchar vcontrol[2048];\n\tchar uri[1024];\n\tchar vtransport[1024];\n\tchar vhost[256];\n\tvhost[0] = '\\0';\n\tchar vbase[256];\n\tvbase[0] = '\\0';\n\tint vsport = 0, vsport_rtcp = 0;\n\tmultiple_fds video_fds = {-1, -1};\n\n\tint apt = -1;\n\tchar artpmap[2048];\n\tartpmap[0] = '\\0';\n\tchar afmtp[2048];\n\tafmtp[0] = '\\0';\n\tchar acontrol[2048];\n\tchar atransport[1024];\n\tchar ahost[256];\n\tahost[0] = '\\0';\n\tchar abase[256];\n\tabase[0] = '\\0';\n\tint asport = 0, asport_rtcp = 0;\n\tmultiple_fds audio_fds = {-1, -1};\n\n\twhile (!janus_mutex_trylock(&mountpoints_mutex)) {\n\t\tif(g_atomic_int_get(&mp->destroyed)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Destroying mountpoint while trying to reconnect, aborting\\n\", mp->name);\n\t\t\tcurl_easy_cleanup(curl);\n\t\t\tg_free(curl_errbuf);\n\t\t\tg_free(curldata->buffer);\n\t\t\tg_free(curldata);\n\t\t\treturn -8;\n\t\t}\n\n\t\tg_usleep(1000);\n\t}\n\n\t/* Parse both video and audio first before proceed to setup as curldata will be reused */\n\tjanus_network_address audio_iface = { 0 }, video_iface = { 0 };\n\tuint32_t audio_ssrc = 0, video_ssrc = 0;\n\tint spslen = 0;\n\tchar *sps = NULL;\n\tint vresult = -1;\n\tif(dovideo) {\n\t\tvresult = janus_streaming_rtsp_parse_sdp(curldata->buffer, name, \"video\", vbase, &vpt,\n\t\t\tvtransport, vhost, vrtpmap, vfmtp, vcontrol, &sps, &spslen, &video_iface, &video_fds);\n\t}\n\tint aresult = -1;\n\tif(doaudio) {\n\t\taresult = janus_streaming_rtsp_parse_sdp(curldata->buffer, name, \"audio\", abase, &apt,\n\t\t\tatransport, ahost, artpmap, afmtp, acontrol, NULL, NULL, &audio_iface, &audio_fds);\n\t}\n\tjanus_mutex_unlock(&mountpoints_mutex);\n\n\tif(vresult == -1 && aresult == -1) {\n\t\t/* Both audio and video failed? Give up... */\n\t\tcurl_easy_cleanup(curl);\n\t\tg_free(curl_errbuf);\n\t\tg_free(curldata->buffer);\n\t\tg_free(curldata);\n\t\treturn -7;\n\t}\n\n\t/* Check if a query string is part of the URL, as that may impact the SETUP request */\n\tchar *rtsp_url = source->rtsp_url, *rtsp_querystring = NULL;\n\tchar **parts = g_strsplit(source->rtsp_url, \"?\", 2);\n\tif(parts[0] != NULL) {\n\t\trtsp_url = parts[0];\n\t\trtsp_querystring = parts[1];\n\t}\n\n\tjanus_videocodec video_codec = JANUS_VIDEOCODEC_NONE;\n\tif(vresult != -1) {\n\t\t/* Identify video codec (useful for keyframe detection) */\n\t\tvideo_codec = janus_videocodec_from_name(janus_sdp_get_rtpmap_codec(vrtpmap));\n\t\t/* Send an RTSP SETUP for video */\n\t\tg_free(curldata->buffer);\n\t\tcurldata->buffer = g_malloc0(1);\n\t\tcurldata->size = 0;\n\t\tgboolean add_qs = (rtsp_querystring != NULL);\n\t\tif(add_qs && strstr(vcontrol, rtsp_querystring) != NULL)\n\t\t\tadd_qs = FALSE;\n\t\tif(strstr(vcontrol, (strlen(vbase) > 0 ? vbase : rtsp_url)) == vcontrol) {\n\t\t\t/* The control attribute already contains the whole URL? */\n\t\t\tg_snprintf(uri, sizeof(uri), \"%s%s%s\", vcontrol,\n\t\t\t\tadd_qs ? \"?\" : \"\", add_qs ? rtsp_querystring : \"\");\n\t\t} else {\n\t\t\t/* Append the control attribute to the URL */\n\t\t\tg_snprintf(uri, sizeof(uri), \"%s/%s%s%s\", (strlen(vbase) > 0 ? vbase : rtsp_url),\n\t\t\t\tvcontrol, add_qs ? \"?\" : \"\", add_qs ? rtsp_querystring : \"\");\n\t\t}\n\t\tcurl_easy_setopt(curl, CURLOPT_RTSP_STREAM_URI, uri);\n\t\tcurl_easy_setopt(curl, CURLOPT_RTSP_TRANSPORT, vtransport);\n\t\tcurl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, (long)CURL_RTSPREQ_SETUP);\n\t\tres = curl_easy_perform(curl);\n\t\tif(res != CURLE_OK) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't send SETUP request: %s (%s)\\n\",\n\t\t\t\tcurl_easy_strerror(res), curl_errbuf);\n\t\t\tg_strfreev(parts);\n\t\t\tcurl_easy_cleanup(curl);\n\t\t\tg_free(curl_errbuf);\n\t\t\tg_free(curldata->buffer);\n\t\t\tg_free(curldata);\n\t\t\tif(video_fds.fd != -1) close(video_fds.fd);\n\t\t\tif(video_fds.rtcp_fd != -1) close(video_fds.rtcp_fd);\n\t\t\tif(audio_fds.fd != -1) close(audio_fds.fd);\n\t\t\tif(audio_fds.rtcp_fd != -1) close(audio_fds.rtcp_fd);\n\t\t\treturn -5;\n\t\t}\n\t\tres = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);\n\t\tif(code != 200) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't SETUP, got error code: %ld\\n\", code);\n\t\t\tg_strfreev(parts);\n\t\t\tcurl_easy_cleanup(curl);\n\t\t\tg_free(curl_errbuf);\n\t\t\tg_free(curldata->buffer);\n\t\t\tg_free(curldata);\n\t\t\tif(video_fds.fd != -1) close(video_fds.fd);\n\t\t\tif(video_fds.rtcp_fd != -1) close(video_fds.rtcp_fd);\n\t\t\tif(audio_fds.fd != -1) close(audio_fds.fd);\n\t\t\tif(audio_fds.rtcp_fd != -1) close(audio_fds.rtcp_fd);\n\t\t\treturn -5;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"SETUP answer:%s\\n\", curldata->buffer);\n\t\t/* Parse the RTSP message: we may need Transport and Session */\n\t\tgboolean success = TRUE;\n\t\tgchar **parts = g_strsplit(curldata->buffer, \"\\n\", -1);\n\t\tif(parts) {\n\t\t\tint index = 0;\n\t\t\tchar *line = NULL, *cr = NULL;\n\t\t\twhile(success && (line = parts[index]) != NULL) {\n\t\t\t\tcr = strchr(line, '\\r');\n\t\t\t\tif(cr != NULL)\n\t\t\t\t\t*cr = '\\0';\n\t\t\t\tif(*line == '\\0') {\n\t\t\t\t\tif(cr != NULL)\n\t\t\t\t\t\t*cr = '\\r';\n\t\t\t\t\tindex++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(strnlen(line, 3) < 3) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid RTSP line (%zu bytes): %s\\n\", strlen(line), line);\n\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* Check if this is a Transport or Session header, and if so parse it */\n\t\t\t\tgboolean is_transport = (strstr(line, \"Transport:\") == line || strstr(line, \"transport:\") == line);\n\t\t\t\tgboolean is_session = (strstr(line, \"Session:\") == line || strstr(line, \"session:\") == line);\n\t\t\t\tif(is_transport || is_session) {\n\t\t\t\t\t/* There is, iterate on all params */\n\t\t\t\t\tchar *p = line, param[100], *pi = NULL;\n\t\t\t\t\tint read = 0;\n\t\t\t\t\tgboolean first = TRUE;\n\t\t\t\t\twhile(sscanf(p, \"%99[^;]%n\", param, &read) == 1) {\n\t\t\t\t\t\tif(first) {\n\t\t\t\t\t\t\t/* Skip */\n\t\t\t\t\t\t\tfirst = FALSE;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tpi = param;\n\t\t\t\t\t\t\twhile(*pi == ' ')\n\t\t\t\t\t\t\t\tpi++;\n\t\t\t\t\t\t\tchar name[50], value[50];\n\t\t\t\t\t\t\tif(sscanf(pi, \"%49[a-zA-Z_0-9]=%49s\", name, value) == 2) {\n\t\t\t\t\t\t\t\tif(is_transport) {\n\t\t\t\t\t\t\t\t\tif(!strcasecmp(name, \"ssrc\")) {\n\t\t\t\t\t\t\t\t\t\t/* Take note of the video SSRC */\n\t\t\t\t\t\t\t\t\t\tvideo_ssrc = strtol(value, NULL, 16);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- SSRC (video): %\"SCNu32\"\\n\", video_ssrc);\n\t\t\t\t\t\t\t\t\t} else if(!strcasecmp(name, \"source\")) {\n\t\t\t\t\t\t\t\t\t\t/* If we got an address via c-line, replace it */\n\t\t\t\t\t\t\t\t\t\tg_snprintf(vhost, sizeof(vhost), \"%s\", value);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Source (video): %s\\n\", vhost);\n\t\t\t\t\t\t\t\t\t} else if(!strcasecmp(name, \"server_port\")) {\n\t\t\t\t\t\t\t\t\t\t/* Take note of the server port */\n\t\t\t\t\t\t\t\t\t\tchar *dash = NULL;\n\t\t\t\t\t\t\t\t\t\tvsport = strtol(value, &dash, 10);\n\t\t\t\t\t\t\t\t\t\tvsport_rtcp = dash ? strtol(++dash, NULL, 10) : 0;\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- RTP port (video): %d\\n\", vsport);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- RTCP port (video): %d\\n\", vsport_rtcp);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if(is_session) {\n\t\t\t\t\t\t\t\t\tif(!strcasecmp(name, \"timeout\")) {\n\t\t\t\t\t\t\t\t\t\t/* Take note of the timeout, for keep-alive */\n\t\t\t\t\t\t\t\t\t\tsource->ka_timeout = janus_streaming_min_if(source->session_timeout, (gint64)atoi(value) / 2 * G_USEC_PER_SEC);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- RTSP session timeout (video): %\"SCNi64\" ms\\n\", source->ka_timeout / 1000);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Move to the next param */\n\t\t\t\t\t\tp += read;\n\t\t\t\t\t\tif(*p != ';')\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\twhile(*p == ';')\n\t\t\t\t\t\t\tp++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(cr != NULL)\n\t\t\t\t\t*cr = '\\r';\n\t\t\t\tindex++;\n\t\t\t}\n\t\t\tif(cr != NULL)\n\t\t\t\t*cr = '\\r';\n\t\t\tg_strfreev(parts);\n\t\t}\n#ifdef HAVE_LIBCURL\n#if CURL_AT_LEAST_VERSION(7, 62, 0)\n\t\t/* If we don't have a host yet (no c-line, no source in Transport), use the server address */\n\t\tif(strlen(vhost) == 0 || !strcmp(vhost, \"0.0.0.0\")) {\n\t\t\tJANUS_LOG(LOG_WARN, \"No c-line or source for RTSP video address, resolving server address...\\n\");\n\t\t\tCURLU *url = curl_url();\n\t\t\tif(url != NULL) {\n\t\t\t\tCURLUcode code = curl_url_set(url, CURLUPART_URL, source->rtsp_url, 0);\n\t\t\t\tif(code == 0) {\n\t\t\t\t\tchar *host = NULL;\n\t\t\t\t\tcode = curl_url_get(url, CURLUPART_HOST, &host, 0);\n\t\t\t\t\tif(code == 0) {\n\t\t\t\t\t\t/* Resolve the address */\n\t\t\t\t\t\tstruct addrinfo *info = NULL, *start = NULL;\n\t\t\t\t\t\tjanus_network_address addr;\n\t\t\t\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\t\t\t\tif(getaddrinfo(host, NULL, NULL, &info) == 0) {\n\t\t\t\t\t\t\tstart = info;\n\t\t\t\t\t\t\twhile(info != NULL) {\n\t\t\t\t\t\t\t\tif(janus_network_address_from_sockaddr(info->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t\t\t\t\t/* Resolved */\n\t\t\t\t\t\t\t\t\tg_snprintf(vhost, sizeof(vhost), \"%s\",\n\t\t\t\t\t\t\t\t\t\tjanus_network_address_string_from_buffer(&addr_buf));\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"   -- %s\\n\", vhost);\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tinfo = info->ai_next;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(start)\n\t\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\t\tcurl_free(host);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurl_url_cleanup(url);\n\t\t\t}\n\t\t}\n#endif\n#endif\n\t\tif(strlen(vhost) == 0 || !strcmp(vhost, \"0.0.0.0\")) {\n\t\t\t/* Still nothing... */\n\t\t\tJANUS_LOG(LOG_WARN, \"No host address for the RTSP video stream, no latching will be performed\\n\");\n\t\t}\n\t}\n\n\tjanus_audiocodec audio_codec = JANUS_AUDIOCODEC_NONE;\n\tif(aresult != -1) {\n\t\t/* Identify audio codec */\n\t\taudio_codec = janus_audiocodec_from_name(janus_sdp_get_rtpmap_codec(artpmap));\n\t\t/* Send an RTSP SETUP for audio */\n\t\tg_free(curldata->buffer);\n\t\tcurldata->buffer = g_malloc0(1);\n\t\tcurldata->size = 0;\n\t\tgboolean add_qs = (rtsp_querystring != NULL);\n\t\tif(add_qs && strstr(acontrol, rtsp_querystring) != NULL)\n\t\t\tadd_qs = FALSE;\n\t\tif(strstr(acontrol, (strlen(abase) > 0 ? abase : rtsp_url)) == acontrol) {\n\t\t\t/* The control attribute already contains the whole URL? */\n\t\t\tg_snprintf(uri, sizeof(uri), \"%s%s%s\", acontrol,\n\t\t\t\tadd_qs ? \"?\" : \"\", add_qs ? rtsp_querystring : \"\");\n\t\t} else {\n\t\t\t/* Append the control attribute to the URL */\n\t\t\tg_snprintf(uri, sizeof(uri), \"%s/%s%s%s\", (strlen(abase) > 0 ? abase : rtsp_url),\n\t\t\t\tacontrol, add_qs ? \"?\" : \"\", add_qs ? rtsp_querystring : \"\");\n\t\t}\n\t\tcurl_easy_setopt(curl, CURLOPT_RTSP_STREAM_URI, uri);\n\t\tcurl_easy_setopt(curl, CURLOPT_RTSP_TRANSPORT, atransport);\n\t\tcurl_easy_setopt(curl, CURLOPT_RTSP_REQUEST, (long)CURL_RTSPREQ_SETUP);\n\t\tres = curl_easy_perform(curl);\n\t\tif(res != CURLE_OK) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't send SETUP request: %s (%s)\\n\",\n\t\t\t\tcurl_easy_strerror(res), curl_errbuf);\n\t\t\tg_strfreev(parts);\n\t\t\tcurl_easy_cleanup(curl);\n\t\t\tg_free(curl_errbuf);\n\t\t\tg_free(curldata->buffer);\n\t\t\tg_free(curldata);\n\t\t\tif(video_fds.fd != -1) close(video_fds.fd);\n\t\t\tif(video_fds.rtcp_fd != -1) close(video_fds.rtcp_fd);\n\t\t\tif(audio_fds.fd != -1) close(audio_fds.fd);\n\t\t\tif(audio_fds.rtcp_fd != -1) close(audio_fds.rtcp_fd);\n\t\t\treturn -6;\n\t\t}\n\t\tres = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);\n\t\tif(code != 200) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't SETUP, got error code: %ld\\n\", code);\n\t\t\tg_strfreev(parts);\n\t\t\tcurl_easy_cleanup(curl);\n\t\t\tg_free(curl_errbuf);\n\t\t\tg_free(curldata->buffer);\n\t\t\tg_free(curldata);\n\t\t\tif(video_fds.fd != -1) close(video_fds.fd);\n\t\t\tif(video_fds.rtcp_fd != -1) close(video_fds.rtcp_fd);\n\t\t\tif(audio_fds.fd != -1) close(audio_fds.fd);\n\t\t\tif(audio_fds.rtcp_fd != -1) close(audio_fds.rtcp_fd);\n\t\t\treturn -6;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"SETUP answer:%s\\n\", curldata->buffer);\n\t\t/* Parse the RTSP message: we may need Transport and Session */\n\t\tgboolean success = TRUE;\n\t\tgchar **parts = g_strsplit(curldata->buffer, \"\\n\", -1);\n\t\tif(parts) {\n\t\t\tint index = 0;\n\t\t\tchar *line = NULL, *cr = NULL;\n\t\t\twhile(success && (line = parts[index]) != NULL) {\n\t\t\t\tcr = strchr(line, '\\r');\n\t\t\t\tif(cr != NULL)\n\t\t\t\t\t*cr = '\\0';\n\t\t\t\tif(*line == '\\0') {\n\t\t\t\t\tif(cr != NULL)\n\t\t\t\t\t\t*cr = '\\r';\n\t\t\t\t\tindex++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(strnlen(line, 3) < 3) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid RTSP line (%zu bytes): %s\\n\", strlen(line), line);\n\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* Check if this is a Transport or Session header, and if so parse it */\n\t\t\t\tgboolean is_transport = (strstr(line, \"Transport:\") == line || strstr(line, \"transport:\") == line);\n\t\t\t\tgboolean is_session = (strstr(line, \"Session:\") == line || strstr(line, \"session:\") == line);\n\t\t\t\tif(is_transport || is_session) {\n\t\t\t\t\t/* There is, iterate on all params */\n\t\t\t\t\tchar *p = line, param[100], *pi = NULL;\n\t\t\t\t\tint read = 0;\n\t\t\t\t\tgboolean first = TRUE;\n\t\t\t\t\twhile(sscanf(p, \"%99[^;]%n\", param, &read) == 1) {\n\t\t\t\t\t\tif(first) {\n\t\t\t\t\t\t\t/* Skip */\n\t\t\t\t\t\t\tfirst = FALSE;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tpi = param;\n\t\t\t\t\t\t\twhile(*pi == ' ')\n\t\t\t\t\t\t\t\tpi++;\n\t\t\t\t\t\t\tchar name[50], value[50];\n\t\t\t\t\t\t\tif(sscanf(pi, \"%49[a-zA-Z_0-9]=%49s\", name, value) == 2) {\n\t\t\t\t\t\t\t\tif(is_transport) {\n\t\t\t\t\t\t\t\t\tif(!strcasecmp(name, \"ssrc\")) {\n\t\t\t\t\t\t\t\t\t\t/* Take note of the audio SSRC */\n\t\t\t\t\t\t\t\t\t\taudio_ssrc = strtol(value, NULL, 16);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- SSRC (audio): %\"SCNu32\"\\n\", audio_ssrc);\n\t\t\t\t\t\t\t\t\t} else if(!strcasecmp(name, \"source\")) {\n\t\t\t\t\t\t\t\t\t\t/* If we got an address via c-line, replace it */\n\t\t\t\t\t\t\t\t\t\tg_snprintf(ahost, sizeof(ahost), \"%s\", value);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Source (audio): %s\\n\", ahost);\n\t\t\t\t\t\t\t\t\t} else if(!strcasecmp(name, \"server_port\")) {\n\t\t\t\t\t\t\t\t\t\t/* Take note of the server port */\n\t\t\t\t\t\t\t\t\t\tchar *dash = NULL;\n\t\t\t\t\t\t\t\t\t\tasport = strtol(value, &dash, 10);\n\t\t\t\t\t\t\t\t\t\tasport_rtcp = dash ? strtol(++dash, NULL, 10) : 0;\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- RTP port (audio): %d\\n\", asport);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- RTCP port (audio): %d\\n\", asport_rtcp);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if(is_session) {\n\t\t\t\t\t\t\t\t\tif(!strcasecmp(name, \"timeout\")) {\n\t\t\t\t\t\t\t\t\t\t/* Take note of the timeout, for keep-alive */\n\t\t\t\t\t\t\t\t\t\tsource->ka_timeout = janus_streaming_min_if(source->session_timeout, (gint64)atoi(value) / 2 * G_USEC_PER_SEC);\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- RTSP session timeout (audio): %\"SCNi64\" ms\\n\", source->ka_timeout / 1000);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Move to the next param */\n\t\t\t\t\t\tp += read;\n\t\t\t\t\t\tif(*p != ';')\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\twhile(*p == ';')\n\t\t\t\t\t\t\tp++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(cr != NULL)\n\t\t\t\t\t*cr = '\\r';\n\t\t\t\tindex++;\n\t\t\t}\n\t\t\tif(cr != NULL)\n\t\t\t\t*cr = '\\r';\n\t\t\tg_strfreev(parts);\n\t\t}\n\t\t/* If we don't have a host yet (no c-line, no source in Transport), use the server address */\n\t\tif(strlen(ahost) == 0 || !strcmp(ahost, \"0.0.0.0\")) {\n\t\t\tif(strlen(vhost) > 0 && strcmp(vhost, \"0.0.0.0\")) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"No c-line or source for RTSP audio stream, copying the video address (%s)\\n\", vhost);\n\t\t\t\tg_snprintf(ahost, sizeof(ahost), \"%s\", vhost);\n\t\t\t} else {\n#ifdef HAVE_LIBCURL\n#if CURL_AT_LEAST_VERSION(7, 62, 0)\n\t\t\t\tJANUS_LOG(LOG_WARN, \"No c-line or source for RTSP audio stream, resolving server address...\\n\");\n\t\t\t\tCURLU *url = curl_url();\n\t\t\t\tif(url != NULL) {\n\t\t\t\t\tCURLUcode code = curl_url_set(url, CURLUPART_URL, source->rtsp_url, 0);\n\t\t\t\t\tif(code == 0) {\n\t\t\t\t\t\tchar *host = NULL;\n\t\t\t\t\t\tcode = curl_url_get(url, CURLUPART_HOST, &host, 0);\n\t\t\t\t\t\tif(code == 0) {\n\t\t\t\t\t\t\t/* Resolve the address */\n\t\t\t\t\t\t\tstruct addrinfo *info = NULL, *start = NULL;\n\t\t\t\t\t\t\tjanus_network_address addr;\n\t\t\t\t\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\t\t\t\t\tif(getaddrinfo(host, NULL, NULL, &info) == 0) {\n\t\t\t\t\t\t\t\tstart = info;\n\t\t\t\t\t\t\t\twhile(info != NULL) {\n\t\t\t\t\t\t\t\t\tif(janus_network_address_from_sockaddr(info->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t\t\t\t\t\t/* Resolved */\n\t\t\t\t\t\t\t\t\t\tg_snprintf(ahost, sizeof(ahost), \"%s\",\n\t\t\t\t\t\t\t\t\t\t\tjanus_network_address_string_from_buffer(&addr_buf));\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"   -- %s\\n\", ahost);\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tinfo = info->ai_next;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(start)\n\t\t\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\t\t\tcurl_free(host);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcurl_url_cleanup(url);\n\t\t\t\t}\n#endif\n#endif\n\t\t\t}\n\t\t}\n\t\tif(strlen(ahost) == 0 || !strcmp(ahost, \"0.0.0.0\")) {\n\t\t\t/* Still nothing... */\n\t\t\tJANUS_LOG(LOG_WARN, \"No host address for the RTSP audio stream, no latching will be performed\\n\");\n\t\t}\n\t}\n\tg_strfreev(parts);\n\n\t/* Do we have media streams already? */\n\tif(source->media == NULL) {\n\t\tif(doaudio) {\n\t\t\tjanus_streaming_rtp_source_stream *stream = g_malloc0(sizeof(janus_streaming_rtp_source_stream));\n\t\t\tstream->mindex = g_list_length(source->media);\n\t\t\tstream->type = JANUS_STREAMING_MEDIA_AUDIO;\n\t\t\tstream->mid = g_strdup(\"a\");\n\t\t\tstream->label = g_strdup(\"RTSP audio\");\n\t\t\tsource->media = g_list_append(source->media, stream);\n\t\t\tjanus_refcount_init(&stream->ref, janus_streaming_rtp_source_stream_free);\n\t\t}\n\t\tif(dovideo) {\n\t\t\tjanus_streaming_rtp_source_stream *stream = g_malloc0(sizeof(janus_streaming_rtp_source_stream));\n\t\t\tstream->mindex = g_list_length(source->media);\n\t\t\tstream->type = JANUS_STREAMING_MEDIA_VIDEO;\n\t\t\tstream->mid = g_strdup(\"v\");\n\t\t\tstream->label = g_strdup(\"RTSP video\");\n\t\t\tsource->media = g_list_append(source->media, stream);\n\t\t\tjanus_refcount_init(&stream->ref, janus_streaming_rtp_source_stream_free);\n\t\t}\n\t}\n\n\t/* Update the source (but check if rtpmap/fmtp need to be overridden) */\n\tGList *temp = source->media;\n\twhile(temp) {\n\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO) {\n\t\t\tstream->codecs.pt = source->rtsp_acodecs.pt != -1 ? source->rtsp_acodecs.pt : apt;\n\t\t\tif(stream->codecs.audio_codec == JANUS_AUDIOCODEC_NONE)\n\t\t\t\tstream->codecs.audio_codec = audio_codec;\n\t\t\tg_free(stream->codecs.fmtp);\n\t\t\tstream->codecs.fmtp = source->rtsp_acodecs.fmtp ? g_strdup(source->rtsp_acodecs.fmtp) : g_strdup(afmtp);\n\t\t\tg_free(source->rtsp_ahost);\n\t\t\tsource->rtsp_ahost = asport > 0 ? g_strdup(ahost) : NULL;\n\t\t\tstream->remote_port = asport;\n\t\t\tstream->remote_rtcp_port = asport_rtcp;\n\t\t\t/* Map this stream by all its file descriptors */\n\t\t\tstream->fd[0] = audio_fds.fd;\n\t\t\tstream->fd[1] = -1;\n\t\t\tstream->fd[2] = -1;\n\t\t\tstream->rtcp_fd = audio_fds.rtcp_fd;\n\t\t\tif(stream->fd[0] != -1)\n\t\t\t\tg_hash_table_insert(source->media_byfd, GINT_TO_POINTER(stream->fd[0]), stream);\n\t\t\tif(stream->rtcp_fd != -1)\n\t\t\t\tg_hash_table_insert(source->media_byfd, GINT_TO_POINTER(stream->rtcp_fd), stream);\n\t\t}\n\t\tif(stream->type == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\t\tstream->codecs.video_codec = video_codec;\n\t\t\tstream->codecs.pt = source->rtsp_vcodecs.pt != -1 ? source->rtsp_vcodecs.pt : vpt;\n\t\t\tif(stream->codecs.video_codec == JANUS_VIDEOCODEC_NONE)\n\t\t\t\tstream->codecs.video_codec = video_codec;\n\t\t\tg_free(stream->codecs.fmtp);\n\t\t\tstream->codecs.fmtp = source->rtsp_vcodecs.fmtp ? g_strdup(source->rtsp_vcodecs.fmtp) : g_strdup(vfmtp);\n\t\t\tg_free(stream->h264_spspps);\n\t\t\tstream->h264_spspps = sps;\n\t\t\tsps = NULL;\n\t\t\tstream->h264_spspps_len = spslen;\n\t\t\tspslen = 0;\n\t\t\tg_free(source->rtsp_vhost);\n\t\t\tsource->rtsp_vhost = vsport > 0 ? g_strdup(vhost) : NULL;\n\t\t\tstream->remote_port = vsport;\n\t\t\tstream->remote_rtcp_port = vsport_rtcp;\n\t\t\t/* Map this stream by all its file descriptors */\n\t\t\tstream->fd[0] = video_fds.fd;\n\t\t\tstream->fd[1] = -1;\n\t\t\tstream->fd[2] = -1;\n\t\t\tstream->rtcp_fd = video_fds.rtcp_fd;\n\t\t\tif(stream->fd[0] != -1)\n\t\t\t\tg_hash_table_insert(source->media_byfd, GINT_TO_POINTER(stream->fd[0]), stream);\n\t\t\tif(stream->rtcp_fd != -1)\n\t\t\t\tg_hash_table_insert(source->media_byfd, GINT_TO_POINTER(stream->rtcp_fd), stream);\n\t\t\tif(source->bufferkf_ms > 0 || source->bufferkf_bytes > 0) {\n\t\t\t\tstream->keyframe.enabled = TRUE;\n\t\t\t\tstream->keyframe.bufferkf_ms = source->bufferkf_ms;\n\t\t\t\tstream->keyframe.bufferkf_bytes = source->bufferkf_bytes;\n\t\t\t\tstream->keyframe.latest_keyframe = NULL;\n\t\t\t\tstream->keyframe.kf_ssrc = 0;\n\t\t\t\tstream->keyframe.kf_ts = 0;\n\t\t\t\tstream->keyframe.kf_bytes = 0;\n\t\t\t\tstream->keyframe.kf_start = 0;\n\t\t\t\tstream->keyframe.first_ts = FALSE;\n\t\t\t\tjanus_mutex_init(&stream->keyframe.mutex);\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tsource->curl = curl;\n\tsource->curl_errbuf = curl_errbuf;\n\tsource->curldata = curldata;\n\treturn 0;\n}\n\n/* Helper method to send a latching packet on an RTSP media socket */\nstatic void janus_streaming_rtsp_latch(int fd, char *host, int port, struct sockaddr *remote) {\n\t/* Resolve address to get an IP */\n\tstruct addrinfo *res = NULL;\n\tjanus_network_address addr;\n\tjanus_network_address_string_buffer addr_buf;\n\tif(getaddrinfo(host, NULL, NULL, &res) != 0 ||\n\t\t\tjanus_network_address_from_sockaddr(res->ai_addr, &addr) != 0 ||\n\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) != 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not resolve %s...\\n\", host);\n\t\tif(res)\n\t\t\tfreeaddrinfo(res);\n\t} else {\n\t\tfreeaddrinfo(res);\n\t\t/* Prepare the recipient */\n\t\tstruct sockaddr_in remote4 = { 0 };\n\t\tstruct sockaddr_in6 remote6 = { 0 };\n\t\tsocklen_t addrlen = 0;\n\t\tif(addr.family == AF_INET) {\n\t\t\tmemset(&remote4, 0, sizeof(remote4));\n\t\t\tremote4.sin_family = AF_INET;\n\t\t\tremote4.sin_port = htons(port);\n\t\t\tmemcpy(&remote4.sin_addr, &addr.ipv4, sizeof(addr.ipv4));\n\t\t\tremote = (struct sockaddr *)(&remote4);\n\t\t\taddrlen = sizeof(remote4);\n\t\t} else if(addr.family == AF_INET6) {\n\t\t\tmemset(&remote6, 0, sizeof(remote6));\n\t\t\tremote6.sin6_family = AF_INET6;\n\t\t\tremote6.sin6_port = htons(port);\n\t\t\tmemcpy(&remote6.sin6_addr, &addr.ipv6, sizeof(addr.ipv6));\n\t\t\tremote6.sin6_addr = addr.ipv6;\n\t\t\tremote = (struct sockaddr *)(&remote6);\n\t\t\taddrlen = sizeof(remote6);\n\t\t}\n\t\t/* Prepare an empty RTP packet */\n\t\tjanus_rtp_header rtp;\n\t\tmemset(&rtp, 0, sizeof(rtp));\n\t\trtp.version = 2;\n\t\t/* Send a couple of latching packets */\n\t\t(void)sendto(fd, &rtp, 12, 0, remote, addrlen);\n\t\t(void)sendto(fd, &rtp, 12, 0, remote, addrlen);\n\t}\n}\n\n/* Helper to send an RTSP PLAY (either when we create the mountpoint, or when we try reconnecting) */\nstatic int janus_streaming_rtsp_play(janus_streaming_rtp_source *source) {\n\tif(source == NULL || source->curldata == NULL)\n\t\treturn -1;\n\t/* First of all, send a latching packet to the RTSP server port(s) */\n\tstruct sockaddr_in6 remote = { 0 };\n\tGList *temp = source->media;\n\twhile(temp) {\n\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\tif(stream->remote_port > 0 && stream->fd[0] >= 0) {\n\t\t\tchar *host = (stream->type == JANUS_STREAMING_MEDIA_AUDIO) ? source->rtsp_ahost : source->rtsp_vhost;\n\t\t\tJANUS_LOG(LOG_VERB, \"RTSP %s latching: %s:%d\\n\",\n\t\t\t\tjanus_streaming_media_str(stream->type), host, stream->remote_port);\n\t\t\tjanus_streaming_rtsp_latch(stream->fd[0], host, stream->remote_port, (struct sockaddr *)&remote);\n\t\t\tif(stream->remote_rtcp_port > 0 && stream->rtcp_fd >= 0) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- RTCP: %s:%d\\n\", host, stream->remote_rtcp_port);\n\t\t\t\tjanus_streaming_rtsp_latch(stream->rtcp_fd, host,\n\t\t\t\t\tstream->remote_rtcp_port, (struct sockaddr *)&stream->rtcp_addr);\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\t/* Send an RTSP PLAY */\n\tjanus_mutex_lock(&source->rtsp_mutex);\n\tg_free(source->curldata->buffer);\n\tsource->curldata->buffer = g_malloc0(1);\n\tsource->curldata->size = 0;\n\tJANUS_LOG(LOG_VERB, \"Sending PLAY request...\\n\");\n\tcurl_easy_setopt(source->curl, CURLOPT_RTSP_STREAM_URI, source->rtsp_stream_uri ? source->rtsp_stream_uri : source->rtsp_url);\n\tcurl_easy_setopt(source->curl, CURLOPT_RANGE, \"npt=0.000-\");\n\tcurl_easy_setopt(source->curl, CURLOPT_RTSP_REQUEST, (long)CURL_RTSPREQ_PLAY);\n\tint res = curl_easy_perform(source->curl);\n\tif(res != CURLE_OK) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't send PLAY request: %s (%s)\\n\",\n\t\t\tcurl_easy_strerror(res), source->curl_errbuf);\n\t\tjanus_mutex_unlock(&source->rtsp_mutex);\n\t\treturn -1;\n\t}\n\tlong code = 0;\n\tres = curl_easy_getinfo(source->curl, CURLINFO_RESPONSE_CODE, &code);\n\tif(code != 200) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't PLAY, got error code: %ld\\n\", code);\n\t\tjanus_mutex_unlock(&source->rtsp_mutex);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_VERB, \"PLAY answer:%s\\n\", source->curldata->buffer);\n\tjanus_mutex_unlock(&source->rtsp_mutex);\n\treturn 0;\n}\n\n/* Helper to create an RTSP source */\njanus_streaming_mountpoint *janus_streaming_create_rtsp_source(\n\t\tuint64_t id, char *id_str, char *name, char *desc, char *metadata,\n\t\tchar *url, char *username, char *password,\n\t\tgboolean quirk, gboolean notify_changes,\n\t\tgboolean doaudio, int apt, char *acodec, char *afmtp,\n\t\tgboolean dovideo, int vpt, char *vcodec, char *vfmtp,\n\t\tuint16_t bufferkf_ms, uint32_t bufferkf_bytes,\n\t\tconst janus_network_address *iface, int threads,\n\t\tgint64 reconnect_delay, gint64 session_timeout, int rtsp_timeout, int rtsp_conn_timeout,\n\t\tgboolean error_on_failure) {\n\tchar id_num[30];\n\tif(!string_ids) {\n\t\tg_snprintf(id_num, sizeof(id_num), \"%\"SCNu64, id);\n\t\tid_str = id_num;\n\t}\n\tif(url == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't add 'rtsp' stream, missing url...\\n\");\n\t\treturn NULL;\n\t}\n\tif(reconnect_delay < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"rtsp_reconnect_delay can't be smaller than zero.\\n\");\n\t\treturn NULL;\n\t}\n\tif(session_timeout < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"rtsp_session_timeout can't be smaller than zero.\\n\");\n\t\treturn NULL;\n\t}\n\tif(rtsp_timeout < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"rtsp_timeout can't be smaller than zero.\\n\");\n\t\treturn NULL;\n\t}\n\tif(rtsp_conn_timeout < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"rtsp_conn_timeout can't be smaller than zero.\\n\");\n\t\treturn NULL;\n\t}\n\n\tJANUS_LOG(LOG_VERB, \"Audio %s, Video %s\\n\", doaudio ? \"enabled\" : \"NOT enabled\", dovideo ? \"enabled\" : \"NOT enabled\");\n\n\t/* Create an RTP source for the media we'll get */\n\tchar tempname[255];\n\tif(name == NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Missing name, will generate a random one...\\n\");\n\t\tmemset(tempname, 0, 255);\n\t\tg_snprintf(tempname, 255, \"%s\", id_str);\n\t} else if(name[0] == '0' || atoi(name) != 0) {\n\t\tJANUS_LOG(LOG_VERB, \"Names can't start with a number, prefixing it...\\n\");\n\t\tmemset(tempname, 0, 255);\n\t\tg_snprintf(tempname, 255, \"mp-%s\", name);\n\t\tname = NULL;\n\t}\n\tchar *sourcename =  g_strdup(name ? name : tempname);\n\tchar *description = NULL;\n\tif(desc != NULL) {\n\t\tdescription = g_strdup(desc);\n\t} else {\n\t\tdescription = g_strdup(name ? name : tempname);\n\t}\n\n\tjanus_network_address nil;\n\tjanus_network_address_nullify(&nil);\n\n\t/* Create the mountpoint and prepare the source */\n\tjanus_streaming_mountpoint *live_rtsp = g_malloc0(sizeof(janus_streaming_mountpoint));\n\tlive_rtsp->id = id;\n\tlive_rtsp->id_str = g_strdup(id_str);\n\tlive_rtsp->name = sourcename;\n\tlive_rtsp->description = description;\n\tlive_rtsp->metadata = (metadata ? g_strdup(metadata) : NULL);\n\tlive_rtsp->enabled = TRUE;\n\tlive_rtsp->active = FALSE;\n\tlive_rtsp->audio = doaudio;\n\tlive_rtsp->video = dovideo;\n\tlive_rtsp->data = FALSE;\n\tlive_rtsp->streaming_type = janus_streaming_type_live;\n\tlive_rtsp->streaming_source = janus_streaming_source_rtp;\n\tjanus_streaming_rtp_source *live_rtsp_source = g_malloc0(sizeof(janus_streaming_rtp_source));\n\tlive_rtsp_source->rtsp = TRUE;\n\tlive_rtsp_source->rtsp_url = g_strdup(url);\n\tlive_rtsp_source->rtsp_username = username ? g_strdup(username) : NULL;\n\tlive_rtsp_source->rtsp_password = password ? g_strdup(password) : NULL;\n\tlive_rtsp_source->rtsp_stream_uri = NULL;\n\tlive_rtsp_source->rtsp_quirk = quirk;\n\tlive_rtsp_source->rtsp_notify_changes = notify_changes;\n\tlive_rtsp_source->media = NULL;\t\t/* We'll prepare this later */\n\tlive_rtsp_source->media_byid = g_hash_table_new(NULL, NULL);\n\tlive_rtsp_source->media_byfd = g_hash_table_new(NULL, NULL);\n\tlive_rtsp_source->pipefd[0] = -1;\n\tlive_rtsp_source->pipefd[1] = -1;\n\tpipe(live_rtsp_source->pipefd);\n\tlive_rtsp_source->bufferkf_ms = bufferkf_ms;\n\tlive_rtsp_source->bufferkf_bytes = bufferkf_bytes;\n\tlive_rtsp_source->ka_timeout = session_timeout;\n\tlive_rtsp_source->reconnect_delay = reconnect_delay;\n\tlive_rtsp_source->session_timeout = session_timeout;\n\tlive_rtsp_source->rtsp_timeout = rtsp_timeout;\n\tlive_rtsp_source->rtsp_conn_timeout = rtsp_conn_timeout;\n\tlive_rtsp_source->reconnect_timer = 0;\n\tjanus_mutex_init(&live_rtsp_source->rtsp_mutex);\n\tlive_rtsp->source = live_rtsp_source;\n\tlive_rtsp->source_destroy = (GDestroyNotify)janus_streaming_rtp_source_free;\n\tlive_rtsp->viewers = NULL;\n\tg_atomic_int_set(&live_rtsp->destroyed, 0);\n\tjanus_refcount_init(&live_rtsp->ref, janus_streaming_mountpoint_free);\n\tjanus_mutex_init(&live_rtsp->mutex);\n\t/* We may have to override the payload type and/or codec and/or fmtp for audio and/or video */\n\tlive_rtsp_source->rtsp_acodecs.pt = doaudio ? apt : -1;\n\tlive_rtsp_source->rtsp_acodecs.audio_codec = JANUS_AUDIOCODEC_NONE;\n\tif(doaudio && acodec)\n\t\tlive_rtsp_source->rtsp_acodecs.audio_codec = janus_audiocodec_from_name(acodec);\n\tlive_rtsp_source->rtsp_acodecs.fmtp = doaudio ? (afmtp ? g_strdup(afmtp) : NULL) : NULL;\n\tlive_rtsp_source->rtsp_vcodecs.pt = dovideo ? vpt : -1;\n\tlive_rtsp_source->rtsp_acodecs.video_codec = JANUS_VIDEOCODEC_NONE;\n\tif(dovideo && vcodec)\n\t\tlive_rtsp_source->rtsp_acodecs.video_codec = janus_videocodec_from_name(vcodec);\n\tlive_rtsp_source->rtsp_vcodecs.fmtp = dovideo ? (vfmtp ? g_strdup(vfmtp) : NULL) : NULL;\n\t/* If we need to return an error on failure, try connecting right now */\n\tif(error_on_failure) {\n\t\t/* Now connect to the RTSP server */\n\t\tif(janus_streaming_rtsp_connect_to_server(live_rtsp) < 0) {\n\t\t\t/* Error connecting, get rid of the mountpoint */\n\t\t\tjanus_refcount_decrease(&live_rtsp->ref);\n\t\t\treturn NULL;\n\t\t}\n\t\t/* Send an RTSP PLAY, now */\n\t\tif(janus_streaming_rtsp_play(live_rtsp_source) < 0) {\n\t\t\t/* Error trying to play, get rid of the mountpoint */\n\t\t\tjanus_refcount_decrease(&live_rtsp->ref);\n\t\t\treturn NULL;\n\t\t}\n\t}\n\t/* If we need helper threads, spawn them now */\n\tGError *error = NULL;\n\tchar tname[16];\n\tif(threads > 0) {\n\t\tint i=0;\n\t\tfor(i=0; i<threads; i++) {\n\t\t\tjanus_streaming_helper *helper = g_malloc0(sizeof(janus_streaming_helper));\n\t\t\thelper->id = i+1;\n\t\t\thelper->mp = live_rtsp;\n\t\t\thelper->queued_packets = g_async_queue_new_full((GDestroyNotify)janus_streaming_rtp_relay_packet_free);\n\t\t\tjanus_mutex_init(&helper->mutex);\n\t\t\tjanus_refcount_init(&helper->ref, janus_streaming_helper_free);\n\t\t\tlive_rtsp->helper_threads++;\n\t\t\t/* Spawn a thread and add references */\n\t\t\tg_snprintf(tname, sizeof(tname), \"help %u-%\"SCNu64, helper->id, live_rtsp->id);\n\t\t\tjanus_refcount_increase(&live_rtsp->ref);\n\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\thelper->thread = g_thread_try_new(tname, &janus_streaming_helper_thread, helper, &error);\n\t\t\tif(error != NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the helper thread...\\n\",\n\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\tg_error_free(error);\n\t\t\t\tjanus_refcount_decrease(&live_rtsp->ref);\t/* This is for the helper thread */\n\t\t\t\tg_async_queue_unref(helper->queued_packets);\n\t\t\t\tjanus_refcount_decrease(&helper->ref);\n\t\t\t\t/* This extra unref is for the init */\n\t\t\t\tjanus_refcount_decrease(&helper->ref);\n\t\t\t\tjanus_refcount_decrease(&live_rtsp->ref);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\tlive_rtsp->threads = g_list_append(live_rtsp->threads, helper);\n\t\t}\n\t}\n\t/* Finally, start the thread that will receive the media packets */\n\tg_snprintf(tname, sizeof(tname), \"mp %s\", live_rtsp->id_str);\n\tjanus_refcount_increase(&live_rtsp->ref);\n\tlive_rtsp->thread = g_thread_try_new(tname, &janus_streaming_relay_thread, live_rtsp, &error);\n\tif(error != NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the RTSP thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_refcount_decrease(&live_rtsp->ref);\t/* This is for the failed thread */\n\t\tjanus_refcount_decrease(&live_rtsp->ref);\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&mountpoints_mutex);\n\tg_hash_table_insert(mountpoints,\n\t\tstring_ids ? (gpointer)g_strdup(live_rtsp->id_str) : (gpointer)janus_uint64_dup(live_rtsp->id),\n\t\tlive_rtsp);\n\tg_hash_table_remove(mountpoints_temp, string_ids ? (gpointer)live_rtsp->id_str : (gpointer)&live_rtsp->id);\n\tjanus_mutex_unlock(&mountpoints_mutex);\n\treturn live_rtsp;\n}\n#else\n/* Helper to create an RTSP source */\njanus_streaming_mountpoint *janus_streaming_create_rtsp_source(\n\t\tuint64_t id, char *id_str, char *name, char *desc, char *metadata,\n\t\tchar *url, char *username, char *password,\n\t\tgboolean quirk, gboolean notify_changes,\n\t\tgboolean doaudio, int apt, char *audiocodec, char *audiofmtp,\n\t\tgboolean dovideo, int vpt, char *videocodec, char *videofmtp,\n\t\tuint16_t bufferkf_ms, uint32_t bufferkf_bytes,\n\t\tconst janus_network_address *iface, int threads,\n\t\tgint64 reconnect_delay, gint64 session_timeout, int rtsp_timeout, int rtsp_conn_timeout,\n\t\tgboolean error_on_failure) {\n\tJANUS_LOG(LOG_ERR, \"RTSP need libcurl\\n\");\n\treturn NULL;\n}\n#endif\n\n/* Thread to send RTP packets from a file (on demand) */\nstatic void *janus_streaming_ondemand_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Filesource (on demand) RTP thread starting...\\n\");\n\tjanus_streaming_session *session = (janus_streaming_session *)data;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid session!\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tjanus_streaming_mountpoint *mountpoint = session->mountpoint;\n\tif(!mountpoint) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid mountpoint!\\n\");\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tif(mountpoint->streaming_source != janus_streaming_source_file) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Not an file source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tif(mountpoint->streaming_type != janus_streaming_type_on_demand) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Not an on-demand file source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tjanus_streaming_file_source *source = mountpoint->source;\n\tif(source == NULL || source->filename == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Invalid file source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%s] Opening file source %s...\\n\", mountpoint->name, source->filename);\n\tFILE *audio = fopen(source->filename, \"rb\");\n\tif(!audio) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Ooops, audio file missing!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tchar *name = g_strdup(mountpoint->name ? mountpoint->name : \"??\");\n\tJANUS_LOG(LOG_VERB, \"[%s] Streaming audio file: %s\\n\", name, source->filename);\n\n#ifdef HAVE_LIBOGG\n\t/* Make sure that, if this is an .opus file, we can open it */\n\tjanus_streaming_opus_context opusctx = { 0 };\n\tif(source->opus) {\n\t\topusctx.name = name;\n\t\topusctx.filename = source->filename;\n\t\topusctx.file = audio;\n\t\tif(janus_streaming_opus_context_init(&opusctx) < 0) {\n\t\t\tg_free(name);\n\t\t\tfclose(audio);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t}\n#endif\n\n\t/* Buffer */\n\tchar buf[1500];\n\tmemset(buf, 0, sizeof(buf));\n\t/* Set up RTP */\n\tguint16 seq = 1;\n\tguint32 ts = 0;\n\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\theader->version = 2;\n\theader->markerbit = 1;\n\theader->type = source->codecs.pt;\n\theader->seq_number = htons(seq);\n\theader->timestamp = htonl(ts);\n\theader->ssrc = htonl(1);\t/* The gateway will fix this anyway */\n\t/* Timer */\n\tstruct timeval now, before;\n\tgettimeofday(&before, NULL);\n\tnow.tv_sec = before.tv_sec;\n\tnow.tv_usec = before.tv_usec;\n\ttime_t passed, d_s, d_us;\n\t/* Loop */\n\tgint read = 0;\n#ifdef HAVE_LIBOGG\n\tconst gint plen = (sizeof(buf)-RTP_HEADER_SIZE);\n#endif\n\tjanus_streaming_rtp_relay_packet packet;\n\twhile(!g_atomic_int_get(&stopping) && !g_atomic_int_get(&mountpoint->destroyed) &&\n\t\t\t!g_atomic_int_get(&session->stopping) && !g_atomic_int_get(&session->destroyed)) {\n\t\t/* See if it's time to prepare a frame */\n\t\tgettimeofday(&now, NULL);\n\t\td_s = now.tv_sec - before.tv_sec;\n\t\td_us = now.tv_usec - before.tv_usec;\n\t\tif(d_us < 0) {\n\t\t\td_us += 1000000;\n\t\t\t--d_s;\n\t\t}\n\t\tpassed = d_s*1000000 + d_us;\n\t\tif(passed < 18000) {\t/* Let's wait about 18ms */\n\t\t\tg_usleep(5000);\n\t\t\tcontinue;\n\t\t}\n\t\t/* Update the reference time */\n\t\tbefore.tv_usec += 20000;\n\t\tif(before.tv_usec > 1000000) {\n\t\t\tbefore.tv_sec++;\n\t\t\tbefore.tv_usec -= 1000000;\n\t\t}\n\t\t/* If not started or paused, wait some more */\n\t\tif(!g_atomic_int_get(&session->started) || g_atomic_int_get(&session->paused) || !mountpoint->enabled)\n\t\t\tcontinue;\n\t\tif(source->opus) {\n#ifdef HAVE_LIBOGG\n\t\t\t/* Get the next frame from the Opus file */\n\t\t\tread = janus_streaming_opus_context_read(&opusctx, buf + RTP_HEADER_SIZE, plen);\n#endif\n\t\t} else {\n\t\t\t/* Read frame from file... */\n\t\t\tread = fread(buf + RTP_HEADER_SIZE, sizeof(char), 160, audio);\n\t\t\tif(feof(audio)) {\n\t\t\t\t/* FIXME We're doing this forever... should this be configurable? */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%s] Rewind! (%s)\\n\", name, source->filename);\n\t\t\t\tfseek(audio, 0, SEEK_SET);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif(read < 0)\n\t\t\tbreak;\n\t\tif(mountpoint->active == FALSE)\n\t\t\tmountpoint->active = TRUE;\n\t\t/* Relay to the listener */\n\t\tpacket.mindex = -1;\n\t\tpacket.data = header;\n\t\tpacket.length = RTP_HEADER_SIZE + read;\n\t\tpacket.is_rtp = TRUE;\n\t\tpacket.is_video = FALSE;\n\t\tpacket.is_kfburst = FALSE;\n\t\t/* Backup the actual payload type, timestamp and sequence number */\n\t\tpacket.ptype = packet.data->type;\n\t\tpacket.timestamp = ntohl(packet.data->timestamp);\n\t\tpacket.seq_number = ntohs(packet.data->seq_number);\n\t\t/* Go! */\n\t\tjanus_streaming_relay_rtp_packet(session, &packet);\n\t\t/* Update header */\n\t\tseq++;\n\t\theader->seq_number = htons(seq);\n\t\tts += (source->opus ? 960 : 160);\n\t\theader->timestamp = htonl(ts);\n\t\theader->markerbit = 0;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%s] Leaving filesource (ondemand) thread\\n\", name);\n#ifdef HAVE_LIBOGG\n\tif(source->opus)\n\t\tjanus_streaming_opus_context_cleanup(&opusctx);\n#endif\n\tg_free(name);\n\tfclose(audio);\n\tjanus_refcount_decrease(&session->ref);\n\tjanus_refcount_decrease(&mountpoint->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\n/* Thread to send RTP packets from a file (live) */\nstatic void *janus_streaming_filesource_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Filesource (live) thread starting...\\n\");\n\tjanus_streaming_mountpoint *mountpoint = (janus_streaming_mountpoint *)data;\n\tif(!mountpoint) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid mountpoint!\\n\");\n\t\treturn NULL;\n\t}\n\tif(mountpoint->streaming_source != janus_streaming_source_file) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Not an file source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\treturn NULL;\n\t}\n\tif(mountpoint->streaming_type != janus_streaming_type_live) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Not a live file source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\treturn NULL;\n\t}\n\tjanus_streaming_file_source *source = mountpoint->source;\n\tif(source == NULL || source->filename == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Invalid file source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%s] Opening file source %s...\\n\", mountpoint->name, source->filename);\n\tFILE *audio = fopen(source->filename, \"rb\");\n\tif(!audio) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Ooops, audio file missing!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\treturn NULL;\n\t}\n\tchar *name = g_strdup(mountpoint->name ? mountpoint->name : \"??\");\n\tJANUS_LOG(LOG_VERB, \"[%s] Streaming audio file: %s\\n\", mountpoint->name, source->filename);\n\n#ifdef HAVE_LIBOGG\n\t/* Make sure that, if this is an .opus file, we can open it */\n\tjanus_streaming_opus_context opusctx = { 0 };\n\tif(source->opus) {\n\t\topusctx.name = name;\n\t\topusctx.filename = source->filename;\n\t\topusctx.file = audio;\n\t\tif(janus_streaming_opus_context_init(&opusctx) < 0) {\n\t\t\tg_free(name);\n\t\t\tfclose(audio);\n\t\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\t\tg_thread_unref(g_thread_self());\n\t\t\treturn NULL;\n\t\t}\n\t}\n#endif\n\n\t/* Buffer */\n\tchar buf[1500];\n\tmemset(buf, 0, sizeof(buf));\n\t/* Set up RTP */\n\tguint16 seq = 1;\n\tguint32 ts = 0;\n\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\theader->version = 2;\n\theader->markerbit = 1;\n\theader->type = source->codecs.pt;\n\theader->seq_number = htons(seq);\n\theader->timestamp = htonl(ts);\n\theader->ssrc = htonl(1);\t/* The Janus core will fix this anyway */\n\t/* Timer */\n\tstruct timeval now, before;\n\tgettimeofday(&before, NULL);\n\tnow.tv_sec = before.tv_sec;\n\tnow.tv_usec = before.tv_usec;\n\ttime_t passed, d_s, d_us;\n\t/* Loop */\n\tgint read = 0;\n#ifdef HAVE_LIBOGG\n\tconst gint plen = (sizeof(buf)-RTP_HEADER_SIZE);\n#endif\n\tjanus_streaming_rtp_relay_packet packet;\n\twhile(!g_atomic_int_get(&stopping) && !g_atomic_int_get(&mountpoint->destroyed)) {\n\t\t/* See if it's time to prepare a frame */\n\t\tgettimeofday(&now, NULL);\n\t\td_s = now.tv_sec - before.tv_sec;\n\t\td_us = now.tv_usec - before.tv_usec;\n\t\tif(d_us < 0) {\n\t\t\td_us += 1000000;\n\t\t\t--d_s;\n\t\t}\n\t\tpassed = d_s*1000000 + d_us;\n\t\tif(passed < 18000) {\t/* Let's wait about 18ms */\n\t\t\tg_usleep(5000);\n\t\t\tcontinue;\n\t\t}\n\t\t/* Update the reference time */\n\t\tbefore.tv_usec += 20000;\n\t\tif(before.tv_usec > 1000000) {\n\t\t\tbefore.tv_sec++;\n\t\t\tbefore.tv_usec -= 1000000;\n\t\t}\n\t\t/* If paused, wait some more */\n\t\tif(!mountpoint->enabled)\n\t\t\tcontinue;\n\t\tif(source->opus) {\n#ifdef HAVE_LIBOGG\n\t\t\t/* Get the next frame from the Opus file */\n\t\t\tread = janus_streaming_opus_context_read(&opusctx, buf + RTP_HEADER_SIZE, plen);\n#endif\n\t\t} else {\n\t\t\t/* Read frame from file... */\n\t\t\tread = fread(buf + RTP_HEADER_SIZE, sizeof(char), 160, audio);\n\t\t\tif(feof(audio)) {\n\t\t\t\t/* FIXME We're doing this forever... should this be configurable? */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%s] Rewind! (%s)\\n\", name, source->filename);\n\t\t\t\tfseek(audio, 0, SEEK_SET);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif(read < 0)\n\t\t\tbreak;\n\t\tif(mountpoint->active == FALSE)\n\t\t\tmountpoint->active = TRUE;\n\t\t/* Relay on all sessions */\n\t\tpacket.mindex = -1;\n\t\tpacket.data = header;\n\t\tpacket.length = RTP_HEADER_SIZE + read;\n\t\tpacket.is_rtp = TRUE;\n\t\tpacket.is_video = FALSE;\n\t\tpacket.is_kfburst = FALSE;\n\t\t/* Backup the actual payload type, timestamp and sequence number */\n\t\tpacket.ptype = packet.data->type;\n\t\tpacket.timestamp = ntohl(packet.data->timestamp);\n\t\tpacket.seq_number = ntohs(packet.data->seq_number);\n\t\t/* Go! */\n\t\tjanus_mutex_lock_nodebug(&mountpoint->mutex);\n\t\tg_list_foreach(mountpoint->viewers, janus_streaming_relay_rtp_packet, &packet);\n\t\tjanus_mutex_unlock_nodebug(&mountpoint->mutex);\n\t\t/* Update header */\n\t\tseq++;\n\t\theader->seq_number = htons(seq);\n\t\tts += (source->opus ? 960 : 160);\n\t\theader->timestamp = htonl(ts);\n\t\theader->markerbit = 0;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%s] Leaving filesource (live) thread\\n\", name);\n#ifdef HAVE_LIBOGG\n\tif(source->opus)\n\t\tjanus_streaming_opus_context_cleanup(&opusctx);\n#endif\n\tg_free(name);\n\tfclose(audio);\n\tjanus_refcount_decrease(&mountpoint->ref);\n\treturn NULL;\n}\n\n/* Helper method to buffer keyframes + deltas, if needed (and if allowed) */\nstatic void janus_streaming_buffer_keyframe_data(janus_streaming_rtp_source_stream *stream, char *buffer, int bytes) {\n\tif(!stream || !stream->keyframe.enabled || !buffer || bytes < 12)\n\t\treturn;\n\t/* Check if this exceeds ms and/or bytes (the keyframe itself is never impacted) */\n\tif(!stream->keyframe.first_ts && stream->keyframe.bufferkf_ms > 0) {\n\t\t/* TODO Check if this exceeds the duration limit */\n\t\tint64_t now = janus_get_monotonic_time() / 1000;\n\t\tif((now - stream->keyframe.kf_start) > (int64_t)stream->keyframe.bufferkf_ms) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[kf] Not buffering keyframe data (exceeds ms limit)\\n\");\n\t\t\treturn;\n\t\t}\n\t}\n\tif(!stream->keyframe.first_ts && stream->keyframe.bufferkf_bytes > 0) {\n\t\t/* Check if this exceeds the bytes limit */\n\t\tif((stream->keyframe.kf_bytes + bytes) > stream->keyframe.bufferkf_bytes) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[kf] Not buffering keyframe data (exceeds bytes limit)\\n\");\n\t\t\treturn;\n\t\t}\n\t}\n\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\tjanus_streaming_rtp_relay_packet *pkt = g_malloc0(sizeof(janus_streaming_rtp_relay_packet));\n\tpkt->mindex = stream->mindex;\n\tpkt->data = g_malloc(bytes);\n\tmemcpy(pkt->data, buffer, bytes);\n\tpkt->data->ssrc = stream->keyframe.kf_ssrc;\n\tpkt->data->type = stream->codecs.pt;\n\tpkt->is_rtp = TRUE;\n\tpkt->is_video = TRUE;\n\tpkt->is_kfburst = TRUE;\n\tpkt->length = bytes;\n\tpkt->ptype = rtp->type;\n\tpkt->timestamp = ntohl(rtp->timestamp);\n\tpkt->seq_number = ntohs(rtp->seq_number);\n\tstream->keyframe.latest_keyframe = g_list_prepend(stream->keyframe.latest_keyframe, pkt);\n\tstream->keyframe.kf_bytes += bytes;\n}\n\n/* Thread to relay RTP frames coming from gstreamer/ffmpeg/others */\nstatic void *janus_streaming_relay_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Starting streaming relay thread\\n\");\n\tjanus_streaming_mountpoint *mountpoint = (janus_streaming_mountpoint *)data;\n\tif(!mountpoint) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid mountpoint!\\n\");\n\t\treturn NULL;\n\t}\n\tif(mountpoint->streaming_source != janus_streaming_source_rtp) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Not an RTP source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\treturn NULL;\n\t}\n\tjanus_streaming_rtp_source *source = mountpoint->source;\n\tif(source == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%s] Invalid RTP source mountpoint!\\n\", mountpoint->name);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\treturn NULL;\n\t}\n\n\t/* Check how many file descriptors we'll need to monitor */\n\tint num = 0;\n\tjanus_streaming_rtp_source_stream *stream = NULL;\n\tGList *temp = source->media;\n\twhile(temp) {\n\t\tstream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\tif(stream->fd[0] != -1)\n\t\t\tnum++;\n\t\tif(stream->fd[1] != -1)\n\t\t\tnum++;\n\t\tif(stream->fd[2] != -1)\n\t\t\tnum++;\n\t\tif(stream->rtcp_fd != -1)\n\t\t\tnum++;\n\t\ttemp = temp->next;\n\t}\n\tnum++;\t/* There's the pipe too */\n\t/* Keep track of how many sockets we're monitoring, as with RTSP that may change later */\n\tint numtot = num;\n\n\t/* Add a reference to the helper threads, if needed */\n\tif(mountpoint->helper_threads > 0) {\n\t\tGList *l = mountpoint->threads;\n\t\twhile(l) {\n\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\tjanus_refcount_increase(&ht->ref);\n\t\t\tl = l->next;\n\t\t}\n\t}\n\n\tchar *name = g_strdup(mountpoint->name ? mountpoint->name : \"??\");\n\t/* Needed to fix seq and ts */\n\tuint32_t ssrc = 0;\n\t/* File descriptors */\n\tsocklen_t addrlen;\n\tstruct sockaddr_storage remote;\n\tint resfd = 0, bytes = 0;\n\tstruct pollfd *fds = g_malloc(numtot * sizeof(struct pollfd));\n\tchar buffer[1500];\n\tmemset(buffer, 0, 1500);\n\t/* We'll have a dynamic number of streams */\n#ifdef HAVE_LIBCURL\n\t/* In case this is an RTSP restreamer, we may have to send keep-alive from time to time */\n\tgint64 now = janus_get_monotonic_time(), before = now, ka_timeout = 0;\n\tif(source->rtsp) {\n\t\tsource->reconnect_timer = now;\n\t\tka_timeout = source->ka_timeout;\n\t}\n\tgboolean connected = TRUE;\n#endif\n\t/* Loop */\n\tjanus_streaming_rtp_relay_packet packet;\n\twhile(!g_atomic_int_get(&stopping) && !g_atomic_int_get(&mountpoint->destroyed)) {\n#ifdef HAVE_LIBCURL\n\t\t/* Let's check regularly if the RTSP server seems to be gone */\n\t\tif(source->rtsp) {\n\t\t\tif(source->reconnecting) {\n\t\t\t\t/* We're still reconnecting, wait some more */\n\t\t\t\tg_usleep(250000);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tnow = janus_get_monotonic_time();\n\t\t\tif(!source->reconnecting && (now - source->reconnect_timer > source->reconnect_delay)) {\n\t\t\t\t/*  Assume the RTSP server has gone and schedule a reconnect */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] %\"SCNi64\"s passed with no media, trying to reconnect the RTSP stream\\n\",\n\t\t\t\t\tname, (now - source->reconnect_timer)/G_USEC_PER_SEC);\n\t\t\t\ttemp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tstream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\tif(stream->fd[0] > -1) {\n\t\t\t\t\t\tg_hash_table_remove(source->media_byfd, GINT_TO_POINTER(stream->fd[0]));\n\t\t\t\t\t\tclose(stream->fd[0]);\n\t\t\t\t\t}\n\t\t\t\t\tstream->fd[0] = -1;\n\t\t\t\t\tif(stream->fd[1] > -1) {\n\t\t\t\t\t\tg_hash_table_remove(source->media_byfd, GINT_TO_POINTER(stream->fd[1]));\n\t\t\t\t\t\tclose(stream->fd[1]);\n\t\t\t\t\t}\n\t\t\t\t\tstream->fd[1] = -1;\n\t\t\t\t\tif(stream->fd[2] > -1) {\n\t\t\t\t\t\tg_hash_table_remove(source->media_byfd, GINT_TO_POINTER(stream->fd[2]));\n\t\t\t\t\t\tclose(stream->fd[2]);\n\t\t\t\t\t}\n\t\t\t\t\tstream->fd[2] = -1;\n\t\t\t\t\tif(stream->rtcp_fd > -1) {\n\t\t\t\t\t\tg_hash_table_remove(source->media_byfd, GINT_TO_POINTER(stream->rtcp_fd));\n\t\t\t\t\t\tclose(stream->rtcp_fd);\n\t\t\t\t\t}\n\t\t\t\t\tstream->rtcp_fd = -1;\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\t/* Let's clean up the source first */\n\t\t\t\tcurl_easy_cleanup(source->curl);\n\t\t\t\tsource->curl = NULL;\n\t\t\t\tg_free(source->curl_errbuf);\n\t\t\t\tsource->curl_errbuf = NULL;\n\t\t\t\tif(source->curldata)\n\t\t\t\t\tg_free(source->curldata->buffer);\n\t\t\t\tg_free(source->curldata);\n\t\t\t\tsource->curldata = NULL;\n\t\t\t\tif(g_atomic_int_get(&mountpoint->destroyed))\n\t\t\t\t\tbreak;\n\t\t\t\tif(connected) {\n\t\t\t\t\t/* Notify users and/or event handlers about this disconnection */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtsp-disconnected\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mountpoint->id_str) : json_integer(mountpoint->id));\n\t\t\t\t\t\tgateway->notify_event(&janus_streaming_plugin, NULL, info);\n\t\t\t\t\t}\n\t\t\t\t\tif(source->rtsp_notify_changes) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtsp-disconnected\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mountpoint->id_str) : json_integer(mountpoint->id));\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoint->mutex);\n\t\t\t\t\t\tjanus_streaming_notify_subscribers(mountpoint, info);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoint->mutex);\n\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Now let's try to reconnect */\n\t\t\t\tsource->reconnect_timer = now;\n\t\t\t\tconnected = FALSE;\n\t\t\t\tsource->reconnecting = TRUE;\n\t\t\t\tif(janus_streaming_rtsp_connect_to_server(mountpoint) < 0) {\n\t\t\t\t\t/* Reconnection failed? Let's try again later */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Reconnection of the RTSP stream failed, trying again in a few seconds...\\n\", name);\n\t\t\t\t} else {\n\t\t\t\t\t/* We're connected, let's send a PLAY */\n\t\t\t\t\tif(janus_streaming_rtsp_play(source) < 0) {\n\t\t\t\t\t\t/* Error trying to play? Let's try again later */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] RTSP PLAY failed, trying again in a few seconds...\\n\", name);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Everything should be back to normal */\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Reconnected to the RTSP server, streaming again\\n\", name);\n\t\t\t\t\t\tka_timeout = source->ka_timeout;\n\t\t\t\t\t\tconnected = TRUE;\n\t\t\t\t\t\t/* Check if the number of sockets to monitor changed */\n\t\t\t\t\t\tnum = 0;\n\t\t\t\t\t\ttemp = source->media;\n\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\tstream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\t\t\tif(stream->fd[0] != -1)\n\t\t\t\t\t\t\t\tnum++;\n\t\t\t\t\t\t\tif(stream->fd[1] != -1)\n\t\t\t\t\t\t\t\tnum++;\n\t\t\t\t\t\t\tif(stream->fd[2] != -1)\n\t\t\t\t\t\t\t\tnum++;\n\t\t\t\t\t\t\tif(stream->rtcp_fd != -1)\n\t\t\t\t\t\t\t\tnum++;\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnum++;\t/* There's the pipe too */\n\t\t\t\t\t\tif(num > numtot) {\n\t\t\t\t\t\t\t/* Reallocate the poll list */\n\t\t\t\t\t\t\tnumtot = num;\n\t\t\t\t\t\t\tfds = g_realloc(fds, numtot * sizeof(struct pollfd));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Notify users and/or event handlers about this reconnection */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtsp-reconnected\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mountpoint->id_str) : json_integer(mountpoint->id));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_streaming_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(source->rtsp_notify_changes) {\n\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtsp-reconnected\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(mountpoint->id_str) : json_integer(mountpoint->id));\n\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoint->mutex);\n\t\t\t\t\t\t\tjanus_streaming_notify_subscribers(mountpoint, info);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoint->mutex);\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsource->reconnect_timer = janus_get_monotonic_time();\n\t\t\t\tsource->reconnecting = FALSE;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif(source->reconnecting || !connected) {\n\t\t\t/* No socket, we may be in the process of reconnecting, or waiting to reconnect */\n\t\t\tg_usleep(source->reconnect_delay);\n\t\t\tcontinue;\n\t\t}\n\t\t/* We may also need to occasionally send a OPTIONS request as a keep-alive */\n\t\tif(ka_timeout > 0) {\n\t\t\t/* Let's be conservative and send a OPTIONS when half of the timeout has passed */\n\t\t\tnow = janus_get_monotonic_time();\n\t\t\tif(now-before > ka_timeout && source->curldata) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%s] %\"SCNi64\"s passed, sending OPTIONS\\n\", name, (now-before)/G_USEC_PER_SEC);\n\t\t\t\tbefore = now;\n\t\t\t\t/* Send an RTSP OPTIONS */\n\t\t\t\tjanus_mutex_lock(&source->rtsp_mutex);\n\t\t\t\tg_free(source->curldata->buffer);\n\t\t\t\tsource->curldata->buffer = g_malloc0(1);\n\t\t\t\tsource->curldata->size = 0;\n\t\t\t\tcurl_easy_setopt(source->curl, CURLOPT_RTSP_STREAM_URI, source->rtsp_url);\n\t\t\t\tcurl_easy_setopt(source->curl, CURLOPT_RTSP_REQUEST, (long)CURL_RTSPREQ_OPTIONS);\n\t\t\t\tresfd = curl_easy_perform(source->curl);\n\t\t\t\tif(resfd != CURLE_OK) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Couldn't send OPTIONS request: %s (%s)\\n\",\n\t\t\t\t\t\tname, curl_easy_strerror(resfd), source->curl_errbuf);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&source->rtsp_mutex);\n\t\t\t}\n\t\t}\n#endif\n\t\t/* Prepare poll */\n\t\tnum = 0;\n\t\ttemp = source->media;\n\t\twhile(temp) {\n\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\tif(stream->fd[0] != -1) {\n\t\t\t\tfds[num].fd = stream->fd[0];\n\t\t\t\tfds[num].events = POLLIN;\n\t\t\t\tfds[num].revents = 0;\n\t\t\t\tnum++;\n\t\t\t}\n\t\t\tif(stream->fd[1] != -1) {\n\t\t\t\tfds[num].fd = stream->fd[1];\n\t\t\t\tfds[num].events = POLLIN;\n\t\t\t\tfds[num].revents = 0;\n\t\t\t\tnum++;\n\t\t\t}\n\t\t\tif(stream->fd[2] != -1) {\n\t\t\t\tfds[num].fd = stream->fd[2];\n\t\t\t\tfds[num].events = POLLIN;\n\t\t\t\tfds[num].revents = 0;\n\t\t\t\tnum++;\n\t\t\t}\n\t\t\tif(stream->rtcp_fd != -1) {\n\t\t\t\tfds[num].fd = stream->rtcp_fd;\n\t\t\t\tfds[num].events = POLLIN;\n\t\t\t\tfds[num].revents = 0;\n\t\t\t\tnum++;\n\t\t\t}\n\t\t\t/* Any PLI and/or REMB we should send back to the source? */\n\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_VIDEO) {\n\t\t\t\tif(g_atomic_int_get(&stream->need_pli))\n\t\t\t\t\tjanus_streaming_rtcp_pli_send(stream);\n\t\t\t\tif(stream->rtcp_fd > -1 && source->lowest_bitrate > 0) {\n\t\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\t\tif(source->remb_latest == 0)\n\t\t\t\t\t\tsource->remb_latest = now;\n\t\t\t\t\telse if(now - source->remb_latest >= G_USEC_PER_SEC)\n\t\t\t\t\t\tjanus_streaming_rtcp_remb_send(source, stream);\n\t\t\t\t}\n\t\t\t}\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tif(source->pipefd[0] != -1) {\n\t\t\tfds[num].fd = source->pipefd[0];\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\t/* Wait for some data */\n\t\tresfd = poll(fds, num, 1000);\n\t\tif(resfd < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s] Got an EINTR (%s), ignoring...\\n\", name, g_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Error polling... %d (%s)\\n\", name, errno, g_strerror(errno));\n\t\t\tmountpoint->enabled = FALSE;\n\t\t\tjanus_mutex_lock(&source->rec_mutex);\n\t\t\tGList *temp = source->media;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\tjanus_recorder_close(stream->rc);\n\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Closed %s recording %s (%s)\\n\", mountpoint->name,\n\t\t\t\t\tjanus_streaming_media_str(stream->type), stream->rc->filename, stream->mid);\n\t\t\t\tjanus_recorder *tmp = stream->rc;\n\t\t\t\tstream->rc = NULL;\n\t\t\t\tjanus_recorder_destroy(tmp);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\tbreak;\n\t\t} else if(resfd == 0) {\n\t\t\t/* No data, keep going */\n\t\t\tcontinue;\n\t\t}\n\t\tint i = 0;\n\t\tfor(i=0; i<num; i++) {\n\t\t\tif(fds[i].revents & (POLLERR | POLLHUP)) {\n\t\t\t\t/* Socket error? */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Error polling: %s... %d (%s)\\n\", name,\n\t\t\t\t\tfds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\", errno, g_strerror(errno));\n\t\t\t\tmountpoint->enabled = FALSE;\n\t\t\t\tjanus_mutex_lock(&source->rec_mutex);\n\t\t\t\tGList *temp = source->media;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\t\t\t\tjanus_recorder_close(stream->rc);\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] Closed %s recording %s (%s)\\n\", mountpoint->name,\n\t\t\t\t\t\tjanus_streaming_media_str(stream->type), stream->rc->filename, stream->mid);\n\t\t\t\t\tjanus_recorder *tmp = stream->rc;\n\t\t\t\t\tstream->rc = NULL;\n\t\t\t\t\tjanus_recorder_destroy(tmp);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&source->rec_mutex);\n\t\t\t\tbreak;\n\t\t\t} else if(fds[i].revents & POLLIN) {\n\t\t\t\t/* Got an RTP or data packet */\n\t\t\t\tif(fds[i].fd == source->pipefd[0]) {\n\t\t\t\t\t/* We're done here */\n\t\t\t\t\tint code = 0;\n\t\t\t\t\tbytes = read(fds[i].fd, &code, sizeof(int));\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%s] Interrupting mountpoint\\n\", mountpoint->name);\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\t/* Check which stream this file descriptor belongs to */\n\t\t\t\t\tstream = g_hash_table_lookup(source->media_byfd, GINT_TO_POINTER(fds[i].fd));\n\t\t\t\t}\n\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t/* No stream..? Shouldn't happen, read the bytes and dump them */\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\t(void)recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(stream->type == JANUS_STREAMING_MEDIA_AUDIO && fds[i].fd == stream->fd[0]) {\n\t\t\t\t\t/* Got something audio (RTP) */\n\t\t\t\t\tif(mountpoint->active == FALSE)\n\t\t\t\t\t\tmountpoint->active = TRUE;\n\t\t\t\t\tgint64 now = janus_get_monotonic_time();\n#ifdef HAVE_LIBCURL\n\t\t\t\t\tsource->reconnect_timer = now;\n#endif\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || !janus_is_rtp(buffer, bytes)) {\n\t\t\t\t\t\t/* Failed to read or not an RTP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t\t\t\t\tssrc = ntohl(rtp->ssrc);\n\t\t\t\t\tif(source->rtp_collision > 0 && stream->last_ssrc[0] && ssrc != stream->last_ssrc[0] &&\n\t\t\t\t\t\t\t(now-stream->last_received[0]) < (gint64)1000*source->rtp_collision) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] RTP collision on audio mountpoint, dropping packet (#%d, ssrc=%\"SCNu32\")\\n\",\n\t\t\t\t\t\t\tname, stream->mindex, ssrc);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tstream->last_received[0] = now;\n\t\t\t\t\t/* Do we have a new stream? */\n\t\t\t\t\tif(ssrc != stream->last_ssrc[0]) {\n\t\t\t\t\t\tstream->ssrc = stream->last_ssrc[0] = ssrc;\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] New audio stream! (#%d, ssrc=%\"SCNu32\")\\n\", name, stream->mindex, ssrc);\n\t\t\t\t\t}\n\t\t\t\t\t/* If paused, ignore this packet */\n\t\t\t\t\tif(!mountpoint->enabled && !stream->rc)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t/* Is this SRTP? */\n\t\t\t\t\tif(source->is_srtp) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect(source->srtp_ctx, buffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t\t\tguint32 timestamp = ntohl(rtp->timestamp);\n\t\t\t\t\t\t\tguint16 seq = ntohs(rtp->seq_number);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Audio (#%d) SRTP unprotect error: %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")\\n\",\n\t\t\t\t\t\t\t\tname, stream->mindex, janus_srtp_error_str(res), bytes, buflen, timestamp, seq);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* Relay on all sessions */\n\t\t\t\t\tpacket.mindex = stream->mindex;\n\t\t\t\t\tpacket.data = rtp;\n\t\t\t\t\tpacket.length = bytes;\n\t\t\t\t\tpacket.is_rtp = TRUE;\n\t\t\t\t\tpacket.is_video = FALSE;\n\t\t\t\t\tpacket.is_kfburst = FALSE;\n\t\t\t\t\tpacket.data->type = stream->codecs.pt;\n\t\t\t\t\t/* Is there a recorder? */\n\t\t\t\t\tjanus_rtp_header_update(packet.data, &stream->context[0], FALSE, 0);\n\t\t\t\t\tif(stream->skew) {\n\t\t\t\t\t\tint ret = janus_rtp_skew_compensate_audio(packet.data, &stream->context[0], now);\n\t\t\t\t\t\tif(ret < 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Dropping %d packets, audio source clock is too fast (#%d, ssrc=%\"SCNu32\")\\n\",\n\t\t\t\t\t\t\t\tname, -ret, stream->mindex, ssrc);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t} else if(ret > 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Jumping %d RTP sequence numbers, audio source clock is too slow (#%d, ssrc=%\"SCNu32\")\\n\",\n\t\t\t\t\t\t\t\tname, ret, stream->mindex, ssrc);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->rc) {\n\t\t\t\t\t\tpacket.data->ssrc = htonl((uint32_t)mountpoint->id);\n\t\t\t\t\t\tjanus_recorder_save_frame(stream->rc, buffer, bytes);\n\t\t\t\t\t}\n\t\t\t\t\tif(mountpoint->enabled) {\n\t\t\t\t\t\tpacket.data->ssrc = htonl(ssrc);\n\t\t\t\t\t\t/* Backup the actual payload type, timestamp and sequence number set by the restreamer, in case switching is involved */\n\t\t\t\t\t\tpacket.ptype = packet.data->type;\n\t\t\t\t\t\tpacket.timestamp = ntohl(packet.data->timestamp);\n\t\t\t\t\t\tpacket.seq_number = ntohs(packet.data->seq_number);\n\t\t\t\t\t\t/* Go! */\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoint->mutex);\n\t\t\t\t\t\tg_list_foreach(mountpoint->helper_threads == 0 ? mountpoint->viewers : mountpoint->threads,\n\t\t\t\t\t\t\tmountpoint->helper_threads == 0 ? janus_streaming_relay_rtp_packet : janus_streaming_helper_rtprtcp_packet,\n\t\t\t\t\t\t\t&packet);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoint->mutex);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_VIDEO && ((fds[i].fd == stream->fd[0]) ||\n\t\t\t\t\t\t(fds[i].fd == stream->fd[1]) || (fds[i].fd == stream->fd[2]))) {\n\t\t\t\t\t/* Got something video (RTP) */\n\t\t\t\t\tint index = -1;\n\t\t\t\t\tif(fds[i].fd == stream->fd[0])\n\t\t\t\t\t\tindex = 0;\n\t\t\t\t\telse if(fds[i].fd == stream->fd[1])\n\t\t\t\t\t\tindex = 1;\n\t\t\t\t\telse if(fds[i].fd == stream->fd[2])\n\t\t\t\t\t\tindex = 2;\n\t\t\t\t\tif(mountpoint->active == FALSE)\n\t\t\t\t\t\tmountpoint->active = TRUE;\n\t\t\t\t\tgint64 now = janus_get_monotonic_time();\n#ifdef HAVE_LIBCURL\n\t\t\t\t\tsource->reconnect_timer = now;\n#endif\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || !janus_is_rtp(buffer, bytes)) {\n\t\t\t\t\t\t/* Failed to read or not an RTP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t\t\t\t\tssrc = ntohl(rtp->ssrc);\n\t\t\t\t\tif(source->rtp_collision > 0 && stream->last_ssrc[index] && ssrc != stream->last_ssrc[index] &&\n\t\t\t\t\t\t\t(now-stream->last_received[index]) < (gint64)1000*source->rtp_collision) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] RTP collision on video mountpoint, dropping packet (#%d, ssrc=%\"SCNu32\")\\n\",\n\t\t\t\t\t\t\tname, stream->mindex, ssrc);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tstream->last_received[index] = now;\n\t\t\t\t\t/* Do we have a new stream? */\n\t\t\t\t\tif(ssrc != stream->last_ssrc[index]) {\n\t\t\t\t\t\tstream->last_ssrc[index] = ssrc;\n\t\t\t\t\t\tif(index == 0)\n\t\t\t\t\t\t\tstream->ssrc = ssrc;\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%s] New video stream! (#%d, ssrc=%\"SCNu32\", index %d)\\n\",\n\t\t\t\t\t\t\tname, stream->mindex, ssrc, index);\n\t\t\t\t\t}\n\t\t\t\t\t/* Is this SRTP? */\n\t\t\t\t\tif(source->is_srtp) {\n\t\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect(source->srtp_ctx, buffer, &buflen);\n\t\t\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t\t\tguint32 timestamp = ntohl(rtp->timestamp);\n\t\t\t\t\t\t\tguint16 seq = ntohs(rtp->seq_number);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Video (#%d) SRTP unprotect error: %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")\\n\",\n\t\t\t\t\t\t\t\tname, stream->mindex, janus_srtp_error_str(res), bytes, buflen, timestamp, seq);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbytes = buflen;\n\t\t\t\t\t}\n\t\t\t\t\t/* First of all, let's check if this is (part of) a keyframe that we may need to save it for future reference */\n\t\t\t\t\tif(index == 0 && stream->keyframe.enabled) {\n\t\t\t\t\t\t/* Check how we should process this packet */\n\t\t\t\t\t\tint plen = 0;\n\t\t\t\t\t\tchar *payload = janus_rtp_payload(buffer, bytes, &plen);\n\t\t\t\t\t\tgboolean keyframe = janus_is_keyframe(stream->codecs.video_codec, payload, plen);\n\t\t\t\t\t\tjanus_mutex_lock(&stream->keyframe.mutex);\n\t\t\t\t\t\tif(!keyframe && stream->keyframe.latest_keyframe != NULL && ntohl(rtp->timestamp) == stream->keyframe.kf_ts) {\n\t\t\t\t\t\t\t/* New fragment of the latest frame we received (keyframe or not),\n\t\t\t\t\t\t\t * re-use the same SSRC we allocated before for this specific frame */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[kf]   -- Updating frame (ts=%\"SCNu32\", ssrc=%\"SCNu32\")\\n\",\n\t\t\t\t\t\t\t\tstream->keyframe.kf_ts, stream->keyframe.kf_ssrc);\n\t\t\t\t\t\t\tjanus_streaming_buffer_keyframe_data(stream, buffer, bytes);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* New frame: check if it's a delta or a keyframe. If it's a\n\t\t\t\t\t\t\t * keyframe, it means we can start a new list and get rid of the\n\t\t\t\t\t\t\t * previous (and now old) one, if we had one; if it's a delta,\n\t\t\t\t\t\t\t * we append it to the list if it exists, and drop it if it\n\t\t\t\t\t\t\t * doesn't (as it makes no sense to start from a delta) */\n\t\t\t\t\t\t\tif(keyframe) {\n\t\t\t\t\t\t\t\t/* This is a keyframe: remove the old list, if\n\t\t\t\t\t\t\t\t * we had one, and start a new one from scratch */\n\t\t\t\t\t\t\t\tif(stream->keyframe.latest_keyframe != NULL)\n\t\t\t\t\t\t\t\t\tg_list_free_full(stream->keyframe.latest_keyframe, (GDestroyNotify)janus_streaming_rtp_relay_packet_free);\n\t\t\t\t\t\t\t\tstream->keyframe.latest_keyframe = NULL;\n\t\t\t\t\t\t\t\tstream->keyframe.kf_ssrc = janus_random_uint32();\n\t\t\t\t\t\t\t\tstream->keyframe.kf_ts = ntohl(rtp->timestamp);\n\t\t\t\t\t\t\t\tstream->keyframe.kf_bytes = 0;\n\t\t\t\t\t\t\t\tstream->keyframe.kf_start = janus_get_monotonic_time() / 1000;\n\t\t\t\t\t\t\t\tstream->keyframe.first_ts = TRUE;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[kf] New keyframe (ts=%\"SCNu32\", ssrc=%\"SCNu32\")\\n\",\n\t\t\t\t\t\t\t\t\tstream->keyframe.kf_ts, stream->keyframe.kf_ssrc);\n\t\t\t\t\t\t\t\tjanus_streaming_buffer_keyframe_data(stream, buffer, bytes);\n\t\t\t\t\t\t\t} else if(stream->keyframe.latest_keyframe != NULL) {\n\t\t\t\t\t\t\t\t/* This is a new delta: track the timestamp, allocate\n\t\t\t\t\t\t\t\t * a new SSRC, and add the packet to our existing list */\n\t\t\t\t\t\t\t\tstream->keyframe.kf_ssrc = janus_random_uint32();\n\t\t\t\t\t\t\t\tstream->keyframe.kf_ts = ntohl(rtp->timestamp);\n\t\t\t\t\t\t\t\tstream->keyframe.first_ts = FALSE;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[kf] New delta (ts=%\"SCNu32\", ssrc=%\"SCNu32\")\\n\",\n\t\t\t\t\t\t\t\t\tstream->keyframe.kf_ts, stream->keyframe.kf_ssrc);\n\t\t\t\t\t\t\t\tjanus_streaming_buffer_keyframe_data(stream, buffer, bytes);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[kf] Dropping initial delta on empty list\\n\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&stream->keyframe.mutex);\n\t\t\t\t\t}\n\t\t\t\t\t/* If paused, ignore this packet */\n\t\t\t\t\tif(!mountpoint->enabled && !stream->rc)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t/* Relay on all sessions */\n\t\t\t\t\tpacket.mindex = stream->mindex;\n\t\t\t\t\tpacket.data = rtp;\n\t\t\t\t\tpacket.length = bytes;\n\t\t\t\t\tpacket.is_rtp = TRUE;\n\t\t\t\t\tpacket.is_video = TRUE;\n\t\t\t\t\tpacket.is_kfburst = FALSE;\n\t\t\t\t\tpacket.simulcast = stream->simulcast;\n\t\t\t\t\tpacket.substream = index;\n\t\t\t\t\tpacket.codec = stream->codecs.video_codec;\n\t\t\t\t\tpacket.svc = FALSE;\n\t\t\t\t\tif(stream->svc) {\n\t\t\t\t\t\t/* We're doing SVC: let's parse this packet to see which layers are there */\n\t\t\t\t\t\tint plen = 0;\n\t\t\t\t\t\tchar *payload = janus_rtp_payload(buffer, bytes, &plen);\n\t\t\t\t\t\tif(payload) {\n\t\t\t\t\t\t\tgboolean found = FALSE;\n\t\t\t\t\t\t\tmemset(&packet.svc_info, 0, sizeof(packet.svc_info));\n\t\t\t\t\t\t\tif(janus_vp9_parse_svc(payload, plen, &found, &packet.svc_info) == 0) {\n\t\t\t\t\t\t\t\tpacket.svc = found;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpacket.data->type = stream->codecs.pt;\n\t\t\t\t\t/* Is there a recorder? (FIXME notice we only record the first substream, if simulcasting) */\n\t\t\t\t\tjanus_rtp_header_update(packet.data, &stream->context[index], TRUE, 0);\n\t\t\t\t\tif(stream->skew) {\n\t\t\t\t\t\tint ret = janus_rtp_skew_compensate_video(packet.data, &stream->context[index], now);\n\t\t\t\t\t\tif(ret < 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Dropping %d packets, video source clock is too fast (#%d, ssrc=%\"SCNu32\", index %d)\\n\",\n\t\t\t\t\t\t\t\tname, -ret, stream->mindex, ssrc, index);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t} else if(ret > 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s] Jumping %d RTP sequence numbers, video source clock is too slow (#%d, ssrc=%\"SCNu32\", index %d)\\n\",\n\t\t\t\t\t\t\t\tname, ret, stream->mindex, ssrc, index);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(stream->h264_spspps) {\n\t\t\t\t\t\tint plen = 0;\n\t\t\t\t\t\tchar *payload = janus_rtp_payload((char *)packet.data, bytes, &plen);\n\t\t\t\t\t\t/* We have our own SPS/PPS to send, check if we just received a keyframe */\n\t\t\t\t\t\tif(payload && janus_h264_is_i_frame(payload, plen)) {\n\t\t\t\t\t\t\t/* This is an I-frame: prepend an SPS/PPS packet */\n\t\t\t\t\t\t\tjanus_rtp_header *sps_rtp = (janus_rtp_header *)stream->h264_spspps;\n\t\t\t\t\t\t\tsps_rtp->type = rtp->type;\n\t\t\t\t\t\t\tsps_rtp->seq_number = rtp->seq_number;\n\t\t\t\t\t\t\trtp->seq_number = htons(ntohs(rtp->seq_number) + 1);\n\t\t\t\t\t\t\tstream->context[index].base_seq--;\n\t\t\t\t\t\t\tsps_rtp->timestamp = rtp->timestamp;\n\t\t\t\t\t\t\t/* Save the packet, if needed */\n\t\t\t\t\t\t\tsps_rtp->ssrc = htonl((uint32_t)mountpoint->id);\n\t\t\t\t\t\t\tjanus_recorder_save_frame(stream->rc, stream->h264_spspps, stream->h264_spspps_len);\n\t\t\t\t\t\t\tsps_rtp->ssrc = rtp->ssrc;\n\t\t\t\t\t\t\t/* Relay on all sessions */\n\t\t\t\t\t\t\tjanus_streaming_rtp_relay_packet spspkt = { 0 };\n\t\t\t\t\t\t\tspspkt.mindex = stream->mindex;\n\t\t\t\t\t\t\tspspkt.data = sps_rtp;\n\t\t\t\t\t\t\tspspkt.length = stream->h264_spspps_len;\n\t\t\t\t\t\t\tspspkt.is_rtp = TRUE;\n\t\t\t\t\t\t\tspspkt.is_video = TRUE;\n\t\t\t\t\t\t\tspspkt.is_kfburst = FALSE;\n\t\t\t\t\t\t\tspspkt.simulcast = FALSE;\n\t\t\t\t\t\t\tspspkt.codec = stream->codecs.video_codec;\n\t\t\t\t\t\t\tspspkt.svc = FALSE;\n\t\t\t\t\t\t\tspspkt.ptype = spspkt.data->type;\n\t\t\t\t\t\t\tspspkt.timestamp = ntohl(spspkt.data->timestamp);\n\t\t\t\t\t\t\tspspkt.seq_number = ntohs(spspkt.data->seq_number);\n\t\t\t\t\t\t\tjanus_mutex_lock(&mountpoint->mutex);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s] Sending SPS/PPS (seq=%\"SCNu16\", ts=%\"SCNu32\")\\n\", name,\n\t\t\t\t\t\t\t\tntohs(spspkt.data->seq_number), ntohl(spspkt.data->timestamp));\n\t\t\t\t\t\t\tg_list_foreach(mountpoint->helper_threads == 0 ? mountpoint->viewers : mountpoint->threads,\n\t\t\t\t\t\t\t\tmountpoint->helper_threads == 0 ? janus_streaming_relay_rtp_packet : janus_streaming_helper_rtprtcp_packet,\n\t\t\t\t\t\t\t\t&spspkt);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&mountpoint->mutex);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(index == 0 && stream->rc) {\n\t\t\t\t\t\tpacket.data->ssrc = htonl((uint32_t)mountpoint->id);\n\t\t\t\t\t\tjanus_recorder_save_frame(stream->rc, buffer, bytes);\n\t\t\t\t\t}\n\t\t\t\t\tif(mountpoint->enabled) {\n\t\t\t\t\t\tpacket.data->ssrc = htonl(ssrc);\n\t\t\t\t\t\t/* Backup the actual payload type, timestamp and sequence number set by the restreamer, in case switching is involved */\n\t\t\t\t\t\tpacket.ptype = packet.data->type;\n\t\t\t\t\t\tpacket.timestamp = ntohl(packet.data->timestamp);\n\t\t\t\t\t\tpacket.seq_number = ntohs(packet.data->seq_number);\n\t\t\t\t\t\t/* Take note of the simulcast SSRCs */\n\t\t\t\t\t\tif(stream->simulcast) {\n\t\t\t\t\t\t\tpacket.ssrc[0] = stream->last_ssrc[0];\n\t\t\t\t\t\t\tpacket.ssrc[1] = stream->last_ssrc[1];\n\t\t\t\t\t\t\tpacket.ssrc[2] = stream->last_ssrc[2];\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Go! */\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoint->mutex);\n\t\t\t\t\t\tg_list_foreach(mountpoint->helper_threads == 0 ? mountpoint->viewers : mountpoint->threads,\n\t\t\t\t\t\t\tmountpoint->helper_threads == 0 ? janus_streaming_relay_rtp_packet : janus_streaming_helper_rtprtcp_packet,\n\t\t\t\t\t\t\t&packet);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoint->mutex);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(stream->type == JANUS_STREAMING_MEDIA_DATA && fds[i].fd == stream->fd[0]) {\n\t\t\t\t\t/* Got something data (text) */\n\t\t\t\t\tif(mountpoint->active == FALSE)\n\t\t\t\t\t\tmountpoint->active = TRUE;\n\t\t\t\t\tstream->last_received[0] = janus_get_monotonic_time();\n#ifdef HAVE_LIBCURL\n\t\t\t\t\tsource->reconnect_timer = janus_get_monotonic_time();\n#endif\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 1) {\n\t\t\t\t\t\t/* Failed to read? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(!mountpoint->enabled && !stream->rc)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t/* Copy the data */\n\t\t\t\t\tchar *data = g_malloc(bytes);\n\t\t\t\t\tmemcpy(data, buffer, bytes);\n\t\t\t\t\t/* Relay on all sessions */\n\t\t\t\t\tpacket.mindex = stream->mindex;\n\t\t\t\t\tpacket.data = (janus_rtp_header *)data;\n\t\t\t\t\tpacket.length = bytes;\n\t\t\t\t\tpacket.is_rtp = FALSE;\n\t\t\t\t\tpacket.is_data = TRUE;\n\t\t\t\t\tpacket.textdata = stream->textdata;\n\t\t\t\t\t/* Is there a recorder? */\n\t\t\t\t\tjanus_recorder_save_frame(stream->rc, data, bytes);\n\t\t\t\t\tif(mountpoint->enabled) {\n\t\t\t\t\t\t/* Are we keeping track of the last message being relayed? */\n\t\t\t\t\t\tif(stream->buffermsg) {\n\t\t\t\t\t\t\tjanus_mutex_lock(&stream->buffermsg_mutex);\n\t\t\t\t\t\t\tif(stream->last_msg != NULL) {\n\t\t\t\t\t\t\t\tjanus_streaming_rtp_relay_packet_free((janus_streaming_rtp_relay_packet *)stream->last_msg);\n\t\t\t\t\t\t\t\tstream->last_msg = NULL;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_streaming_rtp_relay_packet *pkt = g_malloc0(sizeof(janus_streaming_rtp_relay_packet));\n\t\t\t\t\t\t\tpkt->data = g_malloc(bytes);\n\t\t\t\t\t\t\tmemcpy(pkt->data, data, bytes);\n\t\t\t\t\t\t\tpkt->mindex = stream->mindex;\n\t\t\t\t\t\t\tpkt->is_data = TRUE;\n\t\t\t\t\t\t\tpkt->textdata = stream->textdata;\n\t\t\t\t\t\t\tpkt->length = bytes;\n\t\t\t\t\t\t\t/* Store the latest message */\n\t\t\t\t\t\t\tstream->last_msg = pkt;\n\t\t\t\t\t\t\tjanus_mutex_unlock(&stream->buffermsg_mutex);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Go! */\n\t\t\t\t\t\tjanus_mutex_lock(&mountpoint->mutex);\n\t\t\t\t\t\tg_list_foreach(mountpoint->helper_threads == 0 ? mountpoint->viewers : mountpoint->threads,\n\t\t\t\t\t\t\tmountpoint->helper_threads == 0 ? janus_streaming_relay_rtp_packet : janus_streaming_helper_rtprtcp_packet,\n\t\t\t\t\t\t\t&packet);\n\t\t\t\t\t\tjanus_mutex_unlock(&mountpoint->mutex);\n\t\t\t\t\t}\n\t\t\t\t\tg_free(packet.data);\n\t\t\t\t\tpacket.data = NULL;\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(fds[i].fd == stream->rtcp_fd) {\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || (!janus_is_rtp(buffer, bytes) && !janus_is_rtcp(buffer, bytes))) {\n\t\t\t\t\t\t/* For latching we need an RTP or RTCP packet */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(!mountpoint->enabled)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tmemcpy(&stream->rtcp_addr, &remote, addrlen);\n\t\t\t\t\tif(!janus_is_rtcp(buffer, bytes)) {\n\t\t\t\t\t\t/* Failed to read or not an RTCP packet? */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s] Got audio/video RTCP feedback: #%d, SSRC %\"SCNu32\"\\n\",\n\t\t\t\t\t\tname, stream->mindex, janus_rtcp_get_sender_ssrc(buffer, bytes));\n\t\t\t\t\t/* Relay on all sessions */\n\t\t\t\t\tpacket.mindex = stream->mindex;\n\t\t\t\t\tpacket.is_rtp = FALSE;\n\t\t\t\t\tpacket.is_video = (stream->type == JANUS_STREAMING_MEDIA_VIDEO);\n\t\t\t\t\tpacket.data = (janus_rtp_header *)buffer;\n\t\t\t\t\tpacket.length = bytes;\n\t\t\t\t\t/* Go! */\n\t\t\t\t\tjanus_mutex_lock(&mountpoint->mutex);\n\t\t\t\t\tg_list_foreach(mountpoint->helper_threads == 0 ? mountpoint->viewers : mountpoint->threads,\n\t\t\t\t\t\tmountpoint->helper_threads == 0 ? janus_streaming_relay_rtcp_packet : janus_streaming_helper_rtprtcp_packet,\n\t\t\t\t\t\t&packet);\n\t\t\t\t\tjanus_mutex_unlock(&mountpoint->mutex);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Close the ports we bound to */\n\ttemp = source->media;\n\twhile(temp) {\n\t\tjanus_streaming_rtp_source_stream *stream = (janus_streaming_rtp_source_stream *)temp->data;\n\t\tif(stream->fd[0] > -1)\n\t\t\tclose(stream->fd[0]);\n\t\tstream->fd[0] = -1;\n\t\tif(stream->fd[1] > -1)\n\t\t\tclose(stream->fd[1]);\n\t\tstream->fd[1] = -1;\n\t\tif(stream->fd[2] > -1)\n\t\t\tclose(stream->fd[2]);\n\t\tstream->fd[2] = -1;\n\t\tif(stream->rtcp_fd > -1)\n\t\t\tclose(stream->rtcp_fd);\n\t\tstream->rtcp_fd = -1;\n\t\ttemp = temp->next;\n\t}\n\tg_free(fds);\n\n\t/* Notify users this mountpoint is done */\n\tjanus_mutex_lock(&mountpoint->mutex);\n\tGList *viewer = g_list_first(mountpoint->viewers);\n\t/* Prepare JSON event */\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\tjson_t *result = json_object();\n\tjson_object_set_new(result, \"status\", json_string(\"stopped\"));\n\tjson_object_set_new(event, \"result\", result);\n\twhile(viewer) {\n\t\tjanus_streaming_session *session = (janus_streaming_session *)viewer->data;\n\t\tif(session == NULL) {\n\t\t\tmountpoint->viewers = g_list_remove_all(mountpoint->viewers, session);\n\t\t\tviewer = g_list_first(mountpoint->viewers);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tif(session->mountpoint != mountpoint) {\n\t\t\tmountpoint->viewers = g_list_remove_all(mountpoint->viewers, session);\n\t\t\tviewer = g_list_first(mountpoint->viewers);\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tcontinue;\n\t\t}\n\t\tg_atomic_int_set(&session->stopping, 1);\n\t\tg_atomic_int_set(&session->started, 0);\n\t\tg_atomic_int_set(&session->paused, 0);\n\t\tsession->mountpoint = NULL;\n\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\tgateway->close_pc(session->handle);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tjanus_refcount_decrease(&mountpoint->ref);\n\t\tmountpoint->viewers = g_list_remove_all(mountpoint->viewers, session);\n\t\tviewer = g_list_first(mountpoint->viewers);\n\t\tjanus_mutex_unlock(&session->mutex);\n\t}\n\tjson_decref(event);\n\tjanus_mutex_unlock(&mountpoint->mutex);\n\n\t/* Unref the helper threads */\n\tif(mountpoint->helper_threads > 0) {\n\t\tGList *l = mountpoint->threads;\n\t\twhile(l) {\n\t\t\tjanus_streaming_helper *ht = (janus_streaming_helper *)l->data;\n\t\t\tjanus_refcount_decrease(&ht->ref);\n\t\t\tl = l->next;\n\t\t}\n\t}\n\n\tJANUS_LOG(LOG_VERB, \"[%s] Leaving streaming relay thread\\n\", name);\n\tg_free(name);\n\tjanus_refcount_decrease(&mountpoint->ref);\n\treturn NULL;\n}\n\nstatic void janus_streaming_relay_rtp_packet(gpointer data, gpointer user_data) {\n\tjanus_streaming_rtp_relay_packet *packet = (janus_streaming_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_streaming_session *session = (janus_streaming_session *)data;\n\tif(!session || !session->handle) {\n\t\treturn;\n\t}\n\tif(!packet->is_kfburst && (!g_atomic_int_get(&session->started) || g_atomic_int_get(&session->paused))) {\n\t\treturn;\n\t}\n\tjanus_streaming_session_stream *s = g_hash_table_lookup(session->streams_byid, GINT_TO_POINTER(packet->mindex));\n\tif(s == NULL) {\n\t\t/* No session stream for this mindex: maybe the viewer did not subscribe to it */\n\t\treturn;\n\t}\n\t/* Make sure we're allowed to send packets from this stream */\n\tif(!s->send) {\n\t\treturn;\n\t}\n\tjanus_streaming_rtp_source_stream *stream = s->stream;\n\n\tif(packet->is_rtp) {\n\t\t/* Make sure there hasn't been a video source switch by checking the SSRC */\n\t\tif(packet->is_video) {\n\t\t\t/* Check if there's any SVC info to take into account */\n\t\t\tif(packet->svc) {\n\t\t\t\t/* There is: check if this is a layer that can be dropped for this viewer\n\t\t\t\t * Note: Following core inspired by the excellent job done by Sergio Garcia Murillo here:\n\t\t\t\t * https://github.com/medooze/media-server/blob/master/src/vp9/VP9LayerSelector.cpp */\n\t\t\t\tint plen = 0;\n\t\t\t\tchar *payload = janus_rtp_payload((char *)packet->data, packet->length, &plen);\n\t\t\t\tgboolean keyframe = janus_vp9_is_keyframe((const char *)payload, plen);\n\t\t\t\tgboolean override_mark_bit = FALSE, has_marker_bit = packet->data->markerbit;\n\t\t\t\tint spatial_layer = s->spatial_layer;\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\tif(packet->svc_info.spatial_layer >= 0 && packet->svc_info.spatial_layer <= 2)\n\t\t\t\t\ts->last_spatial_layer[packet->svc_info.spatial_layer] = now;\n\t\t\t\tif(s->target_spatial_layer > s->spatial_layer) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"We need to upscale spatially: (%d < %d)\\n\",\n\t\t\t\t\t\ts->spatial_layer, s->target_spatial_layer);\n\t\t\t\t\t/* We need to upscale: wait for a keyframe */\n\t\t\t\t\tif(keyframe) {\n\t\t\t\t\t\tint new_spatial_layer = s->target_spatial_layer;\n\t\t\t\t\t\twhile(new_spatial_layer > s->spatial_layer && new_spatial_layer > 0) {\n\t\t\t\t\t\t\tif(now - s->last_spatial_layer[new_spatial_layer] >= 250000) {\n\t\t\t\t\t\t\t\t/* We haven't received packets from this layer for a while, try a lower layer */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Haven't received packets from layer %d for a while, trying %d instead...\\n\",\n\t\t\t\t\t\t\t\t\tnew_spatial_layer, new_spatial_layer-1);\n\t\t\t\t\t\t\t\tnew_spatial_layer--;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(new_spatial_layer > s->spatial_layer) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Upscaling spatial layer: %d --> %d (need %d)\\n\",\n\t\t\t\t\t\t\t\ts->spatial_layer, new_spatial_layer, s->target_spatial_layer);\n\t\t\t\t\t\t\ts->spatial_layer = new_spatial_layer;\n\t\t\t\t\t\t\tspatial_layer = s->spatial_layer;\n\t\t\t\t\t\t\t/* Notify the viewer */\n\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(result, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\t\tjson_object_set_new(result, \"spatial_layer\", json_integer(s->spatial_layer));\n\t\t\t\t\t\t\tif(s->temporal_layer == -1) {\n\t\t\t\t\t\t\t\t/* We just started: initialize the temporal layer and notify that too */\n\t\t\t\t\t\t\t\ts->temporal_layer = 0;\n\t\t\t\t\t\t\t\tjson_object_set_new(result, \"temporal_layer\", json_integer(s->temporal_layer));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if(s->target_spatial_layer < s->spatial_layer) {\n\t\t\t\t\t/* We need to downscale */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"We need to downscale spatially: (%d > %d)\\n\",\n\t\t\t\t\t\ts->spatial_layer, s->target_spatial_layer);\n\t\t\t\t\tgboolean downscaled = FALSE;\n\t\t\t\t\tif(!packet->svc_info.fbit && keyframe) {\n\t\t\t\t\t\t/* Non-flexible mode: wait for a keyframe */\n\t\t\t\t\t\tdownscaled = TRUE;\n\t\t\t\t\t} else if(packet->svc_info.fbit && packet->svc_info.ebit) {\n\t\t\t\t\t\t/* Flexible mode: check the E bit */\n\t\t\t\t\t\tdownscaled = TRUE;\n\t\t\t\t\t}\n\t\t\t\t\tif(downscaled) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Downscaling spatial layer: %d --> %d\\n\",\n\t\t\t\t\t\t\ts->spatial_layer, s->target_spatial_layer);\n\t\t\t\t\t\ts->spatial_layer = s->target_spatial_layer;\n\t\t\t\t\t\t/* Notify the viewer */\n\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\tjson_object_set_new(result, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\tjson_object_set_new(result, \"spatial_layer\", json_integer(s->spatial_layer));\n\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(spatial_layer < packet->svc_info.spatial_layer) {\n\t\t\t\t\t/* Drop the packet: update the context to make sure sequence number is increased normally later */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (spatial layer %d < %d)\\n\", spatial_layer, packet->svc_info.spatial_layer);\n\t\t\t\t\ts->context.base_seq++;\n\t\t\t\t\treturn;\n\t\t\t\t} else if(packet->svc_info.ebit && spatial_layer == packet->svc_info.spatial_layer) {\n\t\t\t\t\t/* If we stop at layer 0, we need a marker bit now, as the one from layer 1 will not be received */\n\t\t\t\t\toverride_mark_bit = TRUE;\n\t\t\t\t}\n\t\t\t\tint temporal_layer = s->temporal_layer;\n\t\t\t\tif(s->target_temporal_layer > s->temporal_layer) {\n\t\t\t\t\t/* We need to upscale */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"We need to upscale temporally: (%d < %d)\\n\",\n\t\t\t\t\t\ts->temporal_layer, s->target_temporal_layer);\n\t\t\t\t\tif(packet->svc_info.ubit && packet->svc_info.bbit &&\n\t\t\t\t\t\t\tpacket->svc_info.temporal_layer > s->temporal_layer &&\n\t\t\t\t\t\t\tpacket->svc_info.temporal_layer <= s->target_temporal_layer) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Upscaling temporal layer: %d --> %d (want %d)\\n\",\n\t\t\t\t\t\t\ts->temporal_layer, packet->svc_info.temporal_layer, s->target_temporal_layer);\n\t\t\t\t\t\ts->temporal_layer = packet->svc_info.temporal_layer;\n\t\t\t\t\t\ttemporal_layer = s->temporal_layer;\n\t\t\t\t\t\t/* Notify the viewer */\n\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\tjson_object_set_new(result, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\tjson_object_set_new(result, \"temporal_layer\", json_integer(s->temporal_layer));\n\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t}\n\t\t\t\t} else if(s->target_temporal_layer < s->temporal_layer) {\n\t\t\t\t\t/* We need to downscale */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"We need to downscale temporally: (%d > %d)\\n\",\n\t\t\t\t\t\ts->temporal_layer, s->target_temporal_layer);\n\t\t\t\t\tif(packet->svc_info.ebit && packet->svc_info.temporal_layer == s->target_temporal_layer) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Downscaling temporal layer: %d --> %d\\n\",\n\t\t\t\t\t\t\ts->temporal_layer, s->target_temporal_layer);\n\t\t\t\t\t\ts->temporal_layer = s->target_temporal_layer;\n\t\t\t\t\t\t/* Notify the viewer */\n\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\t\tjson_object_set_new(result, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\tjson_object_set_new(result, \"temporal_layer\", json_integer(s->temporal_layer));\n\t\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(temporal_layer < packet->svc_info.temporal_layer) {\n\t\t\t\t\t/* Drop the packet: update the context to make sure sequence number is increased normally later */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (temporal layer %d < %d)\\n\", temporal_layer, packet->svc_info.temporal_layer);\n\t\t\t\t\ts->context.base_seq++;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t/* If we got here, we can send the frame: this doesn't necessarily mean it's\n\t\t\t\t * one of the layers the user wants, as there may be dependencies involved */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Sending packet (spatial=%d, temporal=%d)\\n\",\n\t\t\t\t\tpacket->svc_info.spatial_layer, packet->svc_info.temporal_layer);\n\t\t\t\t/* Fix sequence number and timestamp (publisher switching may be involved) */\n\t\t\t\tjanus_rtp_header_update(packet->data, &s->context, TRUE, 0);\n\t\t\t\tif(override_mark_bit && !has_marker_bit) {\n\t\t\t\t\tpacket->data->markerbit = 1;\n\t\t\t\t}\n\t\t\t\tif(s->pt > 0)\n\t\t\t\t\tpacket->data->type = s->pt;\n\t\t\t\tjanus_plugin_rtp rtp = { .mindex = s->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length };\n\t\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\t\tif(s->min_delay > -1 && s->max_delay > -1) {\n\t\t\t\t\trtp.extensions.min_delay = s->min_delay;\n\t\t\t\t\trtp.extensions.max_delay = s->max_delay;\n\t\t\t\t}\n\t\t\t\tif(session->abscapturetime_src_ext_id > 0) {\n\t\t\t\t\tuint64_t abs_ts = 0;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_abs_capture_time((char *)packet->data, packet->length,\n\t\t\t\t\t\t\tsession->abscapturetime_src_ext_id, &abs_ts) == 0) {\n\t\t\t\t\t\trtp.extensions.abs_capture_ts = abs_ts;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(gateway != NULL)\n\t\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t\tif(override_mark_bit && !has_marker_bit) {\n\t\t\t\t\tpacket->data->markerbit = 0;\n\t\t\t\t}\n\t\t\t\t/* Restore the payload type, timestamp and sequence number to what the publisher set them to */\n\t\t\t\tpacket->data->type = packet->ptype;\n\t\t\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\t\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\t\t} else if(packet->simulcast) {\n\t\t\t\t/* Handle simulcast: don't relay if it's not the substream we wanted to handle */\n\t\t\t\tint plen = 0;\n\t\t\t\tchar *payload = janus_rtp_payload((char *)packet->data, packet->length, &plen);\n\t\t\t\tif(payload == NULL)\n\t\t\t\t\treturn;\n\t\t\t\t/* Process this packet: don't relay if it's not the SSRC/layer we wanted to handle */\n\t\t\t\tgboolean relay = janus_rtp_simulcasting_context_process_rtp(&s->sim_context,\n\t\t\t\t\t(char *)packet->data, packet->length, NULL, 0,\n\t\t\t\t\tpacket->ssrc, NULL, packet->codec, &s->context, NULL);\n\t\t\t\tif(!relay) {\n\t\t\t\t\t/* Did a lot of time pass before we could relay a packet? */\n\t\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\t\tif((now - s->sim_context.last_relayed) >= G_USEC_PER_SEC) {\n\t\t\t\t\t\tg_atomic_int_set(&s->sim_context.need_pli, 1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(s->sim_context.need_pli) {\n\t\t\t\t\t/* Schedule a PLI */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the simulcast context\\n\");\n\t\t\t\t\tg_atomic_int_set(&stream->need_pli, 1);\n\t\t\t\t}\n\t\t\t\t/* Do we need to drop this? */\n\t\t\t\tif(!relay)\n\t\t\t\t\treturn;\n\t\t\t\t/* Any event we should notify? */\n\t\t\t\tif(s->sim_context.changed_substream) {\n\t\t\t\t\t/* Notify the user about the substream change */\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\tjson_object_set_new(result, \"mid\", json_string(stream->mid));\n\t\t\t\t\tjson_object_set_new(result, \"substream\", json_integer(s->sim_context.substream));\n\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t}\n\t\t\t\tif(s->sim_context.changed_temporal) {\n\t\t\t\t\t/* Notify the user about the temporal layer change */\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"streaming\", json_string(\"event\"));\n\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\tjson_object_set_new(result, \"mid\", json_string(stream->mid));\n\t\t\t\t\tjson_object_set_new(result, \"temporal\", json_integer(s->sim_context.templayer));\n\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\tgateway->push_event(session->handle, &janus_streaming_plugin, NULL, event, NULL);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t}\n\t\t\t\t/* If we got here, update the RTP header and send the packet */\n\t\t\t\tjanus_rtp_header_update(packet->data, &s->context, TRUE, 0);\n\t\t\t\tchar vp8pd[6];\n\t\t\t\tif(packet->codec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t\t\t/* For VP8, we save the original payload descriptor, to restore it after */\n\t\t\t\t\tmemcpy(vp8pd, payload, sizeof(vp8pd));\n\t\t\t\t\tjanus_vp8_simulcast_descriptor_update(payload, plen, &s->vp8_context,\n\t\t\t\t\t\ts->sim_context.changed_substream);\n\t\t\t\t}\n\t\t\t\tif(s->pt > 0)\n\t\t\t\t\tpacket->data->type = s->pt;\n\t\t\t\t/* Send the packet */\n\t\t\t\tjanus_plugin_rtp rtp = { .mindex = s->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length };\n\t\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\t\tif(s->min_delay > -1 && s->max_delay > -1) {\n\t\t\t\t\trtp.extensions.min_delay = s->min_delay;\n\t\t\t\t\trtp.extensions.max_delay = s->max_delay;\n\t\t\t\t}\n\t\t\t\tif(session->abscapturetime_src_ext_id > 0) {\n\t\t\t\t\tuint64_t abs_ts = 0;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_abs_capture_time((char *)packet->data, packet->length,\n\t\t\t\t\t\t\tsession->abscapturetime_src_ext_id, &abs_ts) == 0) {\n\t\t\t\t\t\trtp.extensions.abs_capture_ts = abs_ts;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(gateway != NULL)\n\t\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\t\t\tpacket->data->type = packet->ptype;\n\t\t\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\t\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\t\t\tif(packet->codec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t\t\t/* Restore the original payload descriptor as well, as it will be needed by the next viewer */\n\t\t\t\t\tmemcpy(payload, vp8pd, sizeof(vp8pd));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Fix sequence number and timestamp (switching may be involved) */\n\t\t\t\tjanus_rtp_header_update(packet->data, &s->context, TRUE, 0);\n\t\t\t\tif(s->pt > 0)\n\t\t\t\t\tpacket->data->type = s->pt;\n\t\t\t\tjanus_plugin_rtp rtp = { .mindex = s->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length };\n\t\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\t\tif(s->min_delay > -1 && s->max_delay > -1) {\n\t\t\t\t\trtp.extensions.min_delay = s->min_delay;\n\t\t\t\t\trtp.extensions.max_delay = s->max_delay;\n\t\t\t\t}\n\t\t\t\tif(session->abscapturetime_src_ext_id > 0) {\n\t\t\t\t\tuint64_t abs_ts = 0;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_abs_capture_time((char *)packet->data, packet->length,\n\t\t\t\t\t\t\tsession->abscapturetime_src_ext_id, &abs_ts) == 0) {\n\t\t\t\t\t\trtp.extensions.abs_capture_ts = abs_ts;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(gateway != NULL)\n\t\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t\t/* Restore the timestamp and sequence number to what the video source set them to */\n\t\t\t\tpacket->data->type = packet->ptype;\n\t\t\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\t\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\t\t}\n\t\t} else {\n\t\t\t/* Fix sequence number and timestamp (switching may be involved) */\n\t\t\tjanus_rtp_header_update(packet->data, &s->context, FALSE, 0);\n\t\t\tif(s->pt > 0)\n\t\t\t\tpacket->data->type = s->pt;\n\t\t\tjanus_plugin_rtp rtp = { .mindex = s->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length };\n\t\t\tjanus_plugin_rtp_extensions_reset(&rtp.extensions);\n\t\t\tif(gateway != NULL)\n\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t/* Restore the timestamp and sequence number to what the video source set them to */\n\t\t\tpacket->data->type = packet->ptype;\n\t\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\t}\n\t} else {\n\t\t/* We're broadcasting a data channel message */\n\t\tif(gateway != NULL && packet->data != NULL && g_atomic_int_get(&session->dataready)) {\n\t\t\tjanus_plugin_data data = {\n\t\t\t\t.label = NULL,\n\t\t\t\t.protocol = NULL,\n\t\t\t\t.binary = !packet->textdata,\n\t\t\t\t.buffer = (char *)packet->data,\n\t\t\t\t.length = packet->length\n\t\t\t};\n\t\t\tgateway->relay_data(session->handle, &data);\n\t\t}\n\t}\n\n\treturn;\n}\n\nstatic void janus_streaming_relay_rtcp_packet(gpointer data, gpointer user_data) {\n\tjanus_streaming_rtp_relay_packet *packet = (janus_streaming_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_streaming_session *session = (janus_streaming_session *)data;\n\tif(!session || !session->handle) {\n\t\treturn;\n\t}\n\tif(!g_atomic_int_get(&session->started) || g_atomic_int_get(&session->paused)) {\n\t\treturn;\n\t}\n\tjanus_streaming_session_stream *s = g_hash_table_lookup(session->streams_byid, GINT_TO_POINTER(packet->mindex));\n\tif(packet->mindex != -1 && s == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"No session stream for mindex %d...\\n\", packet->mindex);\n\t\treturn;\n\t}\n\n\tjanus_plugin_rtcp rtcp = { .mindex = s->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length };\n\tif(gateway != NULL)\n\t\tgateway->relay_rtcp(session->handle, &rtcp);\n\n\treturn;\n}\n\nstatic void janus_streaming_helper_rtprtcp_packet(gpointer data, gpointer user_data) {\n\tjanus_streaming_rtp_relay_packet *packet = (janus_streaming_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_streaming_helper *helper = (janus_streaming_helper *)data;\n\tif(!helper) {\n\t\treturn;\n\t}\n\t/* Clone the packet and queue it for delivery on the helper thread */\n\tjanus_streaming_rtp_relay_packet *copy = g_malloc0(sizeof(janus_streaming_rtp_relay_packet));\n\tcopy->mindex = packet->mindex;\n\tcopy->data = g_malloc(packet->length);\n\tmemcpy(copy->data, packet->data, packet->length);\n\tcopy->length = packet->length;\n\tcopy->is_rtp = packet->is_rtp;\n\tcopy->is_data = packet->is_data;\n\tcopy->textdata = packet->textdata;\n\tcopy->is_video = packet->is_video;\n\tcopy->is_kfburst = packet->is_kfburst;\n\tcopy->simulcast = packet->simulcast;\n\tcopy->ssrc[0] = packet->ssrc[0];\n\tcopy->ssrc[1] = packet->ssrc[1];\n\tcopy->ssrc[2] = packet->ssrc[2];\n\tcopy->codec = packet->codec;\n\tcopy->substream = packet->substream;\n\tcopy->svc = packet->svc;\n\tif(copy->svc)\n\t\tcopy->svc_info = packet->svc_info;\n\tcopy->ptype = packet->ptype;\n\tcopy->timestamp = packet->timestamp;\n\tcopy->seq_number = packet->seq_number;\n\tg_async_queue_push(helper->queued_packets, copy);\n}\n\nstatic void *janus_streaming_helper_thread(void *data) {\n\tjanus_streaming_helper *helper = (janus_streaming_helper *)data;\n\tjanus_streaming_mountpoint *mp = helper->mp;\n\tJANUS_LOG(LOG_INFO, \"[%s/#%d] Joining Streaming helper thread\\n\", mp->name, helper->id);\n\tjanus_streaming_rtp_relay_packet *pkt = NULL;\n\twhile(!g_atomic_int_get(&stopping) && !g_atomic_int_get(&mp->destroyed) && !g_atomic_int_get(&helper->destroyed)) {\n\t\tpkt = g_async_queue_pop(helper->queued_packets);\n\t\tif(pkt == &exit_packet)\n\t\t\tbreak;\n\t\tjanus_mutex_lock(&helper->mutex);\n\t\tg_list_foreach(helper->viewers,\n\t\t\tpkt->is_rtp || pkt->is_data ? janus_streaming_relay_rtp_packet : janus_streaming_relay_rtcp_packet,\n\t\t\tpkt);\n\t\tjanus_mutex_unlock(&helper->mutex);\n\t\tjanus_streaming_rtp_relay_packet_free(pkt);\n\t}\n\tJANUS_LOG(LOG_INFO, \"[%s/#%d] Leaving Streaming helper thread\\n\", mp->name, helper->id);\n\tjanus_refcount_decrease(&helper->ref);\n\tjanus_refcount_decrease(&mp->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/plugins/janus_textroom.c",
    "content": "/*! \\file   janus_textroom.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus TextRoom plugin\n * \\details Check the \\ref textroom for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page textroom Janus TextRoom documentation\n * This is a plugin implementing a DataChannel only text room.\n * As such, it does NOT support or negotiate audio or video, but only\n * data channels, in order to provide text broadcasting features. The\n * plugin allows users to join multiple text-only rooms via a single\n * PeerConnection. Users can send messages either to a room in general\n * (broadcasting), or to individual users (whispers). This plugin can be\n * used within the context of any application that needs real-time text\n * broadcasting (e.g., chatrooms, but not only).\n *\n * The only message that is typically sent to the plugin through the Janus API is\n * a \\c setup message, by which the user initializes the PeerConnection\n * itself, as explained in the \\ref textroomsetup section.\n * Apart from that, all other messages can be exchanged directly\n * via Data Channels. For room management purposes, though, requests like\n * \\c create , \\c edit , \\c destroy , \\c list , \\c listparticipants , \\c exists\n * and \\c announcement are available through the\n * Janus API as well: notice that in this case you'll have to use \\c request\n * and not \\c textroom as the name of the request. You cannot join a room\n * or send/receive messages via Janus API: that's only possible through\n * the datachannel interface.\n *\n * Each room can also be configured with an HTTP backend to contact for\n * incoming messages. If configured, messages addressed to that room will\n * also be forwarded, by means of an HTTP POST, to the specified address.\n * Notice that this will only work if libcurl was available when\n * configuring and installing Janus.\n *\n * \\note This plugin is only meant to showcase what you can do with\n * data channels involving multiple participants at the same time. While\n * functional, it's not inherently better or faster than doing the same\n * thing using the Janus API messaging itself (e.g., as part of the\n * plugin API messaging) or using existing instant messaging protocols\n * (e.g., Jabber). In fact, while data channels are being used, you're\n * still going through a server, so it's not really peer-to-peer. That\n * said, the plugin can be useful if you don't plan to use any other\n * infrastructure than Janus, and yet you also want to have text-based\n * communication (e.g., to add a chatroom to an audio or video conference).\n *\n * Notice that, in general, all users can create rooms. If you want to\n * limit this functionality, you can configure an admin \\c admin_key in\n * the plugin settings. When configured, only \"create\" requests that\n * include the correct \\c admin_key value in an \"admin_key\" property\n * will succeed, and will be rejected otherwise.\n *\n * Rooms to make available at startup are listed in the plugin configuration file.\n * A pre-filled configuration file is provided in \\c conf/janus.plugin.textroom.cfg\n * and includes a demo room for testing.\n *\n * To add more static rooms or modify the existing one, you can use the following\n * syntax:\n *\n * \\verbatim\n[<unique room ID>]\ndescription = This is my awesome room\nis_private = true|false (whether this room should be in the public list, default=true)\nsecret = <optional password needed for manipulating (e.g. destroying) the room>\npin = <optional password needed for joining the room>\nhistory = <number of messages to store as a history, and send back to new participants (default=0, no history)>\npost = <optional backend to contact via HTTP post for all incoming messages>\n\\endverbatim\n *\n * As explained in the next sections, you can also create rooms programmatically.\n *\n * \\section textroomsetup Establishing a connection\n *\n * While some requests to the plugin can be sent via Janus API, the actual\n * chatroom functionality is only available via datachannel. This means\n * that you need to establish a WebRTC PeerConnection first, if you want\n * to take full advantage of the functionality provided by the TextRoom plugin.\n *\n * To do that, you use the \\c setup request, which doesn't require any\n * argument:\n *\n\\verbatim\n{\n\t\"request\" : \"setup\"\n}\n\\endverbatim\n *\n * Assuming a datachannel wasn't established already, the plugin will\n * react to that request with a basic event, which will contain a JSEP\n * offer too:\n *\n\\verbatim\n{\n\t\"textroom\" : \"event\",\n\t\"result\" : \"ok\"\n}\n\\endverbatim\n *\n * To complete the negotiation process and establish a WebRTC PeerConnection,\n * you need to send an empty \\c ack request with your JSEP answer:\n *\n\\verbatim\n{\n\t\"request\" : \"ack\"\n}\n\\endverbatim\n *\n * Should you need to perform an ICE restart, e.g., to keep the datachannel\n * alive during a connection migration, you can use an empty \\c restart\n * request:\n *\n\\verbatim\n{\n\t\"request\" : \"restart\"\n}\n\\endverbatim\n *\n * Just as in the \\c setup request, this will result in the plugin sending\n * a new JSEP offer with an attempt to restart ICE. You can use the\n * \\c ack request to provide your JSEP answer in this case too.\n *\n * Once the datachannel is active, you can start exchanging messages\n * there, referring to the \\ref textroomapi for the syntax. As explained\n * in the intro, some request described there can be sent over Janus API\n * too, but you'll need to use \\c request instead of \\c textroom as the\n * name of the request.\n *\n * \\section textroomapi Text Room API\n *\n * All TextRoom API requests sent via datachannels are addressed by a \\c textroom named property,\n * and must contain a \\c transaction string property as well, which will\n * be returned in the response. Notice that, for the sake of brevity, the\n * \\c transaction property will not be displayed in the documentation,\n * although, as explained, it MUST be present, and WILL be included in\n * all responses (but not in the unsolicited events, like join/leave\n * or incoming messages).\n *\n * To get a list of the available rooms (excluded those configured or\n * created as private rooms) you can make use of the \\c list request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"list\",\n\t\"admin_key\" : \"<plugin administrator key; optional>\"\n}\n\\endverbatim\n *\n * A successful request will produce a list of rooms in a \\c success response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\",\n\t\"list\" : [\t\t// Array of room objects\n\t\t{\t// Room #1\n\t\t\t\"room\" : <unique numeric ID>,\n\t\t\t\"description\" : \"<Name of the room>\",\n\t\t\t\"pin_required\" : <true|false, depending on whether the room is PIN-protected>,\n\t\t\t\"num_participants\" : <count of the participants>,\n\t\t\t\"history\" : <size of history, if any>\n\t\t},\n\t\t// Other rooms\n\t]\n}\n\\endverbatim\n *\n * To get a list of the participants in a specific room, instead, you\n * can make use of the \\c listparticipants request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listparticipants\",\n\t\"room\" : <unique numeric ID of the room>\n}\n\\endverbatim\n *\n * A successful request will produce a list of participants in a\n * \\c participants response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"participants\" : [\t\t// Array of participant objects\n\t\t{\t// Participant #1\n\t\t\t\"username\" : \"<username of participant>\",\n\t\t\t\"display\" : \"<display name of participant, if any>\"\n\t\t},\n\t\t// Other participants\n\t]\n}\n\\endverbatim\n *\n * To create new TextRoom rooms you can use the \\c create request. The API\n * room creation supports the same fields as creation via configuration files,\n * which means the request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"create\",\n\t\"room\" : <unique numeric room ID to assign; optional, chosen by plugin if missing>,\n\t\"admin_key\" : \"<plugin administrator key; mandatory if configured>\",\n\t\"description\" : \"<description of room; optional>\",\n\t\"secret\" : \"<secret to query/edit the room later; optional>\",\n\t\"pin\" : \"<PIN required for participants to join room; optional>\",\n\t\"is_private\" : <true|false, whether the room should be listable; optional, true by default>,\n\t\"history\" : <number of messages to store as a history, and send back to new participants (default=0, no history)>,\n\t\"post\" : \"<backend to contact via HTTP post for all incoming messages; optional>\",\n\t\"permanent\" : <true|false, whether the mountpoint should be saved to configuration file or not; false by default>\n}\n\\endverbatim\n *\n * A successful creation procedure will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"permanent\" : <true if saved to config file, false if not>\n}\n\\endverbatim\n *\n * If you requested a permanent room but a \\c false value is returned\n * instead, good chances are that there are permission problems.\n *\n * An error instead (and the same applies to all other requests, so this\n * won't be repeated) would provide both an error code and a more verbose\n * description of the cause of the issue:\n *\n\\verbatim\n{\n\t\"textroom\" : \"event\",\n\t\"error_code\" : <numeric ID, check Macros below>,\n\t\"error\" : \"<error description as a string>\"\n}\n\\endverbatim\n *\n * Once a room has been created, you can still edit some (but not all)\n * of its properties using the \\c edit request. This allows you to modify\n * the room description, secret, pin, whether it's private or not and\n * the backend to forward incoming messages to: you won't be able to modify\n * other more static properties, though, like the room ID for instance.\n * If you're interested in changing the ACL, instead, check the \\c allowed\n * message. An \\c edit request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"edit\",\n\t\"room\" : <unique numeric ID of the room to edit; mandatory>,\n\t\"secret\" : \"<room secret; mandatory if configured>\",\n\t\"new_description\" : \"<new pretty name of the room; optional>\",\n\t\"new_secret\" : \"<new password required to edit/destroy the room; optional>\",\n\t\"new_pin\" : \"<new password required to join the room; optional>\",\n\t\"new_is_private\" : <true|false, whether the room should appear in a list request; optional>,\n\t\"permanent\" : <true|false, whether the room should be also removed from the config file; default=false>\n}\n\\endverbatim\n *\n * A successful edit procedure will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"edited\",\n\t\"room\" : <unique numeric ID>,\n\t\"permanent\" : <true if changes were saved to config file, false if not>\n}\n\\endverbatim\n *\n * On the other hand, \\c destroy can be used to destroy an existing text\n * room, whether created dynamically or statically, and has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"destroy\",\n\t\"room\" : <unique numeric ID of the room to destroy; mandatory>,\n\t\"secret\" : \"<room secret; mandatory if configured>\",\n\t\"permanent\" : <true|false, whether the room should be also removed from the config file; default=false>\n}\n\\endverbatim\n *\n * A successful destruction procedure will result in a \\c destroyed response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"destroyed\",\n\t\"room\" : <unique numeric ID>,\n\t\"permanent\" : <true if the room was removed from config file too, false if not>\n}\n\\endverbatim\n *\n * This will also result in a \\c destroyed event being sent to all the\n * participants in the room, which will look like this:\n *\n\\verbatim\n{\n\t\"textroom\" : \"destroyed\",\n\t\"room\" : <unique numeric ID of the destroyed room>\n}\n\\endverbatim\n *\n * You can check whether a room exists using the \\c exists request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"exists\",\n\t\"room\" : <unique numeric ID of the room to check; mandatory>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"exists\" : <true|false>\n}\n\\endverbatim\n *\n * You can configure whether to check tokens or add/remove people who can join\n * a room using the \\c allowed request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"allowed\",\n\t\"secret\" : \"<room secret; mandatory if configured>\",\n\t\"action\" : \"enable|disable|add|remove\",\n\t\"room\" : <unique numeric ID of the room to update; mandatory>,\n\t\"allowed\" : [\n\t\t// Array of strings (tokens users might pass in \"join\", only for add|remove)\n\t]\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"allowed\" : [\n\t\t// Updated, complete, list of allowed tokens (only for enable|add|remove)\n\t]\n}\n\\endverbatim\n *\n * If you're the administrator of a room (that is, you created it and have access\n * to the secret) you can kick participants using the \\c kick request. Notice\n * that this only kicks the user out of the room, but does not prevent them from\n * re-joining: to ban them, you need to first remove them from the list of\n * authorized users (see \\c allowed request) and then \\c kick them. The \\c kick\n * request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"kick\",\n\t\"secret\" : \"<room secret; mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room; mandatory>,\n\t\"username\" : \"<unique username of the participant to kick; mandatory>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\",\n}\n\\endverbatim\n *\n * This will also result in a \\c kicked event being sent to all the other\n * participants in the room, which will look like this:\n *\n\\verbatim\n{\n\t\"textroom\" : \"kicked\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"username\" : \"<unique username of the kicked participant>\"\n}\n\\endverbatim\n *\n * For what concerns room participation, you can join a room using the\n * \\c join request, send messages (public and private) using the\n * \\c message request, and leave a room with \\c leave instead.\n *\n * A \\c join request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"textroom\" : \"join\",\n\t\"room\" : <unique numeric ID of the room to join>,\n\t\"pin\" : \"<pin to join the room; mandatory if configured>\",\n\t\"username\" : \"<unique username to have in the room; mandatory>\",\n\t\"display\" : \"<display name to use in the room; optional>\",\n\t\"token\" : \"<invitation token, in case the room has an ACL; optional>\",\n\t\"history\" : <true|false, whether to retrieve history messages when available (default=true)>\n}\n\\endverbatim\n *\n * A successful join will result in a \\c success response, which will\n * include a list of all the other participants currently in the room:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\",\n\t\"participants\" : [\n\t\t{\n\t\t\t\"username\" : \"<username of participant #1>\",\n\t\t\t\"display\" : \"<display name of participant #1, if any>\"\n\t\t},\n\t\t// Other participants\n\t]\n}\n\\endverbatim\n *\n * As explained previously, there's no hardcoded limit in how many rooms\n * you can join with the same participant and on the same PeerConnection.\n *\n * Notice that a successful \\c join request will also result in a\n * \\c join event being sent to all the other participants, so that\n * they're notified about the new participant getting in the room:\n *\n\\verbatim\n{\n\t\"textroom\" : \"join\",\n\t\"room\" : <room ID>,\n\t\"username\" : \"<username of new participant>\",\n\t\"display\" : \"<display name of new participant, if any>\"\n}\n\\endverbatim\n *\n * To leave a previously joined room, instead, the \\c leave request can\n * be used, which must be formatted like this:\n *\n\\verbatim\n{\n\t\"textroom\" : \"leave\",\n\t\"room\" : <unique numeric ID of the room to leave>\n}\n\\endverbatim\n *\n * A successful leave will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\"\n}\n\\endverbatim\n *\n * Notice that a successful \\c leave request will also result in a\n * \\c leave event being sent to all the other participants, so that\n * they're notified about the participant that just left the room:\n *\n\\verbatim\n{\n\t\"textroom\" : \"leave\",\n\t\"room\" : <room ID>,\n\t\"username\" : \"<username of gone participant>\"\n}\n\\endverbatim\n *\n * Finally, the \\c message request allows you to send public and private\n * messages within the context of a room. It must be formatted like this:\n *\n\\verbatim\n{\n\t\"textroom\" : \"message\",\n\t\"room\" : <unique numeric ID of the room this message will refer to>,\n\t\"to\" : \"<username to send the message to; optional, only needed in case of private messages>\",\n\t\"tos\" : \"<array of usernames to send the message to; optional, only needed in case of private messages>\",\n\t\"text\" : \"<content of the message to send, as a string>\",\n\t\"ack\" : <true|false, whether the sender wants an ack for the sent message(s); optional, true by default>\n}\n\\endverbatim\n *\n * A \\c message with no \\c to and no \\c tos is considered a public message,\n * and so will be sent to all the participants in the room. In case either\n * \\c to or \\c tos is specified, instead, this is considered to be a whisper,\n * that is a private message only meant for the specified recipients. Notice\n * that \\c to and \\c tos are mutually exclusive, and you cannot specify both.\n *\n * \\c text must be a string, but apart from that there's no limit on what\n * you can put in there. It could be, for instance, a serialized JSON string,\n * or a stringified XML document, or whatever makes sense to the application.\n *\n * A successful message delivery will result in a \\c success response, but\n * only if \\c ack was \\c true in the \\c message request. This was done by\n * design, to allow users to disable explicit acks for every outgoing message,\n * especially in case of verbose communications. In case an ack is required,\n * the response will look like this:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\"\n}\n\\endverbatim\n *\n * Incoming messages will come either as \\c message events. In particular,\n * \\c message will notify the user about an incoming public or privave\n * message, that is either a message that was sent to the whole room,\n * or to the user individually:\n *\n\\verbatim\n{\n\t\"textroom\" : \"message\",\n\t\"room\" : <room ID the message was sent to>,\n\t\"from\" : \"<username of participant who sent the public message>\",\n\t\"date\" : \"<date/time of when the message was sent>\",\n\t\"text\" : \"<content of the message>\",\n\t\"whisper\" : <true|false, depending on whether it's a public or private message>\n}\n\\endverbatim\n *\n * In case the \\c whisper attribute is \\c true it means the user actually\n * received a  private message from another participant in the room.\n *\n * Another way of injecting text into rooms is by means of announcements.\n * Announcements are basically messages sent by the room itself, rather\n * than individual users: as such, only users or applications managing\n * the room can send these announcements, as the room secret will be\n * required for the purpose. The \\c announcement request implements this\n * feature in the TextRoom plugin, and must be formatted like this:\n *\n\\verbatim\n{\n\t\"textroom\" : \"announcement\",\n\t\"room\" : <unique numeric ID of the room this announcement will be sent to>,\n\t\"secret\" : \"<room secret; mandatory if configured>\",\n\t\"text\" : \"<content of the announcement to send, as a string>\"\n}\n\\endverbatim\n *\n * In case the \\c announcement request is accepted, the response will look\n * like this:\n *\n\\verbatim\n{\n\t\"textroom\" : \"success\"\n}\n\\endverbatim\n *\n * Incoming announcements will be received by participants as \\c announcement\n * events. The syntax is pretty much identical to how \\c message looks like,\n * with the difference that no \\c from attribute will be included as the\n * announcement will be seen as coming from the room itself:\n *\n\\verbatim\n{\n\t\"textroom\" : \"announcement\",\n\t\"room\" : <room ID the announcement was sent to>,\n\t\"date\" : \"<date/time of when the announcement was sent>\",\n\t\"text\" : \"<content of the announcement>\"\n}\n\\endverbatim\n *\n */\n\n#include \"plugin.h\"\n\n#include <jansson.h>\n\n#ifdef HAVE_LIBCURL\n#include <curl/curl.h>\n#endif\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n\n\n/* Plugin information */\n#define JANUS_TEXTROOM_VERSION\t\t\t2\n#define JANUS_TEXTROOM_VERSION_STRING\t\"0.0.2\"\n#define JANUS_TEXTROOM_DESCRIPTION\t\t\"This is a plugin implementing a text-only room for Janus, using DataChannels.\"\n#define JANUS_TEXTROOM_NAME\t\t\t\t\"JANUS TextRoom plugin\"\n#define JANUS_TEXTROOM_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_TEXTROOM_PACKAGE\t\t\t\"janus.plugin.textroom\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_textroom_init(janus_callbacks *callback, const char *config_path);\nvoid janus_textroom_destroy(void);\nint janus_textroom_get_api_compatibility(void);\nint janus_textroom_get_version(void);\nconst char *janus_textroom_get_version_string(void);\nconst char *janus_textroom_get_description(void);\nconst char *janus_textroom_get_name(void);\nconst char *janus_textroom_get_author(void);\nconst char *janus_textroom_get_package(void);\nvoid janus_textroom_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_textroom_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_textroom_handle_admin_message(json_t *message);\nvoid janus_textroom_setup_media(janus_plugin_session *handle);\nvoid janus_textroom_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_textroom_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_textroom_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet);\nvoid janus_textroom_data_ready(janus_plugin_session *handle);\nvoid janus_textroom_slow_link(janus_plugin_session *handle, int mindex, int uplink, int video);\nvoid janus_textroom_hangup_media(janus_plugin_session *handle);\nvoid janus_textroom_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_textroom_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_textroom_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_textroom_init,\n\t\t.destroy = janus_textroom_destroy,\n\n\t\t.get_api_compatibility = janus_textroom_get_api_compatibility,\n\t\t.get_version = janus_textroom_get_version,\n\t\t.get_version_string = janus_textroom_get_version_string,\n\t\t.get_description = janus_textroom_get_description,\n\t\t.get_name = janus_textroom_get_name,\n\t\t.get_author = janus_textroom_get_author,\n\t\t.get_package = janus_textroom_get_package,\n\n\t\t.create_session = janus_textroom_create_session,\n\t\t.handle_message = janus_textroom_handle_message,\n\t\t.handle_admin_message = janus_textroom_handle_admin_message,\n\t\t.setup_media = janus_textroom_setup_media,\n\t\t.incoming_rtp = janus_textroom_incoming_rtp,\n\t\t.incoming_rtcp = janus_textroom_incoming_rtcp,\n\t\t.incoming_data = janus_textroom_incoming_data,\n\t\t.data_ready = janus_textroom_data_ready,\n\t\t.slow_link = janus_textroom_slow_link,\n\t\t.hangup_media = janus_textroom_hangup_media,\n\t\t.destroy_session = janus_textroom_destroy_session,\n\t\t.query_session = janus_textroom_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_TEXTROOM_NAME);\n\treturn &janus_textroom_plugin;\n}\n\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter transaction_parameters[] = {\n\t{\"textroom\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"transaction\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter room_parameters[] = {\n\t{\"room\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter roomopt_parameters[] = {\n\t{\"room\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter roomstr_parameters[] = {\n\t{\"room\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter roomstropt_parameters[] = {\n\t{\"room\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter adminkey_parameters[] = {\n\t{\"admin_key\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter create_parameters[] = {\n\t{\"description\", JSON_STRING, 0},\n\t{\"secret\", JSON_STRING, 0},\n\t{\"pin\", JSON_STRING, 0},\n\t{\"post\", JSON_STRING, 0},\n\t{\"is_private\", JANUS_JSON_BOOL, 0},\n\t{\"history\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"allowed\", JSON_ARRAY, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter destroy_parameters[] = {\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter edit_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"new_description\", JSON_STRING, 0},\n\t{\"new_secret\", JSON_STRING, 0},\n\t{\"new_pin\", JSON_STRING, 0},\n\t{\"new_post\", JSON_STRING, 0},\n\t{\"new_is_private\", JANUS_JSON_BOOL, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter allowed_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"action\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"allowed\", JSON_ARRAY, 0}\n};\nstatic struct janus_json_parameter kick_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"username\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter join_parameters[] = {\n\t{\"username\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"pin\", JSON_STRING, 0},\n\t{\"token\", JSON_STRING, 0},\n\t{\"display\", JSON_STRING, 0},\n\t{\"history\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter message_parameters[] = {\n\t{\"text\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"to\", JSON_STRING, 0},\n\t{\"tos\", JSON_ARRAY, 0},\n\t{\"ack\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter announcement_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"text\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\n\n/* Static configuration instance */\nstatic janus_config *config = NULL;\nstatic const char *config_folder = NULL;\nstatic janus_mutex config_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic gboolean string_ids = FALSE;\nstatic janus_callbacks *gateway = NULL;\nstatic GThread *handler_thread;\nstatic void *janus_textroom_handler(void *data);\nstatic void janus_textroom_hangup_media_internal(janus_plugin_session *handle);\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n\ntypedef struct janus_textroom_room {\n\tguint64 room_id;\t\t\t/* Unique room ID (when using integers) */\n\tgchar *room_id_str;\t\t\t/* Unique room ID (when using strings) */\n\tgchar *room_name;\t\t\t/* Room description */\n\tgchar *room_secret;\t\t\t/* Secret needed to manipulate (e.g., destroy) this room */\n\tgchar *room_pin;\t\t\t/* Password needed to join this room, if any */\n\tgboolean is_private;\t\t/* Whether this room is 'private' (as in hidden) or not */\n\tgchar *http_backend;\t\t/* Server to contact via HTTP POST for incoming messages, if any */\n\tGHashTable *participants;\t/* Map of participants */\n\tuint16_t history_size;\t\t/* Number of messages we should store in the history */\n\tGQueue *history;\t\t\t/* History of past messages */\n\tgboolean check_tokens;\t\t/* Whether to check tokens when participants join (see below) */\n\tGHashTable *allowed;\t\t/* Map of participants (as tokens) allowed to join */\n\tvolatile gint destroyed;\t/* Whether this room has been destroyed */\n\tjanus_mutex mutex;\t\t\t/* Mutex to lock this room instance */\n\tjanus_refcount ref;\n} janus_textroom_room;\nstatic GHashTable *rooms = NULL;\nstatic janus_mutex rooms_mutex = JANUS_MUTEX_INITIALIZER;\nstatic char *admin_key = NULL;\n\ntypedef struct janus_textroom_session {\n\tjanus_plugin_session *handle;\n\tgint64 sdp_sessid;\n\tgint64 sdp_version;\n\tGHashTable *rooms;\t\t\t/* Map of rooms this user is in, and related participant instance */\n\tjanus_mutex mutex;\t\t\t/* Mutex to lock this session */\n\tvolatile gint setup;\n\tvolatile gint dataready;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_textroom_session;\nstatic GHashTable *sessions;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\ntypedef struct janus_textroom_participant {\n\tjanus_textroom_session *session;\n\tjanus_textroom_room *room;\t/* Room this participant is in */\n\tgchar *username;\t\t\t/* Unique username in the room */\n\tgchar *display;\t\t\t\t/* Display name in the room, if any */\n\tjanus_mutex mutex;\t\t\t/* Mutex to lock this session */\n\tvolatile gint destroyed;\t/* Whether this participant has been destroyed */\n\tjanus_refcount ref;\n} janus_textroom_participant;\n\nstatic void janus_textroom_room_destroy(janus_textroom_room *textroom) {\n\tif(textroom && g_atomic_int_compare_and_exchange(&textroom->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&textroom->ref);\n}\nstatic void janus_textroom_room_free(const janus_refcount *textroom_ref) {\n\tjanus_textroom_room *textroom = janus_refcount_containerof(textroom_ref, janus_textroom_room, ref);\n\t/* This room can be destroyed, free all the resources */\n\tg_free(textroom->room_id_str);\n\tg_free(textroom->room_name);\n\tg_free(textroom->room_secret);\n\tg_free(textroom->room_pin);\n\tg_free(textroom->http_backend);\n\tg_hash_table_destroy(textroom->participants);\n\tg_hash_table_destroy(textroom->allowed);\n\tif(textroom->history)\n\t\tg_queue_free_full(textroom->history, (GDestroyNotify)g_free);\n\tjanus_mutex_destroy(&textroom->mutex);\n\tg_free(textroom);\n}\n\nstatic void janus_textroom_session_destroy(janus_textroom_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\nstatic void janus_textroom_session_free(const janus_refcount *session_ref) {\n\tjanus_textroom_session *session = janus_refcount_containerof(session_ref, janus_textroom_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tg_hash_table_destroy(session->rooms);\n\tjanus_mutex_destroy(&session->mutex);\n\tg_free(session);\n}\n\nstatic void janus_textroom_participant_dereference(janus_textroom_participant *p) {\n\tif(p)\n\t\tjanus_refcount_decrease(&p->ref);\n}\n\nstatic void janus_textroom_participant_destroy(janus_textroom_participant *participant) {\n\tif(participant && g_atomic_int_compare_and_exchange(&participant->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&participant->ref);\n}\nstatic void janus_textroom_participant_free(const janus_refcount *participant_ref) {\n\tjanus_textroom_participant *participant = janus_refcount_containerof(participant_ref, janus_textroom_participant, ref);\n\t/* This participant can be destroyed, free all the resources */\n\tg_free(participant->username);\n\tg_free(participant->display);\n\tjanus_mutex_destroy(&participant->mutex);\n\tg_free(participant);\n}\n\n\ntypedef struct janus_textroom_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_textroom_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_textroom_message exit_message;\n\nstatic void janus_textroom_message_free(janus_textroom_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_textroom_session *session = (janus_textroom_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\n\n/* SDP template: we only offer data channels */\n#define sdp_template \\\n\t\t\"v=0\\r\\n\" \\\n\t\t\"o=- %\"SCNu64\" %\"SCNu64\" IN IP4 127.0.0.1\\r\\n\"\t/* We need current time here */ \\\n\t\t\"s=Janus TextRoom plugin\\r\\n\" \\\n\t\t\"t=0 0\\r\\n\" \\\n\t\t\"m=application 1 UDP/DTLS/SCTP webrtc-datachannel\\r\\n\" \\\n\t\t\"c=IN IP4 1.1.1.1\\r\\n\" \\\n\t\t\"a=sctp-port:5000\\r\\n\"\n\n\n/* Error codes */\n#define JANUS_TEXTROOM_ERROR_NO_MESSAGE\t\t\t411\n#define JANUS_TEXTROOM_ERROR_INVALID_JSON\t\t412\n#define JANUS_TEXTROOM_ERROR_MISSING_ELEMENT\t413\n#define JANUS_TEXTROOM_ERROR_INVALID_ELEMENT\t414\n#define JANUS_TEXTROOM_ERROR_INVALID_REQUEST\t415\n#define JANUS_TEXTROOM_ERROR_ALREADY_SETUP\t\t416\n#define JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM\t\t417\n#define JANUS_TEXTROOM_ERROR_ROOM_EXISTS\t\t418\n#define JANUS_TEXTROOM_ERROR_UNAUTHORIZED\t\t419\n#define JANUS_TEXTROOM_ERROR_USERNAME_EXISTS\t420\n#define JANUS_TEXTROOM_ERROR_ALREADY_IN_ROOM\t421\n#define JANUS_TEXTROOM_ERROR_NOT_IN_ROOM\t\t422\n#define JANUS_TEXTROOM_ERROR_NO_SUCH_USER\t\t423\n#define JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR\t\t499\n\n#ifdef HAVE_LIBCURL\nstatic size_t janus_textroom_write_data(void *buffer, size_t size, size_t nmemb, void *userp) {\n\treturn size*nmemb;\n}\n#endif\n\n/* We use this method to handle incoming requests. Since most of the requests\n * will arrive from data channels, but some may also arrive from the regular\n * plugin messaging (e.g., room management), we have the ability to pass\n * parsed JSON objects instead of strings, which explains why we specify a\n * janus_plugin_result pointer as a return value; messages handles via\n * datachannels would simply return NULL. Besides, some requests are actually\n * originated internally, and don't need any response to be sent to anyone,\n * which is what the additional boolean \"internal\" value is for */\njanus_plugin_result *janus_textroom_handle_incoming_request(janus_plugin_session *handle,\n\tchar *text, json_t *json, gboolean internal);\n\n\n/* Plugin implementation */\nint janus_textroom_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n#ifndef HAVE_SCTP\n\t/* Data channels not supported, no point loading this plugin */\n\tJANUS_LOG(LOG_WARN, \"Data channels support not compiled, disabling TextRoom plugin\\n\");\n\treturn -1;\n#endif\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_TEXTROOM_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tconfig = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_TEXTROOM_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_TEXTROOM_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tconfig_folder = config_path;\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_textroom_session_destroy);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_textroom_message_free);\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Parse configuration to populate the rooms list */\n\tif(config != NULL) {\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\t\tif(item && item->value) {\n\t\t\t/* Check how we need to format/serialize the JSON output */\n\t\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t}\n\t\t}\n\t\t/* Any admin key to limit who can \"create\"? */\n\t\tjanus_config_item *key = janus_config_get(config, config_general, janus_config_type_item, \"admin_key\");\n\t\tif(key != NULL && key->value != NULL)\n\t\t\tadmin_key = g_strdup(key->value);\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_TEXTROOM_NAME);\n\t\t}\n\t\tjanus_config_item *ids = janus_config_get(config, config_general, janus_config_type_item, \"string_ids\");\n\t\tif(ids != NULL && ids->value != NULL)\n\t\t\tstring_ids = janus_is_true(ids->value);\n\t\tif(string_ids) {\n\t\t\tJANUS_LOG(LOG_INFO, \"TextRoom will use alphanumeric IDs, not numeric\\n\");\n\t\t}\n\t}\n\t/* Iterate on all rooms */\n\trooms = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_textroom_room_destroy);\n\tif(config != NULL) {\n\t\tGList *clist = janus_config_get_categories(config, NULL), *cl = clist;\n\t\twhile(cl != NULL) {\n\t\t\tjanus_config_category *cat = (janus_config_category *)cl->data;\n\t\t\tif(cat->name == NULL || !strcasecmp(cat->name, \"general\")) {\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Adding TextRoom room '%s'\\n\", cat->name);\n\t\t\tjanus_config_item *desc = janus_config_get(config, cat, janus_config_type_item, \"description\");\n\t\t\tjanus_config_item *priv = janus_config_get(config, cat, janus_config_type_item, \"is_private\");\n\t\t\tjanus_config_item *secret = janus_config_get(config, cat, janus_config_type_item, \"secret\");\n\t\t\tjanus_config_item *pin = janus_config_get(config, cat, janus_config_type_item, \"pin\");\n\t\t\tjanus_config_item *history = janus_config_get(config, cat, janus_config_type_item, \"history\");\n\t\t\tjanus_config_item *post = janus_config_get(config, cat, janus_config_type_item, \"post\");\n\t\t\t/* Create the text room */\n\t\t\tjanus_textroom_room *textroom = g_malloc0(sizeof(janus_textroom_room));\n\t\t\tconst char *room_num = cat->name;\n\t\t\tif(strstr(room_num, \"room-\") == room_num)\n\t\t\t\troom_num += 5;\n\t\t\tif(!string_ids) {\n\t\t\t\ttextroom->room_id = g_ascii_strtoull(room_num, NULL, 0);\n\t\t\t\tif(textroom->room_id == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the TextRoom room, invalid ID 0...\\n\");\n\t\t\t\t\tg_free(textroom);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Make sure the ID is completely numeric */\n\t\t\t\tchar room_id_str[30];\n\t\t\t\tg_snprintf(room_id_str, sizeof(room_id_str), \"%\"SCNu64, textroom->room_id);\n\t\t\t\tif(strcmp(room_num, room_id_str)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the TextRoom room, ID '%s' is not numeric...\\n\", room_num);\n\t\t\t\t\tg_free(textroom);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Let's make sure the room doesn't exist already */\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\tif(g_hash_table_lookup(rooms, string_ids ? (gpointer)room_num : (gpointer)&textroom->room_id) != NULL) {\n\t\t\t\t/* It does... */\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the TextRoom room, room %s already exists...\\n\", room_num);\n\t\t\t\tg_free(textroom);\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\ttextroom->room_id_str = g_strdup(room_num);\n\t\t\tchar *description = NULL;\n\t\t\tif(desc != NULL && desc->value != NULL && strlen(desc->value) > 0)\n\t\t\t\tdescription = g_strdup(desc->value);\n\t\t\telse\n\t\t\t\tdescription = g_strdup(cat->name);\n\t\t\ttextroom->room_name = description;\n\t\t\ttextroom->is_private = priv && priv->value && janus_is_true(priv->value);\n\t\t\tif(secret != NULL && secret->value != NULL) {\n\t\t\t\ttextroom->room_secret = g_strdup(secret->value);\n\t\t\t}\n\t\t\tif(pin != NULL && pin->value != NULL) {\n\t\t\t\ttextroom->room_pin = g_strdup(pin->value);\n\t\t\t}\n\t\t\tif(history != NULL && history->value != NULL) {\n\t\t\t\tif(janus_string_to_uint16(history->value, &textroom->history_size) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid history size value (%s), disabling history...\\n\", history->value);\n\t\t\t\t} else {\n\t\t\t\t\tif(textroom->history_size > 0)\n\t\t\t\t\t\ttextroom->history = g_queue_new();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(post != NULL && post->value != NULL) {\n#ifdef HAVE_LIBCURL\n\t\t\t\t/* FIXME Should we check if this is a valid HTTP address? */\n\t\t\t\ttextroom->http_backend = g_strdup(post->value);\n#else\n\t\t\t\tJANUS_LOG(LOG_WARN, \"HTTP backend specified, but libcurl support was not built in...\\n\");\n#endif\n\t\t\t}\n\t\t\ttextroom->participants = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)janus_textroom_participant_dereference);\n\t\t\ttextroom->check_tokens = FALSE;\t/* Static rooms can't have an \"allowed\" list yet, no hooks to the configuration file */\n\t\t\ttextroom->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\t\ttextroom->destroyed = 0;\n\t\t\tjanus_mutex_init(&textroom->mutex);\n\t\t\tjanus_refcount_init(&textroom->ref, janus_textroom_room_free);\n\t\t\tJANUS_LOG(LOG_VERB, \"Created TextRoom: %s (%s, %s, secret: %s, pin: %s, history: %\"SCNu16\" messages)\\n\",\n\t\t\t\ttextroom->room_id_str, textroom->room_name,\n\t\t\t\ttextroom->is_private ? \"private\" : \"public\",\n\t\t\t\ttextroom->room_secret ? textroom->room_secret : \"no secret\",\n\t\t\t\ttextroom->room_pin ? textroom->room_pin : \"no pin\", textroom->history_size);\n\t\t\tg_hash_table_insert(rooms,\n\t\t\t\tstring_ids ? (gpointer)g_strdup(textroom->room_id_str) : (gpointer)janus_uint64_dup(textroom->room_id),\n\t\t\t\ttextroom);\n\t\t\tcl = cl->next;\n\t\t}\n\t\tg_list_free(clist);\n\t\t/* Done: we keep the configuration file open in case we get a \"create\" or \"destroy\" with permanent=true */\n\t}\n\n\t/* Show available rooms */\n\tjanus_mutex_lock(&rooms_mutex);\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, rooms);\n\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_textroom_room *tr = value;\n\t\tJANUS_LOG(LOG_VERB, \"  ::: [%s][%s]\\n\", tr->room_id_str, tr->room_name);\n\t}\n\tjanus_mutex_unlock(&rooms_mutex);\n\n#ifdef HAVE_LIBCURL\n\tcurl_global_init(CURL_GLOBAL_ALL);\n#endif\n\n\tg_atomic_int_set(&initialized, 1);\n\n\tGError *error = NULL;\n\t/* Launch the thread that will handle incoming messages */\n\thandler_thread = g_thread_try_new(\"textroom handler\", janus_textroom_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the TextRoom handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_TEXTROOM_NAME);\n\treturn 0;\n}\n\nvoid janus_textroom_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tjanus_mutex_lock(&rooms_mutex);\n\tg_hash_table_destroy(rooms);\n\trooms = NULL;\n\tjanus_mutex_unlock(&rooms_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\n#ifdef HAVE_LIBCURL\n\tcurl_global_cleanup();\n#endif\n\n\tjanus_config_destroy(config);\n\tg_free(admin_key);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_TEXTROOM_NAME);\n}\n\nint janus_textroom_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_textroom_get_version(void) {\n\treturn JANUS_TEXTROOM_VERSION;\n}\n\nconst char *janus_textroom_get_version_string(void) {\n\treturn JANUS_TEXTROOM_VERSION_STRING;\n}\n\nconst char *janus_textroom_get_description(void) {\n\treturn JANUS_TEXTROOM_DESCRIPTION;\n}\n\nconst char *janus_textroom_get_name(void) {\n\treturn JANUS_TEXTROOM_NAME;\n}\n\nconst char *janus_textroom_get_author(void) {\n\treturn JANUS_TEXTROOM_AUTHOR;\n}\n\nconst char *janus_textroom_get_package(void) {\n\treturn JANUS_TEXTROOM_PACKAGE;\n}\n\nstatic janus_textroom_session *janus_textroom_lookup_session(janus_plugin_session *handle) {\n\tjanus_textroom_session *session = NULL;\n\tif (g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_textroom_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_textroom_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_textroom_session *session = g_malloc0(sizeof(janus_textroom_session));\n\tsession->handle = handle;\n\tsession->rooms = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_textroom_participant_dereference);\n\tsession->destroyed = 0;\n\tjanus_mutex_init(&session->mutex);\n\tjanus_refcount_init(&session->ref, janus_textroom_session_free);\n\tg_atomic_int_set(&session->setup, 0);\n\tg_atomic_int_set(&session->dataready, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n\thandle->plugin_handle = session;\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_textroom_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_textroom_session *session = janus_textroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Removing TextRoom session...\\n\");\n\tjanus_textroom_hangup_media_internal(handle);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\njson_t *janus_textroom_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_textroom_session *session = janus_textroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* TODO Return meaningful info: participant details, rooms they're in, etc. */\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"destroyed\", json_integer(session->destroyed));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstruct janus_plugin_result *janus_textroom_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\t/* Pre-parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_textroom_session *session = janus_textroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No session associated with this handle...\");\n\t\tgoto plugin_response;\n\t}\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tJANUS_LOG(LOG_ERR, \"Session has already been destroyed...\\n\");\n\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"Session has already been destroyed...\");\n\t\tgoto plugin_response;\n\t}\n\n\tif(message == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\terror_code = JANUS_TEXTROOM_ERROR_NO_MESSAGE;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\tgoto plugin_response;\n\t}\n\tif(!json_is_object(root)) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_JSON;\n\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\tgoto plugin_response;\n\t}\n\t/* Get the request first */\n\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\tjson_t *request = json_object_get(root, \"request\");\n\t/* Some requests (e.g., 'create' and 'destroy') can be handled synchronously */\n\tconst char *request_text = json_string_value(request);\n\tif(!strcasecmp(request_text, \"list\")\n\t\t\t|| !strcasecmp(request_text, \"listparticipants\")\n\t\t\t|| !strcasecmp(request_text, \"exists\")\n\t\t\t|| !strcasecmp(request_text, \"create\")\n\t\t\t|| !strcasecmp(request_text, \"edit\")\n\t\t\t|| !strcasecmp(request_text, \"announcement\")\n\t\t\t|| !strcasecmp(request_text, \"allowed\")\n\t\t\t|| !strcasecmp(request_text, \"kick\")\n\t\t\t|| !strcasecmp(request_text, \"destroy\")) {\n\t\t/* These requests typically only belong to the datachannel\n\t\t * messaging, but for admin purposes we might use them on\n\t\t * the Janus API as well: add the properties the datachannel\n\t\t * processor would expect and handle everything there */\n\t\tif(json_object_get(root, \"textroom\") == NULL)\n\t\t\tjson_object_set_new(root, \"textroom\", json_string(request_text));\n\t\tjson_object_set_new(root, \"transaction\", json_string(transaction));\n\t\tjanus_plugin_result *result = janus_textroom_handle_incoming_request(session->handle, NULL, root, FALSE);\n\t\tif(result == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_JSON;\n\t\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\t\tgoto plugin_response;\n\t\t}\n\t\tif(root != NULL)\n\t\t\tjson_decref(root);\n\t\tif(jsep != NULL)\n\t\t\tjson_decref(jsep);\n\t\tg_free(transaction);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn result;\n\t} else if(!strcasecmp(request_text, \"setup\") || !strcasecmp(request_text, \"ack\") || !strcasecmp(request_text, \"restart\")) {\n\t\t/* These messages are handled asynchronously */\n\t\tjanus_textroom_message *msg = g_malloc(sizeof(janus_textroom_message));\n\t\tmsg->handle = handle;\n\t\tmsg->transaction = transaction;\n\t\tmsg->message = root;\n\t\tmsg->jsep = jsep;\n\n\t\tg_async_queue_push(messages, msg);\n\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(!response) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"textroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\tif(root != NULL)\n\t\t\t\tjson_decref(root);\n\t\t\tif(jsep != NULL)\n\t\t\t\tjson_decref(jsep);\n\t\t\tg_free(transaction);\n\n\t\t\tif(session != NULL)\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, response);\n\t\t}\n\n}\n\njson_t *janus_textroom_handle_admin_message(json_t *message) {\n\t/* Some requests (e.g., 'create' and 'destroy') can be handled via Admin API */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *response = NULL;\n\n\tJANUS_VALIDATE_JSON_OBJECT(message, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto admin_response;\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\tif(!strcasecmp(request_text, \"list\")\n\t\t\t|| !strcasecmp(request_text, \"listparticipants\")\n\t\t\t|| !strcasecmp(request_text, \"exists\")\n\t\t\t|| !strcasecmp(request_text, \"create\")\n\t\t\t|| !strcasecmp(request_text, \"edit\")\n\t\t\t|| !strcasecmp(request_text, \"announcement\")\n\t\t\t|| !strcasecmp(request_text, \"allowed\")\n\t\t\t|| !strcasecmp(request_text, \"kick\")\n\t\t\t|| !strcasecmp(request_text, \"destroy\")) {\n\t\tif(json_object_get(message, \"textroom\") == NULL)\n\t\t\tjson_object_set_new(message, \"textroom\", json_string(request_text));\n\t\tjanus_plugin_result *result = janus_textroom_handle_incoming_request(NULL, NULL, message, FALSE);\n\t\tif(result == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_JSON;\n\t\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\t\tgoto admin_response;\n\t\t}\n\t\tresponse = result->content;\n\t\tresult->content = NULL;\n\t\tjanus_plugin_result_destroy(result);\n\t\tgoto admin_response;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nadmin_response:\n\t\t{\n\t\t\tif(!response) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"textroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nvoid janus_textroom_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_TEXTROOM_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_textroom_session *session = janus_textroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(session->destroyed) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nvoid janus_textroom_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\t/* We don't do audio/video */\n}\n\nvoid janus_textroom_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\t/* We don't do audio/video */\n}\n\nvoid janus_textroom_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet) {\n\tif(handle == NULL || handle->stopped || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(packet->binary) {\n\t\t/* We don't support binary data in the TextRoom plugin, it has to be text */\n\t\tJANUS_LOG(LOG_ERR, \"Binary data received, dropping...\\n\");\n\t\treturn;\n\t}\n\t/* Incoming request from this user: what should we do? */\n\tjanus_textroom_session *session = (janus_textroom_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tif(session->destroyed) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\tif(buf == NULL || len <= 0) {\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn;\n\t}\n\tchar *text = g_malloc(len+1);\n\tmemcpy(text, buf, len);\n\t*(text+len) = '\\0';\n\tJANUS_LOG(LOG_VERB, \"Got a DataChannel message (%zu bytes): %s\\n\", strlen(text), text);\n\tjanus_textroom_handle_incoming_request(handle, text, NULL, FALSE);\n\tjanus_refcount_decrease(&session->ref);\n}\n\nvoid janus_textroom_data_ready(janus_plugin_session *handle) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) ||\n\t\t\tg_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized) || !gateway)\n\t\treturn;\n\t/* Data channels are writable: we shouldn't send anything before this happens */\n\tjanus_textroom_session *session = (janus_textroom_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tif(g_atomic_int_compare_and_exchange(&session->dataready, 0, 1)) {\n\t\tJANUS_LOG(LOG_INFO, \"[%s-%p] Data channel available\\n\", JANUS_TEXTROOM_PACKAGE, handle);\n\t}\n}\n\n/* Helper method to handle incoming messages from the data channel */\njanus_plugin_result *janus_textroom_handle_incoming_request(janus_plugin_session *handle, char *text, json_t *json, gboolean internal) {\n\tjanus_textroom_session *session = NULL;\n\tif(handle)\n\t\tsession = (janus_textroom_session *)handle->plugin_handle;\n\t/* Parse JSON, if needed */\n\tjson_error_t error;\n\tjson_t *root = text ? json_loads(text, 0, &error) : json;\n\tg_free(text);\n\tif(!root) {\n\t\tJANUS_LOG(LOG_ERR, \"Error parsing data channel message (JSON error: on line %d: %s)\\n\", error.line, error.text);\n\t\treturn NULL;\n\t}\n\t/* Handle request */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(root, transaction_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\tconst char *transaction_text = NULL;\n\tjson_t *reply = NULL;\n\tif(error_code != 0)\n\t\tgoto msg_response;\n\tjson_t *request = json_object_get(root, \"textroom\");\n\tjson_t *transaction = json_object_get(root, \"transaction\");\n\tconst char *request_text = json_string_value(request);\n\ttransaction_text = json_string_value(transaction);\n\tif(!strcasecmp(request_text, \"message\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, message_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&textroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\tjanus_textroom_participant *participant = g_hash_table_lookup(session->rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Not in room %s\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NOT_IN_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"Not in room %s\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&participant->ref);\n\t\tjson_t *username = json_object_get(root, \"to\");\n\t\tjson_t *usernames = json_object_get(root, \"tos\");\n\t\tif(username && usernames) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Both to and tos array provided\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Both to and tos array provided\");\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjson_t *text = json_object_get(root, \"text\");\n\t\tconst char *message = json_string_value(text);\n\t\t/* Prepare outgoing message */\n\t\tjson_t *msg = json_object();\n\t\tjson_object_set_new(msg, \"textroom\", json_string(\"message\"));\n\t\tjson_object_set_new(msg, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(msg, \"from\", json_string(participant->username));\n\t\ttime_t timer;\n\t\ttime(&timer);\n\t\tstruct tm *tm_info = localtime(&timer);\n\t\tchar msgTime[64];\n\t\tstrftime(msgTime, sizeof(msgTime), \"%FT%T%z\", tm_info);\n\t\tjson_object_set_new(msg, \"date\", json_string(msgTime));\n\t\tjson_object_set_new(msg, \"text\", json_string(message));\n\t\tif(username || usernames)\n\t\t\tjson_object_set_new(msg, \"whisper\", json_true());\n\t\tchar *msg_text = json_dumps(msg, json_format);\n\t\tif(msg_text == NULL) {\n\t\t\tjson_decref(msg);\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Failed to stringify message\");\n\t\t\tgoto msg_response;\n\t\t}\n\t\tchar *history_text = NULL;\n\t\tif(textroom->history) {\n\t\t\tjson_object_set_new(msg, \"display\", json_string(participant->display));\n\t\t\thistory_text = json_dumps(msg, json_format);\n\t\t}\n\t\tjson_decref(msg);\n\t\t/* Start preparing the response too */\n\t\treply = json_object();\n\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t/* Who should we send this message to? */\n\t\tif(username) {\n\t\t\t/* A single user */\n\t\t\tjson_t *sent = json_object();\n\t\t\tconst char *to = json_string_value(username);\n\t\t\tJANUS_LOG(LOG_VERB, \"To %s in %s: %s\\n\", to, room_id_str, message);\n\t\t\tjanus_textroom_participant *top = g_hash_table_lookup(textroom->participants, to);\n\t\t\tif(top) {\n\t\t\t\tjanus_refcount_increase(&top->ref);\n\t\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = msg_text, .length = strlen(msg_text) };\n\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t\tjanus_refcount_decrease(&top->ref);\n\t\t\t\tjson_object_set_new(sent, to, json_true());\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"User %s is not in room %s, failed to send message\\n\", to, room_id_str);\n\t\t\t\tjson_object_set_new(sent, to, json_false());\n\t\t\t}\n\t\t\tjson_object_set_new(reply, \"sent\", sent);\n\t\t} else if(usernames) {\n\t\t\t/* A limited number of users */\n\t\t\tjson_t *sent = json_object();\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(usernames); i++) {\n\t\t\t\tjson_t *u = json_array_get(usernames, i);\n\t\t\t\tconst char *to = json_string_value(u);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"To %s in %s: %s\\n\", to, room_id_str, message);\n\t\t\t\tjanus_textroom_participant *top = g_hash_table_lookup(textroom->participants, to);\n\t\t\t\tif(top) {\n\t\t\t\t\tjanus_refcount_increase(&top->ref);\n\t\t\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = msg_text, .length = strlen(msg_text) };\n\t\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t\t\tjanus_refcount_decrease(&top->ref);\n\t\t\t\t\tjson_object_set_new(sent, to, json_true());\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"User %s is not in room %s, failed to send message\\n\", to, room_id_str);\n\t\t\t\t\tjson_object_set_new(sent, to, json_false());\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_object_set_new(reply, \"sent\", sent);\n\t\t} else {\n\t\t\t/* Everybody in the room */\n\t\t\tJANUS_LOG(LOG_VERB, \"To everybody in %s: %s\\n\", room_id_str, message);\n\t\t\tif(textroom->participants) {\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tg_hash_table_iter_init(&iter, textroom->participants);\n\t\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\tjanus_textroom_participant *top = value;\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> To %s in %s: %s\\n\", top->username, room_id_str, message);\n\t\t\t\t\tjanus_refcount_increase(&top->ref);\n\t\t\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = msg_text, .length = strlen(msg_text) };\n\t\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t\t\tjanus_refcount_decrease(&top->ref);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(textroom->history && history_text) {\n\t\t\t\t/* Store in the history */\n\t\t\t\tg_queue_push_tail(textroom->history, history_text);\n\t\t\t\tif(g_queue_get_length(textroom->history) > textroom->history_size) {\n\t\t\t\t\tchar *text = (char *)g_queue_pop_head(textroom->history);\n\t\t\t\t\tg_free(text);\n\t\t\t\t}\n\t\t\t}\n#ifdef HAVE_LIBCURL\n\t\t\t/* Is there a backend waiting for this message too? */\n\t\t\tif(textroom->http_backend) {\n\t\t\t\t/* Prepare the libcurl context */\n\t\t\t\tCURLcode res;\n\t\t\t\tCURL *curl = curl_easy_init();\n\t\t\t\tif(curl == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error initializing CURL context\\n\");\n\t\t\t\t} else {\n\t\t\t\t\tcurl_easy_setopt(curl, CURLOPT_URL, textroom->http_backend);\n\t\t\t\t\tstruct curl_slist *headers = NULL;\n\t\t\t\t\theaders = curl_slist_append(headers, \"Accept: application/json\");\n\t\t\t\t\theaders = curl_slist_append(headers, \"Content-Type: application/json\");\n\t\t\t\t\theaders = curl_slist_append(headers, \"charsets: utf-8\");\n\t\t\t\t\tcurl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n\t\t\t\t\tcurl_easy_setopt(curl, CURLOPT_POSTFIELDS, msg_text);\n\t\t\t\t\tcurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, janus_textroom_write_data);\n\t\t\t\t\t/* Send the request */\n\t\t\t\t\tres = curl_easy_perform(curl);\n\t\t\t\t\tif(res != CURLE_OK) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't relay event to the backend: %s\\n\", curl_easy_strerror(res));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_DBG, \"Event sent!\\n\");\n\t\t\t\t\t}\n\t\t\t\t\tcurl_easy_cleanup(curl);\n\t\t\t\t\tcurl_slist_free_all(headers);\n\t\t\t\t}\n\t\t\t}\n#endif\n\t\t}\n\t\tjanus_refcount_decrease(&participant->ref);\n\t\tfree(msg_text);\n\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t/* By default we send a confirmation back to the user that sent this message:\n\t\t * if the user passed an ack=false, though, we don't do that */\n\t\tjson_t *ack = json_object_get(root, \"ack\");\n\t\tif(!internal && (ack == NULL || json_is_true(ack))) {\n\t\t\t/* Send response back */\n\t\t} else {\n\t\t\tinternal = TRUE;\n\t\t\tjson_decref(reply);\n\t\t\treply = NULL;\n\t\t}\n\t} else if(!strcasecmp(request_text, \"join\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, join_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&textroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\t/* A PIN may be required for this action */\n\t\tJANUS_CHECK_SECRET(textroom->room_pin, root, \"pin\", error_code, error_cause,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tif(g_hash_table_lookup(session->rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id) != NULL) {\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Already in room %s\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_ALREADY_IN_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"Already in room %s\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjson_t *username = json_object_get(root, \"username\");\n\t\tconst char *username_text = json_string_value(username);\n\t\tjanus_textroom_participant *participant = g_hash_table_lookup(textroom->participants, username_text);\n\t\tif(participant != NULL) {\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Username already taken\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_USERNAME_EXISTS;\n\t\t\tg_snprintf(error_cause, 512, \"Username already taken\");\n\t\t\tgoto msg_response;\n\t\t}\n\t\t/* A token might be required too */\n\t\tif(textroom->check_tokens) {\n\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\tconst char *token_text = token ? json_string_value(token) : NULL;\n\t\t\tif(token_text == NULL || g_hash_table_lookup(textroom->allowed, token_text) == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized (not in the allowed list)\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNAUTHORIZED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unauthorized (not in the allowed list)\");\n\t\t\t\tgoto msg_response;\n\t\t\t}\n\t\t}\n\t\tjson_t *display = json_object_get(root, \"display\");\n\t\tconst char *display_text = json_string_value(display);\n\t\t/* Create a participant instance */\n\t\tparticipant = g_malloc(sizeof(janus_textroom_participant));\n\t\tparticipant->session = session;\n\t\tparticipant->room = textroom;\n\t\tparticipant->username = g_strdup(username_text);\n\t\tparticipant->display = display_text ? g_strdup(display_text) : NULL;\n\t\tparticipant->destroyed = 0;\n\t\tjanus_mutex_init(&participant->mutex);\n\t\tjanus_refcount_init(&participant->ref, janus_textroom_participant_free);\n\t\tjanus_refcount_increase(&participant->ref);\n\t\tg_hash_table_insert(session->rooms,\n\t\t\tstring_ids ? (gpointer)g_strdup(textroom->room_id_str) : (gpointer)janus_uint64_dup(textroom->room_id),\n\t\t\tparticipant);\n\t\tjanus_refcount_increase(&participant->ref);\n\t\tg_hash_table_insert(textroom->participants, participant->username, participant);\n\t\t/* Check if we need to send some history back */\n\t\tjson_t *history = json_object_get(root, \"history\");\n\t\tgboolean send_history = history ? json_is_true(history) : TRUE;\n\t\tif(send_history) {\n\t\t\tif(textroom->history != NULL && textroom->history->head != NULL) {\n\t\t\t\tGList *temp = textroom->history->head;\n\t\t\t\tchar *text = NULL;\n\t\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = NULL, .length = 0 };\n\t\t\t\twhile(temp) {\n\t\t\t\t\ttext = (char *)temp->data;\n\t\t\t\t\tdata.buffer = text;\n\t\t\t\t\tdata.length = strlen(text);\n\t\t\t\t\tgateway->relay_data(handle, &data);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Notify all participants */\n\t\tJANUS_LOG(LOG_VERB, \"Notifying all participants about the new join\\n\");\n\t\tjson_t *list = json_array();\n\t\tif(textroom->participants) {\n\t\t\t/* Prepare event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"textroom\", json_string(\"join\"));\n\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(textroom->room_id_str) : json_integer(textroom->room_id));\n\t\t\tjson_object_set_new(event, \"username\", json_string(username_text));\n\t\t\tif(display_text != NULL)\n\t\t\t\tjson_object_set_new(event, \"display\", json_string(display_text));\n\t\t\tchar *event_text = json_dumps(event, json_format);\n\t\t\tjson_decref(event);\n\t\t\tif(event_text == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Failed to stringify message\");\n\t\t\t\tgoto msg_response;\n\t\t\t}\n\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = event_text, .length = strlen(event_text) };\n\t\t\tgateway->relay_data(handle, &data);\n\t\t\t/* Broadcast */\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, textroom->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_textroom_participant *top = value;\n\t\t\t\tif(top == participant)\n\t\t\t\t\tcontinue;\t/* Skip us */\n\t\t\t\tjanus_refcount_increase(&top->ref);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> To %s in %s\\n\", top->username, room_id_str);\n\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t\t/* Take note of this user */\n\t\t\t\tjson_t *p = json_object();\n\t\t\t\tjson_object_set_new(p, \"username\", json_string(top->username));\n\t\t\t\tif(top->display != NULL)\n\t\t\t\t\tjson_object_set_new(p, \"display\", json_string(top->display));\n\t\t\t\tjson_array_append_new(list, p);\n\t\t\t\tjanus_refcount_decrease(&top->ref);\n\t\t\t}\n\t\t\tfree(event_text);\n\t\t}\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\tjanus_refcount_decrease(&textroom->ref);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"participants\", list);\n\t\t}\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"join\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(info, \"username\", json_string(username_text));\n\t\t\tif(display_text)\n\t\t\t\tjson_object_set_new(info, \"display\", json_string(display_text));\n\t\t\tgateway->notify_event(&janus_textroom_plugin, session->handle, info);\n\t\t}\n\t} else if(!strcasecmp(request_text, \"leave\")) {\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&textroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tjanus_textroom_participant *participant = g_hash_table_lookup(session->rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Not in room %s\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NOT_IN_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"Not in room %s\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&participant->ref);\n\t\tg_hash_table_remove(session->rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tg_hash_table_remove(textroom->participants, participant->username);\n\t\tparticipant->session = NULL;\n\t\tparticipant->room = NULL;\n\t\t/* Notify all participants */\n\t\tJANUS_LOG(LOG_VERB, \"Notifying all participants about the new leave\\n\");\n\t\tif(textroom->participants) {\n\t\t\t/* Prepare event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"textroom\", json_string(\"leave\"));\n\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(textroom->room_id_str) : json_integer(textroom->room_id));\n\t\t\tjson_object_set_new(event, \"username\", json_string(participant->username));\n\t\t\tchar *event_text = json_dumps(event, json_format);\n\t\t\tjson_decref(event);\n\t\t\tif(event_text == NULL) {\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\tjanus_textroom_participant_destroy(participant);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Failed to stringify message\");\n\t\t\t\tgoto msg_response;\n\t\t\t}\n\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = event_text, .length = strlen(event_text) };\n\t\t\tgateway->relay_data(handle, &data);\n\t\t\t/* Broadcast */\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, textroom->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_textroom_participant *top = value;\n\t\t\t\tif(top == participant)\n\t\t\t\t\tcontinue;\t/* Skip us */\n\t\t\t\tjanus_refcount_increase(&top->ref);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> To %s in %s\\n\", top->username, room_id_str);\n\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t\tjanus_refcount_decrease(&top->ref);\n\t\t\t}\n\t\t\tfree(event_text);\n\t\t}\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"leave\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(info, \"username\", json_string(participant->username));\n\t\t\tgateway->notify_event(&janus_textroom_plugin, session->handle, info);\n\t\t}\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\tjanus_refcount_decrease(&textroom->ref);\n\t\tjanus_refcount_decrease(&participant->ref);\n\t\tjanus_textroom_participant_destroy(participant);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t}\n\t} else if(!strcasecmp(request_text, \"list\")) {\n\t\t/* List all rooms (but private ones) and their details (except for the secret, of course...) */\n\t\tJANUS_LOG(LOG_VERB, \"Request for the list for all text rooms\\n\");\n\t\tgboolean lock_room_list = TRUE;\n\t\tif(admin_key != NULL) {\n\t\t\tjson_t *admin_key_json = json_object_get(root, \"admin_key\");\n\t\t\t/* Verify admin_key if it was provided */\n\t\t\tif(admin_key_json != NULL && json_is_string(admin_key_json) && strlen(json_string_value(admin_key_json)) > 0) {\n\t\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tgoto msg_response;\n\t\t\t\t} else {\n\t\t\t\t\tlock_room_list = FALSE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjson_t *list = json_array();\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, rooms);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_textroom_room *room = value;\n\t\t\tif(!room)\n\t\t\t\tcontinue;\n\t\t\tjanus_refcount_increase(&room->ref);\n\t\t\tjanus_mutex_lock(&room->mutex);\n\t\t\tif(room->is_private && lock_room_list) {\n\t\t\t\t/* Skip private room if no valid admin_key was provided */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping private room '%s'\\n\", room->room_name);\n\t\t\t\tjanus_mutex_unlock(&room->mutex);\n\t\t\t\tjanus_refcount_decrease(&room->ref);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *rl = json_object();\n\t\t\tjson_object_set_new(rl, \"room\", string_ids ? json_string(room->room_id_str) : json_integer(room->room_id));\n\t\t\tjson_object_set_new(rl, \"description\", json_string(room->room_name));\n\t\t\tjson_object_set_new(rl, \"pin_required\", room->room_pin ? json_true() : json_false());\n\t\t\tjson_object_set_new(rl, \"num_participants\", json_integer(g_hash_table_size(room->participants)));\n\t\t\tjson_object_set_new(rl, \"history\", json_integer(room->history_size));\n\t\t\tjson_array_append_new(list, rl);\n\t\t\tjanus_mutex_unlock(&room->mutex);\n\t\t\tjanus_refcount_decrease(&room->ref);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"list\", list);\n\t\t}\n\t} else if(!strcasecmp(request_text, \"listparticipants\")) {\n\t\t/* List all participants in a room */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL || g_atomic_int_get(&textroom->destroyed)) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&textroom->ref);\n\t\t/* Return a list of all participants */\n\t\tjson_t *list = json_array();\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, textroom->participants);\n\t\twhile (!g_atomic_int_get(&textroom->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_textroom_participant *p = value;\n\t\t\tjson_t *pl = json_object();\n\t\t\tjson_object_set_new(pl, \"username\", json_string(p->username));\n\t\t\tif(p->display != NULL)\n\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\tjson_array_append_new(list, pl);\n\t\t}\n\t\tjanus_refcount_decrease(&textroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(reply, \"participants\", list);\n\t\t}\n\t} else if(!strcasecmp(request_text, \"allowed\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to edit the list of allowed participants in an existing TextRoom room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, allowed_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *action = json_object_get(root, \"action\");\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *allowed = json_object_get(root, \"allowed\");\n\t\tconst char *action_text = json_string_value(action);\n\t\tif(strcasecmp(action_text, \"enable\") && strcasecmp(action_text, \"disable\") &&\n\t\t\t\tstrcasecmp(action_text, \"add\") && strcasecmp(action_text, \"remove\")) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported action '%s' (allowed)\\n\", action_text);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Unsupported action '%s' (allowed)\", action_text);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(textroom->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tif(!strcasecmp(action_text, \"enable\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Enabling the check on allowed authorization tokens for room %s\\n\", room_id_str);\n\t\t\ttextroom->check_tokens = TRUE;\n\t\t} else if(!strcasecmp(action_text, \"disable\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Disabling the check on allowed authorization tokens for room %s (free entry)\\n\", room_id_str);\n\t\t\ttextroom->check_tokens = FALSE;\n\t\t} else {\n\t\t\tgboolean add = !strcasecmp(action_text, \"add\");\n\t\t\tif(allowed) {\n\t\t\t\t/* Make sure the \"allowed\" array only contains strings */\n\t\t\t\tgboolean ok = TRUE;\n\t\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\t\tjson_t *a = json_array_get(allowed, i);\n\t\t\t\t\t\tif(!a || !json_is_string(a)) {\n\t\t\t\t\t\t\tok = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!ok) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element in the allowed array (not a string)\\n\");\n\t\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element in the allowed array (not a string)\");\n\t\t\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\t\tgoto msg_response;\n\t\t\t\t}\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tconst char *token = json_string_value(json_array_get(allowed, i));\n\t\t\t\t\tif(add) {\n\t\t\t\t\t\tif(!g_hash_table_lookup(textroom->allowed, token))\n\t\t\t\t\t\t\tg_hash_table_insert(textroom->allowed, g_strdup(token), GINT_TO_POINTER(TRUE));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tg_hash_table_remove(textroom->allowed, token);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"room\", string_ids ? json_string(textroom->room_id_str) : json_integer(textroom->room_id));\n\t\t\tjson_t *list = json_array();\n\t\t\tif(strcasecmp(action_text, \"disable\")) {\n\t\t\t\tif(g_hash_table_size(textroom->allowed) > 0) {\n\t\t\t\t\tGHashTableIter iter;\n\t\t\t\t\tgpointer key;\n\t\t\t\t\tg_hash_table_iter_init(&iter, textroom->allowed);\n\t\t\t\t\twhile(g_hash_table_iter_next(&iter, &key, NULL)) {\n\t\t\t\t\t\tchar *token = key;\n\t\t\t\t\t\tjson_array_append_new(list, json_string(token));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(reply, \"allowed\", list);\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_VERB, \"TextRoom room allowed list updated\\n\");\n\t\t}\n\t} else if(!strcasecmp(request_text, \"kick\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to kick a participant from an existing TextRoom room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, kick_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *username = json_object_get(root, \"username\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(textroom->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tconst char *user_id = json_string_value(username);\n\t\tjanus_textroom_participant *participant = g_hash_table_lookup(textroom->participants, user_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such participant %s in room %s\\n\", user_id, room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_USER;\n\t\t\tg_snprintf(error_cause, 512, \"No such user %s in room %s\", user_id, room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\t/* Notify all participants */\n\t\tJANUS_LOG(LOG_VERB, \"Notifying all participants about the new kick\\n\");\n\t\tif(textroom->participants) {\n\t\t\t/* Prepare event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"textroom\", json_string(\"kicked\"));\n\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(textroom->room_id_str) : json_integer(textroom->room_id));\n\t\t\tjson_object_set_new(event, \"username\", json_string(participant->username));\n\t\t\tchar *event_text = json_dumps(event, json_format);\n\t\t\tjson_decref(event);\n\t\t\tif(event_text == NULL) {\n\t\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Failed to stringify message\");\n\t\t\t\tgoto msg_response;\n\t\t\t}\n\t\t\t/* Broadcast */\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, textroom->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_textroom_participant *top = value;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> To %s in %s\\n\", top->username, room_id_str);\n\t\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = event_text, .length = strlen(event_text) };\n\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t}\n\t\t\tfree(event_text);\n\t\t}\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"textroom\", json_string(\"kicked\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(textroom->room_id_str) : json_integer(textroom->room_id));\n\t\t\tjson_object_set_new(info, \"username\", json_string(participant->username));\n\t\t\tgateway->notify_event(&janus_textroom_plugin, session->handle, info);\n\t\t}\n\t\t/* Remove user from list */\n\t\tg_hash_table_remove(participant->session->rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tg_hash_table_remove(textroom->participants, participant->username);\n\t\tparticipant->session = NULL;\n\t\tparticipant->room = NULL;\n\t\tg_free(participant->username);\n\t\tg_free(participant->display);\n\t\tg_free(participant);\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t}\n\t} else if(!strcasecmp(request_text, \"announcement\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to send a TextRoom announcement\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, announcement_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&textroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(textroom->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjson_t *text = json_object_get(root, \"text\");\n\t\tconst char *message = json_string_value(text);\n\t\t/* Prepare outgoing message */\n\t\tjson_t *msg = json_object();\n\t\tjson_object_set_new(msg, \"textroom\", json_string(\"announcement\"));\n\t\tjson_object_set_new(msg, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\ttime_t timer;\n\t\ttime(&timer);\n\t\tstruct tm *tm_info = localtime(&timer);\n\t\tchar msgTime[64];\n\t\tstrftime(msgTime, sizeof(msgTime), \"%FT%T%z\", tm_info);\n\t\tjson_object_set_new(msg, \"date\", json_string(msgTime));\n\t\tjson_object_set_new(msg, \"text\", json_string(message));\n\t\tchar *msg_text = json_dumps(msg, json_format);\n\t\tjson_decref(msg);\n\t\tif(msg_text == NULL) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Failed to stringify message\");\n\t\t\tgoto msg_response;\n\t\t}\n\t\t/* Send the announcement to everybody in the room */\n\t\tif(textroom->participants) {\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, textroom->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_textroom_participant *top = value;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> To %s in %s: %s\\n\", top->username, room_id_str, message);\n\t\t\t\tjanus_refcount_increase(&top->ref);\n\t\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = msg_text, .length = strlen(msg_text) };\n\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t\tjanus_refcount_decrease(&top->ref);\n\t\t\t}\n\t\t}\n\t\tif(textroom->history) {\n\t\t\t/* Store in the history */\n\t\t\tg_queue_push_tail(textroom->history, g_strdup(msg_text));\n\t\t\tif(g_queue_get_length(textroom->history) > textroom->history_size) {\n\t\t\t\tchar *text = (char *)g_queue_pop_head(textroom->history);\n\t\t\t\tg_free(text);\n\t\t\t}\n\t\t}\n#ifdef HAVE_LIBCURL\n\t\t/* Is there a backend waiting for this message too? */\n\t\tif(textroom->http_backend) {\n\t\t\t/* Prepare the libcurl context */\n\t\t\tCURLcode res;\n\t\t\tCURL *curl = curl_easy_init();\n\t\t\tif(curl == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error initializing CURL context\\n\");\n\t\t\t} else {\n\t\t\t\tcurl_easy_setopt(curl, CURLOPT_URL, textroom->http_backend);\n\t\t\t\tstruct curl_slist *headers = NULL;\n\t\t\t\theaders = curl_slist_append(headers, \"Accept: application/json\");\n\t\t\t\theaders = curl_slist_append(headers, \"Content-Type: application/json\");\n\t\t\t\theaders = curl_slist_append(headers, \"charsets: utf-8\");\n\t\t\t\tcurl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);\n\t\t\t\tcurl_easy_setopt(curl, CURLOPT_POSTFIELDS, msg_text);\n\t\t\t\tcurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, janus_textroom_write_data);\n\t\t\t\t/* Send the request */\n\t\t\t\tres = curl_easy_perform(curl);\n\t\t\t\tif(res != CURLE_OK) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't relay event to the backend: %s\\n\", curl_easy_strerror(res));\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_DBG, \"Event sent!\\n\");\n\t\t\t\t}\n\t\t\t\tcurl_easy_cleanup(curl);\n\t\t\t\tcurl_slist_free_all(headers);\n\t\t\t}\n\t\t}\n#endif\n\t\tfree(msg_text);\n\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\tjanus_refcount_decrease(&textroom->ref);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t}\n\t} else if(!strcasecmp(request_text, \"create\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, create_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomopt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstropt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto msg_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto msg_response;\n\t\t}\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *desc = json_object_get(root, \"description\");\n\t\tjson_t *is_private = json_object_get(root, \"is_private\");\n\t\tjson_t *allowed = json_object_get(root, \"allowed\");\n\t\tjson_t *secret = json_object_get(root, \"secret\");\n\t\tjson_t *pin = json_object_get(root, \"pin\");\n\t\tjson_t *history = json_object_get(root, \"history\");\n\t\tjson_t *post = json_object_get(root, \"post\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tif(allowed) {\n\t\t\t/* Make sure the \"allowed\" array only contains strings */\n\t\t\tgboolean ok = TRUE;\n\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tjson_t *a = json_array_get(allowed, i);\n\t\t\t\t\tif(!a || !json_is_string(a)) {\n\t\t\t\t\t\tok = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!ok) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element in the allowed array (not a string)\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element in the allowed array (not a string)\");\n\t\t\t\tgoto msg_response;\n\t\t\t}\n\t\t}\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't create permanent room\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't create permanent room\");\n\t\t\tgoto msg_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tif(room_id == 0 && room_id_str == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Desired room ID is empty, which is not allowed... picking random ID instead\\n\");\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tif(room_id > 0 || room_id_str != NULL) {\n\t\t\t/* Let's make sure the room doesn't exist already */\n\t\t\tif(g_hash_table_lookup(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id) != NULL) {\n\t\t\t\t/* It does... */\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_ROOM_EXISTS;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Room %s already exists!\\n\", room_id_str);\n\t\t\t\tg_snprintf(error_cause, 512, \"Room %s already exists\", room_id_str);\n\t\t\t\tgoto msg_response;\n\t\t\t}\n\t\t}\n\t\t/* Create the text room */\n\t\tjanus_textroom_room *textroom = g_malloc0(sizeof(janus_textroom_room));\n\t\t/* Generate a random ID */\n\t\tgboolean room_id_allocated = FALSE;\n\t\tif(!string_ids && room_id == 0) {\n\t\t\twhile(room_id == 0) {\n\t\t\t\troom_id = janus_random_uint64();\n\t\t\t\tif(g_hash_table_lookup(rooms, &room_id) != NULL) {\n\t\t\t\t\t/* Room ID already taken, try another one */\n\t\t\t\t\troom_id = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else if(string_ids && room_id_str == NULL) {\n\t\t\twhile(room_id_str == NULL) {\n\t\t\t\troom_id_str = janus_random_uuid();\n\t\t\t\tif(g_hash_table_lookup(rooms, room_id_str) != NULL) {\n\t\t\t\t\t/* Room ID already taken, try another one */\n\t\t\t\t\tg_clear_pointer(&room_id_str, g_free);\n\t\t\t\t}\n\t\t\t}\n\t\t\troom_id_allocated = TRUE;\n\t\t}\n\t\ttextroom->room_id = room_id;\n\t\ttextroom->room_id_str = room_id_str ? g_strdup(room_id_str) : NULL;\n\t\tchar *description = NULL;\n\t\tif(desc != NULL && strlen(json_string_value(desc)) > 0) {\n\t\t\tdescription = g_strdup(json_string_value(desc));\n\t\t} else {\n\t\t\tchar roomname[255];\n\t\t\tg_snprintf(roomname, 255, \"Room %s\", textroom->room_id_str);\n\t\t\tdescription = g_strdup(roomname);\n\t\t}\n\t\ttextroom->room_name = description;\n\t\ttextroom->is_private = is_private ? json_is_true(is_private) : FALSE;\n\t\tif(secret)\n\t\t\ttextroom->room_secret = g_strdup(json_string_value(secret));\n\t\tif(pin)\n\t\t\ttextroom->room_pin = g_strdup(json_string_value(pin));\n\t\tif(history) {\n\t\t\ttextroom->history_size = json_integer_value(history);\n\t\t\tif(textroom->history_size > 0)\n\t\t\t\ttextroom->history = g_queue_new();\n\t\t}\n\t\tif(post) {\n#ifdef HAVE_LIBCURL\n\t\t\t/* FIXME Should we check if this is a valid HTTP address? */\n\t\t\ttextroom->http_backend = g_strdup(json_string_value(post));\n#else\n\t\t\tJANUS_LOG(LOG_WARN, \"HTTP backend specified, but libcurl support was not built in...\\n\");\n#endif\n\t\t}\n\t\ttextroom->participants = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)janus_textroom_participant_dereference);\n\t\ttextroom->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\tif(allowed != NULL) {\n\t\t\t/* Populate the \"allowed\" list as an ACL for people trying to join */\n\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tconst char *token = json_string_value(json_array_get(allowed, i));\n\t\t\t\t\tif(!g_hash_table_lookup(textroom->allowed, token))\n\t\t\t\t\t\tg_hash_table_insert(textroom->allowed, g_strdup(token), GINT_TO_POINTER(TRUE));\n\t\t\t\t}\n\t\t\t}\n\t\t\ttextroom->check_tokens = TRUE;\n\t\t}\n\t\ttextroom->destroyed = 0;\n\t\tjanus_mutex_init(&textroom->mutex);\n\t\tjanus_refcount_init(&textroom->ref, janus_textroom_room_free);\n\t\tg_hash_table_insert(rooms,\n\t\t\tstring_ids ? (gpointer)g_strdup(textroom->room_id_str) : (gpointer)janus_uint64_dup(textroom->room_id),\n\t\t\ttextroom);\n\t\tJANUS_LOG(LOG_VERB, \"Created TextRoom: %s (%s, %s, secret: %s, pin: %s)\\n\",\n\t\t\ttextroom->room_id_str, textroom->room_name,\n\t\t\ttextroom->is_private ? \"private\" : \"public\",\n\t\t\ttextroom->room_secret ? textroom->room_secret : \"no secret\",\n\t\t\ttextroom->room_pin ? textroom->room_pin : \"no pin\");\n\t\tif(save) {\n\t\t\t/* This room is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Saving room %s permanently in config file\\n\", textroom->room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ], value[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", textroom->room_id_str);\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, cat);\n\t\t\t/* Now for the values */\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", textroom->room_name));\n\t\t\tif(textroom->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tif(textroom->room_secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", textroom->room_secret));\n\t\t\tif(textroom->room_pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", textroom->room_pin));\n\t\t\tif(textroom->history_size) {\n\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", textroom->history_size);\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"history\", value));\n\t\t\t}\n\t\t\tif(textroom->http_backend)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"post\", textroom->http_backend));\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_TEXTROOM_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\t/* Show updated rooms list */\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, rooms);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_textroom_room *tr = value;\n\t\t\tJANUS_LOG(LOG_VERB, \"  ::: [%s][%s]\\n\", tr->room_id_str, tr->room_name);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\t/* Notice that we reply differently if the request came via Janus API */\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(json == NULL ? \"success\" : \"created\"));\n\t\t\tjson_object_set_new(reply, \"room\", string_ids ? json_string(textroom->room_id_str) : json_integer(textroom->room_id));\n\t\t\tjson_object_set_new(reply, \"permanent\", save ? json_true() : json_false());\n\t\t}\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"created\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tgateway->notify_event(&janus_textroom_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tif(room_id_allocated)\n\t\t\tg_free(room_id_str);\n\t} else if(!strcasecmp(request_text, \"exists\")) {\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tgboolean room_exists = g_hash_table_contains(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(\"success\"));\n\t\t\tjson_object_set_new(reply, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(reply, \"exists\", room_exists ? json_true() : json_false());\n\t\t}\n\t} else if(!strcasecmp(request_text, \"edit\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, edit_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\t/* We only allow for a limited set of properties to be edited */\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *desc = json_object_get(root, \"new_description\");\n\t\tjson_t *secret = json_object_get(root, \"new_secret\");\n\t\tjson_t *is_private = json_object_get(root, \"new_is_private\");\n\t\tjson_t *pin = json_object_get(root, \"new_pin\");\n\t\tjson_t *post = json_object_get(root, \"new_post\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't edit room permanently\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't edit room permanently\");\n\t\t\tgoto msg_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(textroom->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto msg_response;\n\t\t}\n\t\t/* Edit the room properties that were provided */\n\t\tif(desc != NULL && strlen(json_string_value(desc)) > 0) {\n\t\t\tchar *old_description = textroom->room_name;\n\t\t\tchar *new_description = g_strdup(json_string_value(desc));\n\t\t\ttextroom->room_name = new_description;\n\t\t\tg_free(old_description);\n\t\t}\n\t\tif(is_private)\n\t\t\ttextroom->is_private = json_is_true(is_private);\n\t\tif(secret && strlen(json_string_value(secret)) > 0) {\n\t\t\tchar *old_secret = textroom->room_secret;\n\t\t\tchar *new_secret = g_strdup(json_string_value(secret));\n\t\t\ttextroom->room_secret = new_secret;\n\t\t\tg_free(old_secret);\n\t\t}\n\t\tif(post && strlen(json_string_value(post)) > 0) {\n\t\t\tchar *old_post = textroom->http_backend;\n\t\t\tchar *new_post = g_strdup(json_string_value(post));\n\t\t\ttextroom->http_backend = new_post;\n\t\t\tg_free(old_post);\n\t\t}\n\t\tif(pin && strlen(json_string_value(pin)) > 0) {\n\t\t\tchar *old_pin = textroom->room_pin;\n\t\t\tchar *new_pin = g_strdup(json_string_value(pin));\n\t\t\ttextroom->room_pin = new_pin;\n\t\t\tg_free(old_pin);\n\t\t}\n\t\tif(save) {\n\t\t\t/* This change is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Modifying room %s permanently in config file\\n\", room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ], value[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", room_id_str);\n\t\t\t/* Remove the old category first */\n\t\t\tjanus_config_remove(config, NULL, cat);\n\t\t\t/* Now write the room details again */\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, cat);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", textroom->room_name));\n\t\t\tif(textroom->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tif(textroom->room_secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", textroom->room_secret));\n\t\t\tif(textroom->room_pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", textroom->room_pin));\n\t\t\tif(textroom->history_size) {\n\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", textroom->history_size);\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"history\", value));\n\t\t\t}\n\t\t\tif(textroom->http_backend)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"post\", textroom->http_backend));\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_TEXTROOM_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room changes are not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\t/* Notice that we reply differently if the request came via Janus API */\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(json == NULL ? \"success\" : \"edited\"));\n\t\t\tjson_object_set_new(reply, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(reply, \"permanent\", save ? json_true() : json_false());\n\t\t}\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"edited\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tgateway->notify_event(&janus_textroom_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t} else if(!strcasecmp(request_text, \"destroy\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, destroy_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto msg_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't destroy room permanently\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't destroy room permanently\");\n\t\t\tgoto msg_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_textroom_room *textroom = g_hash_table_lookup(rooms,\n\t\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(textroom == NULL) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_SUCH_ROOM;\n\t\t\tg_snprintf(error_cause, 512, \"No such room (%s)\", room_id_str);\n\t\t\tgoto msg_response;\n\t\t}\n\t\tjanus_refcount_increase(&textroom->ref);\n\t\tjanus_mutex_lock(&textroom->mutex);\n\t\t/* A secret may be required for this action */\n\t\tJANUS_CHECK_SECRET(textroom->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT, JANUS_TEXTROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\tgoto msg_response;\n\t\t}\n\t\t/* Remove room */\n\t\tg_hash_table_remove(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tif(save) {\n\t\t\t/* This change is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Destroying room %s permanently in config file\\n\", room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", room_id_str);\n\t\t\tjanus_config_remove(config, NULL, cat);\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_TEXTROOM_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room destruction is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\t/* Notify all participants */\n\t\tJANUS_LOG(LOG_VERB, \"Notifying all participants about the destroy\\n\");\n\t\tif(textroom->participants) {\n\t\t\t/* Prepare event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"textroom\", json_string(\"destroyed\"));\n\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(textroom->room_id_str) : json_integer(textroom->room_id));\n\t\t\tchar *event_text = json_dumps(event, json_format);\n\t\t\tjson_decref(event);\n\t\t\tif(event_text == NULL) {\n\t\t\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tjanus_refcount_decrease(&textroom->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Failed to stringify message\");\n\t\t\t\tgoto msg_response;\n\t\t\t}\n\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = event_text, .length = strlen(event_text) };\n\t\t\tgateway->relay_data(handle, &data);\n\t\t\t/* Broadcast */\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, textroom->participants);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_textroom_participant *top = value;\n\t\t\t\tjanus_refcount_increase(&top->ref);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> To %s in %s\\n\", top->username, room_id_str);\n\t\t\t\tgateway->relay_data(top->session->handle, &data);\n\t\t\t\tjanus_mutex_lock(&top->session->mutex);\n\t\t\t\tg_hash_table_remove(top->session->rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\t\t\tjanus_mutex_unlock(&top->session->mutex);\n\t\t\t\tjanus_refcount_decrease(&top->ref);\n\t\t\t\tjanus_textroom_participant_destroy(top);\n\t\t\t}\n\t\t\tfree(event_text);\n\t\t}\n\t\tjanus_mutex_unlock(&textroom->mutex);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_refcount_decrease(&textroom->ref);\n\t\tif(!internal) {\n\t\t\t/* Send response back */\n\t\t\treply = json_object();\n\t\t\t/* Notice that we reply differently if the request came via Janus API */\n\t\t\tjson_object_set_new(reply, \"textroom\", json_string(json == NULL ? \"success\" : \"destroyed\"));\n\t\t\tjson_object_set_new(reply, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(reply, \"permanent\", save ? json_true() : json_false());\n\t\t}\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"destroyed\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tgateway->notify_event(&janus_textroom_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported request %s\\n\", request_text);\n\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unsupported request %s\", request_text);\n\t\tgoto msg_response;\n\t}\n\nmsg_response:\n\t\t{\n\t\t\tif(!internal) {\n\t\t\t\tif(error_code == 0 && !reply) {\n\t\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t\t}\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t/* Prepare JSON error event */\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"textroom\", json_string(\"error\"));\n\t\t\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\t\t\treply = event;\n\t\t\t\t}\n\t\t\t\tif(transaction_text && json == NULL)\n\t\t\t\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction_text));\n\t\t\t\tif(json == NULL) {\n\t\t\t\t\t/* Reply via data channels */\n\t\t\t\t\tchar *reply_text = json_dumps(reply, json_format);\n\t\t\t\t\tjson_decref(reply);\n\t\t\t\t\tif(reply_text == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_plugin_data data = { .label = NULL, .protocol = NULL, .binary = FALSE, .buffer = reply_text, .length = strlen(reply_text) };\n\t\t\t\t\t\tgateway->relay_data(handle, &data);\n\t\t\t\t\t\tfree(reply_text);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Reply via Janus API */\n\t\t\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, reply);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(root != NULL)\n\t\t\t\tjson_decref(root);\n\t\t}\n\treturn NULL;\n}\n\nvoid janus_textroom_slow_link(janus_plugin_session *handle, int mindex, int uplink, int video) {\n\t/* We don't do audio/video */\n}\n\nvoid janus_textroom_hangup_media(janus_plugin_session *handle) {\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_textroom_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_textroom_hangup_media_internal(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_TEXTROOM_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_textroom_session *session = janus_textroom_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(session->destroyed)\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\tg_atomic_int_set(&session->dataready, 0);\n\t/* Get rid of all participants */\n\tjanus_mutex_lock(&session->mutex);\n\tGList *list = NULL;\n\tif(session->rooms) {\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tg_hash_table_iter_init(&iter, session->rooms);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_textroom_participant *p = value;\n\t\t\tjanus_mutex_lock(&p->mutex);\n\t\t\tif(p->room) {\n\t\t\t\tlist = g_list_append(list, string_ids ?\n\t\t\t\t\t(gpointer)g_strdup(p->room->room_id_str) : (gpointer)janus_uint64_dup(p->room->room_id));\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&p->mutex);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t}\n\tjanus_mutex_unlock(&session->mutex);\n\tJANUS_LOG(LOG_VERB, \"Leaving %d rooms\\n\", g_list_length(list));\n\tchar request[200];\n\tGList *first = list;\n\twhile(list) {\n\t\tchar *room_id_str = (char *)list->data;\n\t\tif(string_ids) {\n\t\t\tg_snprintf(request, sizeof(request), \"{\\\"textroom\\\":\\\"leave\\\",\\\"transaction\\\":\\\"internal\\\",\\\"room\\\":\\\"%s\\\"}\", room_id_str);\n\t\t} else {\n\t\t\tguint64 room_id = *(guint64 *)room_id_str;\n\t\t\tg_snprintf(request, sizeof(request), \"{\\\"textroom\\\":\\\"leave\\\",\\\"transaction\\\":\\\"internal\\\",\\\"room\\\":%\"SCNu64\"}\", room_id);\n\t\t}\n\t\tjanus_textroom_handle_incoming_request(handle, g_strdup(request), NULL, TRUE);\n\t\tlist = list->next;\n\t}\n\tg_list_free_full(first, (GDestroyNotify)g_free);\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_textroom_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining TextRoom handler thread\\n\");\n\tjanus_textroom_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\tgboolean do_offer = FALSE, sdp_update = FALSE;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_textroom_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_textroom_session *session = janus_textroom_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_textroom_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_textroom_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = msg->message;\n\t\tif(msg->message == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(!json_is_object(root)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_JSON;\n\t\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\t\tgoto error;\n\t\t}\n\t\t/* Parse request */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_TEXTROOM_ERROR_MISSING_ELEMENT, JANUS_TEXTROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto error;\n\t\tdo_offer = FALSE;\n\t\tsdp_update = FALSE;\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tdo_offer = FALSE;\n\t\tif(!strcasecmp(request_text, \"setup\")) {\n\t\t\tif(!g_atomic_int_compare_and_exchange(&session->setup, 0, 1)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"PeerConnection already setup\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_ALREADY_SETUP;\n\t\t\t\tg_snprintf(error_cause, 512, \"PeerConnection already setup\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tdo_offer = TRUE;\n\t\t} else if(!strcasecmp(request_text, \"restart\")) {\n\t\t\tif(!g_atomic_int_get(&session->setup)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"PeerConnection not setup\\n\");\n\t\t\t\terror_code = JANUS_TEXTROOM_ERROR_ALREADY_SETUP;\n\t\t\t\tg_snprintf(error_cause, 512, \"PeerConnection not setup\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tsdp_update = TRUE;\n\t\t\tdo_offer = TRUE;\n\t\t} else if(!strcasecmp(request_text, \"ack\")) {\n\t\t\t/* The peer sent their answer back: do nothing */\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\t\terror_code = JANUS_TEXTROOM_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Prepare JSON event */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"textroom\", json_string(\"event\"));\n\t\tjson_object_set_new(event, \"result\", json_string(\"ok\"));\n\t\tif(!do_offer) {\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_textroom_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t} else {\n\t\t\t/* Send an offer (whether it's for an ICE restart or not) */\n\t\t\tif(sdp_update) {\n\t\t\t\t/* Renegotiation: increase version */\n\t\t\t\tsession->sdp_version++;\n\t\t\t} else {\n\t\t\t\t/* New session: generate new values */\n\t\t\t\tsession->sdp_version = 1;\t/* This needs to be increased when it changes */\n\t\t\t\tsession->sdp_sessid = janus_get_real_time();\n\t\t\t}\n\t\t\tchar sdp[500];\n\t\t\tg_snprintf(sdp, sizeof(sdp), sdp_template,\n\t\t\t\tsession->sdp_sessid, session->sdp_version);\n\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", \"offer\", \"sdp\", sdp);\n\t\t\tif(sdp_update)\n\t\t\t\tjson_object_set_new(jsep, \"restart\", json_true());\n\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\tint res = gateway->push_event(msg->handle, &janus_textroom_plugin, msg->transaction, event, jsep);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\",\n\t\t\t\tres, janus_get_monotonic_time()-start);\n\t\t\tjson_decref(jsep);\n\t\t}\n\t\tjson_decref(event);\n\t\tjanus_textroom_message_free(msg);\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"textroom\", json_string(\"error\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_textroom_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_textroom_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving TextRoom handler thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/plugins/janus_videocall.c",
    "content": "/*! \\file   janus_videocall.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus VideoCall plugin\n * \\details Check the \\ref videocall for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page videocall VideoCall plugin documentation\n * This is a simple video call plugin for Janus, allowing two\n * WebRTC peers to call each other through the Janus core. The idea is to\n * provide a similar service as the at the time well known (and now\n * discontinued) AppRTC demo (https://github.com/webrtc/apprtc),\n * but with the media flowing through a server rather than being peer-to-peer.\n *\n * The plugin provides a simple fake registration mechanism. A peer attaching\n * to the plugin needs to specify a username, which acts as a \"phone number\":\n * if the username is free, it is associated with the peer, which means\n * he/she can be \"called\" using that username by another peer. Peers can\n * either \"call\" another peer, by specifying their username, or wait for a call.\n * The approach used by this plugin is similar to the one employed by the\n * echo test one: all frames (RTP/RTCP) coming from one peer are relayed\n * to the other.\n *\n * Just as in the janus_videocall.c plugin, there are knobs to control\n * whether audio and/or video should be muted or not, and if the bitrate\n * of the peer needs to be capped by means of REMB messages.\n *\n * \\section vcallapi Video Call API\n *\n * All requests you can send in the Video Call API are asynchronous,\n * which means all responses (successes and errors) will be delivered\n * as events with the same transaction.\n *\n * The supported requests are \\c list , \\c register , \\c call ,\n * \\c accept , \\c set and \\c hangup . \\c list allows you to get a list\n * of all the registered peers; \\c register can be used to register\n * a username to call and be called; \\c call is used to start a video\n * call with somebody through the plugin, while \\c accept is used to\n * accept the call in case one is invited instead of inviting; \\c set\n * can be used to configure some call-related settings (e.g., a cap on\n * the send bandwidth); finally, \\c hangup can be used to terminate the\n * communication at any time, either to hangup an ongoing call or to\n * cancel/decline a call that hasn't started yet.\n *\n * The \\c list request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"list\"\n}\n\\endverbatim\n *\n * A successful request will result in an array of peers to be returned:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"list\": [\t// Array of peers\n\t\t\t\"alice78\",\n\t\t\t\"bob51\",\n\t\t\t// others\n\t\t]\n\t}\n}\n\\endverbatim\n *\n * An error instead (and the same applies to all other requests, so this\n * won't be repeated) would provide both an error code and a more verbose\n * description of the cause of the issue:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"error_code\" : <numeric ID, check Macros below>,\n\t\"error\" : \"<error description as a string>\"\n}\n\\endverbatim\n *\n * To register a username to call and be called, the \\c register request\n * can be used. This works on a \"first come, first served\" basis: there's\n * no authentication involved, you just specify the username you'd like\n * to use and, if free, it's assigned to you. Notice that there's no\n * way to unregister: you have to close the handle to free the username.\n * The \\c register request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"register\",\n\t\"username\" : \"<desired unique username>\"\n}\n\\endverbatim\n *\n * If successful, this will result in a \\c registered event:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"registered\",\n\t\t\"username\" : \"<same username, registered>\"\n\t}\n}\n\\endverbatim\n *\n * Once you're registered, you can either start a new call or wait to\n * be called by someone else who knows your username. To start a new\n * call, the \\c call request can be used: this request must be attached\n * to a JSEP offer containing the WebRTC-related info to setup a new\n * media session. A \\c call request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"call\",\n\t\"username\" : \"<username to call>\"\n}\n\\endverbatim\n *\n * If successful, this will result in a \\c calling event:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"calling\",\n\t\t\"username\" : \"<same username, registered>\"\n\t}\n}\n\\endverbatim\n *\n * At the same time, the user being called will receive an\n * \\c incomingcall event\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"incomingcall\",\n\t\t\"username\" : \"<your username>\"\n\t}\n}\n\\endverbatim\n *\n * To accept the call, the \\c accept request can be used. This request\n * must be attached to a JSEP answer containing the WebRTC-related\n * information to complete the actual PeerConnection setup. A \\c accept\n * request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"accept\"\n}\n\\endverbatim\n *\n * If successful, both the caller and the callee will receive an\n * \\c accepted event to notify them about the success of the signalling:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"accepted\",\n\t\t\"username\" : \"<caller username>\"\n\t}\n}\n\\endverbatim\n *\n * At this point, the media-related settings of the call can be modified\n * on either side by means of a \\c set request, which acts pretty much\n * as the one in the \\ref echoapi . The \\c set request has to be\n * formatted as follows. All the attributes (except \\c request) are\n * optional, so any request can contain a subset of them:\n *\n\\verbatim\n{\n\t\"request\" : \"set\",\n\t\"audio\" : true|false,\n\t\"video\" : true|false,\n\t\"bitrate\" : <numeric bitrate value>,\n\t\"record\" : true|false,\n\t\"filename\" : <base path/filename to use for the recording>,\n\t\"substream\" : <substream to receive (0-2), in case simulcasting is enabled>,\n\t\"temporal\" : <temporal layers to receive (0-2), in case simulcasting is enabled>,\n\t\"fallback\" : <How much time (in us, default 250000) without receiving packets will make us drop to the substream below>\n}\n\\endverbatim\n *\n * \\c audio instructs the plugin to do or do not relay audio frames;\n * \\c video does the same for video; \\c bitrate caps the bandwidth to\n * force on the browser encoding side (e.g., 128000 for 128kbps);\n * \\c record enables or disables the recording of this peer; in case\n * recording is enabled, \\c filename allows to specify a base\n * path/filename to use for the files (-audio.mjr, -video.mjr and -data.mjr\n * are automatically appended). Beware that enabling the recording only\n * records this user's contribution, and not the whole call: to record\n * both sides, you need to enable recording for both the peers in the\n * call. Finally, in case the call uses simulcasting, \\c substream and\n * \\c temporal can be used to manually pick which substream and/or temporal\n * layer should be received from the peer.\n *\n * A successful request will result in a \\c set event:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"set\"\n\t}\n}\n\\endverbatim\n *\n * Notice that the \\c set request is also what you use when you want\n * to renegotiate a session, e.g., for the purpose of adding/removing\n * media streams or forcing an ICE restart. In that case, even an empty\n * \\c set request is fine, as long as it accompanies a new JSEP offer\n * or answer (depending on who originated the session update). The user\n * receiving the updated JSEP offer/answer will get an \\c update event:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"update\",\n\t}\n}\n\\endverbatim\n *\n * To decline an incoming call, cancel an attempt to call or simply\n * hangup an ongoing conversation, the \\c hangup request can be used,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"hangup\"\n}\n\\endverbatim\n *\n * Whatever the reason of a call being closed (e.g., a \\c hangup request,\n * a PeerConnection being closed, or something else), both parties in\n * the communication will receive a \\c hangup event:\n *\n\\verbatim\n{\n\t\"videocall\" : \"event\",\n\t\"result\" : {\n\t\t\"event\" : \"hangup\",\n\t\t\"username\" : \"<username of who closed the communication>\",\n\t\t\"reason\" : \"<description of what happened>\"\n\t}\n}\n\\endverbatim\n */\n\n#include \"plugin.h\"\n\n#include <jansson.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../record.h\"\n#include \"../rtp.h\"\n#include \"../rtcp.h\"\n#include \"../sdp-utils.h\"\n#include \"../utils.h\"\n\n\n/* Plugin information */\n#define JANUS_VIDEOCALL_VERSION\t\t\t6\n#define JANUS_VIDEOCALL_VERSION_STRING\t\"0.0.6\"\n#define JANUS_VIDEOCALL_DESCRIPTION\t\t\"This is a simple video call plugin for Janus, allowing two WebRTC peers to call each other through a server.\"\n#define JANUS_VIDEOCALL_NAME\t\t\t\"JANUS VideoCall plugin\"\n#define JANUS_VIDEOCALL_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_VIDEOCALL_PACKAGE\t\t\t\"janus.plugin.videocall\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_videocall_init(janus_callbacks *callback, const char *config_path);\nvoid janus_videocall_destroy(void);\nint janus_videocall_get_api_compatibility(void);\nint janus_videocall_get_version(void);\nconst char *janus_videocall_get_version_string(void);\nconst char *janus_videocall_get_description(void);\nconst char *janus_videocall_get_name(void);\nconst char *janus_videocall_get_author(void);\nconst char *janus_videocall_get_package(void);\nvoid janus_videocall_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_videocall_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\nvoid janus_videocall_setup_media(janus_plugin_session *handle);\nvoid janus_videocall_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_videocall_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_videocall_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet);\nvoid janus_videocall_data_ready(janus_plugin_session *handle);\nvoid janus_videocall_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink);\nvoid janus_videocall_hangup_media(janus_plugin_session *handle);\nvoid janus_videocall_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_videocall_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_videocall_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_videocall_init,\n\t\t.destroy = janus_videocall_destroy,\n\n\t\t.get_api_compatibility = janus_videocall_get_api_compatibility,\n\t\t.get_version = janus_videocall_get_version,\n\t\t.get_version_string = janus_videocall_get_version_string,\n\t\t.get_description = janus_videocall_get_description,\n\t\t.get_name = janus_videocall_get_name,\n\t\t.get_author = janus_videocall_get_author,\n\t\t.get_package = janus_videocall_get_package,\n\n\t\t.create_session = janus_videocall_create_session,\n\t\t.handle_message = janus_videocall_handle_message,\n\t\t.setup_media = janus_videocall_setup_media,\n\t\t.incoming_rtp = janus_videocall_incoming_rtp,\n\t\t.incoming_rtcp = janus_videocall_incoming_rtcp,\n\t\t.incoming_data = janus_videocall_incoming_data,\n\t\t.data_ready = janus_videocall_data_ready,\n\t\t.slow_link = janus_videocall_slow_link,\n\t\t.hangup_media = janus_videocall_hangup_media,\n\t\t.destroy_session = janus_videocall_destroy_session,\n\t\t.query_session = janus_videocall_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_VIDEOCALL_NAME);\n\treturn &janus_videocall_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter username_parameters[] = {\n\t{\"username\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter set_parameters[] = {\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"restart\", JANUS_JSON_BOOL, 0}\n};\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic janus_callbacks *gateway = NULL;\nstatic GThread *handler_thread;\nstatic void *janus_videocall_handler(void *data);\nstatic void janus_videocall_hangup_media_internal(janus_plugin_session *handle);\n\ntypedef struct janus_videocall_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_videocall_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_videocall_message exit_message;\n\ntypedef struct janus_videocall_session {\n\tjanus_plugin_session *handle;\n\tgchar *username;\n\tgboolean has_audio;\n\tgboolean has_video;\n\tgboolean has_data;\n\tgboolean audio_active;\n\tgboolean video_active;\n\tjanus_audiocodec acodec;/* Codec used for audio, if available */\n\tjanus_videocodec vcodec;/* Codec used for video, if available */\n\tint opusred_pt;\n\tuint32_t bitrate, peer_bitrate;\n\tguint16 slowlink_count;\n\tstruct janus_videocall_session *peer;\n\tjanus_rtp_switching_context context;\n\tuint32_t ssrc[3];\t\t/* Only needed in case VP8 (or H.264) simulcasting is involved */\n\tchar *rid[3];\t\t\t/* Only needed if simulcasting is rid-based */\n\tjanus_mutex rid_mutex;\t/* Mutex to protect access to the rid array */\n\tjanus_rtp_simulcasting_context sim_context;\n\tjanus_vp8_simulcast_context vp8_context;\n\tjanus_recorder *arc;\t/* The Janus recorder instance for this user's audio, if enabled */\n\tjanus_recorder *vrc;\t/* The Janus recorder instance for this user's video, if enabled */\n\tjanus_recorder *drc;\t/* The Janus recorder instance for this user's data, if enabled */\n\tgboolean e2ee;\t\t\t/* Whether media is encrypted, e.g., using Insertable Streams */\n\tjanus_mutex rec_mutex;\t/* Mutex to protect the recorders from race conditions */\n\tvolatile gint incall;\n\tvolatile gint dataready;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_mutex mutex;\n\tjanus_refcount ref;\n} janus_videocall_session;\nstatic GHashTable *sessions = NULL, *usernames = NULL;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_videocall_session_destroy(janus_videocall_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\nstatic void janus_videocall_session_unref(janus_videocall_session *session) {\n\tif(session)\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_videocall_session_free(const janus_refcount *session_ref) {\n\tjanus_videocall_session *session = janus_refcount_containerof(session_ref, janus_videocall_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tjanus_refcount_decrease(&session->handle->ref);\n\t/* This session can be destroyed, free all the resources */\n\tg_free(session->username);\n\tjanus_mutex_destroy(&session->mutex);\n\tjanus_mutex_destroy(&session->rid_mutex);\n\tjanus_mutex_destroy(&session->rec_mutex);\n\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\tg_free(session);\n}\n\nstatic void janus_videocall_message_free(janus_videocall_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_videocall_session *session = (janus_videocall_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\n\n/* Error codes */\n#define JANUS_VIDEOCALL_ERROR_UNKNOWN_ERROR\t\t\t499\n#define JANUS_VIDEOCALL_ERROR_NO_MESSAGE\t\t\t470\n#define JANUS_VIDEOCALL_ERROR_INVALID_JSON\t\t\t471\n#define JANUS_VIDEOCALL_ERROR_INVALID_REQUEST\t\t472\n#define JANUS_VIDEOCALL_ERROR_REGISTER_FIRST\t\t473\n#define JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT\t\t474\n#define JANUS_VIDEOCALL_ERROR_MISSING_ELEMENT\t\t475\n#define JANUS_VIDEOCALL_ERROR_USERNAME_TAKEN\t\t476\n#define JANUS_VIDEOCALL_ERROR_ALREADY_REGISTERED\t477\n#define JANUS_VIDEOCALL_ERROR_NO_SUCH_USERNAME\t\t478\n#define JANUS_VIDEOCALL_ERROR_USE_ECHO_TEST\t\t\t479\n#define JANUS_VIDEOCALL_ERROR_ALREADY_IN_CALL\t\t480\n#define JANUS_VIDEOCALL_ERROR_NO_CALL\t\t\t\t481\n#define JANUS_VIDEOCALL_ERROR_MISSING_SDP\t\t\t482\n#define JANUS_VIDEOCALL_ERROR_INVALID_SDP\t\t\t483\n\n\n/* Plugin implementation */\nint janus_videocall_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_VIDEOCALL_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_VIDEOCALL_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_VIDEOCALL_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_VIDEOCALL_NAME);\n\t\t}\n\t}\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_videocall_session_destroy);\n\tusernames = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videocall_session_unref);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_videocall_message_free);\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming messages */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"videocall handler\", janus_videocall_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the VideoCall handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_VIDEOCALL_NAME);\n\treturn 0;\n}\n\nvoid janus_videocall_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tg_hash_table_destroy(usernames);\n\tusernames = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_VIDEOCALL_NAME);\n}\n\nint janus_videocall_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_videocall_get_version(void) {\n\treturn JANUS_VIDEOCALL_VERSION;\n}\n\nconst char *janus_videocall_get_version_string(void) {\n\treturn JANUS_VIDEOCALL_VERSION_STRING;\n}\n\nconst char *janus_videocall_get_description(void) {\n\treturn JANUS_VIDEOCALL_DESCRIPTION;\n}\n\nconst char *janus_videocall_get_name(void) {\n\treturn JANUS_VIDEOCALL_NAME;\n}\n\nconst char *janus_videocall_get_author(void) {\n\treturn JANUS_VIDEOCALL_AUTHOR;\n}\n\nconst char *janus_videocall_get_package(void) {\n\treturn JANUS_VIDEOCALL_PACKAGE;\n}\n\nstatic janus_videocall_session *janus_videocall_lookup_session(janus_plugin_session *handle) {\n\tjanus_videocall_session *session = NULL;\n\tif(g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_videocall_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_videocall_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_videocall_session *session = g_malloc0(sizeof(janus_videocall_session));\n\tsession->handle = handle;\n\tsession->has_audio = FALSE;\n\tsession->has_video = FALSE;\n\tsession->has_data = FALSE;\n\tsession->audio_active = TRUE;\n\tsession->video_active = TRUE;\n\tsession->bitrate = 0;\t/* No limit */\n\tsession->peer_bitrate = 0;\n\tsession->peer = NULL;\n\tsession->username = NULL;\n\tjanus_rtp_switching_context_reset(&session->context);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tjanus_mutex_init(&session->mutex);\n\tjanus_mutex_init(&session->rec_mutex);\n\tjanus_mutex_init(&session->rid_mutex);\n\tg_atomic_int_set(&session->incall, 0);\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\thandle->plugin_handle = session;\n\tjanus_refcount_init(&session->ref, janus_videocall_session_free);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nvoid janus_videocall_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videocall_session *session = janus_videocall_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Removing VideoCall user %s session...\\n\", session->username ? session->username : \"'unknown'\");\n\tjanus_videocall_hangup_media_internal(handle);\n\tif(session->username != NULL) {\n\t\tint res = g_hash_table_remove(usernames, (gpointer)session->username);\n\t\tJANUS_LOG(LOG_VERB, \"  -- Removed: %d\\n\", res);\n\t}\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\treturn;\n}\n\njson_t *janus_videocall_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videocall_session *session = janus_videocall_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Provide some generic info, e.g., if we're in a call and with whom */\n\tjanus_videocall_session *peer = session->peer;\n\tjson_t *info = json_object();\n\tjson_object_set_new(info, \"state\", json_string(session->peer ? \"incall\" : \"idle\"));\n\tjson_object_set_new(info, \"username\", session->username ? json_string(session->username) : NULL);\n\tif(peer) {\n\t\tjson_object_set_new(info, \"peer\", peer->username ? json_string(peer->username) : NULL);\n\t\tjson_object_set_new(info, \"audio_active\", session->audio_active ? json_true() : json_false());\n\t\tjson_object_set_new(info, \"video_active\", session->video_active ? json_true() : json_false());\n\t\tif(session->acodec != JANUS_AUDIOCODEC_NONE) {\n\t\t\tjson_object_set_new(info, \"audio_codec\", json_string(janus_audiocodec_name(session->acodec)));\n\t\t\tif(session->opusred_pt)\n\t\t\t\tjson_object_set_new(info, \"audio_red\", json_true());\n\t\t}\n\t\tif(session->vcodec != JANUS_VIDEOCODEC_NONE)\n\t\t\tjson_object_set_new(info, \"video_codec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\tjson_object_set_new(info, \"video_active\", session->video_active ? json_true() : json_false());\n\t\tjson_object_set_new(info, \"bitrate\", json_integer(session->bitrate));\n\t\tjson_object_set_new(info, \"peer-bitrate\", json_integer(session->peer_bitrate));\n\t\tjson_object_set_new(info, \"slowlink_count\", json_integer(session->slowlink_count));\n\t}\n\tif(session->ssrc[0] != 0 || session->rid[0] != NULL) {\n\t\tjson_object_set_new(info, \"simulcast\", json_true());\n\t}\n\tif(peer && (peer->ssrc[0] != 0 || peer->rid[0] != NULL)) {\n\t\tjson_object_set_new(info, \"simulcast-peer\", json_true());\n\t\tjson_object_set_new(info, \"substream\", json_integer(session->sim_context.substream));\n\t\tjson_object_set_new(info, \"substream-target\", json_integer(session->sim_context.substream_target));\n\t\tjson_object_set_new(info, \"temporal-layer\", json_integer(session->sim_context.templayer));\n\t\tjson_object_set_new(info, \"temporal-layer-target\", json_integer(session->sim_context.templayer_target));\n\t\tif(session->sim_context.drop_trigger > 0)\n\t\t\tjson_object_set_new(info, \"fallback\", json_integer(session->sim_context.drop_trigger));\n\t}\n\tif(session->arc || session->vrc || session->drc) {\n\t\tjson_t *recording = json_object();\n\t\tif(session->arc && session->arc->filename)\n\t\t\tjson_object_set_new(recording, \"audio\", json_string(session->arc->filename));\n\t\tif(session->vrc && session->vrc->filename)\n\t\t\tjson_object_set_new(recording, \"video\", json_string(session->vrc->filename));\n\t\tif(session->drc && session->drc->filename)\n\t\t\tjson_object_set_new(recording, \"data\", json_string(session->drc->filename));\n\t\tjson_object_set_new(info, \"recording\", recording);\n\t}\n\tjson_object_set_new(info, \"incall\", json_integer(g_atomic_int_get(&session->incall)));\n\tif(session->e2ee)\n\t\tjson_object_set_new(info, \"e2ee\", json_true());\n\tjson_object_set_new(info, \"hangingup\", json_integer(g_atomic_int_get(&session->hangingup)));\n\tjson_object_set_new(info, \"destroyed\", json_integer(g_atomic_int_get(&session->destroyed)));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstruct janus_plugin_result *janus_videocall_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videocall_session *session = janus_videocall_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, \"No session associated with this handle\", NULL);\n\t}\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\tjanus_videocall_message *msg = g_malloc(sizeof(janus_videocall_message));\n\tmsg->handle = handle;\n\tmsg->transaction = transaction;\n\tmsg->message = message;\n\tmsg->jsep = jsep;\n\tg_async_queue_push(messages, msg);\n\n\t/* All the requests to this plugin are handled asynchronously */\n\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n}\n\nvoid janus_videocall_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_VIDEOCALL_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videocall_session *session = janus_videocall_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* We really don't care, as we only relay RTP/RTCP we get in the first place anyway */\n}\n\nvoid janus_videocall_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(gateway) {\n\t\t/* Honour the audio/video active flags */\n\t\tjanus_videocall_session *session = (janus_videocall_session *)handle->plugin_handle;\n\t\tif(!session) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tjanus_videocall_session *peer = session->peer;\n\t\tif(!peer) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Session has no peer...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&peer->destroyed))\n\t\t\treturn;\n\t\tgboolean video = packet->video;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\tif(video && session->video_active && (session->ssrc[0] != 0 || session->rid[0] != NULL)) {\n\t\t\t/* Handle simulcast: backup the header information first */\n\t\t\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t\t\tuint32_t seq_number = ntohs(header->seq_number);\n\t\t\tuint32_t timestamp = ntohl(header->timestamp);\n\t\t\tuint32_t ssrc = ntohl(header->ssrc);\n\t\t\t/* Process this packet: don't relay if it's not the SSRC/layer we wanted to handle\n\t\t\t * The caveat is that the targets in OUR simulcast context are the PEER's targets */\n\t\t\tgboolean relay = janus_rtp_simulcasting_context_process_rtp(&peer->sim_context,\n\t\t\t\tbuf, len, packet->extensions.dd_content, packet->extensions.dd_len,\n\t\t\t\tsession->ssrc, session->rid, session->vcodec, &peer->context, &session->rid_mutex);\n\t\t\t/* Do we need to drop this? */\n\t\t\tif(!relay)\n\t\t\t\treturn;\n\t\t\tif(peer->sim_context.need_pli) {\n\t\t\t\t/* Send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the simulcast context\\n\");\n\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t}\n\t\t\t/* Any event we should notify? */\n\t\t\tif(peer->sim_context.changed_substream) {\n\t\t\t\t/* Notify the user about the substream change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\t\t\tjson_t *result = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"simulcast\"));\n\t\t\t\tjson_object_set_new(result, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(result, \"substream\", json_integer(peer->sim_context.substream));\n\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\tgateway->push_event(peer->handle, &janus_videocall_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\tif(peer->sim_context.changed_temporal) {\n\t\t\t\t/* Notify the user about the temporal layer change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\t\t\tjson_t *result = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"simulcast\"));\n\t\t\t\tjson_object_set_new(result, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\tjson_object_set_new(result, \"temporal\", json_integer(peer->sim_context.templayer));\n\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\tgateway->push_event(peer->handle, &janus_videocall_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\t/* If we got here, update the RTP header and send the packet */\n\t\t\tjanus_rtp_header_update(header, &peer->context, TRUE, 0);\n\t\t\tif(session->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t\tint plen = 0;\n\t\t\t\tchar *payload = janus_rtp_payload(buf, len, &plen);\n\t\t\t\tjanus_vp8_simulcast_descriptor_update(payload, plen, &peer->vp8_context, peer->sim_context.changed_substream);\n\t\t\t}\n\t\t\t/* Save the frame if we're recording (and make sure the SSRC never changes even if the substream does) */\n\t\t\theader->ssrc = htonl(1);\n\t\t\tjanus_recorder_save_frame(session->vrc, buf, len);\n\t\t\t/* Send the frame back */\n\t\t\tgateway->relay_rtp(peer->handle, packet);\n\t\t\t/* Restore header or core statistics will be messed up */\n\t\t\theader->ssrc = htonl(ssrc);\n\t\t\theader->timestamp = htonl(timestamp);\n\t\t\theader->seq_number = htons(seq_number);\n\t\t} else {\n\t\t\tif((!video && session->audio_active) || (video && session->video_active)) {\n\t\t\t\t/* Save the frame if we're recording */\n\t\t\t\tjanus_recorder_save_frame(video ? session->vrc : session->arc, buf, len);\n\t\t\t\t/* Forward the packet to the peer */\n\t\t\t\tgateway->relay_rtp(peer->handle, packet);\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid janus_videocall_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(gateway) {\n\t\tjanus_videocall_session *session = (janus_videocall_session *)handle->plugin_handle;\n\t\tif(!session) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tjanus_videocall_session *peer = session->peer;\n\t\tif(!peer) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Session has no peer...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&peer->destroyed))\n\t\t\treturn;\n\t\tguint32 bitrate = janus_rtcp_get_remb(packet->buffer, packet->length);\n\t\tif(bitrate > 0) {\n\t\t\t/* If a REMB arrived, make sure we cap it to our configuration, and send it as a video RTCP */\n\t\t\tsession->peer_bitrate = bitrate;\n\t\t\t/* No limit ~= 10000000 */\n\t\t\tgateway->send_remb(handle, session->bitrate ? session->bitrate : 10000000);\n\t\t\treturn;\n\t\t}\n\t\tgateway->relay_rtcp(peer->handle, packet);\n\t}\n}\n\nvoid janus_videocall_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tif(gateway) {\n\t\tjanus_videocall_session *session = (janus_videocall_session *)handle->plugin_handle;\n\t\tif(!session) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tjanus_videocall_session *peer = session->peer;\n\t\tif(!peer) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Session has no peer...\\n\");\n\t\t\treturn;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&peer->destroyed) ||\n\t\t\t\t!g_atomic_int_get(&peer->dataready))\n\t\t\treturn;\n\t\tif(packet->buffer == NULL || packet->length == 0)\n\t\t\treturn;\n\t\tchar *label = packet->label;\n\t\tchar *buf = packet->buffer;\n\t\tuint16_t len = packet->length;\n\t\tJANUS_LOG(LOG_VERB, \"Got a %s DataChannel message (%d bytes) to forward\\n\",\n\t\t\t!packet->binary ? \"text\" : \"binary\", len);\n\t\t/* Save the frame if we're recording */\n\t\tjanus_recorder_save_frame(session->drc, buf, len);\n\t\t/* Forward the packet to the peer */\n\t\tjanus_plugin_data r = {\n\t\t\t.label = label,\n\t\t\t.protocol = NULL,\n\t\t\t.binary = packet->binary,\n\t\t\t.buffer = buf,\n\t\t\t.length = len\n\t\t};\n\t\tgateway->relay_data(peer->handle, &r);\n\t}\n}\n\nvoid janus_videocall_data_ready(janus_plugin_session *handle) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) ||\n\t\t\tg_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized) || !gateway)\n\t\treturn;\n\t/* Data channels are writable */\n\tjanus_videocall_session *session = (janus_videocall_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tif(g_atomic_int_compare_and_exchange(&session->dataready, 0, 1)) {\n\t\tJANUS_LOG(LOG_INFO, \"[%s-%p] Data channel available\\n\", JANUS_VIDEOCALL_PACKAGE, handle);\n\t}\n}\n\nvoid janus_videocall_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink) {\n\t/* The core is informing us that our peer got or sent too many NACKs, are we pushing media too hard? */\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_videocall_session *session = (janus_videocall_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tsession->slowlink_count++;\n\tif(uplink && !video && !session->audio_active) {\n\t\t/* We're not relaying audio and the peer is expecting it, so NACKs are normal */\n\t\tJANUS_LOG(LOG_VERB, \"Getting a lot of lost packets (slow uplink) for audio, but that's expected, a configure disabled the audio forwarding\\n\");\n\t} else if(uplink && video && !session->video_active) {\n\t\t/* We're not relaying video and the peer is expecting it, so NACKs are normal */\n\t\tJANUS_LOG(LOG_VERB, \"Getting a lot of lost packets (slow uplink) for video, but that's expected, a configure disabled the video forwarding\\n\");\n\t} else {\n\t\tJANUS_LOG(LOG_WARN, \"Getting a lot of lost packets (slow %s) for %s\\n\",\n\t\t\tuplink ? \"uplink\" : \"downlink\", video ? \"video\" : \"audio\");\n\t\tif(!uplink) {\n\t\t\t/* Send an event on the handle to notify the application: it's\n\t\t\t * up to the application to then choose a policy and enforce it */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\t\t/* Also add info on what the current bitrate cap is */\n\t\t\tjson_t *result = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"slow_link\"));\n\t\t\tjson_object_set_new(result, \"media\", json_string(video ? \"video\" : \"audio\"));\n\t\t\tif(video)\n\t\t\t\tjson_object_set_new(result, \"current-bitrate\", json_integer(session->bitrate));\n\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\tgateway->push_event(session->handle, &janus_videocall_plugin, NULL, event, NULL);\n\t\t\tjson_decref(event);\n\t\t}\n\t}\n}\n\nstatic void janus_videocall_recorder_close(janus_videocall_session *session) {\n\tif(session->arc) {\n\t\tjanus_recorder *rc = session->arc;\n\t\tsession->arc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed audio recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->vrc) {\n\t\tjanus_recorder *rc = session->vrc;\n\t\tsession->vrc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed video recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n\tif(session->drc) {\n\t\tjanus_recorder *rc = session->drc;\n\t\tsession->drc = NULL;\n\t\tjanus_recorder_close(rc);\n\t\tJANUS_LOG(LOG_INFO, \"Closed data recording %s\\n\", rc->filename ? rc->filename : \"??\");\n\t\tjanus_recorder_destroy(rc);\n\t}\n}\n\nvoid janus_videocall_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore\\n\", JANUS_VIDEOCALL_PACKAGE, handle);\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videocall_hangup_media_internal(handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nstatic void janus_videocall_hangup_media_internal(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_videocall_session *session = janus_videocall_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1))\n\t\treturn;\n\tg_atomic_int_set(&session->dataready, 0);\n\t/* Get rid of the recorders, if available */\n\tjanus_mutex_lock(&session->rec_mutex);\n\tjanus_videocall_recorder_close(session);\n\tjanus_mutex_unlock(&session->rec_mutex);\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_videocall_session *peer = session->peer;\n\tsession->peer = NULL;\n\tif(peer) {\n\t\t/* Send event to our peer too */\n\t\tjson_t *call = json_object();\n\t\tjson_object_set_new(call, \"videocall\", json_string(\"event\"));\n\t\tjson_t *calling = json_object();\n\t\tjson_object_set_new(calling, \"event\", json_string(\"hangup\"));\n\t\tjson_object_set_new(calling, \"username\", json_string(session->username));\n\t\tjson_object_set_new(calling, \"reason\", json_string(\"Remote WebRTC hangup\"));\n\t\tjson_object_set_new(call, \"result\", calling);\n\t\tgateway->close_pc(peer->handle);\n\t\tint ret = gateway->push_event(peer->handle, &janus_videocall_plugin, NULL, call, NULL);\n\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\tjson_decref(call);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"hangup\"));\n\t\t\tjson_object_set_new(info, \"reason\", json_string(\"Remote WebRTC hangup\"));\n\t\t\tgateway->notify_event(&janus_videocall_plugin, peer->handle, info);\n\t\t}\n\t\tjanus_refcount_decrease(&peer->ref);\n\t}\n\tjanus_mutex_unlock(&session->mutex);\n\t/* Reset controls */\n\tsession->has_audio = FALSE;\n\tsession->has_video = FALSE;\n\tsession->has_data = FALSE;\n\tsession->audio_active = TRUE;\n\tsession->video_active = TRUE;\n\tsession->acodec = JANUS_AUDIOCODEC_NONE;\n\tsession->vcodec = JANUS_VIDEOCODEC_NONE;\n\tsession->opusred_pt = 0;\n\tsession->bitrate = 0;\n\tsession->peer_bitrate = 0;\n\tsession->e2ee = FALSE;\n\tjanus_rtp_simulcasting_cleanup(NULL, session->ssrc, session->rid, &session->rid_mutex);\n\tjanus_rtp_switching_context_reset(&session->context);\n\tjanus_rtp_simulcasting_context_reset(&session->sim_context);\n\tjanus_vp8_simulcast_context_reset(&session->vp8_context);\n\tif(g_atomic_int_compare_and_exchange(&session->incall, 1, 0) && peer) {\n\t\tjanus_refcount_decrease(&peer->ref);\n\t}\n\tjanus_rtp_switching_context_reset(&session->context);\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_videocall_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining VideoCall handler thread\\n\");\n\tjanus_videocall_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_videocall_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_videocall_session *session = janus_videocall_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_videocall_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_videocall_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = msg->message;\n\t\tif(msg->message == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_VIDEOCALL_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(!json_is_object(root)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_JSON;\n\t\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\t\tgoto error;\n\t\t}\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOCALL_ERROR_MISSING_ELEMENT, JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto error;\n\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\tjson_t *msg_e2ee = json_object_get(msg->jsep, \"e2ee\");\n\t\tif(json_is_true(msg_e2ee))\n\t\t\tsession->e2ee = TRUE;\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tjson_t *result = NULL;\n\t\tgboolean sdp_update = FALSE;\n\t\tif(json_object_get(msg->jsep, \"update\") != NULL)\n\t\t\tsdp_update = json_is_true(json_object_get(msg->jsep, \"update\"));\n\t\tif(!strcasecmp(request_text, \"list\")) {\n\t\t\tresult = json_object();\n\t\t\tjson_t *list = json_array();\n\t\t\tJANUS_LOG(LOG_VERB, \"Request for the list of peers\\n\");\n\t\t\t/* Return a list of all available mountpoints */\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, sessions);\n\t\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_videocall_session *user = value;\n\t\t\t\tif(user != NULL) {\n\t\t\t\t\tjanus_refcount_increase(&user->ref);\n\t\t\t\t\tif(user->username != NULL)\n\t\t\t\t\t\tjson_array_append_new(list, json_string(user->username));\n\t\t\t\t\tjanus_refcount_decrease(&user->ref);\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_object_set_new(result, \"list\", list);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t} else if(!strcasecmp(request_text, \"register\")) {\n\t\t\t/* Map this handle to a username */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, username_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOCALL_ERROR_MISSING_ELEMENT, JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *username = json_object_get(root, \"username\");\n\t\t\tconst char *username_text = json_string_value(username);\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tif(session->username != NULL) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already registered (%s)\\n\", session->username);\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_ALREADY_REGISTERED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already registered (%s)\", session->username);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(g_hash_table_lookup(usernames, username_text) != NULL) {\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Username '%s' already taken\\n\", username_text);\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_USERNAME_TAKEN;\n\t\t\t\tg_snprintf(error_cause, 512, \"Username '%s' already taken\", username_text);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tsession->username = g_strdup(username_text);\n\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\tg_hash_table_insert(usernames, (gpointer)g_strdup(session->username), session);\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"registered\"));\n\t\t\tjson_object_set_new(result, \"username\", json_string(username_text));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"registered\"));\n\t\t\t\tjson_object_set_new(info, \"username\", json_string(username_text));\n\t\t\t\tgateway->notify_event(&janus_videocall_plugin, session->handle, info);\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"call\")) {\n\t\t\t/* Call another peer */\n\t\t\tif(session->username == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Register a username first\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_REGISTER_FIRST;\n\t\t\t\tg_snprintf(error_cause, 512, \"Register a username first\");\n\t\t\t\t/* Hangup the call attempt of the user */\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->peer != NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already in a call\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_ALREADY_IN_CALL;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already in a call\");\n\t\t\t\t/* Hangup the call attempt of the user */\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!g_atomic_int_compare_and_exchange(&session->incall, 0, 1)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already in a call (but no peer?)\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_ALREADY_IN_CALL;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already in a call (but no peer)\");\n\t\t\t\t/* Hangup the call attempt of the user */\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, username_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOCALL_ERROR_MISSING_ELEMENT, JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Hangup the call attempt of the user */\n\t\t\t\tg_atomic_int_set(&session->incall, 0);\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *username = json_object_get(root, \"username\");\n\t\t\tconst char *username_text = json_string_value(username);\n\t\t\tif(!strcmp(username_text, session->username)) {\n\t\t\t\tg_atomic_int_set(&session->incall, 0);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"You can't call yourself... use the EchoTest for that\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_USE_ECHO_TEST;\n\t\t\t\tg_snprintf(error_cause, 512, \"You can't call yourself... use the EchoTest for that\");\n\t\t\t\t/* Hangup the call attempt of the user */\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tjanus_videocall_session *peer = g_hash_table_lookup(usernames, username_text);\n\t\t\tif(peer == NULL || g_atomic_int_get(&peer->destroyed)) {\n\t\t\t\tg_atomic_int_set(&session->incall, 0);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Username '%s' doesn't exist\\n\", username_text);\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_NO_SUCH_USERNAME;\n\t\t\t\tg_snprintf(error_cause, 512, \"Username '%s' doesn't exist\", username_text);\n\t\t\t\t/* Hangup the call attempt of the user */\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\t/* If the call attempt proceeds we keep the references */\n\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\tjanus_refcount_increase(&peer->ref);\n\t\t\tif(g_atomic_int_get(&peer->incall) || peer->peer != NULL) {\n\t\t\t\tif(g_atomic_int_compare_and_exchange(&session->incall, 1, 0) && peer) {\n\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"%s is busy\\n\", username_text);\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"hangup\"));\n\t\t\t\tjson_object_set_new(result, \"username\", json_string(session->username));\n\t\t\t\tjson_object_set_new(result, \"reason\", json_string(\"User busy\"));\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"hangup\"));\n\t\t\t\t\tjson_object_set_new(info, \"reason\", json_string(\"User busy\"));\n\t\t\t\t\tgateway->notify_event(&janus_videocall_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\t/* Hangup the call attempt of the user */\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t} else {\n\t\t\t\t/* Any SDP to handle? if not, something's wrong */\n\t\t\t\tif(!msg_sdp) {\n\t\t\t\t\tif(g_atomic_int_compare_and_exchange(&session->incall, 1, 0) && peer) {\n\t\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_MISSING_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tchar error_str[512];\n\t\t\t\tjanus_sdp *offer = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str));\n\t\t\t\tif(offer == NULL) {\n\t\t\t\t\tif(g_atomic_int_compare_and_exchange(&session->incall, 1, 0) && peer) {\n\t\t\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing offer: %s\\n\", error_str);\n\t\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing offer: %s\", error_str);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\tg_atomic_int_set(&peer->incall, 1);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tjanus_refcount_increase(&peer->ref);\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\tsession->peer = peer;\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tjanus_mutex_lock(&peer->mutex);\n\t\t\t\tpeer->peer = session;\n\t\t\t\tjanus_mutex_unlock(&peer->mutex);\n\t\t\t\tsession->has_audio = (strstr(msg_sdp, \"m=audio\") != NULL);\n\t\t\t\tsession->has_video = (strstr(msg_sdp, \"m=video\") != NULL);\n\t\t\t\tsession->has_data = (strstr(msg_sdp, \"DTLS/SCTP\") != NULL);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"%s is calling %s\\n\", session->username, peer->username);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well:\\n%s\\n\", msg_sdp_type, msg_sdp);\n\t\t\t\t/* Check if this user will simulcast */\n\t\t\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\t\t\tif(msg_simulcast && json_array_size(msg_simulcast) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(msg_simulcast); i++) {\n\t\t\t\t\t\tjson_t *s = json_array_get(msg_simulcast, i);\n\t\t\t\t\t\tint mindex = json_integer_value(json_object_get(s, \"mindex\"));\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"VideoCall caller (%s) is going to do simulcasting (#%d)\\n\", session->username, mindex);\n\t\t\t\t\t\tint rid_ext_id = -1;\n\t\t\t\t\t\tjanus_mutex_lock(&session->rid_mutex);\n\t\t\t\t\t\t/* Clear existing RIDs in case this is a renegotiation */\n\t\t\t\t\t\tjanus_rtp_simulcasting_cleanup(NULL, NULL, session->rid, NULL);\n\t\t\t\t\t\tjanus_rtp_simulcasting_prepare(s, &rid_ext_id, session->ssrc, session->rid);\n\t\t\t\t\t\tsession->sim_context.rid_ext_id = rid_ext_id;\n\t\t\t\t\t\tjanus_mutex_unlock(&session->rid_mutex);\n\t\t\t\t\t\tsession->sim_context.substream_target = 2;\t/* Let's aim for the highest quality */\n\t\t\t\t\t\tsession->sim_context.templayer_target = 2;\t/* Let's aim for all temporal layers */\n\t\t\t\t\t\t/* FIXME We're stopping at the first item, there may be more */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Send SDP to our peer */\n\t\t\t\tjson_t *call = json_object();\n\t\t\t\tjson_object_set_new(call, \"videocall\", json_string(\"event\"));\n\t\t\t\tjson_t *calling = json_object();\n\t\t\t\tjson_object_set_new(calling, \"event\", json_string(\"incomingcall\"));\n\t\t\t\tjson_object_set_new(calling, \"username\", json_string(session->username));\n\t\t\t\tjson_object_set_new(call, \"result\", calling);\n\t\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", msg_sdp_type, \"sdp\", msg_sdp);\n\t\t\t\tif(session->e2ee)\n\t\t\t\t\tjson_object_set_new(jsep, \"e2ee\", json_true());\n\t\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\t\tint ret = gateway->push_event(peer->handle, &janus_videocall_plugin, NULL, call, jsep);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(call);\n\t\t\t\tjson_decref(jsep);\n\t\t\t\t/* Send an ack back */\n\t\t\t\tresult = json_object();\n\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"calling\"));\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"calling\"));\n\t\t\t\t\tgateway->notify_event(&janus_videocall_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(!strcasecmp(request_text, \"accept\")) {\n\t\t\t/* Accept a call from another peer */\n\t\t\tjanus_videocall_session *peer = session->peer;\n\t\t\tif(peer == NULL || !g_atomic_int_get(&session->incall) || !g_atomic_int_get(&peer->incall)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No incoming call to accept\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_NO_CALL;\n\t\t\t\tg_snprintf(error_cause, 512, \"No incoming call to accept\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&peer->ref);\n\t\t\t/* Any SDP to handle? if not, something's wrong */\n\t\t\tif(!msg_sdp) {\n\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing SDP\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_MISSING_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Missing SDP\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tchar error_str[512];\n\t\t\tjanus_sdp *answer = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str));\n\t\t\tif(answer == NULL) {\n\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing answer: %s\\n\", error_str);\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_SDP;\n\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing answer: %s\", error_str);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"%s is accepting a call from %s\\n\", session->username, peer->username);\n\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well:\\n%s\\n\", msg_sdp_type, msg_sdp);\n\t\t\tsession->has_audio = (strstr(msg_sdp, \"m=audio\") != NULL);\n\t\t\tsession->has_video = (strstr(msg_sdp, \"m=video\") != NULL);\n\t\t\tsession->has_data = (strstr(msg_sdp, \"DTLS/SCTP\") != NULL);\n\t\t\t/* Check if this user will simulcast */\n\t\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\t\tif(msg_simulcast) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"VideoCall callee (%s) cannot do simulcast.\\n\", session->username);\n\t\t\t} else {\n\t\t\t\tjanus_rtp_simulcasting_cleanup(NULL, session->ssrc, session->rid, &session->rid_mutex);\n\t\t\t}\n\t\t\t/* Check which codecs we ended up using */\n\t\t\tconst char *acodec = NULL, *vcodec = NULL;\n\t\t\tjanus_sdp_find_first_codec(answer, JANUS_SDP_AUDIO, -1, &acodec);\n\t\t\tsession->acodec = janus_audiocodec_from_name(acodec);\n\t\t\tjanus_sdp_find_first_codec(answer, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\tsession->vcodec = janus_videocodec_from_name(vcodec);\n\t\t\tsession->opusred_pt = janus_sdp_get_opusred_pt(answer, -1);\n\t\t\tif(peer) {\n\t\t\t\tpeer->has_audio = session->has_audio;\n\t\t\t\tpeer->has_video = session->has_video;\n\t\t\t\tpeer->has_data = session->has_data;\n\t\t\t\tpeer->acodec = session->acodec;\n\t\t\t\tpeer->vcodec = session->vcodec;\n\t\t\t\tpeer->opusred_pt = session->opusred_pt;\n\t\t\t}\n\t\t\tif(session->acodec == JANUS_AUDIOCODEC_NONE) {\n\t\t\t\tsession->has_audio = FALSE;\n\t\t\t\tif(peer)\n\t\t\t\t\tpeer->has_audio = FALSE;\n\t\t\t} else if(peer) {\n\t\t\t\tpeer->acodec = session->acodec;\n\t\t\t}\n\t\t\tif(session->vcodec == JANUS_VIDEOCODEC_NONE) {\n\t\t\t\tsession->has_video = FALSE;\n\t\t\t\tif(peer)\n\t\t\t\t\tpeer->has_video = FALSE;\n\t\t\t} else if(peer) {\n\t\t\t\tpeer->vcodec = session->vcodec;\n\t\t\t}\n\t\t\tjanus_sdp_destroy(answer);\n\t\t\t/* Send SDP to our peer */\n\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", msg_sdp_type, \"sdp\", msg_sdp);\n\t\t\tif(session->e2ee)\n\t\t\t\tjson_object_set_new(jsep, \"e2ee\", json_true());\n\t\t\tjson_t *call = json_object();\n\t\t\tjson_object_set_new(call, \"videocall\", json_string(\"event\"));\n\t\t\tjson_t *calling = json_object();\n\t\t\tjson_object_set_new(calling, \"event\", json_string(\"accepted\"));\n\t\t\tjson_object_set_new(calling, \"username\", json_string(session->username));\n\t\t\tjson_object_set_new(call, \"result\", calling);\n\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\tint ret = gateway->push_event(peer->handle, &janus_videocall_plugin, NULL, call, jsep);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(call);\n\t\t\tjson_decref(jsep);\n\t\t\t/* Send an ack back */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"accepted\"));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"accepted\"));\n\t\t\t\tgateway->notify_event(&janus_videocall_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* Is simulcasting involved on either side? */\n\t\t\tif(session->ssrc[0] || session->rid[0]) {\n\t\t\t\tpeer->sim_context.substream_target = 2;\t/* Let's aim for the highest quality */\n\t\t\t\tpeer->sim_context.templayer_target = 2;\t/* Let's aim for all temporal layers */\n\t\t\t}\n\t\t\tif(peer->ssrc[0] || peer->rid[0]) {\n\t\t\t\tsession->sim_context.substream_target = 2;\t/* Let's aim for the highest quality */\n\t\t\t\tsession->sim_context.templayer_target = 2;\t/* Let's aim for all temporal layers */\n\t\t\t}\n\t\t\t/* We don't need this reference anymore, it was already increased by the peer calling us */\n\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t} else if(!strcasecmp(request_text, \"set\")) {\n\t\t\t/* Update the local configuration (audio/video mute/unmute, bitrate cap or recording) */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, set_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOCALL_ERROR_MISSING_ELEMENT, JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\tjson_t *bitrate = json_object_get(root, \"bitrate\");\n\t\t\tjson_t *record = json_object_get(root, \"record\");\n\t\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\t\tjson_t *restart = json_object_get(root, \"restart\");\n\t\t\tjson_t *substream = json_object_get(root, \"substream\");\n\t\t\tif(substream && (!json_is_integer(substream) || json_integer_value(substream) < 0 || json_integer_value(substream) > 2)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (substream should be 0, 1 or 2)\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (substream should be 0, 1 or 2)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *temporal = json_object_get(root, \"temporal\");\n\t\t\tif(temporal && (!json_is_integer(temporal) || json_integer_value(temporal) < 0 || json_integer_value(temporal) > 2)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (temporal should be 0, 1 or 2)\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (temporal should be 0, 1 or 2)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjson_t *fallback = json_object_get(root, \"fallback\");\n\t\t\tif(fallback && (!json_is_integer(fallback) || json_integer_value(fallback) < 0)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (fallback should be a positive integer)\\n\");\n\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (fallback should be a positive integer)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(audio) {\n\t\t\t\tsession->audio_active = json_is_true(audio);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting audio property: %s\\n\", session->audio_active ? \"true\" : \"false\");\n\t\t\t}\n\t\t\tif(video) {\n\t\t\t\tif(!session->video_active && json_is_true(video)) {\n\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Just (re-)enabled video, sending a PLI to recover it\\n\");\n\t\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t\t}\n\t\t\t\tsession->video_active = json_is_true(video);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video property: %s\\n\", session->video_active ? \"true\" : \"false\");\n\t\t\t}\n\t\t\tif(bitrate) {\n\t\t\t\tsession->bitrate = json_integer_value(bitrate);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video bitrate: %\"SCNu32\"\\n\", session->bitrate);\n\t\t\t\tgateway->send_remb(session->handle, session->bitrate ? session->bitrate : 10000000);\n\t\t\t}\n\t\t\tjanus_videocall_session *peer = session->peer;\n\t\t\tif(peer)\n\t\t\t\tjanus_refcount_increase(&peer->ref);\n\t\t\tif(fallback) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting fallback timer (simulcast): %lld (was %\"SCNu32\")\\n\",\n\t\t\t\t\tjson_integer_value(fallback) ? json_integer_value(fallback) : 250000,\n\t\t\t\t\tsession->sim_context.drop_trigger ? session->sim_context.drop_trigger : 250000);\n\t\t\t\tsession->sim_context.drop_trigger = json_integer_value(fallback);\n\t\t\t}\n\t\t\tif(substream) {\n\t\t\t\tsession->sim_context.substream_target = json_integer_value(substream);\n\t\t\t\tif(session->sim_context.substream_target >= 0 && session->sim_context.substream_target <= 2) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video SSRC to let through (simulcast): %\"SCNu32\" (index %d, was %d)\\n\",\n\t\t\t\t\t\tsession->ssrc[session->sim_context.substream_target], session->sim_context.substream_target, session->sim_context.substream);\n\t\t\t\t}\n\t\t\t\tif(session->sim_context.substream_target == session->sim_context.substream) {\n\t\t\t\t\t/* No need to do anything, we're already getting the right substream, so notify the user */\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"simulcast\"));\n\t\t\t\t\tjson_object_set_new(result, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\t\tjson_object_set_new(result, \"substream\", json_integer(session->sim_context.substream));\n\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\tgateway->push_event(session->handle, &janus_videocall_plugin, NULL, event, NULL);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t} else {\n\t\t\t\t\t/* We need to change substream, send the peer a PLI */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Simulcasting substream change, sending a PLI to kickstart it\\n\");\n\t\t\t\t\tif(peer && peer->handle)\n\t\t\t\t\t\tgateway->send_pli(peer->handle);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(temporal) {\n\t\t\t\tsession->sim_context.templayer_target = json_integer_value(temporal);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video temporal layer to let through (simulcast): %d (was %d)\\n\",\n\t\t\t\t\tsession->sim_context.templayer_target, session->sim_context.templayer);\n\t\t\t\tif(session->sim_context.templayer_target == session->sim_context.templayer) {\n\t\t\t\t\t/* No need to do anything, we're already getting the right temporal, so notify the user */\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\t\t\t\tjson_t *result = json_object();\n\t\t\t\t\tjson_object_set_new(result, \"event\", json_string(\"simulcast\"));\n\t\t\t\t\tjson_object_set_new(result, \"videocodec\", json_string(janus_videocodec_name(session->vcodec)));\n\t\t\t\t\tjson_object_set_new(result, \"temporal\", json_integer(session->sim_context.templayer));\n\t\t\t\t\tjson_object_set_new(event, \"result\", result);\n\t\t\t\t\tgateway->push_event(session->handle, &janus_videocall_plugin, NULL, event, NULL);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t} else {\n\t\t\t\t\t/* We need to change temporal, send a PLI */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Simulcasting temporal layer change, sending a PLI to kickstart it\\n\");\n\t\t\t\t\tif(peer && peer->handle)\n\t\t\t\t\t\tgateway->send_pli(peer->handle);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(msg_sdp && msg_sdp_type && !strcasecmp(msg_sdp_type, \"answer\")) {\n\t\t\t\t/* Process the answer to see if there were any changes */\n\t\t\t\tchar error_str[512];\n\t\t\t\tjanus_sdp *answer = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str));\n\t\t\t\tif(answer == NULL) {\n\t\t\t\t\tif(peer)\n\t\t\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing answer: %s\\n\", error_str);\n\t\t\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing answer: %s\", error_str);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"%s is accepting an update from %s\\n\", session->username, peer ? peer->username : \"??\");\n\t\t\t\tsession->has_audio = (strstr(msg_sdp, \"m=audio\") != NULL);\n\t\t\t\tsession->has_video = (strstr(msg_sdp, \"m=video\") != NULL);\n\t\t\t\tsession->has_data = (strstr(msg_sdp, \"DTLS/SCTP\") != NULL);\n\t\t\t\t/* Check if this user will simulcast */\n\t\t\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\t\t\tif(msg_simulcast) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"VideoCall callee (%s) cannot do simulcast.\\n\", session->username);\n\t\t\t\t} else {\n\t\t\t\t\tjanus_rtp_simulcasting_cleanup(NULL, session->ssrc, session->rid, &session->rid_mutex);\n\t\t\t\t}\n\t\t\t\t/* Check which codecs we ended up using */\n\t\t\t\tconst char *acodec = NULL, *vcodec = NULL;\n\t\t\t\tjanus_sdp_find_first_codec(answer, JANUS_SDP_AUDIO, -1, &acodec);\n\t\t\t\tsession->acodec = janus_audiocodec_from_name(acodec);\n\t\t\t\tjanus_sdp_find_first_codec(answer, JANUS_SDP_VIDEO, -1, &vcodec);\n\t\t\t\tsession->vcodec = janus_videocodec_from_name(vcodec);\n\t\t\t\tsession->opusred_pt = janus_sdp_get_opusred_pt(answer, -1);\n\t\t\t\tjanus_videocall_session *peer = session->peer;\n\t\t\t\tif(peer) {\n\t\t\t\t\tpeer->has_audio = session->has_audio;\n\t\t\t\t\tpeer->has_video = session->has_video;\n\t\t\t\t\tpeer->has_data = session->has_data;\n\t\t\t\t\tpeer->acodec = session->acodec;\n\t\t\t\t\tpeer->vcodec = session->vcodec;\n\t\t\t\t\tpeer->opusred_pt = session->opusred_pt;\n\t\t\t\t}\n\t\t\t\tif(session->acodec == JANUS_AUDIOCODEC_NONE) {\n\t\t\t\t\tsession->has_audio = FALSE;\n\t\t\t\t\tif(peer)\n\t\t\t\t\t\tpeer->has_audio = FALSE;\n\t\t\t\t} else if(peer) {\n\t\t\t\t\tpeer->acodec = session->acodec;\n\t\t\t\t}\n\t\t\t\tif(session->vcodec == JANUS_VIDEOCODEC_NONE) {\n\t\t\t\t\tsession->has_video = FALSE;\n\t\t\t\t\tif(peer)\n\t\t\t\t\t\tpeer->has_video = FALSE;\n\t\t\t\t} else if(peer) {\n\t\t\t\t\tpeer->vcodec = session->vcodec;\n\t\t\t\t}\n\t\t\t\tjanus_sdp_destroy(answer);\n\t\t\t}\n\t\t\tif(record) {\n\t\t\t\tgboolean recording = json_is_true(record);\n\t\t\t\tconst char *recording_base = json_string_value(recfile);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording %s (base filename: %s)\\n\", recording ? \"enabled\" : \"disabled\", recording_base ? recording_base : \"not provided\");\n\t\t\t\tjanus_mutex_lock(&session->rec_mutex);\n\t\t\t\tif(!recording) {\n\t\t\t\t\t/* Not recording (anymore?) */\n\t\t\t\t\tjanus_videocall_recorder_close(session);\n\t\t\t\t} else {\n\t\t\t\t\t/* We've started recording, send a PLI and go on */\n\t\t\t\t\tchar filename[255];\n\t\t\t\t\tgint64 now = janus_get_real_time();\n\t\t\t\t\tif(session->has_audio) {\n\t\t\t\t\t\t/* Prepare an audio recording */\n\t\t\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-audio\", recording_base);\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, janus_audiocodec_name(session->acodec), filename);\n\t\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this VideoCall user!\\n\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"videocall-%s-%s-%\"SCNi64\"-audio\",\n\t\t\t\t\t\t\t\tsession->username ? session->username : \"unknown\",\n\t\t\t\t\t\t\t\t(peer && peer->username) ? peer->username : \"unknown\",\n\t\t\t\t\t\t\t\tnow);\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, janus_audiocodec_name(session->acodec), filename);\n\t\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an audio recording file for this VideoCall user!\\n\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* If RED is in use, take note of it */\n\t\t\t\t\t\tif(session->opusred_pt > 0)\n\t\t\t\t\t\t\tjanus_recorder_opusred(rc, session->opusred_pt);\n\t\t\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\t\t\tif(session->e2ee)\n\t\t\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\t\t\tsession->arc = rc;\n\t\t\t\t\t}\n\t\t\t\t\tif(session->has_video) {\n\t\t\t\t\t\t/* Prepare a video recording */\n\t\t\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-video\", recording_base);\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, janus_videocodec_name(session->vcodec), filename);\n\t\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an video recording file for this VideoCall user!\\n\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"videocall-%s-%s-%\"SCNi64\"-video\",\n\t\t\t\t\t\t\t\tsession->username ? session->username : \"unknown\",\n\t\t\t\t\t\t\t\t(peer && peer->username) ? peer->username : \"unknown\",\n\t\t\t\t\t\t\t\tnow);\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, janus_videocodec_name(session->vcodec), filename);\n\t\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an video recording file for this VideoCall user!\\n\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Recording video, sending a PLI to kickstart it\\n\");\n\t\t\t\t\t\tgateway->send_pli(session->handle);\n\t\t\t\t\t\t/* If media is encrypted, mark it in the recording */\n\t\t\t\t\t\tif(session->e2ee)\n\t\t\t\t\t\t\tjanus_recorder_encrypted(rc);\n\t\t\t\t\t\tsession->vrc = rc;\n\t\t\t\t\t}\n\t\t\t\t\tif(session->has_data) {\n\t\t\t\t\t\t/* Prepare a data recording */\n\t\t\t\t\t\tjanus_recorder *rc = NULL;\n\t\t\t\t\t\tmemset(filename, 0, 255);\n\t\t\t\t\t\tif(recording_base) {\n\t\t\t\t\t\t\t/* Use the filename and path we have been provided */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"%s-data\", recording_base);\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, \"text\", filename);\n\t\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open a data recording file for this VideoCall user!\\n\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Build a filename */\n\t\t\t\t\t\t\tg_snprintf(filename, 255, \"videocall-%s-%s-%\"SCNi64\"-data\",\n\t\t\t\t\t\t\t\tsession->username ? session->username : \"unknown\",\n\t\t\t\t\t\t\t\t(peer && peer->username) ? peer->username : \"unknown\",\n\t\t\t\t\t\t\t\tnow);\n\t\t\t\t\t\t\trc = janus_recorder_create(NULL, \"text\", filename);\n\t\t\t\t\t\t\tif(rc == NULL) {\n\t\t\t\t\t\t\t\t/* FIXME We should notify the fact the recorder could not be created */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open a data recording file for this VideoCall user!\\n\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Media encryption doesn't apply to data channels */\n\t\t\t\t\t\tsession->drc = rc;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&session->rec_mutex);\n\t\t\t}\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"configured\"));\n\t\t\t\tjson_object_set_new(info, \"audio_active\", session->audio_active ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"video_active\", session->video_active ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"bitrate\", json_integer(session->bitrate));\n\t\t\t\tif(session->arc || session->vrc || session->drc) {\n\t\t\t\t\tjson_t *recording = json_object();\n\t\t\t\t\tif(session->arc && session->arc->filename)\n\t\t\t\t\t\tjson_object_set_new(recording, \"audio\", json_string(session->arc->filename));\n\t\t\t\t\tif(session->vrc && session->vrc->filename)\n\t\t\t\t\t\tjson_object_set_new(recording, \"video\", json_string(session->vrc->filename));\n\t\t\t\t\tif(session->drc && session->drc->filename)\n\t\t\t\t\t\tjson_object_set_new(recording, \"data\", json_string(session->drc->filename));\n\t\t\t\t\tjson_object_set_new(info, \"recording\", recording);\n\t\t\t\t}\n\t\t\t\tgateway->notify_event(&janus_videocall_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* Send an ack back */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"set\"));\n\t\t\t/* If this is for an ICE restart, prepare the SDP to send back too */\n\t\t\tgboolean do_restart = restart ? json_is_true(restart) : FALSE;\n\t\t\tif(do_restart && !sdp_update) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Got a 'restart' request, but no SDP update? Ignoring...\\n\");\n\t\t\t}\n\t\t\tif(sdp_update && peer != NULL) {\n\t\t\t\t/* Forward new SDP to the peer */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\t\t\tjson_t *update = json_object();\n\t\t\t\tjson_object_set_new(update, \"event\", json_string(\"update\"));\n\t\t\t\tjson_object_set_new(event, \"result\", update);\n\t\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", msg_sdp_type, \"sdp\", msg_sdp);\n\t\t\t\tint ret = gateway->push_event(peer->handle, &janus_videocall_plugin, NULL, event, jsep);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(event);\n\t\t\t\tjson_decref(jsep);\n\t\t\t}\n\t\t\tif(peer)\n\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t} else if(!strcasecmp(request_text, \"hangup\")) {\n\t\t\tjson_t *hangup = json_object_get(root, \"reason\");\n\t\t\tif(hangup && !json_is_string(hangup)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid element (hangup should be a string), ignoring\\n\");\n\t\t\t\thangup = NULL;\n\t\t\t}\n\t\t\tconst char *hangup_text = hangup ? json_string_value(hangup) : \"We did the hangup\";\n\t\t\t/* Hangup an ongoing call or reject an incoming one */\n\t\t\tjanus_videocall_session *peer = session->peer;\n\t\t\tif(peer == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"No call to hangup\\n\");\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"%s is hanging up the call with %s (%s)\\n\", session->username, peer->username, hangup_text);\n\t\t\t}\n\t\t\t/* Check if we still need to remove any reference */\n\t\t\tif(peer && g_atomic_int_compare_and_exchange(&peer->incall, 1, 0)) {\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t}\n\t\t\tif(g_atomic_int_compare_and_exchange(&session->incall, 1, 0) && peer) {\n\t\t\t\tjanus_refcount_decrease(&peer->ref);\n\t\t\t}\n\t\t\t/* Notify the success as an hangup message */\n\t\t\tresult = json_object();\n\t\t\tjson_object_set_new(result, \"event\", json_string(\"hangup\"));\n\t\t\tjson_object_set_new(result, \"username\", json_string(session->username));\n\t\t\tjson_object_set_new(result, \"reason\", json_string(hangup_text));\n\t\t\tjson_object_set_new(result, \"reason\", json_string(\"Explicit hangup\"));\n\t\t\t/* Also notify event handlers */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"hangup\"));\n\t\t\t\tjson_object_set_new(info, \"reason\", json_string(\"Explicit hangup\"));\n\t\t\t\tgateway->notify_event(&janus_videocall_plugin, session->handle, info);\n\t\t\t}\n\t\t\t/* Hangup the call on the user, if it's still up */\n\t\t\tgateway->close_pc(session->handle);\n\t\t\tif(peer != NULL) {\n\t\t\t\t/* Send event to our peer too */\n\t\t\t\tjson_t *call = json_object();\n\t\t\t\tjson_object_set_new(call, \"videocall\", json_string(\"event\"));\n\t\t\t\tjson_t *calling = json_object();\n\t\t\t\tjson_object_set_new(calling, \"event\", json_string(\"hangup\"));\n\t\t\t\tjson_object_set_new(calling, \"username\", json_string(session->username));\n\t\t\t\tjson_object_set_new(calling, \"reason\", json_string(hangup_text));\n\t\t\t\tjson_object_set_new(call, \"result\", calling);\n\t\t\t\tgateway->close_pc(peer->handle);\n\t\t\t\tint ret = gateway->push_event(peer->handle, &janus_videocall_plugin, NULL, call, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event to peer: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(call);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"hangup\"));\n\t\t\t\t\tjson_object_set_new(info, \"reason\", json_string(\"Remote hangup\"));\n\t\t\t\t\tgateway->notify_event(&janus_videocall_plugin, peer->handle, info);\n\t\t\t\t}\n\t\t\t\t/* Hangup the call on the peer, if it's still up */\n\t\t\t\tgateway->close_pc(peer->handle);\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unknown request (%s)\\n\", request_text);\n\t\t\terror_code = JANUS_VIDEOCALL_ERROR_INVALID_REQUEST;\n\t\t\tg_snprintf(error_cause, 512, \"Unknown request (%s)\", request_text);\n\t\t\tgoto error;\n\t\t}\n\n\t\t/* Prepare JSON event */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\tif(result != NULL)\n\t\t\tjson_object_set_new(event, \"result\", result);\n\t\tint ret = gateway->push_event(msg->handle, &janus_videocall_plugin, msg->transaction, event, NULL);\n\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\tjson_decref(event);\n\t\tjanus_videocall_message_free(msg);\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"videocall\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_videocall_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_videocall_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving VideoCall handler thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/plugins/janus_videoroom.c",
    "content": "/*! \\file   janus_videoroom.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus VideoRoom plugin\n * \\details Check the \\ref videoroom for more details.\n *\n * \\ingroup plugins\n * \\ref plugins\n *\n * \\page videoroom VideoRoom plugin documentation\n * This is a plugin implementing a videoconferencing SFU\n * (Selective Forwarding Unit) for Janus, that is an audio/video router.\n * This means that the plugin implements a virtual conferencing room peers\n * can join and leave at any time. This room is based on a Publish/Subscribe\n * pattern. Each peer can publish his/her own live audio/video feeds: this\n * feed becomes an available stream in the room the other participants can\n * subscribe to. This means that this plugin allows the realization of several\n * different scenarios, ranging from a simple webinar (one speaker, several\n * watchers) to a fully meshed video conference (each peer sending and\n * receiving to and from all the others).\n *\n * Notice that, since Janus now supports multistream PeerConnections,\n * subscriptions can be done either in \"bulks\" (you use a single PeerConnection\n * to subscribe to multiple streams from one or more publishers) or\n * separately (each PeerConnections represents a subscription to a single\n * publisher). Same thing for publishers: you may choose to publish, e.g.,\n * audio and video on one PeerConnection, and share your screen on another,\n * or publish everything on the same PeerConnection instead. While\n * functionally both approaches (multistream vs. legacy mode) are the same\n * (the same media flows in both cases), the differences are in how\n * resources are used, and in how the client has to handle incoming and\n * outgoing connections. Besides, one approach might make more sense in\n * some scenarios, and the other make more sense in different use cases.\n * As such, the approach to follow is left to the developer and the application.\n *\n * What is important to point out, though, is that publishers and subscribers\n * will in all cases require different PeerConnections. This means that,\n * even with multistream, you won't be able to use a single PeerConnection\n * to send your contributions and receive those from everyone else. This\n * is a choice done by design, to avoid the issues that would inevitably\n * arise when doing, for instance, renegotiations to update the streams.\n *\n * On a more general note and to give some more context with respect to the\n * core functionality in Janus, notice that, considering this plugin allows\n * for several different WebRTC PeerConnections to be on at the same time\n * for the same peer (different publishers and subscribers for sure, and\n * potentially more than one of each if multistream is not in use), each\n * peer will often need to attach several times to the same plugin for each\n * stream: this means that each peer needs to have at least one handle active\n * for managing its relation with the plugin (joining a room,\n * leaving a room, muting/unmuting, publishing, receiving events), and needs\n * to open others when they want to subscribe to a feed from other participants\n * (the number depends on the subscription approach of choice). Handles\n * used for subscriptions, though, would be logically \"subjects\" to the\n * master one used for managing the room: this means that they cannot be\n * used, for instance, to unmute in the room, as their only purpose would\n * be to provide a context in which creating the recvonly PeerConnections\n * for the subscription(s).\n *\n * Rooms to make available are listed in the plugin configuration file.\n * A pre-filled configuration file is provided in \\c conf/janus.plugin.videoroom.jcfg\n * and includes a demo room for testing. The same plugin is also used\n * dynamically (that is, with rooms created on the fly via API) in the\n * Screen Sharing demo as well.\n *\n * To add more rooms or modify the existing one, you can use the following\n * syntax:\n *\n * \\verbatim\nroom-<unique room ID>: {\n\tdescription = This is my awesome room\n\tis_private = true|false (private rooms don't appear when you do a 'list' request, default=false)\n\tsecret = <optional password needed for manipulating (e.g. destroying) the room>\n\tpin = <optional password needed for joining the room>\n\trequire_pvtid = true|false (whether subscriptions are required to provide a valid private_id\n\t\t\t\t to associate with a publisher, default=false)\n\tsigned_tokens = true|false (whether access to the room requires signed tokens; default=false,\n\t\t\t\t only works if signed tokens are used in the core as well)\n\tpublishers = <max number of concurrent senders> (e.g., 6 for a video\n\t\t\t\t conference or 1 for a webinar, default=3)\n\tbitrate = <max video bitrate for senders> (e.g., 128000)\n\tbitrate_cap = <true|false, whether the above cap should act as a limit to dynamic bitrate changes by publishers, default=false>,\n\tfir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)\n\taudiocodec = opus|g722|pcmu|pcma|isac32|isac16 (audio codec to force on publishers, default=opus\n\t\t\t\tcan be a comma separated list in order of preference, e.g., opus,pcmu)\n\tvideocodec = vp8|vp9|h264|av1|h265 (video codec to force on publishers, default=vp8\n\t\t\t\tcan be a comma separated list in order of preference, e.g., vp9,vp8,h264)\n\tvp9_profile = VP9-specific profile to prefer (e.g., \"2\" for \"profile-id=2\")\n\th264_profile = H.264-specific profile to prefer (e.g., \"42e01f\" for \"profile-level-id=42e01f\")\n\topus_fec = true|false (whether inband FEC must be negotiated; only works for Opus, default=true)\n\topus_dtx = true|false (whether DTX must be negotiated; only works for Opus, default=false)\n\taudiolevel_ext = true|false (whether the ssrc-audio-level RTP extension must be\n\t\tnegotiated/used or not for new publishers, default=true)\n\taudiolevel_event = true|false (whether to emit event to other users or not, default=false)\n\taudio_active_packets = 100 (number of packets with audio level, default=100, 2 seconds)\n\taudio_level_average = 25 (average value of audio level, 127=muted, 0='too loud', default=25)\n\tvideoorient_ext = true|false (whether the video-orientation RTP extension must be\n\t\tnegotiated/used or not for new publishers, default=true)\n\tplayoutdelay_ext = true|false (whether the playout-delay RTP extension must be\n\t\tnegotiated/used or not for new publishers, default=true)\n\ttransport_wide_cc_ext = true|false (whether the transport wide CC RTP extension must be\n\t\tnegotiated/used or not for new publishers, default=true)\n\trecord = true|false (whether this room should be recorded, default=false)\n\trec_dir = <folder where recordings should be stored, when enabled>\n\tlock_record = true|false (whether recording can only be started/stopped if the secret\n\t\t\t\tis provided, or using the global enable_recording request, default=false)\n\tnotify_joining = true|false (optional, whether to notify all participants when a new\n\t\t\t\tparticipant joins the room. The Videoroom plugin by design only notifies\n\t\t\t\tnew feeds (publishers), and enabling this may result extra notification\n\t\t\t\ttraffic. This flag is particularly useful when enabled with require_pvtid\n\t\t\t\tfor admin to manage listening only participants. default=false)\n\trequire_e2ee = true|false (whether all participants are required to publish and subscribe\n\t\t\t\tusing end-to-end media encryption, e.g., via Insertable Streams; default=false)\n\tdummy_publisher = true|false (whether a dummy publisher should be created in this room,\n\t\t\t\twith one separate m-line for each codec supported in the room; this is\n\t\t\t\tuseful when there's a need to create subscriptions with placeholders\n\t\t\t\tfor some or all m-lines, even when they aren't used yet; default=false)\n\tdummy_streams = in case dummy_publisher is set to true, array of codecs to offer,\n\t\t\t\toptionally with a fmtp attribute to match (codec/fmtp properties).\n\t\t\t\tIf not provided, all codecs enabled in the room are offered, with no fmtp.\n\t\t\t\tNotice that the fmtp is parsed, and only a few codecs are supported.\n\tthreads = number of threads to assist with the relaying of publishers in the room; as\n\t\t\t\tin the Streaming plugin, this setting can help if you expect a lot of subscribers\n\t\t\t\tthat may cause the plugin to slow down and fail to catch up (default=0)\n}\n\\endverbatim\n *\n * Note that recording will work with all codecs except iSAC.\n *\n * \\section sfuapi Video Room API\n *\n * The Video Room API supports several requests, some of which are\n * synchronous and some asynchronous. There are some situations, though,\n * (invalid JSON, invalid request) which will always result in a\n * synchronous error response even for asynchronous requests.\n *\n * \\c create , \\c destroy , \\c edit , \\c exists, \\c list, \\c allowed,\n * \\c kick , \\c moderate , \\c enable_recording , \\c listparticipants\n * and \\c listforwarders are synchronous requests, which means you'll\n * get a response directly within the context of the transaction.\n * \\c create allows you to create a new video room dynamically, as an\n * alternative to using the configuration file; \\c edit allows you to\n * dynamically edit some room properties (e.g., the PIN); \\c destroy removes a\n * video room and destroys it, kicking all the users out as part of the\n * process; \\c exists allows you to check whether a specific video room\n * exists; finally, \\c list lists all the available rooms, while \\c\n * listparticipants lists all the active (as in currently publishing\n * something) participants of a specific room and their details.\n *\n * The \\c join , \\c joinandconfigure , \\c configure , \\c publish ,\n * \\c unpublish , \\c start , \\c pause , \\c switch and \\c leave\n * requests instead are all asynchronous, which\n * means you'll get a notification about their success or failure in\n * an event. \\c join allows you to join a specific video room, specifying\n * whether that specific PeerConnection will be used for publishing or\n * watching; \\c configure can be used to modify some of the participation\n * settings (e.g., bitrate cap); \\c joinandconfigure combines the previous\n * two requests in a single one (just for publishers); \\c publish can be\n * used to start sending media to broadcast to the other participants,\n * while \\c unpublish does the opposite; \\c start allows you to start\n * receiving media from a publisher you've subscribed to previously by\n * means of a \\c join , while \\c pause pauses the delivery of the media;\n * the \\c switch request can be used to change the source of the media\n * flowing over a specific PeerConnection (e.g., I was watching Alice,\n * I want to watch Bob now) without having to create a new handle for\n * that; finally, \\c leave allows you to leave a video room for good\n * (or, in the case of viewers, definitely closes a subscription).\n *\n * \\c create can be used to create a new video room, and has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"create\",\n\t\"room\" : <unique numeric ID, optional, chosen by plugin if missing>,\n\t\"permanent\" : <true|false, whether the room should be saved in the config file, default=false>,\n\t\"description\" : \"<pretty name of the room, optional>\",\n\t\"secret\" : \"<password required to edit/destroy the room, optional>\",\n\t\"pin\" : \"<password required to join the room, optional>\",\n\t\"is_private\" : <true|false, whether the room should appear in a list request>,\n\t\"allowed\" : [ array of string tokens users can use to join this room, optional],\n\t...\n}\n\\endverbatim\n *\n * For the sake of brevity, not all of the available settings are listed\n * here. You can refer to the name of the properties in the configuration\n * file as a reference, as the ones used to programmatically create a new\n * room are exactly the same.\n *\n * A successful creation procedure will result in a \\c created response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"created\",\n\t\"room\" : <unique numeric ID>,\n\t\"permanent\" : <true if saved to config file, false if not>\n}\n\\endverbatim\n *\n * If you requested a permanent room but a \\c false value is returned\n * instead, good chances are that there are permission problems.\n *\n * An error instead (and the same applies to all other requests, so this\n * won't be repeated) would provide both an error code and a more verbose\n * description of the cause of the issue:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"error_code\" : <numeric ID, check Macros below>,\n\t\"error\" : \"<error description as a string>\"\n}\n\\endverbatim\n *\n * Notice that, in general, all users can create rooms. If you want to\n * limit this functionality, you can configure an admin \\c admin_key in\n * the plugin settings. When configured, only \"create\" requests that\n * include the correct \\c admin_key value in an \"admin_key\" property\n * will succeed, and will be rejected otherwise. Notice that you can\n * optionally extend this functionality to RTP forwarding as well, in\n * order to only allow trusted clients to use that feature.\n *\n * Once a room has been created, you can still edit some (but not all)\n * of its properties using the \\c edit request. This allows you to modify\n * the room description, secret, pin and whether it's private or not: you\n * won't be able to modify other more static properties, like the room ID,\n * the sampling rate, the extensions-related stuff and so on. If you're\n * interested in changing the ACL, instead, check the \\c allowed message.\n * An \\c edit request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"edit\",\n\t\"room\" : <unique numeric ID of the room to edit>,\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"new_description\" : \"<new pretty name of the room, optional>\",\n\t\"new_secret\" : \"<new password required to edit/destroy the room, optional>\",\n\t\"new_pin\" : \"<new password required to join the room, optional>\",\n\t\"new_is_private\" : <true|false, whether the room should appear in a list request>,\n\t\"new_require_pvtid\" : <true|false, whether the room should require private_id from subscribers>,\n\t\"new_bitrate\" : <new bitrate cap to force on all publishers (except those with custom overrides)>,\n\t\"new_fir_freq\" : <new period for regular PLI keyframe requests to publishers>,\n\t\"new_publishers\" : <new cap on the number of concurrent active WebRTC publishers>,\n\t\"new_lock_record\" : <true|false, whether recording state can only be changed when providing the room secret>,\n\t\"new_rec_dir\" : \"<the new path where the next .mjr files should being saved>\",\n\t\"permanent\" : <true|false, whether the room should be also removed from the config file, default=false>\n}\n\\endverbatim\n *\n * A successful edit procedure will result in an \\c edited response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"edited\",\n\t\"room\" : <unique numeric ID>\n}\n\\endverbatim\n *\n * On the other hand, \\c destroy can be used to destroy an existing video\n * room, whether created dynamically or statically, and has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"destroy\",\n\t\"room\" : <unique numeric ID of the room to destroy>,\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"permanent\" : <true|false, whether the room should be also removed from the config file, default=false>\n}\n\\endverbatim\n *\n * A successful destruction procedure will result in a \\c destroyed response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"destroyed\",\n\t\"room\" : <unique numeric ID>\n}\n\\endverbatim\n *\n * This will also result in a \\c destroyed event being sent to all the\n * participants in the video room, which will look like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"destroyed\",\n\t\"room\" : <unique numeric ID of the destroyed room>\n}\n\\endverbatim\n *\n * You can check whether a room exists using the \\c exists request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"exists\",\n\t\"room\" : <unique numeric ID of the room to check>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"exists\" : <true|false>\n}\n\\endverbatim\n *\n * You can configure whether to check tokens or add/remove people who can join\n * a room using the \\c allowed request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"allowed\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"action\" : \"enable|disable|add|remove\",\n\t\"room\" : <unique numeric ID of the room to update>,\n\t\"allowed\" : [\n\t\t// Array of strings (tokens users might pass in \"join\", only for add|remove)\n\t]\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n\t\"room\" : <unique numeric ID>,\n\t\"allowed\" : [\n\t\t// Updated, complete, list of allowed tokens (only for enable|add|remove)\n\t]\n}\n\\endverbatim\n *\n * If you're the administrator of a room (that is, you created it and have access\n * to the secret) you can kick participants using the \\c kick request. Notice\n * that this only kicks the user out of the room, but does not prevent them from\n * re-joining: to ban them, you need to first remove them from the list of\n * authorized users (see \\c allowed request) and then \\c kick them. The \\c kick\n * request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"kick\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"id\" : <unique numeric ID of the participant to kick>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n}\n\\endverbatim\n *\n * As an administrator, you can also forcibly mute/unmute any of the media\n * streams sent by participants (i.e., audio, video and data streams),\n * using the \\c moderate requests. Notice that if the participant is self\n * muted on a stream, and you unmute that stream with \\c moderate, they\n * will NOT be unmuted: you'll simply remove any moderation block\n * that may have been enforced on the participant for that medium\n * themselves. The \\c moderate request has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"moderate\",\n\t\"secret\" : \"<room secret, mandatory if configured>\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"id\" : <unique numeric ID of the participant to moderate>,\n\t\"mid\" : <mid of the m-line to refer to for this moderate request>,\n\t\"mute\" : <true|false, depending on whether the media addressed by the above mid should be muted by the moderator>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n}\n\\endverbatim\n *\n * To get a list of the available rooms you can make use of the \\c list request.\n * \\c admin_key is optional. If included and correct, rooms configured/created\n * as private will be included in the list as well.\n *\n\\verbatim\n{\n\t\"request\" : \"list\"\n}\n\\endverbatim\n *\n * A successful request will produce a list of rooms in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n\t\"list\" : [\t\t// Array of room objects\n\t\t{\t// Room #1\n\t\t\t\"room\" : <unique numeric ID>,\n\t\t\t\"description\" : \"<Name of the room>\",\n\t\t\t\"pin_required\" : <true|false, whether a PIN is required to join this room>,\n\t\t\t\"is_private\" : <true|false, whether this room is 'private' (as in hidden) or not>,\n\t\t\t\"max_publishers\" : <how many publishers can actually publish via WebRTC at the same time>,\n\t\t\t\"bitrate\" : <bitrate cap that should be forced (via REMB) on all publishers by default>,\n\t\t\t\"bitrate_cap\" : <true|false, whether the above cap should act as a limit to dynamic bitrate changes by publishers (optional)>,\n\t\t\t\"fir_freq\" : <how often a keyframe request is sent via PLI/FIR to active publishers>,\n\t\t\t\"require_pvtid\": <true|false, whether subscriptions in this room require a private_id>,\n\t\t\t\"require_e2ee\": <true|false, whether end-to-end encrypted publishers are required>,\n\t\t\t\"dummy_publisher\": <true|false, whether a dummy publisher exists for placeholder subscriptions>,\n\t\t\t\"notify_joining\": <true|false, whether an event is sent to notify all participants if a new participant joins the room>,\n\t\t\t\"audiocodec\" : \"<comma separated list of allowed audio codecs>\",\n\t\t\t\"videocodec\" : \"<comma separated list of allowed video codecs>\",\n\t\t\t\"opus_fec\": <true|false, whether inband FEC must be negotiated (note: only available for Opus) (optional)>,\n\t\t\t\"opus_dtx\": <true|false, whether DTX must be negotiated (note: only available for Opus) (optional)>,\n\t\t\t\"record\" : <true|false, whether the room is being recorded>,\n\t\t\t\"rec_dir\" : \"<if recording, the path where the .mjr files are being saved>\",\n\t\t\t\"lock_record\" : <true|false, whether the room recording state can only be changed providing the secret>,\n\t\t\t\"num_participants\" : <count of the participants (publishers, active or not; not subscribers)>\n\t\t\t\"audiolevel_ext\": <true|false, whether the ssrc-audio-level extension must be negotiated or not for new publishers>,\n\t\t\t\"audiolevel_event\": <true|false, whether to emit event to other users about audiolevel>,\n\t\t\t\"audio_active_packets\": <amount of packets with audio level for checkup (optional, only if audiolevel_event is true)>,\n\t\t\t\"audio_level_average\": <average audio level (optional, only if audiolevel_event is true)>,\n\t\t\t\"videoorient_ext\": <true|false, whether the video-orientation extension must be negotiated or not for new publishers>,\n\t\t\t\"playoutdelay_ext\": <true|false, whether the playout-delay extension must be negotiated or not for new publishers>,\n\t\t\t\"transport_wide_cc_ext\": <true|false, whether the transport wide cc extension must be negotiated or not for new publishers>\n\t\t},\n\t\t// Other rooms\n\t]\n}\n\\endverbatim\n *\n * To get a list of the participants in a specific room, instead, you\n * can make use of the \\c listparticipants request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listparticipants\",\n\t\"room\" : <unique numeric ID of the room>\n}\n\\endverbatim\n *\n * A successful request will produce a list of participants in a\n * \\c participants response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"participants\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"participants\" : [\t\t// Array of participant objects\n\t\t{\t// Participant #1\n\t\t\t\"id\" : <unique numeric ID of the participant>,\n\t\t\t\"display\" : \"<display name of the participant, if any; optional>\",\n\t\t\t\"metadata\" : <valid json object of metadata, if any; optional>,\n\t\t\t\"publisher\" : \"<true|false, whether user is an active publisher in the room>\",\n\t\t\t\"talking\" : <true|false, whether user is talking or not (only if audio levels are used)>\n\t\t},\n\t\t// Other participants\n\t]\n}\n\\endverbatim\n *\n * This covers almost all the synchronous requests. All the asynchronous requests,\n * plus a couple of additional synchronous requests we'll cover later, refer\n * to participants instead, namely on how they can publish, subscribe, or\n * more in general manage the media streams they may be sending or receiving.\n *\n * Considering the different nature of publishers and subscribers in the room,\n * and more importantly how you establish PeerConnections in the respective\n * cases, their API requests are addressed in separate subsections.\n *\n * \\subsection vroompub VideoRoom Publishers\n *\n * In a VideoRoom, publishers are those participant handles that are able\n * (although may choose not to, more on this later) publish media in the\n * room, and as such become feeds that you can subscribe to.\n *\n * To specify that a handle will be associated with a publisher, you must use\n * the \\c join request with \\c ptype set to \\c publisher (note that, as it\n * will be explained later, you can also use \\c joinandconfigure for the\n * purpose). The exact syntax of the request is the following:\n *\n\\verbatim\n{\n\t\"request\" : \"join\",\n\t\"ptype\" : \"publisher\",\n\t\"room\" : <unique ID of the room to join>,\n\t\"id\" : <unique ID to register for the publisher; optional, will be chosen by the plugin if missing>,\n\t\"display\" : \"<display name for the publisher; optional>\",\n\t\"token\" : \"<invitation token, in case the room has an ACL; optional>\",\n\t\"metadata\" : <valid json object with metadata; optional>\n}\n\\endverbatim\n *\n * This will add the user to the list of participants in the room, although\n * in a non-active role for the time being. Anyway, this participation\n * allows the user to receive notifications about several aspects of the\n * room on the related handle (including streams as they become available\n * and go away). As such, it can be used even just as a way to get\n * notifications in a room, without the need of ever actually publishing\n * any stream at all (which explains why the \"publisher\" role may actually\n * be a bit confusing in this context).\n *\n * A successful \\c join will result in a \\c joined event, which will contain\n * a list of the currently active (as in publishing via WebRTC) publishers,\n * and optionally a list of passive attendees (but only if the room was\n * configured with \\c notify_joining set to \\c TRUE ):\n *\n\\verbatim\n{\n\t\"videoroom\" : \"joined\",\n\t\"room\" : <room ID>,\n\t\"description\" : <description of the room, if available>,\n\t\"id\" : <unique ID of the participant>,\n\t\"private_id\" : <a different unique ID associated to the participant; meant to be private>,\n\t\"publishers\" : [\n\t\t{\n\t\t\t\"id\" : <unique ID of active publisher #1>,\n\t\t\t\"display\" : \"<display name of active publisher #1, if any>\",\n\t\t\t\"metadata\" : <valid json object of metadata, if any>,\n\t\t\t\"dummy\" : <true if this participant is a dummy publisher>,\n\t\t\t\"streams\" : [\n\t\t\t\t{\n\t\t\t\t\t\"type\" : \"<type of published stream #1 (audio|video|data)\">,\n\t\t\t\t\t\"mindex\" : \"<unique mindex of published stream #1>\",\n\t\t\t\t\t\"mid\" : \"<unique mid of of published stream #1>\",\n\t\t\t\t\t\"disabled\" : <if true, it means this stream is currently inactive/disabled (and so codec, description, etc. will be missing)>,\n\t\t\t\t\t\"codec\" : \"<codec used for published stream #1>\",\n\t\t\t\t\t\"description\" : \"<text description of published stream #1, if any>\",\n\t\t\t\t\t\"moderated\" : <true if this stream audio has been moderated for this participant>,\n\t\t\t\t\t\"simulcast\" : \"<true if published stream #1 uses simulcast>\",\n\t\t\t\t\t\"svc\" : \"<true if published stream #1 uses SVC (VP9 and AV1 only)>\",\n\t\t\t\t\t\"talking\" : <true|false, whether the publisher stream has audio activity or not (only if audio levels are used)>,\n\t\t\t\t},\n\t\t\t\t// Other streams, if any\n\t\t\t],\n\t\t\t\"talking\" : <true|false, whether the publisher is talking or not (only if audio levels are used); deprecated, use the stream specific ones>,\n\t\t},\n\t\t// Other active publishers\n\t],\n\t\"attendees\" : [\t\t// Only present when notify_joining is set to TRUE for rooms\n\t\t{\n\t\t\t\"id\" : <unique ID of attendee #1>,\n\t\t\t\"display\" : \"<display name of attendee #1, if any>\",\n\t\t\t\"metadata\" : <valid json object of metadata, if any>\n\t\t},\n\t\t// Other attendees\n\t]\n}\n\\endverbatim\n *\n * Notice that the publishers list will of course be empty if no one is\n * currently active in the room. For what concerns the \\c private_id\n * property, it is meant to be used by the user when they create subscriptions,\n * so that the plugin can associate subscriber handles (which are typically\n * anonymous) to a specific participant; they're usually optional, unless\n * required by the room configuration.\n *\n * As explained, with a simple \\c join you're not an active publisher (there\n * is no WebRTC PeerConnection yet), which means that by default your presence\n * is not notified to other participants. In fact, the publish/subscribe nature\n * of the plugin implies that by default only active publishers are notified,\n * to allow participants to subscribe to existing feeds: notifying all joins/leaves,\n * even those related to who will just lurk, may be overly verbose and chatty,\n * especially in large rooms. Anyway, rooms can be configured to notify those\n * as well, if the \\c notify_joining property is set to true: in that case,\n * regular joins will be notified too, in an event formatted like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"room\" : <room ID>,\n\t\"joining\" : {\n\t\t\"id\" : <unique ID of the new participant>,\n\t\t\"display\" : \"<display name of the new participant, if any>\",\n\t\t\"metadata\" : <valid json object of metadata, if any>\n\t}\n}\n\\endverbatim\n *\n * If you're interested in publishing media within a room, you can do that\n * with a \\c publish request. This request MUST be accompanied by a JSEP\n * SDP offer to negotiate a new PeerConnection. The plugin will match it\n * to the room configuration (e.g., to make sure the codecs you negotiated\n * are allowed in the room), and will reply with a JSEP SDP answer to\n * close the circle and complete the setup of the PeerConnection. As soon\n * as the PeerConnection has been established, the publisher will become\n * active, and a new active feed other participants can subscribe to.\n *\n * The syntax of a \\c publish request is the following:\n *\n\\verbatim\n{\n\t\"request\" : \"publish\",\n\t\"audiocodec\" : \"<audio codec to prefer among the negotiated ones; optional>\",\n\t\"videocodec\" : \"<video codec to prefer among the negotiated ones; optional>\",\n\t\"bitrate\" : <bitrate cap to return via REMB; optional, overrides the global room value if present>,\n\t\"record\" : <true|false, whether this publisher should be recorded or not; optional>,\n\t\"filename\" : \"<if recording, the base path/file to use for the recording files; optional>\",\n\t\"display\" : \"<display name to use in the room; optional>\",\n\t\"metadata\" : <valid json object of metadata; optional>,\n\t\"audio_level_average\" : \"<if provided, overrides the room audio_level_average for this user; optional>\",\n\t\"audio_active_packets\" : \"<if provided, overrides the room audio_active_packets for this user; optional>\",\n\t\"descriptions\" : [\t// Optional\n\t\t{\n\t\t\t\"mid\" : \"<unique mid of a stream being published>\",\n\t\t\t\"description\" : \"<text description of the stream (e.g., My front webcam)>\"\n\t\t},\n\t\t// Other descriptions, if any\n\t]}\n\\endverbatim\n *\n * As anticipated, since this is supposed to be accompanied by a JSEP SDP\n * offer describing the publisher's media streams, the plugin will negotiate\n * and prepare a matching JSEP SDP answer. Notice that, in principle, all\n * published streams will be only identified by their unique \\c mid and\n * by their type (e.g., audio or video). In case you want to provide more\n * information about the streams being published (e.g., to let other\n * participants know that the first video is a camera, while the second\n * video is a screen share), you can use the \\c descriptions array for\n * the purpose: each object in the array can be used to add a text description\n * to associate to a specific mid, in order to help with the UI rendering.\n * The \\c descriptions property is optional, so no text will be provided\n * by default: notice these descriptions can be updated dynamically via\n * \\c configure requests.\n *\n * If successful, a \\c configured event will be sent back, formatted like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"configured\" : \"ok\"\n}\n\\endverbatim\n *\n * This event will be accompanied by the prepared JSEP SDP answer.\n *\n * Notice that you can also use \\c configure as a request instead of\n * \\c publish to start publishing. The two are functionally equivalent\n * for publishing, but from a semantic perspective \\c publish is the\n * right message to send when publishing. The \\c configure request, as\n * it will be clearer later, can also be used to update some properties\n * of the publisher session: in this case the \\c publish request can NOT\n * be used, as it can only be invoked to publish, and will fail if you're\n * already publishing something.\n *\n * As an additional note, notice that you can also join and publish in\n * a single request, which is useful in case you're not interested in\n * first join as a passive attendee and only later publish something,\n * but want to publish something right away. In this case you can use\n * the \\c joinandconfigure request, which as you can imagine combines\n * the properties of both \\c join and \\c publish in a single request:\n * the response to a \\c joinandconfigure will be a \\c joined event, and\n * will again be accompanied by a JSEP SDP answer as usual.\n *\n * However you decided to publish something, as soon as the PeerConnection\n * setup succeeds and the publisher becomes active, an event is sent to\n * all the participants in the room with information on the new feed.\n * The event must contain an array with a single element, and be formatted like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"room\" : <room ID>,\n\t\"publishers\" : [\n\t\t{\n\t\t\t\"id\" : <unique ID of the new publisher>,\n\t\t\t\"display\" : \"<display name of the new publisher, if any>\",\n\t\t\t\"metadata\" : <valid json object of metadata, if any>,\n\t\t\t\"dummy\" : <true if this participant is a dummy publisher>,\n\t\t\t\"streams\" : [\n\t\t\t\t{\n\t\t\t\t\t\"type\" : \"<type of published stream #1 (audio|video|data)\">,\n\t\t\t\t\t\"mindex\" : \"<unique mindex of published stream #1>\",\n\t\t\t\t\t\"mid\" : \"<unique mid of of published stream #1>\",\n\t\t\t\t\t\"disabled\" : <if true, it means this stream is currently inactive/disabled (and so codec, description, etc. will be missing)>,\n\t\t\t\t\t\"codec\" : \"<codec used for published stream #1>\",\n\t\t\t\t\t\"description\" : \"<text description of published stream #1, if any>\",\n\t\t\t\t\t\"moderated\" : <true if this stream audio has been moderated for this participant>,\n\t\t\t\t\t\"simulcast\" : \"<true if published stream #1 uses simulcast>\",\n\t\t\t\t\t\"svc\" : \"<true if published stream #1 uses SVC (VP9 and AV1 only)>\",\n\t\t\t\t\t\"talking\" : <true|false, whether the publisher stream has audio activity or not (only if audio levels are used)>,\n\t\t\t\t},\n\t\t\t\t// Other streams, if any\n\t\t\t],\n\t\t\t\"talking\" : <true|false, whether the publisher is talking or not (only if audio levels are used); deprecated, use the stream specific ones>,\n\t\t}\n\t]\n}\n\\endverbatim\n *\n * To stop publishing and tear down the related PeerConnection, you can\n * use the \\c unpublish request, which requires no arguments as the context\n * is implicit:\n *\n\\verbatim\n{\n\t\"request\" : \"unpublish\"\n}\n\\endverbatim\n *\n * This will have the plugin tear down the PeerConnection, and remove the\n * publisher from the list of active streams. If successful, the response\n * will look like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"unpublished\" : \"ok\"\n}\n\\endverbatim\n *\n * As soon as the PeerConnection is gone, all the other participants will\n * also be notified about the fact that the stream is no longer available:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"room\" : <room ID>,\n\t\"unpublished\" : <unique ID of the publisher who unpublished>\n}\n\\endverbatim\n *\n * Notice that the same event will also be sent whenever the publisher\n * feed disappears for reasons other than an explicit \\c unpublish , e.g.,\n * because the handle was closed or the user lost their connection.\n * Besides, notice that you can publish and unpublish multiple times\n * within the context of the same publisher handle.\n *\n * As anticipated above, you can use a request called \\c configure to\n * tweak some of the properties of an active publisher session. This\n * request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"configure\",\n\t\"bitrate\" : <bitrate cap to return via REMB; optional, overrides the global room value if present (unless bitrate_cap is set)>,\n\t\"keyframe\" : <true|false, whether we should send this publisher a keyframe request>,\n\t\"record\" : <true|false, whether this publisher should be recorded or not; optional>,\n\t\"filename\" : \"<if recording, the base path/file to use for the recording files; optional>\",\n\t\"display\" : \"<new display name to use in the room; optional>\",\n\t\"metadata\" : <new metadata json object; optional>,\n\t\"audio_active_packets\" : \"<new audio_active_packets to overwrite in the room one; optional>\",\n\t\"audio_level_average\" : \"<new audio_level_average to overwrite the room one; optional>\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"mid\" : <mid of the m-line to tweak>,\n\t\t\t\"keyframe\" : <true|false, whether we should send this stream a keyframe request; optional>,\n\t\t\t\"send\" : <true|false, depending on whether the media addressed by the above mid should be relayed or not; optional>,\n\t\t\t\"min_delay\" : <minimum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>,\n\t\t\t\"max_delay\" : <maximum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>\n\t\t},\n\t\t// Other streams, if any\n\t],\n\t\"descriptions\" : [\n\t\t// Updated descriptions for the published streams; see \"publish\" for syntax; optional\n\t]\n}\n\\endverbatim\n *\n * As you can see, it's basically the same properties as those listed for\n * \\c publish , with the addition of a \\c streams array that can be used\n * to tweak individual streams (which is not available when publishing\n * since in that case the stream doesn't exist yet). Notice that the\n * \\c configure request can also be used in renegotiations, to provide\n * an updated SDP with changes to the published media. If successful,\n * a \\c configured event will be sent back as before, formatted like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"configured\" : \"ok\"\n}\n\\endverbatim\n *\n * When configuring the room to request the ssrc-audio-level RTP extension,\n * ad-hoc events might be sent to all publishers if \\c audiolevel_event is\n * set to true. These events will have the following format:\n *\n\\verbatim\n{\n\t\"videoroom\" : <\"talking\"|\"stopped-talking\", whether the publisher started or stopped talking>,\n\t\"room\" : <unique numeric ID of the room the publisher is in>,\n\t\"id\" : <unique numeric ID of the publisher>,\n\t\"audio-level-dBov-avg\" : <average value of audio level, 127=muted, 0='too loud'>\n}\n\\endverbatim\n *\n * An interesting feature VideoRoom publisher can take advantage of is\n * RTP forwarding. In fact, while the main purpose of this plugin is\n * getting media from WebRTC sources (publishers) and relaying it to\n * WebRTC destinations (subscribers), there are actually several use\n * cases and scenarios for making this media available to external,\n * notnecessarily WebRTC-compliant, components. These components may\n * benefit from having access to the RTP media sent by a publisher, e.g.,\n * for media processing, external recording, transcoding to other\n * technologies via other applications, scalability purposes or\n * whatever else makes sense in this context. This is made possible by\n * a request called \\c rtp_forward which, as the name suggests, simply\n * forwards in real-time the media sent by a publisher via RTP (plain\n * or encrypted) to a remote backend. Notice that, although we're using\n * the term \"RTP forwarder\", this feature can be used to forward data\n * channel messages as well.\n *\n * You can add a new RTP forwarder for an existing publisher using the\n * \\c rtp_forward request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"rtp_forward\",\n\t\"room\" : <unique numeric ID of the room the publisher is in>,\n\t\"publisher_id\" : <unique numeric ID of the publisher to relay externally>,\n\t\"host\" : \"<host address to forward the RTP and data packets to>\",\n\t\"host_family\" : \"<ipv4|ipv6, if we need to resolve the host address to an IP; by default, whatever we get>\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"mid\" : \"<mid of publisher stream to forward>\",\n\t\t\t\"host\" : \"<host address to forward the packets to; optional, will use global one if missing>\",\n\t\t\t\"host_family\" : \"<optional, will use global one if missing>\",\n\t\t\t\"port\" : <port to forward the packets to>,\n\t\t\t\"ssrc\" : <SSRC to use to use when forwarding; optional, and only for RTP streams, not data>,\n\t\t\t\"pt\" : <payload type to use when forwarding; optional, and only for RTP streams, not data>,\n\t\t\t\"rtcp_port\" : <port to contact to receive RTCP feedback from the recipient; optional, and only for RTP streams, not data>,\n\t\t\t\"simulcast\" : <true|false, set to true if the source is simulcast and you want the forwarder to act as a regular viewer (single stream being forwarded) or false otherwise (substreams forwarded separately); optional, default=false>,\n\t\t\t\"port_2\" : <if video and simulcasting, port to forward the packets from the second substream/layer to>,\n\t\t\t\"ssrc_2\" : <if video and simulcasting, SSRC to use to use the second substream/layer; optional>,\n\t\t\t\"pt_2\" : <if video and simulcasting, payload type to use the second substream/layer; optional>,\n\t\t\t\"port_3\" : <if video and simulcasting, port to forward the packets from the third substream/layer to>,\n\t\t\t\"ssrc_3\" : <if video and simulcasting, SSRC to use to use the third substream/layer; optional>,\n\t\t\t\"pt_3\" : <if video and simulcasting, payload type to use the third substream/layer; optional>,\n\t\t},\n\t\t{\n\t\t\t.. other streams, if needed..\n\t\t}\n\t],\n\t\"srtp_suite\" : <length of authentication tag (32 or 80); optional>,\n\t\"srtp_crypto\" : \"<key to use as crypto (base64 encoded key as in SDES); optional>\"\n}\n\\endverbatim\n *\n * As you can see, you basically configure each stream to forward in a\n * dedicated object of the \\c streams array: for RTP streams (audio, video)\n * this includes optionally overriding payload type or SSRC; simulcast\n * streams can be forwarded separately for each layer. The only parameters\n * you MUST specify are the host and port to send the packets to: the host\n * part can be put in the global part of the request, if all streams will\n * be sent to the same IP address, while the port must be specific to the\n * stream itself.\n *\n * Notice that, as explained above, in case you configured an \\c admin_key\n * property and extended it to RTP forwarding as well, you'll need to provide\n * it in the request as well or it will be rejected as unauthorized. By\n * default no limitation is posed on \\c rtp_forward .\n *\n * It's worth spending some more words on how to forward simulcast publishers,\n * as this can lead to some confusion. There are basically two ways to forward\n * a simulcast publisher:\n *\n * -# you treat the forwarder as a regular viewer, which means you still only\n * forward a single stream to the recipient, that is the highest quality\n * available at any given time: you can do that by setting\n * <code>simulcast: true</code> in the \\c rtp_forward request;\n * -# you forward each substream separately instead, to different target\n * ports: you do that by specifying \\c video_port_2 , \\c video_port_3 and\n * optionally the other related \\c _2 and \\c _3 properties; this is what\n * you should use when you want to forward to a simulcast-aware Streaming\n * mountpoint (see the \\ref streaming for more details).\n *\n * The two approaches are mutually exclusive: you can NOT use them together\n * in the same RTP forwarder.\n *\n * A successful request will result in an \\c rtp_forward response, containing\n * the relevant info associated to the new forwarder(s):\n *\n\\verbatim\n{\n\t\"videoroom\" : \"rtp_forward\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"publisher_id\" : <unique numeric ID, same as request>,\n\t\"forwarders\" : [\n\t\t{\n\t\t\t\"stream_id\" : <unique numeric ID assigned to this forwarder, if any>,\n\t\t\t\"type\" : \"<audio|video|data>\",\n\t\t\t\"host\" : \"<host this forwarder is streaming to, same as request if not resolved>\",\n\t\t\t\"port\" : <port this forwarder is streaming to, same as request if configured>,\n\t\t\t\"local_rtcp_port\" : <local port this forwarder is using to get RTCP feedback, if any>,\n\t\t\t\"remote_rtcp_port\" : <remote port this forwarder is getting RTCP feedback from, if any>,\n\t\t\t\"ssrc\" : <SSRC this forwarder is using, same as request if configured>,\n\t\t\t\"pt\" : <payload type this forwarder is using, same as request if configured>,\n\t\t\t\"substream\" : <video substream this video forwarder is relaying, if any>,\n\t\t\t\"srtp\" : <true|false, whether the RTP stream is encrypted (not used for data)>\n\t\t},\n\t\t// Other forwarders, if configured\n\t]\n}\n\\endverbatim\n *\n * To stop a previously created RTP forwarder and stop it, you can use\n * the \\c stop_rtp_forward request, which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"stop_rtp_forward\",\n\t\"room\" : <unique numeric ID of the room the publisher is in>,\n\t\"publisher_id\" : <unique numeric ID of the publisher to update>,\n\t\"stream_id\" : <unique numeric ID of the RTP forwarder>\n}\n\\endverbatim\n *\n * A successful request will result in a \\c stop_rtp_forward response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"stop_rtp_forward\",\n\t\"room\" : <unique numeric ID, same as request>,\n\t\"publisher_id\" : <unique numeric ID, same as request>,\n\t\"stream_id\" : <unique numeric ID, same as request>\n}\n\\endverbatim\n *\n * To get a list of all the forwarders in a specific room, instead, you\n * can make use of the \\c listforwarders request, which has to be\n * formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"listforwarders\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"secret\" : \"<room secret; mandatory if configured>\"\n}\n\\endverbatim\n *\n * A successful request will produce a list of RTP forwarders in a\n * \\c forwarders response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"forwarders\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"publishers\" : [\t\t// Array of publishers with RTP forwarders\n\t\t{\t// Publisher #1\n\t\t\t\"publisher_id\" : <unique numeric ID of publisher #1>,\n\t\t\t\"forwarders\" : [\t\t// Array of RTP forwarders\n\t\t\t\t{\t// RTP forwarder #1\n\t\t\t\t\t\"stream_id\" : <unique numeric ID assigned to this RTP forwarder, if any>,\n\t\t\t\t\t\"type\" : \"<audio|video|data>\",\n\t\t\t\t\t\"host\" : \"<host this forwarder is streaming to>\",\n\t\t\t\t\t\"port\" : <port this forwarder is streaming to>,\n\t\t\t\t\t\"local_rtcp_port\" : <local port this forwarder is using to get RTCP feedback, if any>,\n\t\t\t\t\t\"remote_rtcp_port\" : <remote port this forwarder getting RTCP feedback from, if any>,\n\t\t\t\t\t\"ssrc\" : <SSRC this forwarder is using, if any>,\n\t\t\t\t\t\"pt\" : <payload type this forwarder is using, if any>,\n\t\t\t\t\t\"substream\" : <video substream this video forwarder is relaying, if any>,\n\t\t\t\t\t\"srtp\" : <true|false, whether the RTP stream is encrypted>\n\t\t\t\t},\n\t\t\t\t// Other forwarders for this publisher\n\t\t\t],\n\t\t},\n\t\t// Other publishers\n\t]\n}\n\\endverbatim\n *\n * To enable or disable recording on all participants while the conference\n * is in progress, you can make use of the \\c enable_recording request,\n * which has to be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"enable_recording\",\n\t\"room\" : <unique numeric ID of the room>,\n\t\"secret\" : \"<room secret; mandatory if configured>\"\n\t\"record\" : <true|false, whether participants in this room should be automatically recorded or not>,\n}\n\\endverbatim\n *\n * Notice that, as we'll see later, participants can normally change their\n * own recording state via \\c configure requests as well: this was done to\n * allow the maximum flexibility, where rather than globally or automatically\n * record something, you may want to individually record some streams and\n * to a specific file. That said, if you'd rather ensure that participants\n * can't stop their recording if a global recording is enabled, or start\n * it when the room is not supposed to be recorded instead, then you should\n * make sure the room is created with the \\c lock_record property set to\n * \\c true : this way, the recording state can only be changed if the room\n * secret is provided, thus ensuring that only an administrator will normally\n * be able to do that (e.g., using the \\c enable_recording just introduced).\n *\n * To conclude, you can leave a room you previously joined as publisher\n * using the \\c leave request. This will also implicitly unpublish you\n * if you were an active publisher in the room. The \\c leave request\n * looks like follows:\n *\n\\verbatim\n{\n\t\"request\" : \"leave\"\n}\n\\endverbatim\n *\n * If successful, the response will look like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"leaving\" : \"ok\"\n}\n\\endverbatim\n *\n * Other participants will receive a \"leaving\" event to notify them the\n * circumstance:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"room\" : <room ID>,\n\t\"leaving : <unique ID of the participant who left>,\n\t\"display\" : \"<display name of the leaving participant, if any>\"\n}\n\\endverbatim\n *\n * If you were an active publisher, other users will also receive the\n * corresponding \"unpublished\" event to notify them the stream is not longer\n * available, as explained above. If you were simply lurking and not\n * publishing, the other participants will only receive the \"leaving\" event.\n *\n * \\subsection vroomsub VideoRoom Subscribers\n *\n * In a VideoRoom, subscribers are NOT participants, but simply handles\n * that will be used exclusively to receive media from one or more publishers\n * in the room. Since they're not participants per se, they're basically\n * streams that can be (and typically are) associated to publisher handles\n * as the ones we introduced in the previous section, whether active or not.\n * In fact, the typical use case is publishers being notified about new\n * participants becoming active in the room, and as a result new subscriber\n * sessions being created to receive their media streams; as soon as the\n * publisher goes away, other participants are notified so that the related\n * subscriber handles can be removed/updated accordingly as well. As such,\n * these subscriber sessions are dependent on feedback obtained by\n * publishers, and can't exist on their own, unless you feed them the\n * right info out of band (which is impossible in rooms configured with\n * \\c require_pvtid).\n *\n * To specify that a handle will be associated with a subscriber, you must use\n * the \\c join request with \\c ptype set to \\c subscriber and specify which\n * feed to subscribe to. The exact syntax of the request is the following:\n *\n\\verbatim\n{\n\t\"request\" : \"join\",\n\t\"ptype\" : \"subscriber\",\n\t\"room\" : <unique ID of the room to subscribe in>,\n\t\"use_msid\" : <whether subscriptions should include an msid that references the publisher; false by default>,\n\t\"autoupdate\" : <whether a new SDP offer is sent automatically when a subscribed publisher leaves; true by default>,\n\t\"private_id\" : <unique ID of the publisher that originated this request; optional, unless mandated by the room configuration>,\n\t\"streams\" : [\n\t\t{\n\t\t\t\"feed\" : <unique ID of publisher owning the stream to subscribe to>,\n\t\t\t\"mid\" : \"<unique mid of the publisher stream to subscribe to; optional>\"\n\t\t\t\"crossrefid\" : \"<id to map this subscription with entries in streams list; optional>\"\n\t\t\t// Optionally, simulcast or SVC targets (defaults if missing)\n\t\t},\n\t\t// Other streams to subscribe to\n\t]\n}\n\\endverbatim\n *\n * As you can see, it's just a matter of specifying the list of streams to\n * subscribe to: in particular, you have to provide an array of objects,\n * where each objects represents a specific stream (or group of streams)\n * you're interested in. For each object, the \\c feed_id indicating the\n * publisher owning the stream(s) is mandatory, while the related \\c mid\n * is optional: this gives you some flexibility when subscribing, as\n * only providing a \\c feed_id will indicate you're interested in ALL\n * the stream from that publisher, while providing a \\c mid as well will\n * indicate you're interested in a stream in particular. Since you can\n * provide an array of streams, just specifying the \\c feed_id or explicitly\n * listing all the \\c feed_id + \\c mid combinations is equivalent: of\n * course, different objects in the array can indicate different publishers,\n * allowing you to combine streams from different sources in the same subscription.\n * Notice that if a publisher stream is marked as \\c disabled and you try\n * to subscribe to it, it will be skipped silently.\n *\n * Depending on whether the subscription will refer to a\n * single publisher (legacy approach) or to streams coming from different\n * publishers (multistream), the list of streams may differ. The ability\n * to single out the streams to subscribe to is particularly useful in\n * case you don't want to, or can't, subscribe to all available media:\n * e.g., you know a publisher is sending both audio and video, but video\n * is in a codec you don't support or you don't have bandwidth for both;\n * or maybe there are 10 participants in the room, but you only want video\n * from the 3 most active speakers; and so on. The content of the \\c streams\n * array will shape what the SDP offer the plugin will send will look like,\n * so that eventually a subscription for the specified streams will take place.\n * Notice that, while for backwards compatibility you can still use the\n * old \\c feed, \\c audio, \\c video, \\c data, \\c offer_audio, \\c offer_video and\n * \\c offer_data named properties, they're now deprecated and so you're\n * highly encouraged to use this new drill-down \\c streams list instead.\n *\n * As anticipated, if successful this request will generate a new JSEP SDP\n * offer, which will accompany an \\c attached event:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"attached\",\n\t\"room\" : <room ID>,\n\t\"streams\" : [\n\t\t{\n\t\t\t\"mindex\" : <unique m-index of this stream>,\n\t\t\t\"mid\" : \"<unique mid of this stream>\",\n\t\t\t\"type\" : \"<type of this stream's media (audio|video|data)>\",\n\t\t\t\"active\" : <true|false, whether this stream is currently active>,\n\t\t\t\"feed_id\" : <unique ID of the publisher originating this stream>,\n\t\t\t\"feed_mid\" : \"<unique mid of this publisher's stream>\",\n\t\t\t\"feed_display\" : \"<display name of this publisher, if any>\",\n\t\t\t\"send\" : <true|false; whether we configured the stream to relay media>,\n\t\t\t\"codec\" : \"<codec used by this stream>\",\n\t\t\t\"h264-profile\" : \"<in case H.264 is used by the stream, the negotiated profile>\",\n\t\t\t\"vp9-profile\" : \"<in case VP9 is used by the stream, the negotiated profile>\",\n\t\t\t\"ready\" : <true|false; whether this stream is ready to start sending media (will be false at the beginning)>,\n\t\t\t\"simulcast\" : { .. optional object containing simulcast info, if simulcast is used by this stream .. },\n\t\t\t\"svc\" : { .. optional object containing SVC info, if SVC is used by this stream .. },\n\t\t\t\"playout-delay\" : { .. optional object containing info on the playout-delay extension configuration, if in use .. },\n\t\t\t\"sources\" : <if this is a data channel stream, the number of data channel subscriptions>,\n\t\t\t\"source_ids\" : [ .. if this is a data channel stream, an array containing the IDs of participants we've subscribed to .. ],\n\t\t},\n\t\t// Other streams in the subscription, if any\n\t]\n}\n\\endverbatim\n *\n * As you can see, a summary of the streams we subscribed to will be sent back,\n * which will be useful on the client side for both mapping and rendering purposes.\n *\n * At this stage, to complete the setup of the PeerConnection the subscriber is\n * supposed to send a JSEP SDP answer back to the plugin. This is done\n * by means of a \\c start request, which in this case MUST be associated\n * with a JSEP SDP answer but otherwise requires no arguments:\n *\n\\verbatim\n{\n\t\"request\" : \"start\"\n}\n\\endverbatim\n *\n * If successful this request returns a \\c started event:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"started\" : \"ok\"\n}\n\\endverbatim\n *\n * Once this is done, all that's needed is waiting for the WebRTC PeerConnection\n * establishment to succeed. As soon as that happens, the VideoRoom plugin\n * can start relaying media the recipient subscribed to.\n *\n * Once a WebRTC PeerConnection has been established for a subscriber, in\n * case you want to update a subscription you have to use the \\c subscribe ,\n * \\c unsubscribe or \\c update methods: as the names of the requests suggest, the\n * former allows you to add more streams to subscribe to, the second\n * instructs the plugin to remove streams you're currently subscribe to,\n * while the latter allows you to perform both operations at the same time.\n * Any of those requests will trigger a renegotiation, if they were successful,\n * meaning the plugin will send you a new JSEP offer you'll have to reply\n * to with an answer: to send the answer, just use the same \\c start request\n * we already described above. Notice that renegotiations may not be\n * triggered right away, e.g., whenever you're trying to update a session\n * and the plugin is still in the process of renegoting a previous update\n * for the same subscription: in that case, an update will be scheduled\n * and a renegotiation will be triggered as soon as it's viable, and an\n * empty \\c updating event will be triggered instead to notify the caller\n * that the management of that request has been postponed. It's also\n * important to point out that the number of offers generated in response\n * to those requests may not match the amount of requests: in fact, since\n * requests are postponed, a single offer may be sent in response to\n * multiple requests to update a subscription at the same time, thus\n * addressing them all in a cumulative way. This means clients should\n * never expect an offer any time they request one.\n *\n * The syntax of the \\c subscribe mirrors the one for new subscriptions,\n * meaning you use the same \\c streams array to address the new streams\n * you want to receive, and formatted the same way:\n *\n\\verbatim\n{\n\t\"request\" : \"subscribe\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"feed\" : <unique ID of publisher owning the new stream to subscribe to>,\n\t\t\t\"mid\" : \"<unique mid of the publisher stream to subscribe to; optional>\"\n\t\t\t\"crossrefid\" : \"<id to map this subscription with entries in streams list; optional>\"\n\t\t\t// Optionally, send, simulcast or SVC targets (defaults if missing)\n\t\t},\n\t\t// Other new streams to subscribe to\n\t]\n}\n\\endverbatim\n *\n * This means the exact same considerations we made on \\c streams before\n * apply here as well: whatever they represent, will indicate the willingness\n * to subscribe to the related stream. Notice that if you were already\n * subscribed to one of the new streams indicated here, you'll subscribe\n * to it again in a different m-line, so it's up to you to ensure you\n * avoid duplicates (unless that's what you wanted, e.g., for testing\n * purposes). In case the update was successful, you'll get an \\c updated\n * event, containing the updated layout of all subscriptions (pre-existing\n * and new ones), and a new JSEP offer to renegotiate the session:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"updated\",\n\t\"room\" : <room ID>,\n\t\"streams\": [\n\t\t{\n\t\t\t\"mindex\" : <unique m-index of this stream>,\n\t\t\t\"mid\" : \"<unique mid of this stream>\",\n\t\t\t\"type\" : \"<type of this stream's media (audio|video|data)>\",\n\t\t\t\"feed_id\" : <unique ID of the publisher originating this stream>,\n\t\t\t\"feed_mid\" : \"<unique mid of this publisher's stream>\",\n\t\t\t\"feed_display\" : \"<display name of this publisher, if any>\",\n\t\t\t\"send\" : <true|false; whether we configured the stream to relay media>,\n\t\t\t\"ready\" : <true|false; whether this stream is ready to start sending media (will be false at the beginning)>\n\t\t},\n\t\t// Other streams in the subscription, if any; old and new\n\t]\n}\n\\endverbatim\n *\n * Notice that if your \\c subscribe request didn't change anything as far\n * as the SDP negotiation is concerned (e.g., subscribing to new data streams\n * where a datachannel existed already), you'll simply get an \\c updated\n * event back with no \\c streams object.\n *\n * As explained before, in case the message contains a JSEP offer (which may\n * not be the case if no change occurred), then clients will need to send\n * a new JSEP answer with a \\c start request to close this renegotiation.\n *\n * The \\c unsubscribe request works pretty much the same way, with the\n * difference that the \\c streams array you provide to specify what to\n * unsubscribe from may look different. Specifically, the syntax looks\n * like this:\n *\n\\verbatim\n{\n\t\"request\" : \"unsubscribe\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"feed\" : <unique ID of publisher owning the new stream to unsubscribe from; optional>,\n\t\t\t\"mid\" : \"<unique mid of the publisher stream to unsubscribe from; optional>\"\n\t\t\t\"sub_mid\" : \"<unique mid of the subscriber stream to unsubscribe; optional>\"\n\t\t},\n\t\t// Other streams to unsubscribe from\n\t]\n}\n\\endverbatim\n *\n * This means that you have different ways to specify what to unsubscribe from:\n * if an object only specifies \\c feed_id, then all the subscription streams that\n * were receiving media from that publisher will be removed; if an object\n * specifies \\c feed_id and \\c mid, then all the subscription streams that\n * were receiving media from the publisher stream with the related mid will be\n * removed; finally, if an object only specifies \\c sub_mid instead, then\n * only the stream in the subscription that is addressed by the related mid\n * (subscription mid, no relation to the publishers') will be removed. As\n * such, you have a great deal of flexibility in how to unsubscribe from\n * media. Notice that multiple streams may be removed in case you refer\n * to the \"source\" ( \\c feed_id ), rather than the \"sink\" ( \\c sub_mid ),\n * especially in case the subscription contained duplicates or multiple\n * streams from the same publisher.\n *\n * A successful \\c unsubscribe will result in exactly the same \\c updated\n * event \\c subscribe triggers, so the same considerations apply with\n * respect to the potential need of a renegotiation and how to complete\n * it with a \\c start along a JSEP answer. Again, if \\c unsubscribe didn't\n * result in SDP changes (e.g., unsubscribing from a data channel stream),\n * you'll simply get an \\c updated event back with no \\c streams object.\n *\n * As anticipated, the \\c update request allows you to combine changes\n * to a subscription where you may want to both subscribe to new streams,\n * and unsubscribe from existing ones, which the existing \\c subscribe\n * and \\c unsubscribe requests wouldn't allow you to do as they work\n * exclusively on the action specified by their name. The syntax for\n * the \\c update request is very similar to the previous method, meaning\n * arrays are still used to address the streams to work on, with the key\n * difference that they won't be named \\c streams, but \\c subscribe and\n * \\c unsubscribe instead:\n *\n\\verbatim\n{\n\t\"request\" : \"update\",\n\t\"subscribe\" : [\n\t\t{\n\t\t\t\"feed\" : <unique ID of publisher owning the new stream to subscribe to>,\n\t\t\t\"mid\" : \"<unique mid of the publisher stream to subscribe to; optional>\"\n\t\t\t\"crossrefid\" : \"<id to map this subscription with entries in streams list; optional>\"\n\t\t\t// Optionally, send, simulcast or SVC targets (defaults if missing)\n\t\t},\n\t\t// Other new streams to subscribe to\n\t],\n\t\"unsubscribe\" : [\n\t\t{\n\t\t\t\"feed\" : <unique ID of publisher owning the new stream to unsubscribe from; optional>,\n\t\t\t\"mid\" : \"<unique mid of the publisher stream to unsubscribe from; optional>\"\n\t\t\t\"sub_mid\" : \"<unique mid of the subscriber stream to unsubscribe; optional>\"\n\t\t},\n\t\t// Other streams to unsubscribe from\n\t]\n}\n\\endverbatim\n *\n * Both the \\c subscribe and \\c unsubscribe arrays are optional, which means\n * that an \\c update request to only subscribe to new streams will be\n * functionally equivalent to a \\c subscribe request, and an \\c update\n * request to only unsubscribe will be functionally equivalent to an\n * \\c unsubscribe request instead. That said, one of the two must be\n * provided, which means that an \\c update request that doesn't include\n * either of them will result in an error.\n *\n * A successful \\c update will result in exactly the same \\c updated event\n * \\c subscribe and \\c unsubscribe trigger, so the same considerations apply\n * with respect to the potential need of a renegotiation and how to complete\n * it with a \\c start along a JSEP answer. Again, if \\c update didn't\n * result in SDP changes, you'll simply get an \\c updated event back with\n * no \\c streams object.\n *\n * Notice that, in case you want to trigger an ICE restart rather than\n * updating a subscription, you'll have to use a different request, named\n * \\c configure: this will be explained in a few paragraphs.\n *\n * As a subscriber, you can temporarily pause and resume the whole media delivery\n * with a \\c pause and, again, \\c start request (in this case without any JSEP\n * SDP answer attached). Neither expect other arguments, as the context\n * is implicitly derived from the handle they're sent on:\n *\n\\verbatim\n{\n\t\"request\" : \"pause\"\n}\n\\endverbatim\n *\n\\verbatim\n{\n\t\"request\" : \"start\"\n}\n\\endverbatim\n *\n * Unsurprisingly, they just result in, respectively, \\c paused and\n * \\c started events:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"paused\" : \"ok\"\n}\n\\endverbatim\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"started\" : \"ok\"\n}\n\\endverbatim\n *\n * For more drill-down manipulations of a subscription, a \\c configure\n * request can be used instead. This request allows subscribers to dynamically\n * change some properties associated to their media subscription, e.g.,\n * in terms of what should and should not be sent at a specific time. A\n * \\c configure request must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"configure\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"mid\" : <mid of the m-line to refer to>,\n\t\t\t\"send\" : <true|false, depending on whether the mindex media should be relayed or not; optional>,\n\t\t\t\"substream\" : <substream to receive (0-2), in case simulcasting is enabled; optional>,\n\t\t\t\"temporal\" : <temporal layers to receive (0-2), in case simulcasting is enabled; optional>,\n\t\t\t\"fallback\" : <How much time (in us, default 250000) without receiving packets will make us drop to the substream below; optional>,\n\t\t\t\"spatial_layer\" : <spatial layer to receive (0-2), in case SVC is enabled; optional>,\n\t\t\t\"temporal_layer\" : <temporal layers to receive (0-2), in case SVC is enabled; optional>,\n\t\t\t\"audio_level_average\" : \"<if provided, overrides the room audio_level_average for this user; optional>\",\n\t\t\t\"audio_active_packets\" : \"<if provided, overrides the room audio_active_packets for this user; optional>\",\n\t\t\t\"min_delay\" : <minimum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>,\n\t\t\t\"max_delay\" : <maximum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>,\n\t\t},\n\t\t// Other streams, if any\n\t],\n\t\"restart\" : <trigger an ICE restart; optional>\n}\n\\endverbatim\n *\n * As you can see, the \\c mid and \\c send properties can be used as a media-level\n * pause/resume functionality (\"only mute/unmute this mid\"), whereas \\c pause\n * and \\c start simply pause and resume all streams at the same time.\n * The \\c substream and \\c temporal properties, instead, only make sense\n * when the publisher is configured with video simulcasting support, and\n * as such the subscriber is interested in receiving a specific substream\n * or temporal layer, rather than any other of the available ones: notice\n * that for them to work you'll have to specify the \\c mid as well, as the same\n * subscription may be receiving simulcast stream from multiple publishers.\n * The \\c spatial_layer and \\c temporal_layer have exactly the same meaning,\n * but within the context of SVC publishers, and will have no effect\n * on subscriptions associated to regular publishers.\n *\n * As anticipated, \\c configure is also the request you use when you want\n * to trigger an ICE restart for a subscriber: in fact, while publishers\n * can force a restart themselves by providing the right JSEP offer, subscribers\n * always receive an offer from Janus instead, and as such have to\n * explicitly ask for a dedicated offer when an ICE restart is needed;\n * in that case, just set \\c restart to \\c true in a \\c configure request,\n * and a new JSEP offer with ICE restart information will be sent to the\n * client, to which the client will have to reply, as usual, via \\c start\n * along a JSEP answer. This documentation doesn't explain when or why\n * an ICE restart is needed or appropriate: please refer to the ICE RFC\n * or other sources of information for that.\n *\n * Another interesting feature that subscribers can take advantage of is the\n * so-called publisher \"switching\". Basically, when subscribed to one or more\n * publishers and receiving media from them, you can at any time \"switch\"\n * any of the subscription streams to a different publisher, and as such\n * start receiving media on the related m-line from that publisher instead,\n * all without doing a new \\c subscribe or \\c unsubscribe, and so without\n * the need of doing any renegotiation at all; just some logic changes.\n * Think of it as changing channel on a TV: you keep on using the same\n * PeerConnection, the plugin simply changes the source of the media\n * transparently. Of course, while powerful and effective this request has\n * some limitations: in fact, the source (audio or video) that you switch\n * to must have the same media configuration (e.g., same codec) as the source\n * you're replacing. In fact, since the same PeerConnection is used for this\n * feature and no renegotiation is taking place, switching to a stream with\n * a different configuration would result in media incompatible with the\n * PeerConnection setup being relayed to the subscriber (e.g., negotiated\n * VP9, but new source is H.264), and as such in no audio/video being played;\n * in that case, you'll need a \\c subscribe instead, and a new m-line.\n *\n * That said, a \\c switch request must be formatted like this:\n *\n\\verbatim\n{\n\t\"request\" : \"switch\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"feed\" : <unique ID of the publisher the new source is from>,\n\t\t\t\"mid\" : \"<unique mid of the source we want to switch to>\",\n\t\t\t\"sub_mid\" : \"<unique mid of the stream we want to pipe the new source to>\"\n\t\t\t.. other properties, e.g., substream, temporal, etc.\n\t\t},\n\t\t{\n\t\t\t// Other updates, if any\n\t\t}\n\t]\n}\n\\endverbatim\n *\n * While apparently convoluted, this is actually a quite effective and powerful\n * way of updating subscriptions without renegotiating. In fact, it allows for\n * full or partial switches: for instance, sometimes you may want to replace all\n * audio and video streams (e.g., switching from Bob to Alice in a \"legacy\"\n * VideoRoom usage, where each PeerConnection subscription is a different\n * publisher), or just replace a subset of them (e.g., you have a subscription\n * with three video slots, and you change one of them depending on the loudest\n * speaker). What to replace is dictated by the \\c streams array, where each\n * object in the array contains all the info needed for the switch to take\n * place: in particular, you must specify which of your subscription m-lines\n * you're going to update, via \\c sub_mid , and which publisher stream should\n * now start to feed it via \\c feed and \\c mid.\n *\n * If successful, the specified subscriptions will be updated, meaning they'll\n * be unsubscribed from the previous publisher stream, and subscribed to the\n * new publisher stream instead, all without a renegotiation (so no new SDP\n * offer/answer exchange to take care of). The event to confirm the switch\n * was successful will look like this:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"switched\" : \"ok\",\n\t\"room\" : <room ID>,\n\t\"changes\" : <number of successful changes (may be smaller than the size of the streams array provided in the request)>,\n\t\"streams\" : [\n\t\t// Current configuration of the subscription, same format as when subscribing\n\t\t// Will contain info on all streams, not only those that have been updated\n\t]\n}\n\\endverbatim\n *\n * Notice that, while a \\c switch request usually doesn't require a renegotiation,\n * it \\b MIGHT trigger one nevertheless: in fact, if a \"switch\" request assigns\n * a new publisher stream to a previously inactive subscriber stream, then\n * a renegotiation to re-activate that stream will be needed as well, as\n * otherwise the packets from the new source will not be relayed.\n *\n * Finally, to close a subscription and tear down the related PeerConnection,\n * you can use the \\c leave request. Since context is implicit, no other\n * argument is required:\n *\n\\verbatim\n{\n\t\"request\" : \"leave\"\n}\n\\endverbatim\n *\n * If successful, the plugin will attempt to tear down the PeerConnection,\n * and will send back a \\c left event:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"event\",\n\t\"left\" : \"ok\",\n}\n\\endverbatim\n *\n * \\subsection vroomcasc Remote publishers (room cascading)\n *\n * Normally, the VideoRoom plugin can only route streams associated to\n * users connected to the Janus instance the plugin lives in: this means\n * that, within the context of a room, you can only subscribe to publishers\n * connected to the same server (and room) you're on.\n *\n * That said, there are obviously ways to address this constraint. In\n * the past, a typical approach for handling this (e.g., for scalability\n * or geo-distribution purposes) was to use the \\c rtp_forward request\n * to feed one or more local/remote Streaming plugin mountpoints, so that\n * a VideoRoom publisher could be consumed using the Streaming plugin\n * instead, possibly on a completely different Janus instance. This works\n * and has been used extensively (by ourselves too), but has the downside\n * that this completely excludes the VideoRoom API in terms of presence\n * and subscriptions: it's up to you, for instance, to advertise these\n * redistributed streams somehow, and associate them to the original\n * publisher from a semantics perspective.\n *\n * That said, the VideoRoom plugin now also has a concept of remote\n * publishers, that allows you to remotize local VideoRoom publishers\n * to different VideoRoom instances, which can in turn advertise the\n * presence of these remote subscribers along with their local publishers.\n * This allows subscribers to use the VideoRoom API, transparently, to\n * subscribe to both local and remote publishers seamlessly, knowing\n * that the involved VideoRoom instances will exchange the media packets\n * among them to make it happen.\n *\n * It's important to point out that this is not something that's completely\n * automated: it's still up to you, via API calls, to instruct all involved\n * VideoRoom instances, so that the remotization can happen, and to keep\n * it up do that (e.g., after renegotiations occur).\n *\n * Specifically, the VideoRoom API exposes the \\c add_remote_publisher ,\n * \\c update_remote_publisher , \\c remove_remote_publisher ,\n * \\c publish_remotely , \\c unpublish_remotely and \\c list_remotes\n * requests.\n *\n * Assuming that \\b Janus \\b A wants to make one of its local publishers available\n * in a room on \\b Janus \\b B as well, this is the process you must follow:\n *\n *   - you use \\c add_remote_publisher on \\b Janus \\b B (the target instance)\n * to add a new remote publisher; this will return some connectivity info\n * to the caller, and immediately advertise the new publisher to other\n * attendees in \\b Janus \\b B even before media actually arrives;\n *   - you use \\c publish_remotely on \\b Janus \\b A (the source instance),\n * using the info returned from the previous call; this has the result\n * of instructing \\b Janus \\b A to start relaying all RTP packets associated\n * to that publisher to \\b Janus \\b B ;\n *   - any time the publisher on \\b Janus \\b A renegotiates their session (e.g.,\n * a new audio or video stream is added, or removed), you should use\n * \\c update_remote_publisher on \\b Janus \\b B so that the remote instance\n * is aware of the changes, and can notify people in the room accordingly\n * (e.g., so that they can update their subscriptions accordingly);\n *   - when the publisher on \\b Janus \\b A leaves, an \\c unpublish_remotely\n * request must be sent on \\b Janus \\b A to ensure no media is forwarded anymore,\n * and at the same time a \\c remove_remote_publisher must be sent to\n * \\b Janus \\b B so that other attendees can be notified the participant\n * has left.\n *\n * Using these requests, the two Janus instances will transparently and\n * automatically communicate using internally created RTP forwarders. The\n * same ports are used for all RTP packets, so multiplexing is performed\n * using a simple math on SSRC identifiers: this means that there's no need\n * to open new ports as a consequence of renegotiations of a publisher,\n * but only to notify the recipient about what media is on its way, and\n * demultiplexing will be performed automatically.\n *\n * Everything else (subscribing to, and unsubscribing from, remote publishers)\n * works exactly the same way as shown in the previous sections. As far as\n * local attendees are concerned, a remote publisher is advertised and looks\n * exactly like any other local publisher. The details about how the\n * remotization works behind the scenes is hidden from them, and not\n * relevant to the subscription process.\n *\n * Coming to how the requests need to be formatted, the \\c add_remote_publisher\n * must be formatted like the following:\n *\n\\verbatim\n{\n\t\"request\" : \"add_remote_publisher\",\n\t\"room\" : <unique ID of the room to add the remote publisher to>,\n\t\"id\" : <unique ID to register for the remote publisher; optional, will be chosen by the plugin if missing; doesn't need to be the same as the source one>,\n\t\"secret\" : \"<password required to edit the room, mandatory if configured in the room>\",\n\t\"display\" : \"<display name for the remote publisher; optional>\",\n\t\"mcast\" : \"<multicast group port for receiving RTP packets, if any>\",\n\t\"iface\" : \"<network interface or IP address to bind to, if any (binds to all otherwise)>\",\n\t\"port\" : <local port for receiving all RTP packets; 0 will bind to a random one (default)>,\n\t\"srtp_suite\" : <length of authentication tag (32 or 80); optional>,\n\t\"srtp_crypto\" : \"<key to use as crypto (base64 encoded key as in SDES); optional>\",\n\t\"streams\" : [\n\t\t{\n\t\t\t\"type\" : \"<type of published stream #1 (audio|video|data)\">,\n\t\t\t\"mindex\" : \"<unique mindex of published stream #1>\",\n\t\t\t\"mid\" : \"<unique mid of of published stream #1>\",\n\t\t\t\"disabled\" : <if true, it means this stream is currently inactive/disabled (and so codec, description, etc. will be missing)>,\n\t\t\t\"codec\" : \"<codec used for published stream #1>\",\n\t\t\t\"description\" : \"<text description of published stream #1, if any>\",\n\t\t\t\"disabled\" : <true if published stream #1 is currently disabled>,\n\t\t\t\"stereo\" : <true if published stream #1 is audio and stereo>,\n\t\t\t\"fec\" : <true if published stream #1 is audio and uses FEC>,\n\t\t\t\"dtx\" : <true if published stream #1 is audio and uses DTX>,\n\t\t\t\"h264-profile\" : \"<in case H.264 is used by the stream, the negotiated profile>\",\n\t\t\t\"vp9-profile\" : \"<in case VP9 is used by the stream, the negotiated profile>\",\n\t\t\t\"simulcast\" : <true if published stream #1 is video and uses simulcast>,\n\t\t\t\"svc\" : <true if published stream #1 is video and uses SVC (VP9 and AV1 only)>,\n\t\t\t\"audiolevel_ext_id\" : <in case the audio level extension is used by this stream, its ID>,\n\t\t\t\"videoorient_ext_id\" : <in case the video orientation extension is used by this stream, its ID>,\n\t\t\t\"playoutdelay_ext_id\" : <in case the playout delay extension is used by this stream, its ID>\n\t\t},\n\t\t// Other streams, if any\n\t]\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n\t\"room\" : <same as request>,\n\t\"id\" : <unique ID associated to the new remote publisher>,\n\t\"ip\" : \"<host address to use to send RTP associated to this remote publisher>\",\n\t\"port\" : <port to use to send RTP associated to this remote publisher>,\n\t\"rtcp_port\" : <port to latch to in order to receive RTCP feedback from this remote publisher>\n}\n\\endverbatim\n *\n * To update a previously created remote publisher, the \\c update_remote_publisher\n * request is used, which must be formatted like the following:\n *\n\\verbatim\n{\n\t\"request\" : \"update_remote_publisher\",\n\t\"room\" : <unique ID of the room the remote publisher is in>,\n\t\"id\" : <unique ID of the remote publisher>,\n\t\"secret\" : \"<password required to edit the room, mandatory if configured in the room>\",\n\t\"display\" : \"<new display name for the remote publisher; optional>\",\n\t\"metadata\" : <new valid json object of metadata; optional>,\n\t\"srtp_suite\" : <length of authentication tag (32 or 80); optional>,\n\t\"srtp_crypto\" : \"<key to use as crypto (base64 encoded key as in SDES); optional>\",\n\t\"streams\" : [\n\t\t{\n\t\t\t// Same syntax as add_remote_publisher: only needs to\n\t\t\t// reference new or modified streams, not all of them\n\t\t},\n\t\t// Other streams, if any\n\t]\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\"\n}\n\\endverbatim\n *\n * To remove a previously created remote publisher, the \\c remove_remote_publisher\n * request is used, which must be formatted like the following:\n *\n\\verbatim\n{\n\t\"request\" : \"remove_remote_publisher\",\n\t\"room\" : <unique ID of the room the remote publisher is in>,\n\t\"id\" : <unique ID of the remote publisher>,\n\t\"secret\" : \"<password required to edit the room, mandatory if configured in the room>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\"\n}\n\\endverbatim\n *\n * Other attendees in the same room as the remote publishers will be\n * notified accordingly, exactly as it happens when a local publisher\n * goes aeay or close their PeerConnection.\n *\n * For what concerns the source instance (from where the publisher is\n * remotized to a different VideoRoom instance), the \\c publish_remotely\n * request is used, which must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"publish_remotely\",\n\t\"room\" : <unique ID of the room the local publisher to remotize is in>,\n\t\"publisher_id\" : <unique ID of the local publisher to remotize>,\n\t\"remote_id\" : \"<unique ID to associate to this remotization; this has nothing to do with the ID the publisher will have in the remote instance, and is only used to address this specific remotization on the source instance>\",\n\t\"secret\" : \"<password required to edit the room, mandatory if configured in the room>\",\n\t\"host\" : \"<host address to forward the RTP and data packets to>\",\n\t\"host_family\" : \"<ipv4|ipv6, if we need to resolve the host address to an IP; by default, whatever we get>\",\n\t\"port\" : <port to forward the packets to>,\n\t\"rtcp_port\" : <port to contact to receive RTCP feedback from the recipient; optional, and only for RTP streams, not data>,\n\t\"srtp_suite\" : <length of authentication tag (32 or 80); optional>,\n\t\"srtp_crypto\" : \"<key to use as crypto (base64 encoded key as in SDES); optional>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n\t\"room\" : <same as request>,\n\t\"id\" : <unique ID of the local publisher>,\n\t\"remote_id\" : \"<unique ID of this remotization (needed for unpublish_remotely)>\"\n}\n\\endverbatim\n *\n * Notice that, as explained before, \\c publish_remotely expects a remote publisher\n * ready to receive their media, which is why \\c add_remote_publisher must\n * be sent on the target Janus instance first: the info returned by that\n * request (IP and ports) are what you then feed to \\c publish_remotely .\n *\n * The \\c publish_remotely request can be used multiple times for the same\n * local publisher, e.g., to make the same publisher available on more than\n * one remote Janus/VideoRoom instance. This is why \\c remote_id is needed\n * to be able to individually address each specific remotization, in case\n * you want to, e.g., stop making a specific publisher available on a\n * specific Janus instance, but keep it available on others.\n *\n * To disable a specific remotization of a local publisher, the \\c unpublish_remotely\n * request is used, which must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"unpublish_remotely\",\n\t\"room\" : <unique ID of the room the local publisher is in>,\n\t\"publisher_id\" : <unique ID of the local publisher>,\n\t\"remote_id\" : \"<unique ID to associate to this remotization of the local publisher>\",\n\t\"secret\" : \"<password required to edit the room, mandatory if configured in the room>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n\t\"room\" : <same as request>,\n\t\"id\" : <unique ID of the local publisher>\n}\n\\endverbatim\n *\n * Notice that removing a remotization from the source instance only stops\n * the delivery of RTP packets to the target of the remotization: it does\n * \\b NOT also remove the remote publisher from the remote instance. It's\n * up to you to notify the target instance with \\c remove_remote_publisher .\n *\n * You can list all the remotizations for a local publisher using\n * \\c list_remotes, which must be formatted as follows:\n *\n\\verbatim\n{\n\t\"request\" : \"list_remotes\",\n\t\"room\" : <unique ID of the room the local publisher is in>,\n\t\"publisher_id\" : <unique ID of the local publisher>,\n\t\"secret\" : \"<password required to edit the room, mandatory if configured in the room>\"\n}\n\\endverbatim\n *\n * A successful request will result in a \\c success response:\n *\n\\verbatim\n{\n\t\"videoroom\" : \"success\",\n\t\"room\" : <same as request>,\n\t\"id\" : <unique ID of the local publisher>,\n\t\"list\" : [\n\t\t{\n\t\t\t\"remote_id\" : \"<unique ID of this remotization of this local publisher\">,\n\t\t\t\"host\" : \"<address all RTP packets are being sent to\">,\n\t\t\t\"port\" : \"port all RTP packets are being sent to>\n\t\t\t\"rtcp_port\" : \"RTCP port, if enabled>\n\t\t},\n\t\t// Other remotizations, if any\n\t]\n}\n\\endverbatim\n *\n *\n */\n\n#include \"plugin.h\"\n\n#include <jansson.h>\n#include <netdb.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../rtp.h\"\n#include \"../rtpsrtp.h\"\n#include \"../rtcp.h\"\n#include \"../rtpfwd.h\"\n#include \"../record.h\"\n#include \"../sdp-utils.h\"\n#include \"../utils.h\"\n#include \"../ip-utils.h\"\n#include <sys/types.h>\n#include <sys/socket.h>\n#include <poll.h>\n\n\n/* Plugin information */\n#define JANUS_VIDEOROOM_VERSION\t\t\t10\n#define JANUS_VIDEOROOM_VERSION_STRING\t\"0.0.10\"\n#define JANUS_VIDEOROOM_DESCRIPTION\t\t\"This is a plugin implementing a videoconferencing SFU (Selective Forwarding Unit) for Janus, that is an audio/video router.\"\n#define JANUS_VIDEOROOM_NAME\t\t\t\"JANUS VideoRoom plugin\"\n#define JANUS_VIDEOROOM_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_VIDEOROOM_PACKAGE\t\t\t\"janus.plugin.videoroom\"\n\n/* Plugin methods */\njanus_plugin *create(void);\nint janus_videoroom_init(janus_callbacks *callback, const char *config_path);\nvoid janus_videoroom_destroy(void);\nint janus_videoroom_get_api_compatibility(void);\nint janus_videoroom_get_version(void);\nconst char *janus_videoroom_get_version_string(void);\nconst char *janus_videoroom_get_description(void);\nconst char *janus_videoroom_get_name(void);\nconst char *janus_videoroom_get_author(void);\nconst char *janus_videoroom_get_package(void);\nvoid janus_videoroom_create_session(janus_plugin_session *handle, int *error);\nstruct janus_plugin_result *janus_videoroom_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\njson_t *janus_videoroom_handle_admin_message(json_t *message);\nvoid janus_videoroom_setup_media(janus_plugin_session *handle);\nvoid janus_videoroom_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *packet);\nvoid janus_videoroom_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet);\nvoid janus_videoroom_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet);\nvoid janus_videoroom_data_ready(janus_plugin_session *handle);\nvoid janus_videoroom_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink);\nvoid janus_videoroom_hangup_media(janus_plugin_session *handle);\nvoid janus_videoroom_destroy_session(janus_plugin_session *handle, int *error);\njson_t *janus_videoroom_query_session(janus_plugin_session *handle);\n\n/* Plugin setup */\nstatic janus_plugin janus_videoroom_plugin =\n\tJANUS_PLUGIN_INIT (\n\t\t.init = janus_videoroom_init,\n\t\t.destroy = janus_videoroom_destroy,\n\n\t\t.get_api_compatibility = janus_videoroom_get_api_compatibility,\n\t\t.get_version = janus_videoroom_get_version,\n\t\t.get_version_string = janus_videoroom_get_version_string,\n\t\t.get_description = janus_videoroom_get_description,\n\t\t.get_name = janus_videoroom_get_name,\n\t\t.get_author = janus_videoroom_get_author,\n\t\t.get_package = janus_videoroom_get_package,\n\n\t\t.create_session = janus_videoroom_create_session,\n\t\t.handle_message = janus_videoroom_handle_message,\n\t\t.handle_admin_message = janus_videoroom_handle_admin_message,\n\t\t.setup_media = janus_videoroom_setup_media,\n\t\t.incoming_rtp = janus_videoroom_incoming_rtp,\n\t\t.incoming_rtcp = janus_videoroom_incoming_rtcp,\n\t\t.incoming_data = janus_videoroom_incoming_data,\n\t\t.data_ready = janus_videoroom_data_ready,\n\t\t.slow_link = janus_videoroom_slow_link,\n\t\t.hangup_media = janus_videoroom_hangup_media,\n\t\t.destroy_session = janus_videoroom_destroy_session,\n\t\t.query_session = janus_videoroom_query_session,\n\t);\n\n/* Plugin creator */\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_VIDEOROOM_NAME);\n\treturn &janus_videoroom_plugin;\n}\n\n/* Parameter validation */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter adminkey_parameters[] = {\n\t{\"admin_key\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter create_parameters[] = {\n\t{\"description\", JSON_STRING, 0},\n\t{\"is_private\", JANUS_JSON_BOOL, 0},\n\t{\"allowed\", JSON_ARRAY, 0},\n\t{\"secret\", JSON_STRING, 0},\n\t{\"pin\", JSON_STRING, 0},\n\t{\"require_pvtid\", JANUS_JSON_BOOL, 0},\n\t{\"signed_tokens\", JANUS_JSON_BOOL, 0},\n\t{\"bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"bitrate_cap\", JANUS_JSON_BOOL, 0},\n\t{\"fir_freq\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"publishers\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"videocodec\", JSON_STRING, 0},\n\t{\"vp9_profile\", JSON_STRING, 0},\n\t{\"h264_profile\", JSON_STRING, 0},\n\t{\"opus_fec\", JANUS_JSON_BOOL, 0},\n\t{\"opus_dtx\", JANUS_JSON_BOOL, 0},\n\t{\"audiolevel_ext\", JANUS_JSON_BOOL, 0},\n\t{\"audiolevel_event\", JANUS_JSON_BOOL, 0},\n\t{\"audio_active_packets\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_level_average\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"videoorient_ext\", JANUS_JSON_BOOL, 0},\n\t{\"playoutdelay_ext\", JANUS_JSON_BOOL, 0},\n\t{\"transport_wide_cc_ext\", JANUS_JSON_BOOL, 0},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"rec_dir\", JSON_STRING, 0},\n\t{\"lock_record\", JANUS_JSON_BOOL, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0},\n\t{\"notify_joining\", JANUS_JSON_BOOL, 0},\n\t{\"require_e2ee\", JANUS_JSON_BOOL, 0},\n\t{\"dummy_publisher\", JANUS_JSON_BOOL, 0},\n\t{\"dummy_streams\", JANUS_JSON_ARRAY, 0},\n\t{\"dummy_e2ee\", JANUS_JSON_BOOL, 0},\n\t{\"threads\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n};\nstatic struct janus_json_parameter edit_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"new_description\", JSON_STRING, 0},\n\t{\"new_is_private\", JANUS_JSON_BOOL, 0},\n\t{\"new_secret\", JSON_STRING, 0},\n\t{\"new_pin\", JSON_STRING, 0},\n\t{\"new_require_pvtid\", JANUS_JSON_BOOL, 0},\n\t{\"new_bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"new_fir_freq\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"new_publishers\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"new_lock_record\", JANUS_JSON_BOOL, 0},\n\t{\"new_rec_dir\", JSON_STRING, 0},\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter room_parameters[] = {\n\t{\"room\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter roomopt_parameters[] = {\n\t{\"room\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter roomstr_parameters[] = {\n\t{\"room\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter roomstropt_parameters[] = {\n\t{\"room\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter id_parameters[] = {\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter idopt_parameters[] = {\n\t{\"id\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter idstr_parameters[] = {\n\t{\"id\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter idstropt_parameters[] = {\n\t{\"id\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter pid_parameters[] = {\n\t{\"publisher_id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter pidstr_parameters[] = {\n\t{\"publisher_id\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter feed_parameters[] = {\n\t{\"feed\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter feedstr_parameters[] = {\n\t{\"feed\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter feedopt_parameters[] = {\n\t{\"feed\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter feedstropt_parameters[] = {\n\t{\"feed\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter destroy_parameters[] = {\n\t{\"permanent\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter allowed_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"action\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"allowed\", JSON_ARRAY, 0}\n};\nstatic struct janus_json_parameter kick_parameters[] = {\n\t{\"secret\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter moderate_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"mid\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"mute\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter join_parameters[] = {\n\t{\"ptype\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"data\", JANUS_JSON_BOOL, 0},\n\t{\"bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"token\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter publish_parameters[] = {\n\t{\"descriptions\", JANUS_JSON_ARRAY, 0},\n\t{\"audiocodec\", JSON_STRING, 0},\n\t{\"videocodec\", JSON_STRING, 0},\n\t{\"bitrate\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"keyframe\", JANUS_JSON_BOOL, 0},\n\t{\"record\", JANUS_JSON_BOOL, 0},\n\t{\"filename\", JSON_STRING, 0},\n\t{\"display\", JSON_STRING, 0},\n\t{\"metadata\", JSON_OBJECT, 0},\n\t{\"secret\", JSON_STRING, 0},\n\t{\"audio_level_averge\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_active_packets\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* Deprecated, use mid+send instead */\n\t{\"audio\", JANUS_JSON_BOOL, 0},\t/* Deprecated! */\n\t{\"video\", JANUS_JSON_BOOL, 0},\t/* Deprecated! */\n\t{\"data\", JANUS_JSON_BOOL, 0},\t/* Deprecated! */\n\t/* The following are just to force a renegotiation and/or an ICE restart */\n\t{\"update\", JANUS_JSON_BOOL, 0},\n\t{\"restart\", JANUS_JSON_BOOL, 0}\n};\nstatic struct janus_json_parameter publish_stream_parameters[] = {\n\t{\"mid\", JANUS_JSON_STRING, 0},\n\t{\"send\", JANUS_JSON_BOOL, 0},\n\t/* For the playout-delay RTP extension, if negotiated */\n\t{\"min_delay\", JSON_INTEGER, 0},\n\t{\"max_delay\", JSON_INTEGER, 0},\n};\nstatic struct janus_json_parameter publish_desc_parameters[] = {\n\t{\"mid\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"description\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter record_parameters[] = {\n\t{\"record\", JANUS_JSON_BOOL, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter rtp_forward_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"host\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host_family\", JSON_STRING, 0},\n\t{\"simulcast\", JANUS_JSON_BOOL, 0},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0},\n\t{\"streams\", JANUS_JSON_ARRAY, 0},\n\t/* Deprecated parameters, use the streams array instead */\n\t{\"video_port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_rtcp_port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_ssrc\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_pt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_port_2\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_ssrc_2\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_pt_2\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_port_3\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_ssrc_3\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"video_pt_3\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_rtcp_port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_ssrc\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_pt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"data_port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n};\nstatic struct janus_json_parameter rtp_forward_stream_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"mid\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host\", JSON_STRING, 0},\n\t{\"host_family\", JSON_STRING, 0},\n\t{\"port\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"rtcp_port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"ssrc\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"pt\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"simulcast\", JANUS_JSON_BOOL, 0},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0},\n\t{\"port_2\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"ssrc_2\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"pt_2\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"port_3\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"ssrc_3\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"pt_3\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter stop_rtp_forward_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"stream_id\", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter publisher_parameters[] = {\n\t{\"display\", JSON_STRING, 0},\n\t{\"metadata\", JSON_OBJECT, 0}\n};\nstatic struct janus_json_parameter configure_stream_parameters[] = {\n\t{\"mid\", JANUS_JSON_STRING, 0},\n\t{\"send\", JANUS_JSON_BOOL, 0},\n\t/* For talk detection */\n\t{\"audio_level_averge\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"audio_active_packets\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For simulcast */\n\t{\"substream\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"fallback\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For SVC */\n\t{\"spatial_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For the playout-delay RTP extension, if negotiated */\n\t{\"min_delay\", JSON_INTEGER, 0},\n\t{\"max_delay\", JSON_INTEGER, 0},\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"streams\", JANUS_JSON_ARRAY, 0},\n\t/* The following is to handle a renegotiation */\n\t{\"update\", JANUS_JSON_BOOL, 0},\n\t/* The following is to force a restart */\n\t{\"restart\", JANUS_JSON_BOOL, 0},\n\t/* Deprecated properties, use mid+send instead */\n\t{\"audio\", JANUS_JSON_BOOL, 0},\t/* Deprecated */\n\t{\"video\", JANUS_JSON_BOOL, 0},\t/* Deprecated */\n\t{\"data\", JANUS_JSON_BOOL, 0}\t/* Deprecated */\n};\nstatic struct janus_json_parameter subscriber_parameters[] = {\n\t{\"streams\", JANUS_JSON_ARRAY, 0},\n\t{\"private_id\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"autoupdate\", JANUS_JSON_BOOL, 0},\n\t/* All the following parameters are deprecated: use streams instead */\n\t{\"audio\", JANUS_JSON_BOOL, 0},\n\t{\"video\", JANUS_JSON_BOOL, 0},\n\t{\"data\", JANUS_JSON_BOOL, 0},\n\t{\"offer_audio\", JANUS_JSON_BOOL, 0},\n\t{\"offer_video\", JANUS_JSON_BOOL, 0},\n\t{\"offer_data\", JANUS_JSON_BOOL, 0},\n\t/* For simulcast */\n\t{\"substream\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"fallback\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For SVC */\n\t{\"spatial_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n};\nstatic struct janus_json_parameter subscriber_stream_parameters[] = {\n\t{\"mid\", JANUS_JSON_STRING, 0},\n\t{\"crossrefid\", JANUS_JSON_STRING, 0},\n\t{\"send\", JANUS_JSON_BOOL, 0},\n\t/* For simulcast */\n\t{\"substream\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For SVC */\n\t{\"spatial_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For the playout-delay RTP extension, if negotiated */\n\t{\"min_delay\", JSON_INTEGER, 0},\n\t{\"max_delay\", JSON_INTEGER, 0}\n};\nstatic struct janus_json_parameter subscriber_update_parameters[] = {\n\t{\"streams\", JANUS_JSON_ARRAY, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter subscriber_combined_update_parameters[] = {\n\t{\"subscribe\", JANUS_JSON_ARRAY, 0},\n\t{\"unsubscribe\", JANUS_JSON_ARRAY, 0}\n};\nstatic struct janus_json_parameter subscriber_remove_parameters[] = {\n\t//~ {\"feed\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"mid\", JANUS_JSON_STRING, 0},\n\t{\"sub_mid\", JANUS_JSON_STRING, 0}\n};\nstatic struct janus_json_parameter switch_parameters[] = {\n\t{\"streams\", JANUS_JSON_ARRAY, 0}\n};\nstatic struct janus_json_parameter switch_update_parameters[] = {\n\t//~ {\"feed\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"mid\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"sub_mid\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t/* For simulcast */\n\t{\"substream\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t/* For SVC */\n\t{\"spatial_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"temporal_layer\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}\n};\nstatic struct janus_json_parameter publish_remotely_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"remote_id\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"host_family\", JSON_STRING, 0},\n\t{\"port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE | JANUS_JSON_PARAM_REQUIRED},\n\t{\"rtcp_port\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter unpublish_remotely_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"remote_id\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter remote_publisher_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"display\", JANUS_JSON_STRING, 0},\n\t{\"mcast\", JANUS_JSON_STRING, 0},\n\t{\"iface\", JANUS_JSON_STRING, 0},\n\t{\"port\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"streams\", JANUS_JSON_ARRAY, JANUS_JSON_PARAM_REQUIRED},\n\t{\"metadata\", JSON_OBJECT, 0},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter remote_publisher_update_parameters[] = {\n\t{\"secret\", JSON_STRING, 0},\n\t{\"display\", JANUS_JSON_STRING, 0},\n\t{\"metadata\", JSON_OBJECT, 0},\n\t{\"streams\", JANUS_JSON_ARRAY, JANUS_JSON_PARAM_REQUIRED},\n\t{\"srtp_suite\", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},\n\t{\"srtp_crypto\", JSON_STRING, 0}\n};\nstatic struct janus_json_parameter remote_publisher_stream_parameters[] = {\n\t{\"mid\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"mindex\", JANUS_JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},\n\t{\"type\", JANUS_JSON_STRING, JANUS_JSON_PARAM_REQUIRED},\n\t{\"codec\", JANUS_JSON_STRING, 0},\n\t{\"description\", JANUS_JSON_STRING, 0},\n\t{\"disabled\", JANUS_JSON_BOOL, 0},\n\t{\"stereo\", JANUS_JSON_BOOL, 0},\n\t{\"fec\", JANUS_JSON_BOOL, 0},\n\t{\"dtx\", JANUS_JSON_BOOL, 0},\n\t{\"h264_profile\", JSON_STRING, 0},\n\t{\"vp9_profile\", JSON_STRING, 0},\n\t{\"simulcast\", JANUS_JSON_BOOL, 0},\n\t{\"svc\", JANUS_JSON_BOOL, 0},\n\t{\"audiolevel_ext_id\", JANUS_JSON_INTEGER, 0},\n\t{\"videoorient_ext_id\", JANUS_JSON_INTEGER, 0},\n\t{\"playoutdelay_ext_id\", JANUS_JSON_INTEGER, 0},\n};\n\n/* Static configuration instance */\nstatic janus_config *config = NULL;\nstatic const char *config_folder = NULL;\nstatic janus_mutex config_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* Useful stuff */\nstatic volatile gint initialized = 0, stopping = 0;\nstatic gboolean notify_events = TRUE;\nstatic gboolean string_ids = FALSE;\nstatic gboolean ipv6_disabled = FALSE;\nstatic janus_callbacks *gateway = NULL;\nstatic GThread *handler_thread;\nstatic void *janus_videoroom_handler(void *data);\nstatic void janus_videoroom_relay_rtp_packet(gpointer data, gpointer user_data);\nstatic void janus_videoroom_relay_data_packet(gpointer data, gpointer user_data);\nstatic void janus_videoroom_hangup_media_internal(gpointer session_data);\n\ntypedef enum janus_videoroom_p_type {\n\tjanus_videoroom_p_type_none = 0,\n\tjanus_videoroom_p_type_subscriber,\t\t\t/* Generic subscriber */\n\tjanus_videoroom_p_type_publisher,\t\t\t/* Participant (for receiving events) and optionally publisher */\n} janus_videoroom_p_type;\n\ntypedef enum janus_videoroom_media {\n\tJANUS_VIDEOROOM_MEDIA_NONE = 0,\n\tJANUS_VIDEOROOM_MEDIA_AUDIO,\n\tJANUS_VIDEOROOM_MEDIA_VIDEO,\n\tJANUS_VIDEOROOM_MEDIA_DATA\n} janus_videoroom_media;\nstatic const char *janus_videoroom_media_str(janus_videoroom_media type) {\n\tswitch(type) {\n\t\tcase JANUS_VIDEOROOM_MEDIA_AUDIO: return \"audio\";\n\t\tcase JANUS_VIDEOROOM_MEDIA_VIDEO: return \"video\";\n\t\tcase JANUS_VIDEOROOM_MEDIA_DATA: return \"data\";\n\t\tcase JANUS_VIDEOROOM_MEDIA_NONE:\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\nstatic janus_sdp_mtype janus_videoroom_media_sdptype(janus_videoroom_media type) {\n\tswitch(type) {\n\t\tcase JANUS_VIDEOROOM_MEDIA_AUDIO: return JANUS_SDP_AUDIO;\n\t\tcase JANUS_VIDEOROOM_MEDIA_VIDEO: return JANUS_SDP_VIDEO;\n\t\tcase JANUS_VIDEOROOM_MEDIA_DATA: return JANUS_SDP_APPLICATION;\n\t\tcase JANUS_VIDEOROOM_MEDIA_NONE:\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn JANUS_SDP_OTHER;\n}\nstatic janus_videoroom_media janus_videoroom_media_from_str(const char *type) {\n\tif(type == NULL)\n\t\treturn JANUS_VIDEOROOM_MEDIA_NONE;\n\telse if(!strcasecmp(type, \"audio\"))\n\t\treturn JANUS_VIDEOROOM_MEDIA_AUDIO;\n\telse if(!strcasecmp(type, \"video\"))\n\t\treturn JANUS_VIDEOROOM_MEDIA_VIDEO;\n\telse if(!strcasecmp(type, \"data\"))\n\t\treturn JANUS_VIDEOROOM_MEDIA_DATA;\n\treturn JANUS_VIDEOROOM_MEDIA_NONE;\n}\n\ntypedef struct janus_videoroom_message {\n\tjanus_plugin_session *handle;\n\tchar *transaction;\n\tjson_t *message;\n\tjson_t *jsep;\n} janus_videoroom_message;\nstatic GAsyncQueue *messages = NULL;\nstatic janus_videoroom_message exit_message;\n\n\ntypedef struct janus_videoroom {\n\tguint64 room_id;\t\t\t/* Unique room ID (when using integers) */\n\tgchar *room_id_str;\t\t\t/* Unique room ID (when using strings) */\n\tgchar *room_name;\t\t\t/* Room description */\n\tgchar *room_secret;\t\t\t/* Secret needed to manipulate (e.g., destroy) this room */\n\tgchar *room_pin;\t\t\t/* Password needed to join this room, if any */\n\tgboolean is_private;\t\t/* Whether this room is 'private' (as in hidden) or not */\n\tgboolean require_pvtid;\t\t/* Whether subscriptions in this room require a private_id */\n\tgboolean signed_tokens;\t\t/* Whether signed tokens are required (assuming they're enabled in the core)  */\n\tgboolean require_e2ee;\t\t/* Whether end-to-end encrypted publishers are required */\n\tgboolean dummy_publisher;\t/* Whether this room has a dummy publisher to use for placeholder subscriptions */\n\tint max_publishers;\t\t\t/* Maximum number of concurrent publishers */\n\tuint32_t bitrate;\t\t\t/* Global bitrate limit */\n\tgboolean bitrate_cap;\t\t/* Whether the above limit is insormountable */\n\tuint16_t fir_freq;\t\t\t/* Regular FIR frequency (0=disabled) */\n\tjanus_audiocodec acodec[5];\t/* Audio codec(s) to force on publishers */\n\tjanus_videocodec vcodec[5];\t/* Video codec(s) to force on publishers */\n\tchar *vp9_profile;\t\t\t/* VP9 codec profile to prefer, if more are negotiated */\n\tchar *h264_profile;\t\t\t/* H.264 codec profile to prefer, if more are negotiated */\n\tgboolean do_opusfec;\t\t/* Whether inband FEC must be negotiated (note: only available for Opus) */\n\tgboolean do_opusdtx;\t\t/* Whether DTX must be negotiated (note: only available for Opus) */\n\tgboolean audiolevel_ext;\t/* Whether the ssrc-audio-level extension must be negotiated or not for new publishers */\n\tgboolean audiolevel_event;\t/* Whether to emit event to other users about audiolevel */\n\tint audio_active_packets;\t/* Amount of packets with audio level for checkup */\n\tint audio_level_average;\t/* Average audio level */\n\tgboolean videoorient_ext;\t/* Whether the video-orientation extension must be negotiated or not for new publishers */\n\tgboolean playoutdelay_ext;\t/* Whether the playout-delay extension must be negotiated or not for new publishers */\n\tgboolean transport_wide_cc_ext;\t/* Whether the transport wide cc extension must be negotiated or not for new publishers */\n\tgboolean record;\t\t\t/* Whether the feeds from publishers in this room should be recorded */\n\tchar *rec_dir;\t\t\t\t/* Where to save the recordings of this room, if enabled */\n\tgboolean lock_record;\t\t/* Whether recording state can only be changed providing the room secret */\n\tGHashTable *participants;\t/* Map of potential publishers (we get subscribers from them) */\n\tGHashTable *private_ids;\t/* Map of existing private IDs */\n\tvolatile gint destroyed;\t/* Whether this room has been destroyed */\n\tgboolean check_allowed;\t\t/* Whether to check tokens when participants join (see below) */\n\tGHashTable *allowed;\t\t/* Map of participants (as tokens) allowed to join */\n\tgboolean notify_joining;\t/* Whether an event is sent to notify all participants if a new participant joins the room */\n\tint helper_threads;\t\t\t/* Number of helper threads for relaying purposes */\n\tGList *threads;\t\t\t\t/* List of helper threads, if any */\n\tjanus_mutex mutex;\t\t\t/* Mutex to lock this room instance */\n\tjanus_refcount ref;\t\t\t/* Reference counter for this room */\n} janus_videoroom;\nstatic GHashTable *rooms;\nstatic janus_mutex rooms_mutex = JANUS_MUTEX_INITIALIZER;\nstatic char *admin_key = NULL;\nstatic gboolean lock_rtpfwd = FALSE;\n\ntypedef struct janus_videoroom_session {\n\tjanus_plugin_session *handle;\n\tgint64 sdp_sessid;\n\tgint64 sdp_version;\n\tjanus_videoroom_p_type participant_type;\n\tgpointer participant;\n\tvolatile gint started;\n\tvolatile gint dataready;\n\tvolatile gint hangingup;\n\tvolatile gint destroyed;\n\tjanus_mutex mutex;\n\tjanus_refcount ref;\n} janus_videoroom_session;\nstatic GHashTable *sessions;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* Abstraction of a relay helper thread, that decouples incoming media\n * from publishers from the task of distributing it to subscribers;\n * this is a port of the helper threads concept from the Streaming plugin */\ntypedef struct janus_videoroom_helper {\n\tstruct janus_videoroom *room;\n\tguint id;\n\tGThread *thread;\n\tint num_subscribers;\n\tGHashTable *subscribers;\n\tGAsyncQueue *queued_packets;\n\tvolatile gint destroyed;\n\tjanus_mutex mutex;\n\tjanus_refcount ref;\n} janus_videoroom_helper;\nstatic void janus_videoroom_helper_destroy(janus_videoroom_helper *helper) {\n\tif(helper && g_atomic_int_compare_and_exchange(&helper->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&helper->ref);\n}\nstatic void janus_videoroom_helper_free(const janus_refcount *helper_ref) {\n\tjanus_videoroom_helper *helper = janus_refcount_containerof(helper_ref, janus_videoroom_helper, ref);\n\t/* This helper can be destroyed, free all the resources */\n\tg_async_queue_unref(helper->queued_packets);\n\tif(helper->subscribers != NULL)\n\t\tg_hash_table_destroy(helper->subscribers);\n\tg_free(helper);\n}\nstatic void *janus_videoroom_helper_thread(void *data);\nstatic void janus_videoroom_helper_rtpdata_packet(gpointer data, gpointer user_data);\n\ntypedef struct janus_videoroom_publisher {\n\tjanus_videoroom_session *session;\n\tjanus_videoroom *room;\t/* Room */\n\tguint64 room_id;\t/* Unique room ID */\n\tgchar *room_id_str;\t/* Unique room ID (when using strings) */\n\tguint64 user_id;\t/* Unique ID in the room */\n\tgchar *user_id_str;\t/* Unique ID in the room (when using strings) */\n\tguint32 pvt_id;\t\t/* This is sent to the publisher for mapping purposes, but shouldn't be shared with others */\n\tgchar *display;\t\t/* Display name (just for fun) */\n\tgboolean dummy;\t\t/* Whether this is a dummy publisher used just for placeholder subscriptions */\n\tjanus_audiocodec acodec;\t\t\t\t/* Audio codec preference for this publisher (if audio) */\n\tjanus_videocodec vcodec;\t\t\t\t/* Video codec preference for this publisher (if video) */\n\tint user_audio_active_packets;\t/* Participant's audio_active_packets overwriting global room setting */\n\tint user_audio_level_average;\t/* Participant's audio_level_average overwriting global room setting */\n\tgboolean talking; \t/* Whether this participant is currently talking (uses audio levels extension) */\n\tgboolean firefox;\t/* We send Firefox users a different kind of FIR */\n\tGList *streams;\t\t\t\t/* List of media streams sent by this publisher (audio, video and/or data) */\n\tGHashTable *streams_byid;\t/* As above, indexed by mindex */\n\tGHashTable *streams_bymid;\t/* As above, indexed by mid */\n\tint data_mindex;\t\t\t/* We keep track of the mindex for data, as there can only be one */\n\tjanus_mutex streams_mutex;\n\tuint32_t bitrate;\n\tgint64 remb_startup;/* Incremental changes on REMB to reach the target at startup */\n\tgint64 remb_latest;\t/* Time of latest sent REMB (to avoid flooding) */\n\tgboolean recording_active;\t/* Whether this publisher has to be recorded or not */\n\tgchar *recording_base;\t/* Base name for the recording (e.g., /path/to/filename, will generate /path/to/filename-audio.mjr and/or /path/to/filename-video.mjr) */\n\tjanus_mutex rec_mutex;\t/* Mutex to protect the recorders from race conditions */\n\tGSList *subscriptions;\t/* Subscriptions this publisher has created (who this publisher is watching) */\n\tjanus_mutex subscribers_mutex;\n\tjanus_mutex own_subscriptions_mutex;\n\t/* In case this local publisher is being forwarder remotely */\n\tGHashTable *remote_recipients;\n\t/* In case this is a remote publisher */\n\tgboolean remote;\t\t\t/* Whether this is a remote publisher */\n\tuint32_t remote_ssrc_offset;\t/* SSRC offset to apply to the incoming RTP traffic */\n\tint remote_fd, remote_rtcp_fd, pipefd[2];\t/* Remote publisher sockets */\n\tstruct sockaddr_storage rtcp_addr;\t/* RTCP address of the remote publisher */\n\tGThread *remote_thread;\t\t/* Remote publisher incoming packets thread */\n\tvolatile gint remote_leaving;\n\t/* Index of RTP (or data) forwarders for this participant (all streams), if any */\n\tGHashTable *rtp_forwarders;\n\tjanus_mutex rtp_forwarders_mutex;\n\tint udp_sock; /* The udp socket on which to forward rtp packets */\n\tgboolean kicked;\t/* Whether this participant has been kicked */\n\tgboolean e2ee;\t\t/* If media from this publisher is end-to-end encrypted */\n\tjanus_mutex mutex;\t\t\t/* Mutex to lock this instance */\n\tjson_t *metadata;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_videoroom_publisher;\n/* Each VideoRoom publisher can share multiple streams, so each stream is its own structure */\ntypedef struct janus_videoroom_publisher_stream {\n\tjanus_videoroom_publisher *publisher;\t/* Publisher instance this stream belongs to */\n\tjanus_videoroom_media type;\t\t\t\t/* Type of this stream (audio, video or data) */\n\tint mindex;\t\t\t\t\t\t\t\t/* mindex of this stream */\n\tchar *mid;\t\t\t\t\t\t\t\t/* mid of this stream */\n\tchar *description;\t\t\t\t\t\t/* Description of this stream (user provided) */\n\tgboolean disabled;\t\t\t\t\t\t/* Whether this stream is temporarily disabled or not */\n\tgboolean active;\t\t\t\t\t\t/* Whether this stream is active or not */\n\tgboolean muted;\t\t\t\t\t\t\t/* Whether this stream has been muted by a moderator */\n\tjanus_audiocodec acodec;\t\t\t\t/* Audio codec this publisher is using (if audio) */\n\tjanus_videocodec vcodec;\t\t\t\t/* Video codec this publisher is using (if video) */\n\tint pt;\t\t\t\t\t\t\t\t\t/* Payload type of this stream (if audio or video) */\n\tchar *fmtp;\t\t\t\t\t\t\t\t/* fmtp that ended up being negotiated, if any (for video profiles) */\n\tchar *h264_profile;\t\t\t\t\t\t/* H264 profile used for this stream (if video and H264 codec) */\n\tchar *vp9_profile;\t\t\t\t\t\t/* VP9 profile this publisher is using (if video and VP9 codec) */\n\tgint64 fir_latest;\t\t\t\t\t\t/* Time of latest sent PLI (to avoid flooding) */\n\tgint fir_seq;\t\t\t\t\t\t\t/* FIR sequence number, if needed */\n\tgboolean opusfec;\t\t\t\t\t\t/* Whether this stream is sending inband Opus FEC */\n\tgboolean opusdtx;\t\t\t\t\t\t/* Whether this publisher is using Opus DTX (Discontinuous Transmission) */\n\tgboolean opusstereo;\t\t\t\t\t/* Whether this publisher is doing stereo Opus */\n\tgboolean simulcast, svc;\t\t\t\t/* Whether this stream uses simulcast or SVC */\n\tuint32_t vssrc[3];\t\t\t\t\t\t/* Only needed in case simulcasting is involved */\n\tchar *rid[3];\t\t\t\t\t\t\t/* Only needed if simulcasting is rid-based */\n\tint rid_extmap_id;\t\t\t\t\t\t/* rid extmap ID */\n\tjanus_mutex rid_mutex;\t\t\t\t\t/* Mutex to protect access to the rid array and the extmap ID */\n\t/* RTP extensions, if negotiated */\n\tguint8 audio_level_extmap_id;\t\t\t/* Audio level extmap ID */\n\tguint8 video_orient_extmap_id;\t\t\t/* Video orientation extmap ID */\n\tguint8 playout_delay_extmap_id;\t\t\t/* Playout delay extmap ID */\n\tjanus_sdp_mdirection audio_level_mdir, video_orient_mdir, playout_delay_mdir;\n\t/* Playout delays to enforce when relaying this stream, if the extension has been negotiated */\n\tint16_t min_delay, max_delay;\n\t/* Audio level processing, if enabled */\n\tint audio_dBov_level;\t\t\t\t\t/* Value in dBov of the audio level (last value from extension) */\n\tint audio_active_packets;\t\t\t\t/* Participant's number of audio packets to accumulate */\n\tint audio_dBov_sum;\t\t\t\t\t\t/* Participant's accumulated dBov value for audio level */\n\tgboolean talking;\t\t\t\t\t\t/* Whether this participant is currently talking (uses audio levels extension) */\n\t/* Recording related stuff, if enabled */\n\tjanus_recorder *rc;\n\tjanus_rtp_switching_context rec_ctx;\n\tjanus_rtp_simulcasting_context rec_simctx;\n\t/* RTP (or data) forwarders for this stream, if any */\n\tGHashTable *rtp_forwarders;\n\tjanus_mutex rtp_forwarders_mutex;\n\t/* In case this is a stream from a remote publisher */\n\tvolatile gint need_pli;\t\t/* Whether we need to send a PLI later */\n\tvolatile gint sending_pli;\t/* Whether we're currently sending a PLI */\n\tgint64 pli_latest;\t\t\t/* Time of latest sent PLI (to avoid flooding) */\n\t/* Only needed for SRTP support for remote publisher */\n\tgboolean is_srtp;\n\tint srtp_suite;\n\tchar *srtp_crypto;\n\tsrtp_t srtp_ctx;\n\tsrtp_policy_t srtp_policy;\n\t/* Subscriptions to this publisher stream (who's receiving it)  */\n\tGSList *subscribers;\n\tjanus_mutex subscribers_mutex;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_videoroom_publisher_stream;\n/* Helper to add a new RTP forwarder for a specific stream sent by publisher */\nstatic janus_rtp_forwarder *janus_videoroom_rtp_forwarder_add_helper(janus_videoroom_publisher *p,\n\tjanus_videoroom_publisher_stream *ps,\n\tconst gchar *host, int port, int rtcp_port, int pt, uint32_t ssrc,\n\tgboolean simulcast, int srtp_suite, const char *srtp_crypto,\n\tint substream, gboolean is_video, gboolean is_data);\nstatic void janus_videoroom_rtp_forwarder_rtcp_receive(janus_rtp_forwarder *rf, char *buffer, int len);\nstatic json_t *janus_videoroom_rtp_forwarder_summary(janus_rtp_forwarder *f);\nstatic void janus_videoroom_create_dummy_publisher(janus_videoroom *room, gboolean e2ee, GHashTable *streams);\n\n/* We support remote publishers as well, for which we use plain RTP,\n * which means we need to create and work with generic file descriptors */\n#define DEFAULT_RTP_RANGE_MIN 10000\n#define DEFAULT_RTP_RANGE_MAX 60000\nstatic uint16_t rtp_range_min = DEFAULT_RTP_RANGE_MIN;\nstatic uint16_t rtp_range_max = DEFAULT_RTP_RANGE_MAX;\nstatic uint16_t rtp_range_slider = DEFAULT_RTP_RANGE_MIN;\nstatic janus_mutex fd_mutex = JANUS_MUTEX_INITIALIZER;\n#define REMOTE_PUBLISHER_BASE_SSRC\t1000\n#define REMOTE_PUBLISHER_SSRC_STEP\t10\n/* Helpers to create a listener filedescriptor */\nstatic int janus_videoroom_create_fd(int port, in_addr_t mcast, const janus_network_address *iface, char *host, size_t hostlen);\n/* Helper to return fd port */\nstatic int janus_videoroom_get_fd_port(int fd);\n/* Thread responsible for a specific remote publisher */\nstatic void *janus_videoroom_remote_publisher_thread(void *data);\n\ntypedef struct janus_videoroom_subscriber {\n\tjanus_videoroom_session *session;\n\tjanus_videoroom *room;\t/* Room */\n\tguint64 room_id;\t\t/* Unique room ID */\n\tgchar *room_id_str;\t\t/* Unique room ID (when using strings) */\n\tGList *streams;\t\t\t\t/* List of media stream subscriptions originated by this subscriber (audio, video and/or data) */\n\tGHashTable *streams_byid;\t/* As above, indexed by mindex */\n\tGHashTable *streams_bymid;\t/* As above, indexed by mid */\n\tjanus_mutex streams_mutex;\n\tgboolean use_msid;\t\t/* Whether we should add custom msid attributes to offers, to match publishers and streams */\n\tgboolean autoupdate;\t/* Whether we should trigger a renegotiation automatically when a subscribed publisher goes away */\n\tguint32 pvt_id;\t\t\t/* Private ID of the participant that is subscribing (if available/provided) */\n\tgboolean paused;\n\tgboolean kicked;\t/* Whether this subscription belongs to a participant that has been kicked */\n\tgboolean e2ee;\t\t/* If media for this subscriber is end-to-end encrypted */\n\tvolatile gint answered, pending_offer, pending_restart, skipped_autoupdate;\n\tvolatile gint destroyed;\n\tjanus_refcount ref;\n} janus_videoroom_subscriber;\n/* Each VideoRoom subscriber can be subscribed to multiple streams, belonging to\n * the same or different publishers: as such, each stream is its own structure */\ntypedef struct janus_videoroom_subscriber_stream {\n\tjanus_videoroom_subscriber *subscriber;\t\t\t/* Subscriber instance this stream belongs to */\n\tGSList *publisher_streams;\t\t\t\t\t\t/* Complete list of publisher streams (e.g., when this is data) */\n\tint mindex;\t\t\t\t/* The media index of this stream (may not be the same as the publisher stream) */\n\tchar *mid;\t\t\t\t/* The mid of this stream (may not be the same as the publisher stream) */\n\tchar *msid, *mstid;\t\t/* In case msid must be used, the values to use in the SDP */\n\tchar *crossrefid;\t\t/* An id provided while subscribing to uniquely identify the subscription in the list of subscriptions */\n\tgboolean send;\t\t\t/* Whether this stream media must be sent to this subscriber */\n\t/* The following properties are copied from the source, in case this stream becomes inactive */\n\tjanus_videoroom_media type;\t\t\t/* Type of this stream (audio, video or data) */\n\tjanus_audiocodec acodec;\t\t\t/* Audio codec this publisher is using (if audio) */\n\tjanus_videocodec vcodec;\t\t\t/* Video codec this publisher is using (if video) */\n\tchar *h264_profile;\t\t\t\t\t/* H264 profile used for this stream (if video and H264 codec) */\n\tchar *vp9_profile;\t\t\t\t\t/* VP9 profile this publisher is using (if video and VP9 codec) */\n\tint pt;\t\t\t\t\t\t\t\t/* Payload type of this stream (if audio or video) */\n\tgboolean opusfec;\t\t\t\t\t/* Whether this stream is using inband Opus FEC */\n\t/* RTP and simulcasting contexts */\n\tjanus_rtp_switching_context context;\n\tjanus_rtp_simulcasting_context sim_context;\n\tjanus_vp8_simulcast_context vp8_context;\n\t/* SVC context */\n\tjanus_rtp_svc_context svc_context;\n\t/* Playout delays to enforce when relaying this stream, if the extension has been negotiated */\n\tint16_t min_delay, max_delay;\n\tvolatile gint ready, destroyed;\n\tjanus_refcount ref;\n} janus_videoroom_subscriber_stream;\n\ntypedef struct janus_videoroom_stream_mapping {\n\tjanus_videoroom_publisher_stream *ps;\n\tjanus_videoroom_subscriber *subscriber;\n\tjanus_videoroom_subscriber_stream *ss;\n\tgboolean unref_ss;\n} janus_videoroom_stream_mapping;\n\ntypedef struct janus_videoroom_rtp_relay_packet {\n\tjanus_videoroom_publisher_stream *source;\n\tjanus_rtp_header *data;\n\tgint length;\n\tgboolean is_rtp;\t/* This may be a data packet and not RTP */\n\tgboolean is_video;\n\tuint32_t ssrc[3];\n\tuint32_t timestamp;\n\tuint16_t seq_number;\n\t/* Extensions to add, if any */\n\tjanus_plugin_rtp_extensions extensions;\n\t/* Whether simulcast is involved */\n\tgboolean simulcast;\n\t/* The following are only relevant if we're doing SVC*/\n\tgboolean svc;\n\tjanus_vp9_svc_info svc_info;\n\t/* The following is only relevant for datachannels */\n\tgboolean textdata;\n} janus_videoroom_rtp_relay_packet;\nstatic janus_videoroom_rtp_relay_packet exit_packet;\nstatic void janus_videoroom_rtp_relay_packet_free(janus_videoroom_rtp_relay_packet *pkt) {\n\tif(pkt == NULL || pkt == &exit_packet)\n\t\treturn;\n\tg_free(pkt->data);\n\tg_free(pkt);\n}\n\n/* VideoRoom publishers can be forwarder remotely: we use the following\n * struct to track specific recipients of a local publisher */\ntypedef struct janus_videoroom_remote_recipient {\n\tchar *remote_id;\t\t/* ID of this publisher remotization */\n\tchar *host;\t\t\t\t/* Address this publisher is being relayed to */\n\tuint16_t port;\t\t\t/* Port this publisher is being relayed to */\n\tuint16_t rtcp_port;\t\t/* RTCP port this publisher is going to latch to */\n\tgboolean rtcp_added;\t/* Whether we created an RTCP socket for this remotization */\n\t/* Only needed for SRTP support for remote publisher */\n\tint srtp_suite;\n\tchar *srtp_crypto;\n} janus_videoroom_remote_recipient;\nstatic void janus_videoroom_remote_recipient_free(janus_videoroom_remote_recipient *r) {\n\tif(r) {\n\t\tg_free(r->remote_id);\n\t\tg_free(r->host);\n\t\tg_free(r->srtp_crypto);\n\t\tg_free(r);\n\t}\n}\n\n/* Start / stop recording */\nstatic void janus_videoroom_recorder_create(janus_videoroom_publisher_stream *ps);\nstatic void janus_videoroom_recorder_close(janus_videoroom_publisher *participant);\n\n/* Freeing stuff */\nstatic void janus_videoroom_subscriber_stream_destroy(janus_videoroom_subscriber_stream *s) {\n\tif(s && g_atomic_int_compare_and_exchange(&s->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&s->ref);\n\t/* TODO Should unref the subscriber instance? */\n}\n\nstatic void janus_videoroom_subscriber_stream_unref(janus_videoroom_subscriber_stream *s) {\n\t/* Decrease the counter */\n\tif(s)\n\t\tjanus_refcount_decrease(&s->ref);\n}\n\nstatic void janus_videoroom_subscriber_stream_free(const janus_refcount *s_ref) {\n\tjanus_videoroom_subscriber_stream *s = janus_refcount_containerof(s_ref, janus_videoroom_subscriber_stream, ref);\n\t/* This subscriber stream can be destroyed, free all the resources */\n\t\t/* TODO Anything else we should free? */\n\tg_free(s->mid);\n\tg_free(s->msid);\n\tg_free(s->mstid);\n\tg_free(s->crossrefid);\n\tg_free(s->h264_profile);\n\tg_free(s->vp9_profile);\n\tjanus_rtp_svc_context_reset(&s->svc_context);\n\tg_free(s);\n}\n\nstatic void janus_videoroom_subscriber_destroy(janus_videoroom_subscriber *s) {\n\tif(s && g_atomic_int_compare_and_exchange(&s->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&s->ref);\n}\n\nstatic void janus_videoroom_subscriber_free(const janus_refcount *s_ref) {\n\tjanus_videoroom_subscriber *s = janus_refcount_containerof(s_ref, janus_videoroom_subscriber, ref);\n\t/* This subscriber can be destroyed, free all the resources */\n\tg_free(s->room_id_str);\n\tg_list_free_full(s->streams, (GDestroyNotify)(janus_videoroom_subscriber_stream_destroy));\n\tg_hash_table_unref(s->streams_byid);\n\tg_hash_table_unref(s->streams_bymid);\n\n\tg_free(s);\n}\n\nstatic void janus_videoroom_publisher_stream_destroy(janus_videoroom_publisher_stream *ps) {\n\tif(ps && g_atomic_int_compare_and_exchange(&ps->destroyed, 0, 1)) {\n\t\tif(ps->publisher)\n\t\t\tjanus_refcount_decrease(&ps->publisher->ref);\n\t\tps->publisher = NULL;\n\t\tjanus_refcount_decrease(&ps->ref);\n\t}\n\t/* TODO Should unref the publisher instance? */\n}\n\nstatic void janus_videoroom_publisher_stream_unref(janus_videoroom_publisher_stream *ps) {\n\t/* Decrease the counter */\n\tif(ps)\n\t\tjanus_refcount_decrease(&ps->ref);\n}\n\nstatic void janus_videoroom_publisher_stream_free(const janus_refcount *ps_ref) {\n\tjanus_videoroom_publisher_stream *ps = janus_refcount_containerof(ps_ref, janus_videoroom_publisher_stream, ref);\n\t/* This publisher stream can be destroyed, free all the resources */\n\t\t/* TODO Anything else we should free? */\n\tg_free(ps->mid);\n\tg_free(ps->description);\n\tg_free(ps->fmtp);\n\tg_free(ps->h264_profile);\n\tg_free(ps->vp9_profile);\n\tjanus_recorder_destroy(ps->rc);\n\tg_slist_free(ps->subscribers);\n\tjanus_mutex_destroy(&ps->subscribers_mutex);\n\tg_hash_table_destroy(ps->rtp_forwarders);\n\tps->rtp_forwarders = NULL;\n\tjanus_mutex_destroy(&ps->rtp_forwarders_mutex);\n\tjanus_mutex_destroy(&ps->rid_mutex);\n\tjanus_rtp_simulcasting_cleanup(NULL, NULL, ps->rid, NULL);\n\tif(ps->is_srtp) {\n\t\tg_free(ps->srtp_crypto);\n\t\tsrtp_dealloc(ps->srtp_ctx);\n\t\tg_free(ps->srtp_policy.key);\n\t}\n\tg_free(ps);\n}\n\nstatic void janus_videoroom_publisher_dereference(janus_videoroom_publisher *p) {\n\t/* This is used by g_hash_table_new_full so that NULL is only possible\n\t * if that was inserted into the hash table. Notice that this also\n\t * dereferences the session the participant is associated with, since\n\t * we add an extra ref to the session to when inserting in the table */\n\tif(p->dummy) {\n\t\t/* Dummy publisher, free streams */\n\t\tjanus_mutex_lock(&p->streams_mutex);\n\t\tif(p->streams != NULL) {\n\t\t\tg_list_free_full(p->streams, (GDestroyNotify)(janus_videoroom_publisher_stream_unref));\n\t\t\tp->streams = NULL;\n\t\t\tg_hash_table_remove_all(p->streams_byid);\n\t\t\tg_hash_table_remove_all(p->streams_bymid);\n\t\t}\n\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t}\n\tif(p->session)\n\t\tjanus_refcount_decrease(&p->session->ref);\n\tjanus_refcount_decrease(&p->ref);\n}\n\nstatic void janus_videoroom_publisher_dereference_nodebug(janus_videoroom_publisher *p) {\n\tjanus_refcount_decrease_nodebug(&p->ref);\n}\n\nstatic void janus_videoroom_publisher_destroy(janus_videoroom_publisher *p) {\n\tif(p && g_atomic_int_compare_and_exchange(&p->destroyed, 0, 1)) {\n\t\tjanus_mutex_lock(&p->streams_mutex);\n\t\t/* Forwarders with RTCP support may have an extra reference, stop their source */\n\t\tjanus_mutex_lock(&p->rtp_forwarders_mutex);\n\t\tif(g_hash_table_size(p->rtp_forwarders) > 0) {\n\t\t\tjanus_videoroom_publisher_stream *ps = NULL;\n\t\t\tGList *temp = p->streams;\n\t\t\twhile(temp) {\n\t\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\t\t\t\tif(g_hash_table_size(ps->rtp_forwarders) == 0) {\n\t\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tGHashTableIter iter_f;\n\t\t\t\tgpointer key_f, value_f;\n\t\t\t\tg_hash_table_iter_init(&iter_f, ps->rtp_forwarders);\n\t\t\t\twhile(g_hash_table_iter_next(&iter_f, &key_f, &value_f)) {\n\t\t\t\t\tjanus_rtp_forwarder *rpv = value_f;\n\t\t\t\t\tif(rpv->rtcp_recv) {\n\t\t\t\t\t\tGSource *source = rpv->rtcp_recv;\n\t\t\t\t\t\trpv->rtcp_recv = NULL;\n\t\t\t\t\t\tg_source_destroy(source);\n\t\t\t\t\t\tg_source_unref(source);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&p->rtp_forwarders_mutex);\n\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t\tjanus_refcount_decrease(&p->ref);\n\t}\n}\n\nstatic void janus_videoroom_publisher_free(const janus_refcount *p_ref) {\n\tjanus_videoroom_publisher *p = janus_refcount_containerof(p_ref, janus_videoroom_publisher, ref);\n\tg_free(p->room_id_str);\n\tg_free(p->user_id_str);\n\tg_free(p->display);\n\tg_free(p->recording_base);\n\tif(p->metadata != NULL)\n\t\tjson_decref(p->metadata);\n\t/* Get rid of all the streams */\n\tg_list_free_full(p->streams, (GDestroyNotify)(janus_videoroom_publisher_stream_destroy));\n\tg_hash_table_unref(p->streams_byid);\n\tg_hash_table_unref(p->streams_bymid);\n\n\tif(p->udp_sock > 0)\n\t\tclose(p->udp_sock);\n\tg_hash_table_destroy(p->remote_recipients);\n\tg_hash_table_destroy(p->rtp_forwarders);\n\tg_slist_free(p->subscriptions);\n\n\tif(p->remote_fd > 0)\n\t\tclose(p->remote_fd);\n\tif(p->remote_rtcp_fd > 0)\n\t\tclose(p->remote_rtcp_fd);\n\tif(p->pipefd[0] > 0)\n\t\tclose(p->pipefd[0]);\n\tif(p->pipefd[1] > 0)\n\t\tclose(p->pipefd[1]);\n\n\tjanus_mutex_destroy(&p->subscribers_mutex);\n\tjanus_mutex_destroy(&p->own_subscriptions_mutex);\n\tjanus_mutex_destroy(&p->streams_mutex);\n\tjanus_mutex_destroy(&p->rtp_forwarders_mutex);\n\tjanus_mutex_destroy(&p->mutex);\n\n\t/* If this is a dummy publisher, get rid of the session too */\n\tif(p->dummy && p->session)\n\t\tjanus_refcount_decrease(&p->session->ref);\n\n\tg_free(p);\n}\n\nstatic void janus_videoroom_session_destroy(janus_videoroom_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_videoroom_session_free(const janus_refcount *session_ref) {\n\tjanus_videoroom_session *session = janus_refcount_containerof(session_ref, janus_videoroom_session, ref);\n\t/* Remove the reference to the core plugin session */\n\tif(session->handle) {\n\t\t/* Could be NULL for dummy publishers */\n\t\tjanus_refcount_decrease(&session->handle->ref);\n\t}\n\t/* This session can be destroyed, free all the resources */\n\tjanus_mutex_destroy(&session->mutex);\n\tg_free(session);\n}\n\nstatic void janus_videoroom_room_dereference(janus_videoroom *room) {\n\tjanus_refcount_decrease(&room->ref);\n}\n\nstatic void janus_videoroom_room_destroy(janus_videoroom *room) {\n\tif(room && g_atomic_int_compare_and_exchange(&room->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&room->ref);\n}\n\nstatic void janus_videoroom_room_free(const janus_refcount *room_ref) {\n\tjanus_videoroom *room = janus_refcount_containerof(room_ref, janus_videoroom, ref);\n\t/* This room can be destroyed, free all the resources */\n\tGList *l = room->threads;\n\twhile(l) {\n\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\tg_async_queue_push(ht->queued_packets, &exit_packet);\n\t\tjanus_videoroom_helper_destroy(ht);\n\t\tl = l->next;\n\t}\n\tg_list_free(room->threads);\n\tg_free(room->room_id_str);\n\tg_free(room->room_name);\n\tg_free(room->room_secret);\n\tg_free(room->room_pin);\n\tg_free(room->rec_dir);\n\tg_free(room->vp9_profile);\n\tg_free(room->h264_profile);\n\tg_hash_table_destroy(room->participants);\n\tg_hash_table_destroy(room->private_ids);\n\tg_hash_table_destroy(room->allowed);\n\tg_free(room);\n}\n\nstatic void janus_videoroom_message_free(janus_videoroom_message *msg) {\n\tif(!msg || msg == &exit_message)\n\t\treturn;\n\n\tif(msg->handle && msg->handle->plugin_handle) {\n\t\tjanus_videoroom_session *session = (janus_videoroom_session *)msg->handle->plugin_handle;\n\t\tjanus_refcount_decrease(&session->ref);\n\t}\n\tmsg->handle = NULL;\n\n\tg_free(msg->transaction);\n\tmsg->transaction = NULL;\n\tif(msg->message)\n\t\tjson_decref(msg->message);\n\tmsg->message = NULL;\n\tif(msg->jsep)\n\t\tjson_decref(msg->jsep);\n\tmsg->jsep = NULL;\n\n\tg_free(msg);\n}\n\nstatic void janus_videoroom_codecstr(janus_videoroom *videoroom, char *audio_codecs, char *video_codecs, int str_len, const char *split) {\n\tif (audio_codecs) {\n\t\taudio_codecs[0] = 0;\n\t\tg_snprintf(audio_codecs, str_len, \"%s\", janus_audiocodec_name(videoroom->acodec[0]));\n\t\tif (videoroom->acodec[1] != JANUS_AUDIOCODEC_NONE) {\n\t\t\tjanus_strlcat(audio_codecs, split, str_len);\n\t\t\tjanus_strlcat(audio_codecs, janus_audiocodec_name(videoroom->acodec[1]), str_len);\n\t\t}\n\t\tif (videoroom->acodec[2] != JANUS_AUDIOCODEC_NONE) {\n\t\t\tjanus_strlcat(audio_codecs, split, str_len);\n\t\t\tjanus_strlcat(audio_codecs, janus_audiocodec_name(videoroom->acodec[2]), str_len);\n\t\t}\n\t\tif (videoroom->acodec[3] != JANUS_AUDIOCODEC_NONE) {\n\t\t\tjanus_strlcat(audio_codecs, split, str_len);\n\t\t\tjanus_strlcat(audio_codecs, janus_audiocodec_name(videoroom->acodec[3]), str_len);\n\t\t}\n\t\tif (videoroom->acodec[4] != JANUS_AUDIOCODEC_NONE) {\n\t\t\tjanus_strlcat(audio_codecs, split, str_len);\n\t\t\tjanus_strlcat(audio_codecs, janus_audiocodec_name(videoroom->acodec[4]), str_len);\n\t\t}\n\t}\n\tif (video_codecs) {\n\t\tvideo_codecs[0] = 0;\n\t\tg_snprintf(video_codecs, str_len, \"%s\", janus_videocodec_name(videoroom->vcodec[0]));\n\t\tif (videoroom->vcodec[1] != JANUS_VIDEOCODEC_NONE) {\n\t\t\tjanus_strlcat(video_codecs, split, str_len);\n\t\t\tjanus_strlcat(video_codecs, janus_videocodec_name(videoroom->vcodec[1]), str_len);\n\t\t}\n\t\tif (videoroom->vcodec[2] != JANUS_VIDEOCODEC_NONE) {\n\t\t\tjanus_strlcat(video_codecs, split, str_len);\n\t\t\tjanus_strlcat(video_codecs, janus_videocodec_name(videoroom->vcodec[2]), str_len);\n\t\t}\n\t\tif (videoroom->vcodec[3] != JANUS_VIDEOCODEC_NONE) {\n\t\t\tjanus_strlcat(video_codecs, split, str_len);\n\t\t\tjanus_strlcat(video_codecs, janus_videocodec_name(videoroom->vcodec[3]), str_len);\n\t\t}\n\t\tif (videoroom->vcodec[4] != JANUS_VIDEOCODEC_NONE) {\n\t\t\tjanus_strlcat(video_codecs, split, str_len);\n\t\t\tjanus_strlcat(video_codecs, janus_videocodec_name(videoroom->vcodec[4]), str_len);\n\t\t}\n\t}\n}\n\n/* Helper method to send PLI to publishers.\n * Send an PLI to local publisher and RTCP PLI to a remote publishers */\nstatic void janus_videoroom_reqpli(janus_videoroom_publisher_stream *ps, const char *reason) {\n\tif(ps == NULL || g_atomic_int_get(&ps->destroyed))\n\t\treturn;\n\tif(ps->publisher == NULL || g_atomic_int_get(&ps->publisher->destroyed))\n\t\treturn;\n\tjanus_videoroom_publisher *remote_publisher = NULL;\n\tif(ps->publisher->remote) {\n\t\tremote_publisher = ps->publisher;\n\t\tif(remote_publisher->remote_rtcp_fd < 0 || remote_publisher->rtcp_addr.ss_family == 0)\n\t\t\treturn;\n\t}\n\tif(!g_atomic_int_compare_and_exchange(&ps->sending_pli, 0, 1))\n\t\treturn;\n\tgint64 now = janus_get_monotonic_time();\n\tif(now - ps->pli_latest < G_USEC_PER_SEC) {\n\t\t/* We just sent a PLI less than a second ago, schedule a new delivery later */\n\t\tg_atomic_int_set(&ps->need_pli, 1);\n\t\tg_atomic_int_set(&ps->sending_pli, 0);\n\t\treturn;\n\t}\n\tJANUS_LOG(LOG_VERB, \"%s, sending PLI to %s (#%d, %s)\\n\", reason,\n\t\tps->publisher->user_id_str, ps->mindex, ps->publisher->display ? ps->publisher->display : \"??\");\n\tg_atomic_int_set(&ps->need_pli, 0);\n\tps->pli_latest = janus_get_monotonic_time();\n\t/* Update the time of when we last sent a keyframe request */\n\tps->fir_latest = ps->pli_latest;\n\tif(remote_publisher == NULL) {\n\t\tif(ps->publisher && ps->publisher->session && !g_atomic_int_get(&ps->publisher->session->destroyed) && ps->publisher->session->handle) {\n\t\t\t/* Local publisher so we ask the Janus core to send a PLI */\n\t\t\tgateway->send_pli_stream(ps->publisher->session->handle, ps->mindex);\n\t\t}\n\t} else {\n\t\t/* Generate a PLI */\n\t\tchar rtcp_buf[12];\n\t\tint rtcp_len = 12;\n\t\tjanus_rtcp_pli((char *)&rtcp_buf, rtcp_len);\n\t\tuint32_t ssrc = REMOTE_PUBLISHER_BASE_SSRC + (ps->mindex*REMOTE_PUBLISHER_SSRC_STEP);\n\t\tjanus_rtcp_fix_ssrc(NULL, rtcp_buf, rtcp_len, 1, 1, ssrc);\n\t\t/* Send the packet */\n\t\tsocklen_t addrlen = remote_publisher->rtcp_addr.ss_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);\n\t\tint sent = 0;\n\t\tif((sent = sendto(remote_publisher->remote_rtcp_fd, rtcp_buf, rtcp_len, 0,\n\t\t\t\t(struct sockaddr *)&remote_publisher->rtcp_addr, addrlen)) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error in sendto... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_HUGE, \"Sent %d/%d bytes\\n\", sent, rtcp_len);\n\t\t}\n\t}\n\tg_atomic_int_set(&ps->sending_pli, 0);\n}\n\n/* Error codes */\n#define JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR\t\t499\n#define JANUS_VIDEOROOM_ERROR_NO_MESSAGE\t\t421\n#define JANUS_VIDEOROOM_ERROR_INVALID_JSON\t\t422\n#define JANUS_VIDEOROOM_ERROR_INVALID_REQUEST\t423\n#define JANUS_VIDEOROOM_ERROR_JOIN_FIRST\t\t424\n#define JANUS_VIDEOROOM_ERROR_ALREADY_JOINED\t425\n#define JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM\t\t426\n#define JANUS_VIDEOROOM_ERROR_ROOM_EXISTS\t\t427\n#define JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED\t\t428\n#define JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT\t429\n#define JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT\t430\n#define JANUS_VIDEOROOM_ERROR_INVALID_SDP_TYPE\t431\n#define JANUS_VIDEOROOM_ERROR_PUBLISHERS_FULL\t432\n#define JANUS_VIDEOROOM_ERROR_UNAUTHORIZED\t\t433\n#define JANUS_VIDEOROOM_ERROR_ALREADY_PUBLISHED\t434\n#define JANUS_VIDEOROOM_ERROR_NOT_PUBLISHED\t\t435\n#define JANUS_VIDEOROOM_ERROR_ID_EXISTS\t\t\t436\n#define JANUS_VIDEOROOM_ERROR_INVALID_SDP\t\t437\n#define JANUS_VIDEOROOM_ERROR_INVALID_FEED\t\t438\n\n\n/* RTP forwarder helpers */\nstatic janus_rtp_forwarder *janus_videoroom_rtp_forwarder_add_helper(janus_videoroom_publisher *p,\n\t\tjanus_videoroom_publisher_stream *ps,\n\t\tconst gchar *host, int port, int rtcp_port, int pt, uint32_t ssrc,\n\t\tgboolean simulcast, int srtp_suite, const char *srtp_crypto,\n\t\tint substream, gboolean is_video, gboolean is_data) {\n\tif(!p || !ps || !host)\n\t\treturn NULL;\n\tjanus_refcount_increase(&p->ref);\n\tjanus_refcount_increase(&ps->ref);\n\t/* Create a new RTP forwarder */\n\tjanus_rtp_forwarder *rf = janus_rtp_forwarder_create(JANUS_VIDEOROOM_NAME, 0,\n\t\tp->udp_sock, host, port, ssrc, pt, srtp_suite, srtp_crypto, simulcast, substream, is_video, is_data);\n\tif(rf == NULL)\n\t\treturn NULL;\n\trf->source = ps;\n\tif(simulcast && ps->rid_extmap_id > 0)\n\t\trf->sim_context.rid_ext_id = ps->rid_extmap_id;\n\t/* Add the forwarder to the ones we have for the publisher stream */\n\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\tg_hash_table_insert(ps->rtp_forwarders, GUINT_TO_POINTER(rf->stream_id), rf);\n\tg_hash_table_insert(p->rtp_forwarders, GUINT_TO_POINTER(rf->stream_id), GUINT_TO_POINTER(rf->stream_id));\n\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t/* If we need to add RTCP too, do that now */\n\tif(rtcp_port > 0) {\n\t\tint res = janus_rtp_forwarder_add_rtcp(rf, rtcp_port, &janus_videoroom_rtp_forwarder_rtcp_receive);\n\t\tif(res < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Error adding RTCP support to new RTP forwarder (%d)...\\n\", res);\n\t\t}\n\t}\n\t/* Done */\n\tjanus_refcount_decrease(&ps->ref);\n\tjanus_refcount_decrease(&p->ref);\n\tJANUS_LOG(LOG_VERB, \"Added %s/%d rtp_forward to participant %s host: %s:%d stream_id: %\"SCNu32\"\\n\",\n\t\tis_data ? \"data\" : (is_video ? \"video\" : \"audio\"), substream, p->user_id_str, host, port, rf->stream_id);\n\treturn rf;\n}\n\nstatic json_t *janus_videoroom_rtp_forwarder_summary(janus_rtp_forwarder *f) {\n\tif(f == NULL)\n\t\treturn NULL;\n\tjson_t *json = json_object();\n\tjson_object_set_new(json, \"stream_id\", json_integer(f->stream_id));\n\tchar address[100];\n\tif(f->serv_addr.sin_family == AF_INET) {\n\t\tjson_object_set_new(json, \"host\", json_string(\n\t\t\tinet_ntop(AF_INET, &f->serv_addr.sin_addr, address, sizeof(address))));\n\t} else {\n\t\tjson_object_set_new(json, \"host\", json_string(\n\t\t\tinet_ntop(AF_INET6, &f->serv_addr6.sin6_addr, address, sizeof(address))));\n\t}\n\tjson_object_set_new(json, \"port\", json_integer(ntohs(f->serv_addr.sin_port)));\n\tif(f->is_data) {\n\t\tjson_object_set_new(json, \"type\", json_string(\"data\"));\n\t} else if(f->is_video) {\n\t\tjson_object_set_new(json, \"type\", json_string(\"video\"));\n\t\tif(f->local_rtcp_port > 0)\n\t\t\tjson_object_set_new(json, \"local_rtcp_port\", json_integer(f->local_rtcp_port));\n\t\tif(f->remote_rtcp_port > 0)\n\t\t\tjson_object_set_new(json, \"remote_rtcp_port\", json_integer(f->remote_rtcp_port));\n\t\tif(f->payload_type)\n\t\t\tjson_object_set_new(json, \"pt\", json_integer(f->payload_type));\n\t\tif(f->ssrc)\n\t\t\tjson_object_set_new(json, \"ssrc\", json_integer(f->ssrc));\n\t\tif(f->substream)\n\t\t\tjson_object_set_new(json, \"substream\", json_integer(f->substream));\n\t} else {\n\t\tjson_object_set_new(json, \"type\", json_string(\"audio\"));\n\t\tif(f->local_rtcp_port > 0)\n\t\t\tjson_object_set_new(json, \"local_rtcp_port\", json_integer(f->local_rtcp_port));\n\t\tif(f->remote_rtcp_port > 0)\n\t\t\tjson_object_set_new(json, \"remote_rtcp_port\", json_integer(f->remote_rtcp_port));\n\t\tif(f->payload_type)\n\t\t\tjson_object_set_new(json, \"pt\", json_integer(f->payload_type));\n\t\tif(f->ssrc)\n\t\t\tjson_object_set_new(json, \"ssrc\", json_integer(f->ssrc));\n\t}\n\tif(f->is_srtp)\n\t\tjson_object_set_new(json, \"srtp\", json_true());\n\treturn json;\n}\n\n/* Helper to create a dummy publisher, with placeholder streams for each supported codec */\nstatic void janus_videoroom_create_dummy_publisher(janus_videoroom *room, gboolean e2ee, GHashTable *streams) {\n\tif(room == NULL || !room->dummy_publisher)\n\t\treturn;\n\t/* We create a dummy session first, that's not actually bound to anything */\n\tjanus_videoroom_session *session = g_malloc0(sizeof(janus_videoroom_session));\n\tsession->handle = NULL;\n\tsession->participant_type = janus_videoroom_p_type_publisher;\n\tg_atomic_int_set(&session->started, 1);\n\tjanus_mutex_init(&session->mutex);\n\tjanus_refcount_init(&session->ref, janus_videoroom_session_free);\n\t/* We actually create a publisher instance, which has no associated session but looks like it's publishing */\n\tjanus_videoroom_publisher *publisher = g_malloc0(sizeof(janus_videoroom_publisher));\n\tpublisher->session = session;\n\tsession->participant = publisher;\n\tpublisher->room_id = room->room_id;\n\tpublisher->room_id_str = room->room_id_str ? g_strdup(room->room_id_str) : NULL;\n\tpublisher->room = room;\n\tpublisher->user_id = janus_random_uint64();\n\tchar user_id_num[30];\n\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, publisher->user_id);\n\tpublisher->user_id_str = g_strdup(user_id_num);\n\tpublisher->display = g_strdup(\"Dummy publisher\");\n\tpublisher->acodec = JANUS_AUDIOCODEC_NONE;\n\tpublisher->vcodec = JANUS_VIDEOCODEC_NONE;\n\tpublisher->dummy = TRUE;\n\tpublisher->e2ee = room->require_e2ee || e2ee;\n\tjanus_mutex_init(&publisher->subscribers_mutex);\n\tjanus_mutex_init(&publisher->own_subscriptions_mutex);\n\tpublisher->streams_byid = g_hash_table_new_full(NULL, NULL,\n\t\tNULL, (GDestroyNotify)janus_videoroom_publisher_stream_destroy);\n\tpublisher->streams_bymid = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_publisher_stream_unref);\n\tjanus_mutex_init(&publisher->streams_mutex);\n\tjanus_mutex_init(&publisher->rtp_forwarders_mutex);\n\tpublisher->remote_recipients = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_remote_recipient_free);\n\tpublisher->rtp_forwarders = g_hash_table_new(NULL, NULL);\n\tpublisher->udp_sock = -1;\n\tg_atomic_int_set(&publisher->destroyed, 0);\n\tjanus_mutex_init(&publisher->mutex);\n\tjanus_refcount_init(&publisher->ref, janus_videoroom_publisher_free);\n\t/* Now we create a separate publisher stream for each supported codec in the room */\n\tjanus_videoroom_publisher_stream *ps = NULL;\n\tint mindex = 0;\n\tint i=0;\n\tfor(i=0; i<5; i++) {\n\t\tif(room->acodec[i] == JANUS_AUDIOCODEC_NONE)\n\t\t\tcontinue;\n\t\tchar *fmtp = streams ? g_hash_table_lookup(streams, janus_audiocodec_name(room->acodec[i])) : NULL;\n\t\tif(streams != NULL && fmtp == NULL) {\n\t\t\t/* This codec is not in the dummy streams list, skip it */\n\t\t\tcontinue;\n\t\t}\n\t\tps = g_malloc0(sizeof(janus_videoroom_publisher_stream));\n\t\tps->type = JANUS_VIDEOROOM_MEDIA_AUDIO;\n\t\tps->mindex = mindex;\n\t\tchar mid[5];\n\t\tg_snprintf(mid, sizeof(mid), \"%d\", mindex);\n\t\tps->mid = g_strdup(mid);\n\t\tps->publisher = publisher;\n\t\tjanus_refcount_increase(&publisher->ref);\t/* Add a reference to the publisher */\n\t\tps->active = TRUE;\n\t\tps->acodec = room->acodec[i];\n\t\tps->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\tps->pt = janus_audiocodec_pt(ps->acodec);\n\t\tif(fmtp != NULL && strcmp(fmtp, \"none\")) {\n\t\t\t/* Parse the fmtp string to see what we support (opus only) */\n\t\t\tif(ps->acodec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\tif(strstr(fmtp, \"useinbandfec=1\") && room->do_opusfec)\n\t\t\t\t\tps->opusfec = TRUE;\n\t\t\t\tif(strstr(fmtp, \"usedtx=1\") && room->do_opusdtx)\n\t\t\t\t\tps->opusdtx = TRUE;\n\t\t\t\tif(strstr(fmtp, \"stereo=1\"))\n\t\t\t\t\tps->opusstereo = TRUE;\n\t\t\t}\n\t\t}\n\t\tps->min_delay = -1;\n\t\tps->max_delay = -1;\n\t\tg_atomic_int_set(&ps->destroyed, 0);\n\t\tjanus_refcount_init(&ps->ref, janus_videoroom_publisher_stream_free);\n\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the id-indexed hashtable */\n\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the mid-indexed hashtable */\n\t\tjanus_mutex_init(&ps->subscribers_mutex);\n\t\tjanus_mutex_init(&ps->rtp_forwarders_mutex);\n\t\tjanus_mutex_init(&ps->rid_mutex);\n\t\tps->rtp_forwarders = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\t\tpublisher->streams = g_list_append(publisher->streams, ps);\n\t\tg_hash_table_insert(publisher->streams_byid, GINT_TO_POINTER(ps->mindex), ps);\n\t\tg_hash_table_insert(publisher->streams_bymid, g_strdup(ps->mid), ps);\n\t\tmindex++;\n\t}\n\tfor(i=0; i<5; i++) {\n\t\tif(room->vcodec[i] == JANUS_VIDEOCODEC_NONE)\n\t\t\tcontinue;\n\t\tchar *fmtp = streams ? g_hash_table_lookup(streams, janus_videocodec_name(room->vcodec[i])) : NULL;\n\t\tif(streams != NULL && fmtp == NULL) {\n\t\t\t/* This codec is not in the dummy streams list, skip it */\n\t\t\tcontinue;\n\t\t}\n\t\tps = g_malloc0(sizeof(janus_videoroom_publisher_stream));\n\t\tps->type = JANUS_VIDEOROOM_MEDIA_VIDEO;\n\t\tps->mindex = mindex;\n\t\tchar mid[5];\n\t\tg_snprintf(mid, sizeof(mid), \"%d\", mindex);\n\t\tps->mid = g_strdup(mid);\n\t\tps->publisher = publisher;\n\t\tjanus_refcount_increase(&publisher->ref);\t/* Add a reference to the publisher */\n\t\tps->active = TRUE;\n\t\tps->acodec = JANUS_AUDIOCODEC_NONE;\n\t\tps->vcodec = room->vcodec[i];\n\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\tif(fmtp != NULL && strcmp(fmtp, \"none\")) {\n\t\t\t/* Parse the fmtp string to see what we support (H.264 and VP9 profiles only) */\n\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264)\n\t\t\t\tps->h264_profile = janus_sdp_get_video_profile(ps->vcodec, fmtp);\n\t\t\telse if(ps->vcodec == JANUS_VIDEOCODEC_VP9) {\n\t\t\t\tps->vp9_profile = janus_sdp_get_video_profile(ps->vcodec, fmtp);\n\t\t\t}\n\t\t}\n\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264 && ps->h264_profile == NULL && room->h264_profile != NULL)\n\t\t\tps->h264_profile = g_strdup(room->h264_profile);\n\t\telse if(ps->vcodec == JANUS_VIDEOCODEC_VP9 && ps->vp9_profile == NULL && room->vp9_profile != NULL)\n\t\t\tps->vp9_profile = g_strdup(room->vp9_profile);\n\t\tps->min_delay = -1;\n\t\tps->max_delay = -1;\n\t\tg_atomic_int_set(&ps->destroyed, 0);\n\t\tjanus_refcount_init(&ps->ref, janus_videoroom_publisher_stream_free);\n\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the id-indexed hashtable */\n\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the mid-indexed hashtable */\n\t\tjanus_mutex_init(&ps->subscribers_mutex);\n\t\tjanus_mutex_init(&ps->rtp_forwarders_mutex);\n\t\tjanus_mutex_init(&ps->rid_mutex);\n\t\tps->rtp_forwarders = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\t\tpublisher->streams = g_list_append(publisher->streams, ps);\n\t\tg_hash_table_insert(publisher->streams_byid, GINT_TO_POINTER(ps->mindex), ps);\n\t\tg_hash_table_insert(publisher->streams_bymid, g_strdup(ps->mid), ps);\n\t\tmindex++;\n\t}\n\t/* Done: add the dummy publisher to the list */\n\tjanus_refcount_increase(&publisher->session->ref);\n\tg_hash_table_insert(room->participants,\n\t\tstring_ids ? (gpointer)g_strdup(publisher->user_id_str) : (gpointer)janus_uint64_dup(publisher->user_id),\n\t\tpublisher);\n}\n\n/* Helpers for subscription streams */\nstatic janus_videoroom_subscriber_stream *janus_videoroom_subscriber_stream_add(janus_videoroom_subscriber *subscriber,\n\t\tjanus_videoroom_publisher_stream *ps, const char *crossrefid,\n\t\tgboolean legacy, gboolean do_audio, gboolean do_video, gboolean do_data) {\n\t/* If this is a legacy subscription (\"feed\"), use the deprecated properties */\n\tif(legacy && ((ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO && !do_audio) ||\n\t\t\t(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && !do_video) ||\n\t\t\t(ps->type == JANUS_VIDEOROOM_MEDIA_DATA && !do_data))) {\n\t\t/* Skip this */\n\t\tJANUS_LOG(LOG_WARN, \"Skipping %s stream (legacy subscription)\\n\", janus_videoroom_media_str(ps->type));\n\t\treturn NULL;\n\t}\n\t/* Allocate a new subscriber stream instance */\n\tjanus_videoroom_subscriber_stream *stream = g_malloc0(sizeof(janus_videoroom_subscriber_stream));\n\tstream->mindex = g_list_length(subscriber->streams);\n\tstream->crossrefid = g_strdup(crossrefid);\n\tstream->subscriber = subscriber;\n\tstream->publisher_streams = g_slist_append(stream->publisher_streams, ps);\n\t/* Copy properties from the source */\n\tstream->type = ps->type;\n\tstream->acodec = ps->acodec;\n\tstream->vcodec = ps->vcodec;\n\tif(stream->vcodec == JANUS_VIDEOCODEC_H264 && ps->h264_profile) {\n\t\tstream->h264_profile = g_strdup(ps->h264_profile);\n\t} else if(stream->vcodec == JANUS_VIDEOCODEC_VP9 && ps->vp9_profile) {\n\t\tstream->vp9_profile = g_strdup(ps->vp9_profile);\n\t}\n\tstream->pt = ps->pt;\n\tstream->opusfec = ps->opusfec;\n\tstream->min_delay = -1;\n\tstream->max_delay = -1;\n\tchar mid[5];\n\tg_snprintf(mid, sizeof(mid), \"%d\", stream->mindex);\n\tstream->mid = g_strdup(mid);\n\tif(subscriber->use_msid && ps->publisher && ps->publisher->user_id_str) {\n\t\t/* We set the stream ID to the publisher ID */\n\t\tstream->msid = g_strdup(ps->publisher->user_id_str);\n\t\t/* FIXME To keep things easier, we make the track ID the same as the mid */\n\t\tstream->mstid = g_strdup(stream->mid);\n\t}\n\tsubscriber->streams = g_list_append(subscriber->streams, stream);\n\tg_hash_table_insert(subscriber->streams_byid, GINT_TO_POINTER(stream->mindex), stream);\n\tg_hash_table_insert(subscriber->streams_bymid, g_strdup(stream->mid), stream);\n\t/* Initialize the stream */\n\tjanus_rtp_switching_context_reset(&stream->context);\n\tstream->send = TRUE;\n\tg_atomic_int_set(&stream->destroyed, 0);\n\tjanus_refcount_init(&stream->ref, janus_videoroom_subscriber_stream_free);\n\tjanus_refcount_increase(&stream->ref);\t/* This is for the mid-indexed hashtable */\n\tjanus_rtp_simulcasting_context_reset(&stream->sim_context);\n\tstream->sim_context.rid_ext_id = ps->rid_extmap_id;\n\tstream->sim_context.substream_target = 2;\n\tstream->sim_context.templayer_target = 2;\n\tjanus_vp8_simulcast_context_reset(&stream->vp8_context);\n\tjanus_rtp_svc_context_reset(&stream->svc_context);\n\tstream->svc_context.spatial_target = 2;\t/* FIXME Actually depends on the scalabilityMode */\n\tstream->svc_context.temporal_target = 2;\t/* FIXME Actually depends on the scalabilityMode */\n\tjanus_mutex_lock(&ps->subscribers_mutex);\n\tps->subscribers = g_slist_append(ps->subscribers, stream);\n\t/* If we're using helper threads, add the subscriber to one of those */\n\tif(subscriber->room && subscriber->room->helper_threads > 0) {\n\t\tint subscribers = -1;\n\t\tjanus_videoroom_helper *helper = NULL;\n\t\tGList *l = subscriber->room->threads;\n\t\twhile(l) {\n\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\tif(subscribers == -1 || (helper == NULL && ht->num_subscribers == 0) || ht->num_subscribers < subscribers) {\n\t\t\t\tsubscribers = ht->num_subscribers;\n\t\t\t\thelper = ht;\n\t\t\t}\n\t\t\tl = l->next;\n\t\t}\n\t\tjanus_mutex_lock(&helper->mutex);\n\t\tGList *list = g_hash_table_lookup(helper->subscribers, ps);\n\t\tlist = g_list_append(list, stream);\n\t\tg_hash_table_insert(helper->subscribers, ps, list);\n\t\thelper->num_subscribers++;\n\t\tJANUS_LOG(LOG_VERB, \"Added subscriber stream to helper thread #%d (%d subscribers)\\n\",\n\t\t\thelper->id, helper->num_subscribers);\n\t\tjanus_mutex_unlock(&helper->mutex);\n\t}\n\t/* The two streams reference each other */\n\tjanus_refcount_increase(&stream->ref);\n\tjanus_refcount_increase(&ps->ref);\n\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\treturn stream;\n}\n\nstatic janus_videoroom_subscriber_stream *janus_videoroom_subscriber_stream_add_or_replace(janus_videoroom_subscriber *subscriber,\n\t\tjanus_videoroom_publisher_stream *ps, const char *crossrefid) {\n\tif(subscriber == NULL || ps == NULL)\n\t\treturn NULL;\n\t/* First of all, let's check if there's an m-line we can reuse */\n\tgboolean found = FALSE;\n\tjanus_videoroom_subscriber_stream *stream = NULL;\n\tGList *temp = subscriber->streams;\n\twhile(temp) {\n\t\tstream = (janus_videoroom_subscriber_stream *)temp->data;\n\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\tjanus_videoroom_publisher_stream *stream_ps = stream->publisher_streams ? stream->publisher_streams->data : NULL;\n\t\tif(stream_ps != NULL && stream_ps->type == ps->type && stream->type == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t/* We already have a datachannel m-line, no need for others: just update the subscribers list */\n\t\t\tif(g_slist_find(ps->subscribers, stream) == NULL && g_slist_find(stream->publisher_streams, ps) == NULL) {\n\t\t\t\tps->subscribers = g_slist_append(ps->subscribers, stream);\n\t\t\t\tstream->publisher_streams = g_slist_append(stream->publisher_streams, ps);\n\t\t\t\t/* The two streams reference each other */\n\t\t\t\tjanus_refcount_increase(&stream->ref);\n\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t/* If we're using helper threads, add the subscriber to one of those */\n\t\t\t\tif(subscriber->room && subscriber->room->helper_threads > 0) {\n\t\t\t\t\tint subscribers = -1;\n\t\t\t\t\tjanus_videoroom_helper *helper = NULL;\n\t\t\t\t\tGList *l = subscriber->room->threads;\n\t\t\t\t\twhile(l) {\n\t\t\t\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\t\t\t\tif(subscribers == -1 || (helper == NULL && ht->num_subscribers == 0) || ht->num_subscribers < subscribers) {\n\t\t\t\t\t\t\tsubscribers = ht->num_subscribers;\n\t\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\t\tGList *list = g_hash_table_lookup(helper->subscribers, ps);\n\t\t\t\t\tlist = g_list_append(list, stream);\n\t\t\t\t\tg_hash_table_insert(helper->subscribers, ps, list);\n\t\t\t\t\thelper->num_subscribers++;\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Added subscriber stream to helper thread #%d (%d subscribers)\\n\",\n\t\t\t\t\t\thelper->id, helper->num_subscribers);\n\t\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\treturn NULL;\n\t\t}\n\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\tif(stream_ps == NULL && stream->type == ps->type) {\n\t\t\t/* There's an empty m-line of the right type, check if codecs match */\n\t\t\tif(stream->type == JANUS_VIDEOROOM_MEDIA_DATA ||\n\t\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO && stream->acodec == ps->acodec) ||\n\t\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO && stream->vcodec == ps->vcodec)) {\n\t\t\t\tfound = TRUE;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Reusing m-line %d for this subscription\\n\", stream->mindex);\n\t\t\t\tstream->opusfec = ps->opusfec;\n\t\t\t\tif(subscriber->use_msid && ps->publisher && ps->publisher->user_id_str) {\n\t\t\t\t\t/* Update the stream ID to the publisher ID */\n\t\t\t\t\tchar *msid = stream->msid;\n\t\t\t\t\tstream->msid = g_strdup(ps->publisher->user_id_str);\n\t\t\t\t\tg_free(msid);\n\t\t\t\t}\n\t\t\t\tstream->send = TRUE;\n\t\t\t\tjanus_rtp_simulcasting_context_reset(&stream->sim_context);\n\t\t\t\tif(ps->simulcast) {\n\t\t\t\t\tstream->sim_context.rid_ext_id = ps->rid_extmap_id;\n\t\t\t\t\tstream->sim_context.substream_target = 2;\n\t\t\t\t\tstream->sim_context.templayer_target = 2;\n\t\t\t\t}\n\t\t\t\tjanus_vp8_simulcast_context_reset(&stream->vp8_context);\n\t\t\t\tif(ps->svc) {\n\t\t\t\t\tjanus_rtp_svc_context_reset(&stream->svc_context);\n\t\t\t\t\tstream->svc_context.spatial_target = 2;\t\t/* FIXME Actually depends on the scalabilityMode */\n\t\t\t\t\tstream->svc_context.temporal_target = 2;\t/* FIXME Actually depends on the scalabilityMode */\n\t\t\t\t}\n\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\t\tif(g_slist_find(ps->subscribers, stream) == NULL && g_slist_find(stream->publisher_streams, ps) == NULL) {\n\t\t\t\t\tps->subscribers = g_slist_append(ps->subscribers, stream);\n\t\t\t\t\tstream->publisher_streams = g_slist_append(stream->publisher_streams, ps);\n\t\t\t\t\t/* The two streams reference each other */\n\t\t\t\t\tjanus_refcount_increase(&stream->ref);\n\t\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t\t/* If we're using helper threads, add the subscriber to one of those */\n\t\t\t\t\tif(subscriber->room && subscriber->room->helper_threads > 0) {\n\t\t\t\t\t\tint subscribers = -1;\n\t\t\t\t\t\tjanus_videoroom_helper *helper = NULL;\n\t\t\t\t\t\tGList *l = subscriber->room->threads;\n\t\t\t\t\t\twhile(l) {\n\t\t\t\t\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\t\t\t\t\tif(subscribers == -1 || (helper == NULL && ht->num_subscribers == 0) || ht->num_subscribers < subscribers) {\n\t\t\t\t\t\t\t\tsubscribers = ht->num_subscribers;\n\t\t\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\t\t\tGList *list = g_hash_table_lookup(helper->subscribers, ps);\n\t\t\t\t\t\tlist = g_list_append(list, stream);\n\t\t\t\t\t\tg_hash_table_insert(helper->subscribers, ps, list);\n\t\t\t\t\t\thelper->num_subscribers++;\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Added subscriber stream to helper thread #%d (%d subscribers)\\n\",\n\t\t\t\t\t\t\thelper->id, helper->num_subscribers);\n\t\t\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tif(found)  {\n\t\tg_free(stream->crossrefid);\n\t\tstream->crossrefid = g_strdup(crossrefid);\n\t\treturn stream;\n\t}\n\t/* We couldn't find any, add a new one */\n\treturn janus_videoroom_subscriber_stream_add(subscriber, ps, crossrefid, FALSE, FALSE, FALSE, FALSE);\n}\n\nstatic void janus_videoroom_subscriber_stream_remove(janus_videoroom_subscriber_stream *s,\n\t\tjanus_videoroom_publisher_stream *ps, gboolean lock_ps) {\n\tif(ps != NULL) {\n\t\t/* Unsubscribe from this stream in particular (datachannels can have multiple sources) */\n\t\tif(g_slist_find(s->publisher_streams, ps) != NULL) {\n\t\t\t/* Remove the subscription from the list of recipients */\n\t\t\tif(lock_ps)\n\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\tgboolean unref_ps = FALSE, unref_ss = FALSE;\n\t\t\tif(g_slist_find(s->publisher_streams, ps) != NULL) {\n\t\t\t\ts->publisher_streams = g_slist_remove(s->publisher_streams, ps);\n\t\t\t\tunref_ps = TRUE;\n\t\t\t\tif(s->publisher_streams == NULL)\n\t\t\t\t\tg_atomic_int_set(&s->ready, 0);\n\t\t\t}\n\t\t\ts->opusfec = FALSE;\n\t\t\tif(g_slist_find(ps->subscribers, s) != NULL) {\n\t\t\t\tps->subscribers = g_slist_remove(ps->subscribers, s);\n\t\t\t\tunref_ss = TRUE;\n\t\t\t}\n\t\t\t/* Remove the subscriber from the helper threads too, if any */\n\t\t\tif(s->subscriber && s->subscriber->room && s->subscriber->room->helper_threads > 0) {\n\t\t\t\tGList *l = s->subscriber->room->threads;\n\t\t\t\twhile(l) {\n\t\t\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\t\t\tjanus_mutex_lock(&ht->mutex);\n\t\t\t\t\tGList *list = g_hash_table_lookup(ht->subscribers, ps);\n\t\t\t\t\tif(g_list_find(list, s) != NULL) {\n\t\t\t\t\t\tht->num_subscribers--;\n\t\t\t\t\t\tlist = g_list_remove_all(list, s);\n\t\t\t\t\t\tg_hash_table_insert(ht->subscribers, ps, list);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Removing subscriber stream from helper thread #%d (%d subscribers)\\n\",\n\t\t\t\t\t\t\tht->id, ht->num_subscribers);\n\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\tl = l->next;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(lock_ps)\n\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t/* Unref the two streams, as they're not related anymore */\n\t\t\tif(unref_ps)\n\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\tif(unref_ss)\n\t\t\t\tjanus_refcount_decrease(&s->ref);\n\t\t}\n\t} else {\n\t\t/* Unsubscribe from all sources (which will be one for audio/video, potentially more for datachannels) */\n\t\twhile(s->publisher_streams) {\n\t\t\tps = s->publisher_streams->data;\n\t\t\tjanus_videoroom_subscriber_stream_remove(s, ps, lock_ps);\n\t\t}\n\t}\n}\n\nstatic json_t *janus_videoroom_subscriber_streams_summary(janus_videoroom_subscriber *subscriber, gboolean legacy, json_t *event) {\n\tjson_t *media = json_array();\n\tGList *temp = subscriber->streams;\n\twhile(temp) {\n\t\tjanus_videoroom_subscriber_stream *stream = (janus_videoroom_subscriber_stream *)temp->data;\n\t\tjanus_refcount_increase(&stream->ref);\n\t\tjanus_videoroom_publisher_stream *ps = stream->publisher_streams ? stream->publisher_streams->data : NULL;\n\t\tif(ps)\n\t\t\tjanus_refcount_increase(&ps->ref);\n\t\tjson_t *m = json_object();\n\t\tjson_object_set_new(m, \"type\", json_string(janus_videoroom_media_str(stream->type)));\n\t\tjson_object_set_new(m, \"active\", (ps || stream->type == JANUS_VIDEOROOM_MEDIA_DATA) ? json_true() : json_false());\n\t\tjson_object_set_new(m, \"mindex\", json_integer(stream->mindex));\n\t\tjson_object_set_new(m, \"mid\", json_string(stream->mid));\n\t\tif(stream->crossrefid)\n\t\t\tjson_object_set_new(m, \"crossrefid\", json_string(stream->crossrefid));\n\t\tjson_object_set_new(m, \"ready\", g_atomic_int_get(&stream->ready) ? json_true() : json_false());\n\t\tjson_object_set_new(m, \"send\", stream->send ? json_true() : json_false());\n\t\tif(ps && stream->type == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\tjson_object_set_new(m, \"sources\", json_integer(g_slist_length(stream->publisher_streams)));\n\t\t\tjson_t *ids = json_array();\n\t\t\tGSList *temp = stream->publisher_streams;\n\t\t\tjanus_videoroom_publisher_stream *dps = NULL;\n\t\t\twhile(temp) {\n\t\t\t\tdps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\tif(dps && dps->publisher)\n\t\t\t\t\tjson_array_append_new(ids, string_ids ? json_string(dps->publisher->user_id_str) : json_integer(dps->publisher->user_id));\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tjson_object_set_new(m, \"source_ids\", ids);\n\t\t} else if(ps && stream->type != JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\tif(ps->publisher) {\n\t\t\t\tjson_object_set_new(m, \"feed_id\", string_ids ? json_string(ps->publisher->user_id_str) : json_integer(ps->publisher->user_id));\n\t\t\t\tif(ps->publisher->display)\n\t\t\t\t\tjson_object_set_new(m, \"feed_display\", json_string(ps->publisher->display));\n\t\t\t\t/* If this is a legacy subscription, put the info in the generic part too */\n\t\t\t\tif(legacy && event) {\n\t\t\t\t\tjson_object_set_new(event, \"id\", string_ids ? json_string(ps->publisher->user_id_str) : json_integer(ps->publisher->user_id));\n\t\t\t\t\tif(ps->publisher->display)\n\t\t\t\t\t\tjson_object_set_new(event, \"display\", json_string(ps->publisher->display));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(ps->mid)\n\t\t\t\tjson_object_set_new(m, \"feed_mid\", json_string(ps->mid));\n\t\t\tif(ps->description)\n\t\t\t\tjson_object_set_new(m, \"feed_description\", json_string(ps->description));\n\t\t\tif(stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\tjson_object_set_new(m, \"codec\", json_string(janus_audiocodec_name(stream->acodec)));\n\t\t\t} else if(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\tjson_object_set_new(m, \"codec\", json_string(janus_videocodec_name(stream->vcodec)));\n\t\t\t\tif(stream->vcodec == JANUS_VIDEOCODEC_H264 && stream->h264_profile != NULL)\n\t\t\t\t\tjson_object_set_new(m, \"h264-profile\", json_string(stream->h264_profile));\n\t\t\t\tif(stream->vcodec == JANUS_VIDEOCODEC_VP9 && stream->vp9_profile != NULL)\n\t\t\t\t\tjson_object_set_new(m, \"vp9-profile\", json_string(stream->vp9_profile));\n\t\t\t\tif(stream->min_delay > -1 && stream->max_delay > -1) {\n\t\t\t\t\tjson_t *pd = json_object();\n\t\t\t\t\tjson_object_set_new(pd, \"min-delay\", json_integer(stream->min_delay));\n\t\t\t\t\tjson_object_set_new(pd, \"max-delay\", json_integer(stream->max_delay));\n\t\t\t\t\tjson_object_set_new(m, \"playout-delay\", pd);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(ps->simulcast) {\n\t\t\t\tjson_t *simulcast = json_object();\n\t\t\t\tjson_object_set_new(simulcast, \"substream\", json_integer(stream->sim_context.substream));\n\t\t\t\tjson_object_set_new(simulcast, \"substream-target\", json_integer(stream->sim_context.substream_target));\n\t\t\t\tjson_object_set_new(simulcast, \"temporal-layer\", json_integer(stream->sim_context.templayer));\n\t\t\t\tjson_object_set_new(simulcast, \"temporal-layer-target\", json_integer(stream->sim_context.templayer_target));\n\t\t\t\tif(stream->sim_context.drop_trigger > 0)\n\t\t\t\t\tjson_object_set_new(simulcast, \"fallback\", json_integer(stream->sim_context.drop_trigger));\n\t\t\t\tjson_object_set_new(m, \"simulcast\", simulcast);\n\t\t\t}\n\t\t\tif(ps->svc) {\n\t\t\t\tjson_t *svc = json_object();\n\t\t\t\tjson_object_set_new(svc, \"spatial-layer\", json_integer(stream->svc_context.spatial));\n\t\t\t\tjson_object_set_new(svc, \"target-spatial-layer\", json_integer(stream->svc_context.spatial_target));\n\t\t\t\tjson_object_set_new(svc, \"temporal-layer\", json_integer(stream->svc_context.temporal));\n\t\t\t\tjson_object_set_new(svc, \"target-temporal-layer\", json_integer(stream->svc_context.temporal_target));\n\t\t\t\tjson_object_set_new(m, \"svc\", svc);\n\t\t\t}\n\t\t}\n\t\tif(ps)\n\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\tjanus_refcount_decrease(&stream->ref);\n\t\tjson_array_append_new(media, m);\n\t\ttemp = temp->next;\n\t}\n\treturn media;\n}\n\n/* Helper to generate a new offer with the subscriber streams */\nstatic json_t *janus_videoroom_subscriber_offer(janus_videoroom_subscriber *subscriber) {\n\tg_atomic_int_set(&subscriber->answered, 0);\n\tchar s_name[100], audio_fmtp[256];\n\tg_snprintf(s_name, sizeof(s_name), \"VideoRoom %s\", subscriber->room->room_id_str);\n\tjanus_sdp *offer = janus_sdp_generate_offer(s_name, \"0.0.0.0\",\n\t\tJANUS_SDP_OA_DONE);\n\tGList *temp = subscriber->streams;\n\twhile(temp) {\n\t\tjanus_videoroom_subscriber_stream *stream = (janus_videoroom_subscriber_stream *)temp->data;\n\t\tjanus_videoroom_publisher_stream *ps = stream->publisher_streams ? stream->publisher_streams->data : NULL;\n\t\tint pt = -1;\n\t\tconst char *codec = NULL;\n\t\taudio_fmtp[0] = '\\0';\n\t\tif(ps && stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\tif(ps->opusfec)\n\t\t\t\tg_snprintf(audio_fmtp, sizeof(audio_fmtp), \"useinbandfec=1\");\n\t\t\tif(ps->opusdtx) {\n\t\t\t\tif(strlen(audio_fmtp) == 0) {\n\t\t\t\t\tg_snprintf(audio_fmtp, sizeof(audio_fmtp), \"usedtx=1\");\n\t\t\t\t} else {\n\t\t\t\t\tjanus_strlcat(audio_fmtp, \";usedtx=1\", sizeof(audio_fmtp));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(ps->opusstereo) {\n\t\t\t\tif(strlen(audio_fmtp) == 0) {\n\t\t\t\t\tg_snprintf(audio_fmtp, sizeof(audio_fmtp), \"stereo=1\");\n\t\t\t\t} else {\n\t\t\t\t\tjanus_strlcat(audio_fmtp, \";stereo=1\", sizeof(audio_fmtp));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(stream->type != JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\tpt = stream->pt;\n\t\t\tcodec = (stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO ?\n\t\t\t\tjanus_audiocodec_name(stream->acodec) : janus_videocodec_name(stream->vcodec));\n\t\t}\n\t\tgboolean add_msid = (subscriber->use_msid && ps && !ps->disabled);\n\t\tjanus_sdp_generate_offer_mline(offer,\n\t\t\tJANUS_SDP_OA_MLINE, janus_videoroom_media_sdptype(stream->type),\n\t\t\tJANUS_SDP_OA_MID, stream->mid,\n\t\t\tJANUS_SDP_OA_MSID, add_msid ? stream->msid : NULL, add_msid ? stream->mstid : NULL,\n\t\t\tJANUS_SDP_OA_PT, pt,\n\t\t\tJANUS_SDP_OA_CODEC, codec,\n\t\t\tJANUS_SDP_OA_FMTP, (stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO && strlen(audio_fmtp) ? audio_fmtp : NULL),\n\t\t\tJANUS_SDP_OA_H264_PROFILE, (stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO ? stream->h264_profile : NULL),\n\t\t\tJANUS_SDP_OA_VP9_PROFILE, (stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO ? stream->vp9_profile : NULL),\n\t\t\tJANUS_SDP_OA_DIRECTION, ((ps && !ps->disabled) || stream->type == JANUS_VIDEOROOM_MEDIA_DATA) ? JANUS_SDP_SENDONLY : JANUS_SDP_INACTIVE,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_AUDIO_LEVEL,\n\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO && (ps && ps->audio_level_extmap_id > 0)) ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_AUDIO_LEVEL) : 0,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_MID, janus_rtp_extension_id(JANUS_RTP_EXTMAP_MID),\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION,\n\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO && (ps && ps->video_orient_extmap_id > 0)) ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_VIDEO_ORIENTATION) : 0,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_PLAYOUT_DELAY,\n\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO && (ps && ps->playout_delay_extmap_id > 0)) ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_PLAYOUT_DELAY) : 0,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC,\n\t\t\t\tsubscriber->room->transport_wide_cc_ext ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC) : 0,\n\t\t\tJANUS_SDP_OA_EXTENSION, JANUS_RTP_EXTMAP_ABS_SEND_TIME,\n\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO) ? janus_rtp_extension_id(JANUS_RTP_EXTMAP_ABS_SEND_TIME) : 0,\n\t\t\t/* TODO Add other properties from original SDP */\n\t\t\tJANUS_SDP_OA_DONE);\n\t\ttemp = temp->next;\n\t}\n\t/* Update (or set) the SDP version */\n\tsubscriber->session->sdp_version++;\n\toffer->o_version = subscriber->session->sdp_version;\n\tchar *sdp = janus_sdp_write(offer);\n\tjanus_sdp_destroy(offer);\n\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", \"offer\", \"sdp\", sdp);\n\tif(subscriber->e2ee)\n\t\tjson_object_set_new(jsep, \"e2ee\", json_true());\n\tg_free(sdp);\n\t/* Done */\n\treturn jsep;\n}\n\n\n/* Plugin implementation */\nint janus_videoroom_init(janus_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_VIDEOROOM_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tconfig = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_VIDEOROOM_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_VIDEOROOM_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tconfig_folder = config_path;\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\n\tsessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_videoroom_session_destroy);\n\tmessages = g_async_queue_new_full((GDestroyNotify) janus_videoroom_message_free);\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Parse configuration to populate the rooms list */\n\tif(config != NULL) {\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\t/* Any admin key to limit who can \"create\"? */\n\t\tjanus_config_item *key = janus_config_get(config, config_general, janus_config_type_item, \"admin_key\");\n\t\tif(key != NULL && key->value != NULL)\n\t\t\tadmin_key = g_strdup(key->value);\n\t\tjanus_config_item *lrf = janus_config_get(config, config_general, janus_config_type_item, \"lock_rtp_forward\");\n\t\tif(admin_key && lrf != NULL && lrf->value != NULL)\n\t\t\tlock_rtpfwd = janus_is_true(lrf->value);\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_VIDEOROOM_NAME);\n\t\t}\n\t\tjanus_config_item *ids = janus_config_get(config, config_general, janus_config_type_item, \"string_ids\");\n\t\tif(ids != NULL && ids->value != NULL)\n\t\t\tstring_ids = janus_is_true(ids->value);\n\t\tif(string_ids) {\n\t\t\tJANUS_LOG(LOG_INFO, \"VideoRoom will use alphanumeric IDs, not numeric\\n\");\n\t\t}\n\t}\n\trooms = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_room_destroy);\n\t/* Iterate on all rooms */\n\tif(config != NULL) {\n\t\tGList *clist = janus_config_get_categories(config, NULL), *cl = clist;\n\t\twhile(cl != NULL) {\n\t\t\tjanus_config_category *cat = (janus_config_category *)cl->data;\n\t\t\tif(cat->name == NULL || !strcasecmp(cat->name, \"general\")) {\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Adding VideoRoom room '%s'\\n\", cat->name);\n\t\t\tjanus_config_item *desc = janus_config_get(config, cat, janus_config_type_item, \"description\");\n\t\t\tjanus_config_item *priv = janus_config_get(config, cat, janus_config_type_item, \"is_private\");\n\t\t\tjanus_config_item *secret = janus_config_get(config, cat, janus_config_type_item, \"secret\");\n\t\t\tjanus_config_item *pin = janus_config_get(config, cat, janus_config_type_item, \"pin\");\n\t\t\tjanus_config_item *req_pvtid = janus_config_get(config, cat, janus_config_type_item, \"require_pvtid\");\n\t\t\tjanus_config_item *signed_tokens = janus_config_get(config, cat, janus_config_type_item, \"signed_tokens\");\n\t\t\tjanus_config_item *bitrate = janus_config_get(config, cat, janus_config_type_item, \"bitrate\");\n\t\t\tjanus_config_item *bitrate_cap = janus_config_get(config, cat, janus_config_type_item, \"bitrate_cap\");\n\t\t\tjanus_config_item *maxp = janus_config_get(config, cat, janus_config_type_item, \"publishers\");\n\t\t\tjanus_config_item *firfreq = janus_config_get(config, cat, janus_config_type_item, \"fir_freq\");\n\t\t\tjanus_config_item *audiocodec = janus_config_get(config, cat, janus_config_type_item, \"audiocodec\");\n\t\t\tjanus_config_item *videocodec = janus_config_get(config, cat, janus_config_type_item, \"videocodec\");\n\t\t\tjanus_config_item *vp9profile = janus_config_get(config, cat, janus_config_type_item, \"vp9_profile\");\n\t\t\tjanus_config_item *h264profile = janus_config_get(config, cat, janus_config_type_item, \"h264_profile\");\n\t\t\tjanus_config_item *fec = janus_config_get(config, cat, janus_config_type_item, \"opus_fec\");\n\t\t\tjanus_config_item *dtx = janus_config_get(config, cat, janus_config_type_item, \"opus_dtx\");\n\t\t\tjanus_config_item *audiolevel_ext = janus_config_get(config, cat, janus_config_type_item, \"audiolevel_ext\");\n\t\t\tjanus_config_item *audiolevel_event = janus_config_get(config, cat, janus_config_type_item, \"audiolevel_event\");\n\t\t\tjanus_config_item *audio_active_packets = janus_config_get(config, cat, janus_config_type_item, \"audio_active_packets\");\n\t\t\tjanus_config_item *audio_level_average = janus_config_get(config, cat, janus_config_type_item, \"audio_level_average\");\n\t\t\tjanus_config_item *videoorient_ext = janus_config_get(config, cat, janus_config_type_item, \"videoorient_ext\");\n\t\t\tjanus_config_item *playoutdelay_ext = janus_config_get(config, cat, janus_config_type_item, \"playoutdelay_ext\");\n\t\t\tjanus_config_item *transport_wide_cc_ext = janus_config_get(config, cat, janus_config_type_item, \"transport_wide_cc_ext\");\n\t\t\tjanus_config_item *notify_joining = janus_config_get(config, cat, janus_config_type_item, \"notify_joining\");\n\t\t\tjanus_config_item *req_e2ee = janus_config_get(config, cat, janus_config_type_item, \"require_e2ee\");\n\t\t\tjanus_config_item *dummy_pub = janus_config_get(config, cat, janus_config_type_item, \"dummy_publisher\");\n\t\t\tjanus_config_item *dummy_str = janus_config_get(config, cat, janus_config_type_array, \"dummy_streams\");\n\t\t\tjanus_config_item *dummy_e2ee = janus_config_get(config, cat, janus_config_type_item, \"dummy_e2ee\");\n\t\t\tjanus_config_item *record = janus_config_get(config, cat, janus_config_type_item, \"record\");\n\t\t\tjanus_config_item *rec_dir = janus_config_get(config, cat, janus_config_type_item, \"rec_dir\");\n\t\t\tjanus_config_item *lock_record = janus_config_get(config, cat, janus_config_type_item, \"lock_record\");\n\t\t\tjanus_config_item *threads = janus_config_get(config, cat, janus_config_type_item, \"threads\");\n\t\t\t/* Create the video room */\n\t\t\tjanus_videoroom *videoroom = g_malloc0(sizeof(janus_videoroom));\n\t\t\tconst char *room_num = cat->name;\n\t\t\tif(strstr(room_num, \"room-\") == room_num)\n\t\t\t\troom_num += 5;\n\t\t\tif(!string_ids) {\n\t\t\t\tvideoroom->room_id = g_ascii_strtoull(room_num, NULL, 0);\n\t\t\t\tif(videoroom->room_id == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the VideoRoom room, invalid ID 0...\\n\");\n\t\t\t\t\tg_free(videoroom);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Make sure the ID is completely numeric */\n\t\t\t\tchar room_id_str[30];\n\t\t\t\tg_snprintf(room_id_str, sizeof(room_id_str), \"%\"SCNu64, videoroom->room_id);\n\t\t\t\tif(strcmp(room_num, room_id_str)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the VideoRoom room, ID '%s' is not numeric...\\n\", room_num);\n\t\t\t\t\tg_free(videoroom);\n\t\t\t\t\tcl = cl->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Let's make sure the room doesn't exist already */\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\tif(g_hash_table_lookup(rooms, string_ids ? (gpointer)room_num : (gpointer)&videoroom->room_id) != NULL) {\n\t\t\t\t/* It does... */\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't add the VideoRoom room, room %s already exists...\\n\", room_num);\n\t\t\t\tg_free(videoroom);\n\t\t\t\tcl = cl->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tvideoroom->room_id_str = g_strdup(room_num);\n\t\t\tchar *description = NULL;\n\t\t\tif(desc != NULL && desc->value != NULL && strlen(desc->value) > 0)\n\t\t\t\tdescription = g_strdup(desc->value);\n\t\t\telse\n\t\t\t\tdescription = g_strdup(cat->name);\n\t\t\tvideoroom->room_name = description;\n\t\t\tif(secret != NULL && secret->value != NULL) {\n\t\t\t\tvideoroom->room_secret = g_strdup(secret->value);\n\t\t\t}\n\t\t\tif(pin != NULL && pin->value != NULL) {\n\t\t\t\tvideoroom->room_pin = g_strdup(pin->value);\n\t\t\t}\n\t\t\tvideoroom->is_private = priv && priv->value && janus_is_true(priv->value);\n\t\t\tvideoroom->require_pvtid = req_pvtid && req_pvtid->value && janus_is_true(req_pvtid->value);\n\t\t\tif(signed_tokens && signed_tokens->value && janus_is_true(signed_tokens->value)) {\n\t\t\t\tif(!gateway->auth_is_signed()) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Can't enforce signed tokens for this room, signed-mode not in use in the core\\n\");\n\t\t\t\t} else {\n\t\t\t\t\tvideoroom->signed_tokens = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tvideoroom->require_e2ee = req_e2ee && req_e2ee->value && janus_is_true(req_e2ee->value);\n\t\t\tvideoroom->max_publishers = 3;\t/* FIXME How should we choose a default? */\n\t\t\tif(maxp != NULL && maxp->value != NULL)\n\t\t\t\tvideoroom->max_publishers = atol(maxp->value);\n\t\t\tif(videoroom->max_publishers < 0)\n\t\t\t\tvideoroom->max_publishers = 3;\t/* FIXME How should we choose a default? */\n\t\t\tvideoroom->bitrate = 0;\n\t\t\tif(bitrate != NULL && bitrate->value != NULL)\n\t\t\t\tvideoroom->bitrate = atol(bitrate->value);\n\t\t\tif(videoroom->bitrate > 0 && videoroom->bitrate < 64000)\n\t\t\t\tvideoroom->bitrate = 64000;\t/* Don't go below 64k */\n\t\t\tvideoroom->bitrate_cap = bitrate_cap && bitrate_cap->value && janus_is_true(bitrate_cap->value);\n\t\t\tvideoroom->fir_freq = 0;\n\t\t\tif(firfreq != NULL && firfreq->value != NULL)\n\t\t\t\tvideoroom->fir_freq = atol(firfreq->value);\n\t\t\t/* By default, we force Opus as the only audio codec */\n\t\t\tvideoroom->acodec[0] = JANUS_AUDIOCODEC_OPUS;\n\t\t\tvideoroom->acodec[1] = JANUS_AUDIOCODEC_NONE;\n\t\t\tvideoroom->acodec[2] = JANUS_AUDIOCODEC_NONE;\n\t\t\tvideoroom->acodec[3] = JANUS_AUDIOCODEC_NONE;\n\t\t\tvideoroom->acodec[4] = JANUS_AUDIOCODEC_NONE;\n\t\t\t/* Check if we're forcing a different single codec, or allowing more than one */\n\t\t\tif(audiocodec && audiocodec->value) {\n\t\t\t\tgchar **list = g_strsplit(audiocodec->value, \",\", 6);\n\t\t\t\tgchar *codec = list[0];\n\t\t\t\tif(codec != NULL) {\n\t\t\t\t\tint i=0;\n\t\t\t\t\twhile(codec != NULL) {\n\t\t\t\t\t\tif(i == 5) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring extra audio codecs: %s\\n\", codec);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(strlen(codec) > 0)\n\t\t\t\t\t\t\tvideoroom->acodec[i] = janus_audiocodec_from_name(codec);\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tcodec = list[i];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tg_clear_pointer(&list, g_strfreev);\n\t\t\t}\n\t\t\t/* By default, we force VP8 as the only video codec */\n\t\t\tvideoroom->vcodec[0] = JANUS_VIDEOCODEC_VP8;\n\t\t\tvideoroom->vcodec[1] = JANUS_VIDEOCODEC_NONE;\n\t\t\tvideoroom->vcodec[2] = JANUS_VIDEOCODEC_NONE;\n\t\t\tvideoroom->vcodec[3] = JANUS_VIDEOCODEC_NONE;\n\t\t\tvideoroom->vcodec[4] = JANUS_VIDEOCODEC_NONE;\n\t\t\t/* Check if we're forcing a different single codec, or allowing more than one */\n\t\t\tif(videocodec && videocodec->value) {\n\t\t\t\tgchar **list = g_strsplit(videocodec->value, \",\", 6);\n\t\t\t\tgchar *codec = list[0];\n\t\t\t\tif(codec != NULL) {\n\t\t\t\t\tint i=0;\n\t\t\t\t\twhile(codec != NULL) {\n\t\t\t\t\t\tif(i == 5) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring extra video codecs: %s\\n\", codec);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(strlen(codec) > 0)\n\t\t\t\t\t\t\tvideoroom->vcodec[i] = janus_videocodec_from_name(codec);\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tcodec = list[i];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tg_clear_pointer(&list, g_strfreev);\n\t\t\t}\n\t\t\tif(vp9profile && vp9profile->value && (videoroom->vcodec[0] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\t\tvideoroom->vcodec[1] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\t\tvideoroom->vcodec[2] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\t\tvideoroom->vcodec[3] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\t\tvideoroom->vcodec[4] == JANUS_VIDEOCODEC_VP9)) {\n\t\t\t\tvideoroom->vp9_profile = g_strdup(vp9profile->value);\n\t\t\t}\n\t\t\tif(h264profile && h264profile->value && (videoroom->vcodec[0] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\t\tvideoroom->vcodec[1] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\t\tvideoroom->vcodec[2] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\t\tvideoroom->vcodec[3] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\t\tvideoroom->vcodec[4] == JANUS_VIDEOCODEC_H264)) {\n\t\t\t\tvideoroom->h264_profile = g_strdup(h264profile->value);\n\t\t\t}\n\t\t\tvideoroom->do_opusfec = TRUE;\n\t\t\tif(fec && fec->value) {\n\t\t\t\tvideoroom->do_opusfec = janus_is_true(fec->value);\n\t\t\t\tif(videoroom->acodec[0] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[1] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[2] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[3] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[4] != JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\tvideoroom->do_opusfec = FALSE;\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Inband FEC is only supported for rooms that allow Opus: disabling it...\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(dtx && dtx->value) {\n\t\t\t\tvideoroom->do_opusdtx = janus_is_true(dtx->value);\n\t\t\t\tif(videoroom->acodec[0] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[1] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[2] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[3] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\t\tvideoroom->acodec[4] != JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\tvideoroom->do_opusdtx = FALSE;\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"DTX is only supported for rooms that allow Opus: disabling it...\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tvideoroom->audiolevel_ext = TRUE;\n\t\t\tif(audiolevel_ext != NULL && audiolevel_ext->value != NULL)\n\t\t\t\tvideoroom->audiolevel_ext = janus_is_true(audiolevel_ext->value);\n\t\t\tvideoroom->audiolevel_event = FALSE;\n\t\t\tif(audiolevel_event != NULL && audiolevel_event->value != NULL)\n\t\t\t\tvideoroom->audiolevel_event = janus_is_true(audiolevel_event->value);\n\t\t\tif(videoroom->audiolevel_event) {\n\t\t\t\tvideoroom->audio_active_packets = 100;\n\t\t\t\tif(audio_active_packets != NULL && audio_active_packets->value != NULL){\n\t\t\t\t\tif(atoi(audio_active_packets->value) > 0) {\n\t\t\t\t\t\tvideoroom->audio_active_packets = atoi(audio_active_packets->value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_active_packets value, using default: %d\\n\", videoroom->audio_active_packets);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tvideoroom->audio_level_average = 25;\n\t\t\t\tif(audio_level_average != NULL && audio_level_average->value != NULL) {\n\t\t\t\t\tif(atoi(audio_level_average->value) > 0) {\n\t\t\t\t\t\tvideoroom->audio_level_average = atoi(audio_level_average->value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_level_average value provided, using default: %d\\n\", videoroom->audio_level_average);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tvideoroom->videoorient_ext = TRUE;\n\t\t\tif(videoorient_ext != NULL && videoorient_ext->value != NULL)\n\t\t\t\tvideoroom->videoorient_ext = janus_is_true(videoorient_ext->value);\n\t\t\tvideoroom->playoutdelay_ext = TRUE;\n\t\t\tif(playoutdelay_ext != NULL && playoutdelay_ext->value != NULL)\n\t\t\t\tvideoroom->playoutdelay_ext = janus_is_true(playoutdelay_ext->value);\n\t\t\tvideoroom->transport_wide_cc_ext = TRUE;\n\t\t\tif(transport_wide_cc_ext != NULL && transport_wide_cc_ext->value != NULL)\n\t\t\t\tvideoroom->transport_wide_cc_ext = janus_is_true(transport_wide_cc_ext->value);\n\t\t\tif(record && record->value) {\n\t\t\t\tvideoroom->record = janus_is_true(record->value);\n\t\t\t}\n\t\t\tif(rec_dir && rec_dir->value) {\n\t\t\t\tvideoroom->rec_dir = g_strdup(rec_dir->value);\n\t\t\t}\n\t\t\tif(lock_record && lock_record->value) {\n\t\t\t\tvideoroom->lock_record = janus_is_true(lock_record->value);\n\t\t\t}\n\t\t\t/* By default, the VideoRoom plugin does not notify about participants simply joining the room.\n\t\t\t\tIt only notifies when the participant actually starts publishing media. */\n\t\t\tvideoroom->notify_joining = FALSE;\n\t\t\tif(notify_joining != NULL && notify_joining->value != NULL)\n\t\t\t\tvideoroom->notify_joining = janus_is_true(notify_joining->value);\n\t\t\tg_atomic_int_set(&videoroom->destroyed, 0);\n\t\t\tjanus_mutex_init(&videoroom->mutex);\n\t\t\tjanus_refcount_init(&videoroom->ref, janus_videoroom_room_free);\n\t\t\tvideoroom->participants = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_publisher_dereference);\n\t\t\tvideoroom->private_ids = g_hash_table_new(NULL, NULL);\n\t\t\tvideoroom->check_allowed = FALSE;\t/* Static rooms can't have an \"allowed\" list yet, no hooks to the configuration file */\n\t\t\tvideoroom->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\t\t/* Should we create a dummy participant for placeholder m-lines? */\n\t\t\tif(dummy_pub && dummy_pub->value && janus_is_true(dummy_pub->value)) {\n\t\t\t\tvideoroom->dummy_publisher = TRUE;\n\t\t\t\t/* Check if we only need a subset of codecs, and&/or a specific fmtp */\n\t\t\t\tGHashTable *dummy_streams = NULL;\n\t\t\t\tif(dummy_str != NULL) {\n\t\t\t\t\tGList *l = dummy_str->list;\n\t\t\t\t\twhile(l) {\n\t\t\t\t\t\tjanus_config_item *m = (janus_config_item *)l->data;\n\t\t\t\t\t\tif(m == NULL || m->type != janus_config_type_category) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid dummy stream item (not a category?), skipping in '%s'...\\n\", cat->name);\n\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_config_item *codec = janus_config_get(config, m, janus_config_type_item, \"codec\");\n\t\t\t\t\t\tif(codec == NULL || codec->value == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid dummy stream codec, skipping in '%s'...\\n\", cat->name);\n\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_config_item *fmtp = janus_config_get(config, m, janus_config_type_item, \"fmtp\");\n\t\t\t\t\t\tif(fmtp != NULL && fmtp->value == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid dummy stream fmtp, skipping in '%s'...\\n\", cat->name);\n\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(dummy_streams == NULL)\n\t\t\t\t\t\t\tdummy_streams = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free);\n\t\t\t\t\t\tg_hash_table_insert(dummy_streams, g_strdup(codec->value), g_strdup(fmtp ? fmtp->value : \"none\"));\n\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Create the dummy publisher */\n\t\t\t\tgboolean e2ee = dummy_e2ee && dummy_e2ee->value && janus_is_true(dummy_e2ee->value);\n\t\t\t\tjanus_videoroom_create_dummy_publisher(videoroom, e2ee, dummy_streams);\n\t\t\t\tif(dummy_streams != NULL)\n\t\t\t\t\tg_hash_table_destroy(dummy_streams);\n\t\t\t}\n\t\t\tif(threads && threads->value) {\n\t\t\t\tint helper_threads = atoi(threads->value);\n\t\t\t\tif(helper_threads < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid threads configuration '%d' in room '%s', ignoring...\\n\", helper_threads, cat->name);\n\t\t\t\t} else {\n\t\t\t\t\t/* If we need helper threads, spawn them now */\n\t\t\t\t\tvideoroom->helper_threads = helper_threads;\n\t\t\t\t\tif(helper_threads > 0) {\n\t\t\t\t\t\tGError *error = NULL;\n\t\t\t\t\t\tchar tname[16];\n\t\t\t\t\t\tint i=0;\n\t\t\t\t\t\tfor(i=0; i<helper_threads; i++) {\n\t\t\t\t\t\t\tjanus_videoroom_helper *helper = g_malloc0(sizeof(janus_videoroom_helper));\n\t\t\t\t\t\t\thelper->id = i+1;\n\t\t\t\t\t\t\thelper->room = videoroom;\n\t\t\t\t\t\t\thelper->subscribers = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\thelper->queued_packets = g_async_queue_new_full((GDestroyNotify)janus_videoroom_rtp_relay_packet_free);\n\t\t\t\t\t\t\tjanus_mutex_init(&helper->mutex);\n\t\t\t\t\t\t\tjanus_refcount_init(&helper->ref, janus_videoroom_helper_free);\n\t\t\t\t\t\t\t/* Spawn a thread and add references */\n\t\t\t\t\t\t\tg_snprintf(tname, sizeof(tname), \"vhelp %u-%s\", helper->id, videoroom->room_id_str);\n\t\t\t\t\t\t\tjanus_refcount_increase(&videoroom->ref);\n\t\t\t\t\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\t\t\t\t\thelper->thread = g_thread_try_new(tname, &janus_videoroom_helper_thread, helper, &error);\n\t\t\t\t\t\t\tif(error != NULL) {\n\t\t\t\t\t\t\t\t/* TODO Should this be a hard failure? */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the helper thread...\\n\",\n\t\t\t\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\t\t\t\t\t\tvideoroom->threads = g_list_append(videoroom->threads, helper);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\tg_hash_table_insert(rooms,\n\t\t\t\tstring_ids ? (gpointer)g_strdup(videoroom->room_id_str) : (gpointer)janus_uint64_dup(videoroom->room_id),\n\t\t\t\tvideoroom);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t/* Compute a list of the supported codecs for the summary */\n\t\t\tchar audio_codecs[100], video_codecs[100];\n\t\t\tjanus_videoroom_codecstr(videoroom, audio_codecs, video_codecs, sizeof(audio_codecs), \"|\");\n\t\t\tJANUS_LOG(LOG_VERB, \"Created VideoRoom: %s (%s, %s, %s/%s codecs, secret: %s, pin: %s, pvtid: %s)\\n\",\n\t\t\t\tvideoroom->room_id_str, videoroom->room_name,\n\t\t\t\tvideoroom->is_private ? \"private\" : \"public\",\n\t\t\t\taudio_codecs, video_codecs,\n\t\t\t\tvideoroom->room_secret ? videoroom->room_secret : \"no secret\",\n\t\t\t\tvideoroom->room_pin ? videoroom->room_pin : \"no pin\",\n\t\t\t\tvideoroom->require_pvtid ? \"required\" : \"optional\");\n\t\t\tif(videoroom->record) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Room is going to be recorded in %s\\n\",\n\t\t\t\t\tvideoroom->rec_dir ? videoroom->rec_dir : \"the current folder\");\n\t\t\t}\n\t\t\tif(videoroom->require_e2ee) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- All publishers MUST use end-to-end encryption\\n\");\n\t\t\t}\n\t\t\tif(videoroom->dummy_publisher) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- The room is going to have a dummy publisher for placeholder subscriptions\\n\");\n\t\t\t}\n\t\t\tcl = cl->next;\n\t\t}\n\t\tg_list_free(clist);\n\t\t/* Done: we keep the configuration file open in case we get a \"create\" or \"destroy\" with permanent=true */\n\t}\n\n\t/* Show available rooms */\n\tjanus_mutex_lock(&rooms_mutex);\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, rooms);\n\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_videoroom *vr = value;\n\t\t/* Compute a list of the supported codecs for the summary */\n\t\tchar audio_codecs[100], video_codecs[100];\n\t\tjanus_videoroom_codecstr(vr, audio_codecs, video_codecs, sizeof(audio_codecs), \"|\");\n\t\tJANUS_LOG(LOG_VERB, \"  ::: [%s][%s] %\"SCNu32\", max %d publishers, FIR frequency of %d seconds, %s audio codec(s), %s video codec(s)\\n\",\n\t\t\tvr->room_id_str, vr->room_name, vr->bitrate, vr->max_publishers, vr->fir_freq,\n\t\t\taudio_codecs, video_codecs);\n\t}\n\tjanus_mutex_unlock(&rooms_mutex);\n\n\t/* Finally, let's check if IPv6 is disabled, as we may need to know for forwarders */\n\tint fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\tif(fd < 0) {\n\t\tipv6_disabled = TRUE;\n\t} else {\n\t\tint v6only = 0;\n\t\tif(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)\n\t\t\tipv6_disabled = TRUE;\n\t}\n\tif(fd >= 0)\n\t\tclose(fd);\n\tif(ipv6_disabled) {\n\t\tJANUS_LOG(LOG_WARN, \"IPv6 disabled, will only create VideoRoom forwarders to IPv4 addresses\\n\");\n\t}\n\n\tg_atomic_int_set(&initialized, 1);\n\n\t/* Launch the thread that will handle incoming messages */\n\tGError *error = NULL;\n\thandler_thread = g_thread_try_new(\"videoroom handler\", janus_videoroom_handler, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the VideoRoom handler thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\tjanus_config_destroy(config);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_VIDEOROOM_NAME);\n\treturn 0;\n}\n\nvoid janus_videoroom_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_async_queue_push(messages, &exit_message);\n\tif(handler_thread != NULL) {\n\t\tg_thread_join(handler_thread);\n\t\thandler_thread = NULL;\n\t}\n\n\t/* FIXME We should destroy the sessions cleanly */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\tjanus_mutex_lock(&rooms_mutex);\n\tg_hash_table_destroy(rooms);\n\trooms = NULL;\n\tjanus_mutex_unlock(&rooms_mutex);\n\n\tg_async_queue_unref(messages);\n\tmessages = NULL;\n\n\tjanus_config_destroy(config);\n\tg_free(admin_key);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_VIDEOROOM_NAME);\n}\n\nint janus_videoroom_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_PLUGIN_API_VERSION;\n}\n\nint janus_videoroom_get_version(void) {\n\treturn JANUS_VIDEOROOM_VERSION;\n}\n\nconst char *janus_videoroom_get_version_string(void) {\n\treturn JANUS_VIDEOROOM_VERSION_STRING;\n}\n\nconst char *janus_videoroom_get_description(void) {\n\treturn JANUS_VIDEOROOM_DESCRIPTION;\n}\n\nconst char *janus_videoroom_get_name(void) {\n\treturn JANUS_VIDEOROOM_NAME;\n}\n\nconst char *janus_videoroom_get_author(void) {\n\treturn JANUS_VIDEOROOM_AUTHOR;\n}\n\nconst char *janus_videoroom_get_package(void) {\n\treturn JANUS_VIDEOROOM_PACKAGE;\n}\n\nstatic janus_videoroom_session *janus_videoroom_lookup_session(janus_plugin_session *handle) {\n\tjanus_videoroom_session *session = NULL;\n\tif (g_hash_table_contains(sessions, handle)) {\n\t\tsession = (janus_videoroom_session *)handle->plugin_handle;\n\t}\n\treturn session;\n}\n\nvoid janus_videoroom_create_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_videoroom_session *session = g_malloc0(sizeof(janus_videoroom_session));\n\tsession->handle = handle;\n\tsession->participant_type = janus_videoroom_p_type_none;\n\tsession->participant = NULL;\n\tg_atomic_int_set(&session->hangingup, 0);\n\tg_atomic_int_set(&session->destroyed, 0);\n\thandle->plugin_handle = session;\n\tjanus_mutex_init(&session->mutex);\n\tjanus_refcount_init(&session->ref, janus_videoroom_session_free);\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, handle, session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\treturn;\n}\n\nstatic janus_videoroom_publisher *janus_videoroom_session_get_publisher(janus_videoroom_session *session) {\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)session->participant;\n\tif(publisher)\n\t\tjanus_refcount_increase(&publisher->ref);\n\tjanus_mutex_unlock(&session->mutex);\n\treturn publisher;\n}\n\nstatic janus_videoroom_publisher *janus_videoroom_session_get_publisher_nodebug(janus_videoroom_session *session) {\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)session->participant;\n\tif(publisher)\n\t\tjanus_refcount_increase_nodebug(&publisher->ref);\n\tjanus_mutex_unlock(&session->mutex);\n\treturn publisher;\n}\n\nstatic janus_videoroom_subscriber *janus_videoroom_session_get_subscriber(janus_videoroom_session *session) {\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_videoroom_subscriber *subscriber = (janus_videoroom_subscriber *)session->participant;\n\tif(subscriber)\n\t\tjanus_refcount_increase(&subscriber->ref);\n\tjanus_mutex_unlock(&session->mutex);\n\treturn subscriber;\n}\n\nstatic janus_videoroom_subscriber *janus_videoroom_session_get_subscriber_nodebug(janus_videoroom_session *session) {\n\tjanus_mutex_lock(&session->mutex);\n\tjanus_videoroom_subscriber *subscriber = (janus_videoroom_subscriber *)session->participant;\n\tif(subscriber)\n\t\tjanus_refcount_increase_nodebug(&subscriber->ref);\n\tjanus_mutex_unlock(&session->mutex);\n\treturn subscriber;\n}\n\nstatic void janus_videoroom_notify_participants(janus_videoroom_publisher *participant, json_t *msg, gboolean notify_source_participant) {\n\t/* participant->room->mutex has to be locked. */\n\tif(participant->room == NULL)\n\t\treturn;\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, participant->room->participants);\n\twhile (participant->room && !g_atomic_int_get(&participant->room->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_videoroom_publisher *p = value;\n\t\tif(p && !g_atomic_int_get(&p->destroyed) && p->session && (p != participant || notify_source_participant) && !participant->dummy) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Notifying participant %s (%s)\\n\", p->user_id_str, p->display ? p->display : \"??\");\n\t\t\tint ret = gateway->push_event(p->session->handle, &janus_videoroom_plugin, NULL, msg, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t}\n\t}\n}\n\nstatic void janus_videoroom_notify_about_publisher(janus_videoroom_publisher *p, gboolean update) {\n\tif(p == NULL)\n\t\treturn;\n\t/* Notify all other participants that there's a new boy in town */\n\tjson_t *list = json_array();\n\tjson_t *pl = json_object();\n\tjson_object_set_new(pl, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\tif(p->display)\n\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\tif(p->metadata)\n\t\tjson_object_set_new(pl, \"metadata\", json_deep_copy(p->metadata));\n\t/* Add proper info on all the streams */\n\tgboolean audio_added = FALSE, video_added = FALSE;\n\tjson_t *media = json_array();\n\tGList *temp = p->streams;\n\twhile(temp) {\n\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"type\", json_string(janus_videoroom_media_str(ps->type)));\n\t\tjson_object_set_new(info, \"mindex\", json_integer(ps->mindex));\n\t\tjson_object_set_new(info, \"mid\", json_string(ps->mid));\n\t\tif(ps->disabled) {\n\t\t\tjson_object_set_new(info, \"disabled\", json_true());\n\t\t} else {\n\t\t\tif(ps->description)\n\t\t\t\tjson_object_set_new(info, \"description\", json_string(ps->description));\n\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t/* FIXME For backwards compatibility, we need audio_codec in the global info */\n\t\t\t\tif(!audio_added) {\n\t\t\t\t\taudio_added = TRUE;\n\t\t\t\t\tjson_object_set_new(pl, \"audio_codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t}\n\t\t\t\tif(ps->acodec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\tif(ps->opusstereo)\n\t\t\t\t\t\tjson_object_set_new(info, \"stereo\", json_true());\n\t\t\t\t\tif(ps->opusfec)\n\t\t\t\t\t\tjson_object_set_new(info, \"fec\", json_true());\n\t\t\t\t\tif(ps->opusdtx)\n\t\t\t\t\t\tjson_object_set_new(info, \"dtx\", json_true());\n\t\t\t\t}\n\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t/* FIXME For backwards compatibility, we need video_codec in the global info */\n\t\t\t\tif(!video_added) {\n\t\t\t\t\tvideo_added = TRUE;\n\t\t\t\t\tjson_object_set_new(pl, \"video_codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t}\n\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264 && ps->h264_profile != NULL)\n\t\t\t\t\tjson_object_set_new(info, \"h264_profile\", json_string(ps->h264_profile));\n\t\t\t\telse if(ps->vcodec == JANUS_VIDEOCODEC_VP9)\n\t\t\t\t\tjson_object_set_new(info, \"vp9_profile\", json_string(ps->vp9_profile));\n\t\t\t\tif(ps->muted)\n\t\t\t\t\tjson_object_set_new(info, \"moderated\", json_true());\n\t\t\t\tif(ps->simulcast)\n\t\t\t\t\tjson_object_set_new(info, \"simulcast\", json_true());\n\t\t\t\tif(ps->svc)\n\t\t\t\t\tjson_object_set_new(info, \"svc\", json_true());\n\t\t\t}\n\t\t}\n\t\tjson_array_append_new(media, info);\n\t\ttemp = temp->next;\n\t}\n\tjson_object_set_new(pl, \"streams\", media);\n\tjson_array_append_new(list, pl);\n\tjson_t *pub = json_object();\n\tjson_object_set_new(pub, \"videoroom\", json_string(\"event\"));\n\tjson_object_set_new(pub, \"room\", string_ids ? json_string(p->room_id_str) : json_integer(p->room_id));\n\tjson_object_set_new(pub, \"publishers\", list);\n \tjanus_videoroom *room = p->room;\n \tif(room && !g_atomic_int_get(&room->destroyed)) {\n \t\tjanus_refcount_increase(&room->ref);\n\t\tjanus_videoroom_notify_participants(p, pub, FALSE);\n \t\tjanus_refcount_decrease(&room->ref);\n\t}\n\tjson_decref(pub);\n\t/* Also notify event handlers */\n\tif(notify_events && gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(update ? \"updated\" : \"published\"));\n\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(p->room_id_str) : json_integer(p->room_id));\n\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\tif(p->display)\n\t\t\t\tjson_object_set_new(info, \"display\", json_string(p->display));\n\t\tif(p->metadata)\n\t\t\t\tjson_object_set_new(info, \"metadata\", json_deep_copy(p->metadata));\n\t\tjson_t *media = json_array();\n\t\tGList *temp = p->streams;\n\t\twhile(temp) {\n\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\tjson_t *mediainfo = json_object();\n\t\t\tjson_object_set_new(mediainfo, \"type\", json_string(janus_videoroom_media_str(ps->type)));\n\t\t\tjson_object_set_new(mediainfo, \"mindex\", json_integer(ps->mindex));\n\t\t\tjson_object_set_new(mediainfo, \"mid\", json_string(ps->mid));\n\t\t\tif(ps->disabled) {\n\t\t\t\tjson_object_set_new(mediainfo, \"disabled\", json_true());\n\t\t\t} else {\n\t\t\t\tif(ps->description)\n\t\t\t\t\tjson_object_set_new(mediainfo, \"description\", json_string(ps->description));\n\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\tjson_object_set_new(mediainfo, \"codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\tjson_object_set_new(mediainfo, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\tif(ps->muted)\n\t\t\t\t\t\tjson_object_set_new(mediainfo, \"moderated\", json_true());\n\t\t\t\t\tif(ps->simulcast)\n\t\t\t\t\t\tjson_object_set_new(mediainfo, \"simulcast\", json_true());\n\t\t\t\t\tif(ps->svc)\n\t\t\t\t\t\tjson_object_set_new(mediainfo, \"svc\", json_true());\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_array_append_new(media, mediainfo);\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tjson_object_set_new(info, \"streams\", media);\n\t\tgateway->notify_event(&janus_videoroom_plugin, p->session->handle, info);\n\t}\n}\n\nstatic void janus_videoroom_participant_joining(janus_videoroom_publisher *p) {\n\t/* we need to check if the room still exists, may have been destroyed already */\n\tif(p->room == NULL)\n\t\treturn;\n\tif(!g_atomic_int_get(&p->room->destroyed) && p->room->notify_joining) {\n\t\tjson_t *event = json_object();\n\t\tjson_t *user = json_object();\n\t\tjson_object_set_new(user, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\tif (p->display) {\n\t\t\tjson_object_set_new(user, \"display\", json_string(p->display));\n\t\t}\n\t\tif (p->metadata) {\n\t\t\tjson_object_set_new(user, \"metadata\", json_deep_copy(p->metadata));\n\t\t}\n\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(p->room_id_str) : json_integer(p->room_id));\n\t\tjson_object_set_new(event, \"joining\", user);\n\t\tjanus_videoroom_notify_participants(p, event, FALSE);\n\t\t/* user gets deref-ed by the owner event */\n\t\tjson_decref(event);\n\t}\n}\n\nstatic void janus_videoroom_leave_or_unpublish(janus_videoroom_publisher *participant, gboolean is_leaving, gboolean kicked) {\n\t/* We need to check if the room still exists, may have been destroyed already */\n\tif(participant->room == NULL || participant->dummy)\n\t\treturn;\n\tjanus_mutex_lock(&rooms_mutex);\n\tif(!g_hash_table_lookup(rooms, string_ids ? (gpointer)participant->room_id_str : (gpointer)&participant->room_id)) {\n\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", participant->room_id_str);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\treturn;\n\t}\n\tjanus_videoroom *room = participant->room;\n\tif(!room || g_atomic_int_get(&room->destroyed)) {\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&room->ref);\n\tjanus_mutex_unlock(&rooms_mutex);\n\tjanus_mutex_lock(&room->mutex);\n\tif (!participant->room) {\n\t\tjanus_mutex_unlock(&room->mutex);\n\t\tjanus_refcount_decrease(&room->ref);\n\t\treturn;\n\t}\n\tjson_t *event = json_object();\n\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\tjson_object_set_new(event, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\tif(participant->display)\n\t\tjson_object_set_new(event, \"display\", json_string(participant->display));\n\tif(participant->metadata)\n\t\tjson_object_set_new(event, \"metadata\", json_deep_copy(participant->metadata));\n\tjson_object_set_new(event, is_leaving ? (kicked ? \"kicked\" : \"leaving\") : \"unpublished\",\n\t\tstring_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\tjanus_videoroom_notify_participants(participant, event, FALSE);\n\t/* Also notify event handlers */\n\tif(notify_events && gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(is_leaving ? (kicked ? \"kicked\" : \"leaving\") : \"unpublished\"));\n\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\tif(participant->display)\n\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\tif(participant->metadata)\n\t\t\tjson_object_set_new(info, \"metadata\", json_deep_copy(participant->metadata));\n\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t}\n\tif(is_leaving) {\n\t\tg_hash_table_remove(participant->room->participants,\n\t\t\tstring_ids ? (gpointer)participant->user_id_str : (gpointer)&participant->user_id);\n\t\tg_hash_table_remove(participant->room->private_ids, GUINT_TO_POINTER(participant->pvt_id));\n\t\tjanus_mutex_lock(&participant->mutex);\n\t\tg_clear_pointer(&participant->room, janus_videoroom_room_dereference);\n\t\tjanus_mutex_unlock(&participant->mutex);\n\t}\n\tjanus_mutex_unlock(&room->mutex);\n\tjanus_refcount_decrease(&room->ref);\n\tjson_decref(event);\n}\n\nvoid janus_videoroom_destroy_session(janus_plugin_session *handle, int *error) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\t*error = -1;\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videoroom_session *session = janus_videoroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No VideoRoom session associated with this handle...\\n\");\n\t\t*error = -2;\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_WARN, \"VideoRoom session already marked as destroyed...\\n\");\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tg_hash_table_remove(sessions, handle);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Any related WebRTC PeerConnection is not available anymore either */\n\tjanus_videoroom_hangup_media_internal(session);\n\t/* Cleaning up and removing the session is done in a lazy way */\n\tif(session->participant_type == janus_videoroom_p_type_publisher) {\n\t\t/* Get rid of publisher */\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tjanus_videoroom_publisher *p = (janus_videoroom_publisher *)session->participant;\n\t\tif(p)\n\t\t\tjanus_refcount_increase(&p->ref);\n\t\tsession->participant = NULL;\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\tif(p && p->room) {\n\t\t\tjanus_videoroom_leave_or_unpublish(p, TRUE, FALSE);\n\t\t}\n\t\tjanus_videoroom_publisher_destroy(p);\n\t\tif(p)\n\t\t\tjanus_refcount_decrease(&p->ref);\n\t} else if(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tjanus_videoroom_subscriber *s = (janus_videoroom_subscriber *)session->participant;\n\t\tif(s)\n\t\t\tjanus_refcount_increase(&s->ref);\n\t\tsession->participant = NULL;\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\tif(s && s->room) {\n\t\t\tif(s->pvt_id > 0) {\n\t\t\t\tjanus_mutex_lock(&s->room->mutex);\n\t\t\t\tjanus_videoroom_publisher *owner = g_hash_table_lookup(s->room->private_ids, GUINT_TO_POINTER(s->pvt_id));\n\t\t\t\tif(owner != NULL) {\n\t\t\t\t\tjanus_mutex_lock(&owner->subscribers_mutex);\n\t\t\t\t\t/* Note: we should refcount these subscription-publisher mappings as well */\n\t\t\t\t\towner->subscriptions = g_slist_remove(owner->subscriptions, s);\n\t\t\t\t\tjanus_mutex_unlock(&owner->subscribers_mutex);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&s->room->mutex);\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&s->room->ref);\n\t\t}\n\t\tjanus_videoroom_subscriber_destroy(s);\n\t\tif(s)\n\t\t\tjanus_refcount_decrease(&s->ref);\n\t}\n\tjanus_refcount_decrease(&session->ref);\n\treturn;\n}\n\njson_t *janus_videoroom_query_session(janus_plugin_session *handle) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videoroom_session *session = janus_videoroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Show the participant/room info, if any */\n\tjson_t *info = json_object();\n\tif(session->participant) {\n\t\tif(session->participant_type == janus_videoroom_p_type_none) {\n\t\t\tjson_object_set_new(info, \"type\", json_string(\"none\"));\n\t\t} else if(session->participant_type == janus_videoroom_p_type_publisher) {\n\t\t\tjson_object_set_new(info, \"type\", json_string(\"publisher\"));\n\t\t\tjanus_videoroom_publisher *participant = janus_videoroom_session_get_publisher(session);\n\t\t\tif(participant && participant->room) {\n\t\t\t\tjanus_videoroom *room = participant->room;\n\t\t\t\tjson_object_set_new(info, \"room\", room ?\n\t\t\t\t\t(string_ids ? json_string(room->room_id_str) : json_integer(room->room_id)) : NULL);\n\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(participant->pvt_id));\n\t\t\t\tif(participant->display)\n\t\t\t\t\tjson_object_set_new(info, \"display\", json_string(participant->display));\n\t\t\t\tif(participant->metadata)\n\t\t\t\t\tjson_object_set_new(info, \"metadata\", json_deep_copy(participant->metadata));\n\t\t\t\t/* TODO Fix the summary of viewers, since everything is stream based now */\n\t\t\t\t//~ if(participant->subscribers)\n\t\t\t\t\t//~ json_object_set_new(info, \"viewers\", json_integer(g_slist_length(participant->subscribers)));\n\t\t\t\tjson_object_set_new(info, \"bitrate\", json_integer(participant->bitrate));\n\t\t\t\tif(participant->e2ee)\n\t\t\t\t\tjson_object_set_new(info, \"e2ee\", json_true());\n\t\t\t\tjson_t *media = json_array();\n\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\tGList *temp = participant->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t\tjson_t *m = json_object();\n\t\t\t\t\tjson_object_set_new(m, \"type\", json_string(janus_videoroom_media_str(ps->type)));\n\t\t\t\t\tjson_object_set_new(m, \"mindex\", json_integer(ps->mindex));\n\t\t\t\t\tjson_object_set_new(m, \"mid\", json_string(ps->mid));\n\t\t\t\t\tif(ps->description)\n\t\t\t\t\t\tjson_object_set_new(m, \"description\", json_string(ps->description));\n\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\t\tjson_object_set_new(m, \"codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\tjson_object_set_new(m, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264 && ps->h264_profile != NULL)\n\t\t\t\t\t\t\tjson_object_set_new(m, \"h264-profile\", json_string(ps->h264_profile));\n\t\t\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_VP9 && ps->vp9_profile != NULL)\n\t\t\t\t\t\t\tjson_object_set_new(m, \"vp9-profile\", json_string(ps->vp9_profile));\n\t\t\t\t\t\tif(ps->min_delay > -1 && ps->max_delay > -1) {\n\t\t\t\t\t\t\tjson_t *pd = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(pd, \"min-delay\", json_integer(ps->min_delay));\n\t\t\t\t\t\t\tjson_object_set_new(pd, \"max-delay\", json_integer(ps->max_delay));\n\t\t\t\t\t\t\tjson_object_set_new(m, \"playout-delay\", pd);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(ps->simulcast)\n\t\t\t\t\t\tjson_object_set_new(m, \"simulcast\", json_true());\n\t\t\t\t\tif(ps->svc)\n\t\t\t\t\t\tjson_object_set_new(m, \"svc\", json_true());\n\t\t\t\t\tif(ps->rc && ps->rc->filename)\n\t\t\t\t\t\tjson_object_set_new(m, \"recording\", json_string(ps->rc->filename));\n\t\t\t\t\tif(ps->audio_level_extmap_id > 0) {\n\t\t\t\t\t\tjson_object_set_new(m, \"audio-level-dBov\", json_integer(ps->audio_dBov_level));\n\t\t\t\t\t\tjson_object_set_new(m, \"talking\", ps->talking ? json_true() : json_false());\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\t\t\tjson_object_set_new(m, \"subscribers\", json_integer(g_slist_length(ps->subscribers)));\n\t\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\t\tjson_array_append_new(media, m);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\tjson_object_set_new(info, \"streams\", media);\n\t\t\t}\n\t\t\tif(participant != NULL)\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t} else if(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\t\tjson_object_set_new(info, \"type\", json_string(\"subscriber\"));\n\t\t\tjanus_videoroom_subscriber *participant = janus_videoroom_session_get_subscriber(session);\n\t\t\tif(participant && participant->room) {\n\t\t\t\tjanus_videoroom *room = participant->room;\n\t\t\t\tjson_object_set_new(info, \"room\", room ?\n\t\t\t\t\t(string_ids ? json_string(room->room_id_str) : json_integer(room->room_id)) : NULL);\n\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(participant->pvt_id));\n\t\t\t\tjson_object_set_new(info, \"answered\", g_atomic_int_get(&participant->answered) ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"pending_offer\", g_atomic_int_get(&participant->pending_offer) ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"pending_restart\", g_atomic_int_get(&participant->pending_restart) ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"paused\", participant->paused ? json_true() : json_false());\n\t\t\t\tif(participant->e2ee)\n\t\t\t\t\tjson_object_set_new(info, \"e2ee\", json_true());\n\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(participant, FALSE, NULL);\n\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\tjson_object_set_new(info, \"streams\", media);\n\t\t\t}\n\t\t\tif(participant)\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t}\n\t}\n\tjson_object_set_new(info, \"hangingup\", json_integer(g_atomic_int_get(&session->hangingup)));\n\tjson_object_set_new(info, \"destroyed\", json_integer(g_atomic_int_get(&session->destroyed)));\n\tjanus_refcount_decrease(&session->ref);\n\treturn info;\n}\n\nstatic int janus_videoroom_access_room(json_t *root, gboolean check_modify, gboolean check_join, janus_videoroom **videoroom, char *error_cause, int error_cause_size) {\n\t/* rooms_mutex has to be locked */\n\tint error_code = 0;\n\tjson_t *room = json_object_get(root, \"room\");\n\tguint64 room_id = 0;\n\tchar room_id_num[30], *room_id_str = NULL;\n\tif(!string_ids) {\n\t\troom_id = json_integer_value(room);\n\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\troom_id_str = room_id_num;\n\t} else {\n\t\troom_id_str = (char *)json_string_value(room);\n\t}\n\t*videoroom = g_hash_table_lookup(rooms,\n\t\tstring_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\tif(*videoroom == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\tif(error_cause)\n\t\t\tg_snprintf(error_cause, error_cause_size, \"No such room (%s)\", room_id_str);\n\t\treturn error_code;\n\t}\n\tif(g_atomic_int_get(&((*videoroom)->destroyed))) {\n\t\tJANUS_LOG(LOG_ERR, \"No such room (%s)\\n\", room_id_str);\n\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\tif(error_cause)\n\t\t\tg_snprintf(error_cause, error_cause_size, \"No such room (%s)\", room_id_str);\n\t\treturn error_code;\n\t}\n\tif(check_modify) {\n\t\tchar error_cause2[100];\n\t\tJANUS_CHECK_SECRET((*videoroom)->room_secret, root, \"secret\", error_code, error_cause2,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tg_strlcpy(error_cause, error_cause2, error_cause_size);\n\t\t\treturn error_code;\n\t\t}\n\t}\n\tif(check_join) {\n\t\tchar error_cause2[100];\n\t\t/* Signed tokens are enforced, so they precede any pin validation */\n\t\tif(gateway->auth_is_signed() && (*videoroom)->signed_tokens) {\n\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\tchar room_descriptor[100];\n\t\t\tg_snprintf(room_descriptor, sizeof(room_descriptor), \"room=%s\", room_id_str);\n\t\t\tif(!gateway->auth_signature_contains(&janus_videoroom_plugin, json_string_value(token), room_descriptor)) {\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED;\n\t\t\t\tif(error_cause)\n\t\t\t\t\tg_snprintf(error_cause, error_cause_size, \"Unauthorized (wrong token)\");\n\t\t\t\treturn error_code;\n\t\t\t}\n\t\t}\n\t\tJANUS_CHECK_SECRET((*videoroom)->room_pin, root, \"pin\", error_code, error_cause2,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\tif(error_code != 0) {\n\t\t\tg_strlcpy(error_cause, error_cause2, error_cause_size);\n\t\t\treturn error_code;\n\t\t}\n\t}\n\treturn 0;\n}\n\n/* Helper method to process synchronous requests */\nstatic json_t *janus_videoroom_process_synchronous_request(janus_videoroom_session *session, json_t *message) {\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\n\t/* Parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\n\tif(!strcasecmp(request_text, \"create\")) {\n\t\t/* Create a new VideoRoom */\n\t\tJANUS_LOG(LOG_VERB, \"Creating a new VideoRoom room\\n\");\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, create_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomopt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstropt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\tjson_t *desc = json_object_get(root, \"description\");\n\t\tjson_t *is_private = json_object_get(root, \"is_private\");\n\t\tjson_t *req_pvtid = json_object_get(root, \"require_pvtid\");\n\t\tjson_t *signed_tokens = json_object_get(root, \"signed_tokens\");\n\t\tjson_t *req_e2ee = json_object_get(root, \"require_e2ee\");\n\t\tjson_t *dummy_pub = json_object_get(root, \"dummy_publisher\");\n\t\tjson_t *dummy_str = json_object_get(root, \"dummy_streams\");\n\t\tjson_t *dummy_e2ee = json_object_get(root, \"dummy_e2ee\");\n\t\tjson_t *threads = json_object_get(root, \"threads\");\n\t\tjson_t *secret = json_object_get(root, \"secret\");\n\t\tjson_t *pin = json_object_get(root, \"pin\");\n\t\tjson_t *bitrate = json_object_get(root, \"bitrate\");\n\t\tjson_t *bitrate_cap = json_object_get(root, \"bitrate_cap\");\n\t\tjson_t *fir_freq = json_object_get(root, \"fir_freq\");\n\t\tjson_t *publishers = json_object_get(root, \"publishers\");\n\t\tjson_t *allowed = json_object_get(root, \"allowed\");\n\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\tif(audiocodec) {\n\t\t\tconst char *audiocodec_value = json_string_value(audiocodec);\n\t\t\tgchar **list = g_strsplit(audiocodec_value, \",\", 6);\n\t\t\tgchar *codec = list[0];\n\t\t\tif(codec != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(codec != NULL) {\n\t\t\t\t\tif(i == 5) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(strlen(codec) == 0 || JANUS_AUDIOCODEC_NONE == janus_audiocodec_from_name(codec)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (audiocodec can only be or contain opus, isac32, isac16, pcmu, pcma or g722)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (audiocodec can only be or contain opus, isac32, isac16, pcmu, pcma or g722)\");\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tcodec = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_clear_pointer(&list, g_strfreev);\n\t\t}\n\t\tjson_t *videocodec = json_object_get(root, \"videocodec\");\n\t\tif(videocodec) {\n\t\t\tconst char *videocodec_value = json_string_value(videocodec);\n\t\t\tgchar **list = g_strsplit(videocodec_value, \",\", 6);\n\t\t\tgchar *codec = list[0];\n\t\t\tif(codec != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(codec != NULL) {\n\t\t\t\t\tif(i == 5) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(strlen(codec) == 0 || JANUS_VIDEOCODEC_NONE == janus_videocodec_from_name(codec)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (videocodec can only be or contain vp8, vp9, h264, av1 or h265)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (videocodec can only be or contain vp8, vp9, av1, h264 or h265)\");\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tcodec = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_clear_pointer(&list, g_strfreev);\n\t\t}\n\t\tjson_t *vp9profile = json_object_get(root, \"vp9_profile\");\n\t\tjson_t *h264profile = json_object_get(root, \"h264_profile\");\n\t\tjson_t *fec = json_object_get(root, \"opus_fec\");\n\t\tjson_t *dtx = json_object_get(root, \"opus_dtx\");\n\t\tjson_t *audiolevel_ext = json_object_get(root, \"audiolevel_ext\");\n\t\tjson_t *audiolevel_event = json_object_get(root, \"audiolevel_event\");\n\t\tjson_t *audio_active_packets = json_object_get(root, \"audio_active_packets\");\n\t\tjson_t *audio_level_average = json_object_get(root, \"audio_level_average\");\n\t\tjson_t *videoorient_ext = json_object_get(root, \"videoorient_ext\");\n\t\tjson_t *playoutdelay_ext = json_object_get(root, \"playoutdelay_ext\");\n\t\tjson_t *transport_wide_cc_ext = json_object_get(root, \"transport_wide_cc_ext\");\n\t\tjson_t *notify_joining = json_object_get(root, \"notify_joining\");\n\t\tjson_t *record = json_object_get(root, \"record\");\n\t\tjson_t *rec_dir = json_object_get(root, \"rec_dir\");\n\t\tjson_t *lock_record = json_object_get(root, \"lock_record\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tif(allowed) {\n\t\t\t/* Make sure the \"allowed\" array only contains strings */\n\t\t\tgboolean ok = TRUE;\n\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tjson_t *a = json_array_get(allowed, i);\n\t\t\t\t\tif(!a || !json_is_string(a)) {\n\t\t\t\t\t\tok = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(!ok) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element in the allowed array (not a string)\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element in the allowed array (not a string)\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't create permanent room\\n\");\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't create permanent room\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tif(room_id == 0 && room_id_str == NULL) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Desired room ID is empty, which is not allowed... picking random ID instead\\n\");\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tif(room_id > 0 || room_id_str != NULL) {\n\t\t\t/* Let's make sure the room doesn't exist already */\n\t\t\tif(g_hash_table_lookup(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id) != NULL) {\n\t\t\t\t/* It does... */\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ROOM_EXISTS;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Room %s already exists!\\n\", room_id_str);\n\t\t\t\tg_snprintf(error_cause, 512, \"Room %s already exists\", room_id_str);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\t/* Create the room */\n\t\tjanus_videoroom *videoroom = g_malloc0(sizeof(janus_videoroom));\n\t\t/* Generate a random ID */\n\t\tgboolean room_id_allocated = FALSE;\n\t\tif(!string_ids && room_id == 0) {\n\t\t\twhile(room_id == 0) {\n\t\t\t\troom_id = janus_random_uint64();\n\t\t\t\tif(g_hash_table_lookup(rooms, &room_id) != NULL) {\n\t\t\t\t\t/* Room ID already taken, try another one */\n\t\t\t\t\troom_id = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else if(string_ids && room_id_str == NULL) {\n\t\t\twhile(room_id_str == NULL) {\n\t\t\t\troom_id_str = janus_random_uuid();\n\t\t\t\tif(g_hash_table_lookup(rooms, room_id_str) != NULL) {\n\t\t\t\t\t/* Room ID already taken, try another one */\n\t\t\t\t\tg_clear_pointer(&room_id_str, g_free);\n\t\t\t\t}\n\t\t\t}\n\t\t\troom_id_allocated = TRUE;\n\t\t}\n\t\tvideoroom->room_id = room_id;\n\t\tvideoroom->room_id_str = room_id_str ? g_strdup(room_id_str) : NULL;\n\t\tif(room_id_allocated)\n\t\t\tg_free(room_id_str);\n\t\tchar *description = NULL;\n\t\tif(desc != NULL && strlen(json_string_value(desc)) > 0) {\n\t\t\tdescription = g_strdup(json_string_value(desc));\n\t\t} else {\n\t\t\tchar roomname[255];\n\t\t\tg_snprintf(roomname, 255, \"Room %s\", videoroom->room_id_str);\n\t\t\tdescription = g_strdup(roomname);\n\t\t}\n\t\tvideoroom->room_name = description;\n\t\tvideoroom->is_private = is_private ? json_is_true(is_private) : FALSE;\n\t\tvideoroom->require_pvtid = req_pvtid ? json_is_true(req_pvtid) : FALSE;\n\t\tif(signed_tokens && json_is_true(signed_tokens)) {\n\t\t\tif(!gateway->auth_is_signed()) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Can't enforce signed tokens for this room, signed-mode not in use in the core\\n\");\n\t\t\t} else {\n\t\t\t\tvideoroom->signed_tokens = TRUE;\n\t\t\t}\n\t\t}\n\t\tvideoroom->require_e2ee = req_e2ee ? json_is_true(req_e2ee) : FALSE;\n\t\tif(secret)\n\t\t\tvideoroom->room_secret = g_strdup(json_string_value(secret));\n\t\tif(pin)\n\t\t\tvideoroom->room_pin = g_strdup(json_string_value(pin));\n\t\tvideoroom->max_publishers = 3;\t/* FIXME How should we choose a default? */\n\t\tif(publishers)\n\t\t\tvideoroom->max_publishers = json_integer_value(publishers);\n\t\tif(videoroom->max_publishers < 0)\n\t\t\tvideoroom->max_publishers = 3;\t/* FIXME How should we choose a default? */\n\t\tvideoroom->bitrate = 0;\n\t\tif(bitrate)\n\t\t\tvideoroom->bitrate = json_integer_value(bitrate);\n\t\tif(videoroom->bitrate > 0 && videoroom->bitrate < 64000)\n\t\t\tvideoroom->bitrate = 64000;\t/* Don't go below 64k */\n\t\tvideoroom->bitrate_cap = bitrate_cap ? json_is_true(bitrate_cap) : FALSE;\n\t\tvideoroom->fir_freq = 0;\n\t\tif(fir_freq)\n\t\t\tvideoroom->fir_freq = json_integer_value(fir_freq);\n\t\t/* If we need helper threads, spawn them now */\n\t\tvideoroom->helper_threads = json_integer_value(threads);;\n\t\tif(videoroom->helper_threads > 0) {\n\t\t\tGError *error = NULL;\n\t\t\tchar tname[16];\n\t\t\tint i=0;\n\t\t\tfor(i=0; i<videoroom->helper_threads; i++) {\n\t\t\t\tjanus_videoroom_helper *helper = g_malloc0(sizeof(janus_videoroom_helper));\n\t\t\t\thelper->id = i+1;\n\t\t\t\thelper->room = videoroom;\n\t\t\t\thelper->subscribers = g_hash_table_new(NULL, NULL);\n\t\t\t\thelper->queued_packets = g_async_queue_new_full((GDestroyNotify)janus_videoroom_rtp_relay_packet_free);\n\t\t\t\tjanus_mutex_init(&helper->mutex);\n\t\t\t\tjanus_refcount_init(&helper->ref, janus_videoroom_helper_free);\n\t\t\t\t/* Spawn a thread and add references */\n\t\t\t\tg_snprintf(tname, sizeof(tname), \"vhelp %u-%s\", helper->id, videoroom->room_id_str);\n\t\t\t\tjanus_refcount_increase(&videoroom->ref);\n\t\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\t\thelper->thread = g_thread_try_new(tname, &janus_videoroom_helper_thread, helper, &error);\n\t\t\t\tif(error != NULL) {\n\t\t\t\t\t/* TODO Should this be a hard failure? */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the helper thread...\\n\",\n\t\t\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\t\t} else {\n\t\t\t\t\tjanus_refcount_increase(&helper->ref);\n\t\t\t\t\tvideoroom->threads = g_list_append(videoroom->threads, helper);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* By default, we force Opus as the only audio codec */\n\t\tvideoroom->acodec[0] = JANUS_AUDIOCODEC_OPUS;\n\t\tvideoroom->acodec[1] = JANUS_AUDIOCODEC_NONE;\n\t\tvideoroom->acodec[2] = JANUS_AUDIOCODEC_NONE;\n\t\tvideoroom->acodec[3] = JANUS_AUDIOCODEC_NONE;\n\t\tvideoroom->acodec[4] = JANUS_AUDIOCODEC_NONE;\n\t\t/* Check if we're forcing a different single codec, or allowing more than one */\n\t\tif(audiocodec) {\n\t\t\tconst char *audiocodec_value = json_string_value(audiocodec);\n\t\t\tgchar **list = g_strsplit(audiocodec_value, \",\", 6);\n\t\t\tgchar *codec = list[0];\n\t\t\tif(codec != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(codec != NULL) {\n\t\t\t\t\tif(i == 5) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring extra audio codecs: %s\\n\", codec);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(strlen(codec) > 0)\n\t\t\t\t\t\tvideoroom->acodec[i] = janus_audiocodec_from_name(codec);\n\t\t\t\t\ti++;\n\t\t\t\t\tcodec = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_clear_pointer(&list, g_strfreev);\n\t\t}\n\t\t/* By default, we force VP8 as the only video codec */\n\t\tvideoroom->vcodec[0] = JANUS_VIDEOCODEC_VP8;\n\t\tvideoroom->vcodec[1] = JANUS_VIDEOCODEC_NONE;\n\t\tvideoroom->vcodec[2] = JANUS_VIDEOCODEC_NONE;\n\t\tvideoroom->vcodec[3] = JANUS_VIDEOCODEC_NONE;\n\t\tvideoroom->vcodec[4] = JANUS_VIDEOCODEC_NONE;\n\t\t/* Check if we're forcing a different single codec, or allowing more than one */\n\t\tif(videocodec) {\n\t\t\tconst char *videocodec_value = json_string_value(videocodec);\n\t\t\tgchar **list = g_strsplit(videocodec_value, \",\", 6);\n\t\t\tgchar *codec = list[0];\n\t\t\tif(codec != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(codec != NULL) {\n\t\t\t\t\tif(i == 5) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring extra video codecs: %s\\n\", codec);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(strlen(codec) > 0)\n\t\t\t\t\t\tvideoroom->vcodec[i] = janus_videocodec_from_name(codec);\n\t\t\t\t\ti++;\n\t\t\t\t\tcodec = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_clear_pointer(&list, g_strfreev);\n\t\t}\n\t\tconst char *vp9_profile = json_string_value(vp9profile);\n\t\tif(vp9_profile && (videoroom->vcodec[0] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\tvideoroom->vcodec[1] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\tvideoroom->vcodec[2] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\tvideoroom->vcodec[3] == JANUS_VIDEOCODEC_VP9 ||\n\t\t\t\tvideoroom->vcodec[4] == JANUS_VIDEOCODEC_VP9)) {\n\t\t\tvideoroom->vp9_profile = g_strdup(vp9_profile);\n\t\t}\n\t\tconst char *h264_profile = json_string_value(h264profile);\n\t\tif(h264_profile && (videoroom->vcodec[0] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\tvideoroom->vcodec[1] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\tvideoroom->vcodec[2] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\tvideoroom->vcodec[3] == JANUS_VIDEOCODEC_H264 ||\n\t\t\t\tvideoroom->vcodec[4] == JANUS_VIDEOCODEC_H264)) {\n\t\t\tvideoroom->h264_profile = g_strdup(h264_profile);\n\t\t}\n\t\tvideoroom->do_opusfec = TRUE;\n\t\tif(fec) {\n\t\t\tvideoroom->do_opusfec = json_is_true(fec);\n\t\t\tif(videoroom->acodec[0] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[1] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[2] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[3] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[4] != JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\tvideoroom->do_opusfec = FALSE;\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Inband FEC is only supported for rooms that allow Opus: disabling it...\\n\");\n\t\t\t}\n\t\t}\n\t\tif(dtx) {\n\t\t\tvideoroom->do_opusdtx = json_is_true(dtx);\n\t\t\tif(videoroom->acodec[0] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[1] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[2] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[3] != JANUS_AUDIOCODEC_OPUS &&\n\t\t\t\t\tvideoroom->acodec[4] != JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\tvideoroom->do_opusdtx = FALSE;\n\t\t\t\tJANUS_LOG(LOG_WARN, \"DTX is only supported for rooms that allow Opus: disabling it...\\n\");\n\t\t\t}\n\t\t}\n\t\tvideoroom->audiolevel_ext = audiolevel_ext ? json_is_true(audiolevel_ext) : TRUE;\n\t\tvideoroom->audiolevel_event = audiolevel_event ? json_is_true(audiolevel_event) : FALSE;\n\t\tif(videoroom->audiolevel_event) {\n\t\t\tvideoroom->audio_active_packets = 100;\n\t\t\tif(json_integer_value(audio_active_packets) > 0) {\n\t\t\t\tvideoroom->audio_active_packets = json_integer_value(audio_active_packets);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_active_packets value provided, using default: %d\\n\", videoroom->audio_active_packets);\n\t\t\t}\n\t\t\tvideoroom->audio_level_average = 25;\n\t\t\tif(json_integer_value(audio_level_average) > 0) {\n\t\t\t\tvideoroom->audio_level_average = json_integer_value(audio_level_average);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid audio_level_average value provided, using default: %d\\n\", videoroom->audio_level_average);\n\t\t\t}\n\t\t}\n\t\tvideoroom->videoorient_ext = videoorient_ext ? json_is_true(videoorient_ext) : TRUE;\n\t\tvideoroom->playoutdelay_ext = playoutdelay_ext ? json_is_true(playoutdelay_ext) : TRUE;\n\t\tvideoroom->transport_wide_cc_ext = transport_wide_cc_ext ? json_is_true(transport_wide_cc_ext) : TRUE;\n\t\t/* By default, the VideoRoom plugin does not notify about participants simply joining the room.\n\t\t   It only notifies when the participant actually starts publishing media. */\n\t\tvideoroom->notify_joining = notify_joining ? json_is_true(notify_joining) : FALSE;\n\t\tif(record) {\n\t\t\tvideoroom->record = json_is_true(record);\n\t\t}\n\t\tif(rec_dir) {\n\t\t\tvideoroom->rec_dir = g_strdup(json_string_value(rec_dir));\n\t\t}\n\t\tif(lock_record) {\n\t\t\tvideoroom->lock_record = json_is_true(lock_record);\n\t\t}\n\t\tg_atomic_int_set(&videoroom->destroyed, 0);\n\t\tjanus_mutex_init(&videoroom->mutex);\n\t\tjanus_refcount_init(&videoroom->ref, janus_videoroom_room_free);\n\t\tvideoroom->participants = g_hash_table_new_full(string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal,\n\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_publisher_dereference);\n\t\tvideoroom->private_ids = g_hash_table_new(NULL, NULL);\n\t\tvideoroom->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL);\n\t\tif(allowed != NULL) {\n\t\t\t/* Populate the \"allowed\" list as an ACL for people trying to join */\n\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tconst char *token = json_string_value(json_array_get(allowed, i));\n\t\t\t\t\tif(!g_hash_table_lookup(videoroom->allowed, token))\n\t\t\t\t\t\tg_hash_table_insert(videoroom->allowed, g_strdup(token), GINT_TO_POINTER(TRUE));\n\t\t\t\t}\n\t\t\t}\n\t\t\tvideoroom->check_allowed = TRUE;\n\t\t}\n\t\t/* Should we create a dummy publisher for placeholder m-lines? */\n\t\tif(dummy_pub && json_is_true(dummy_pub)) {\n\t\t\tvideoroom->dummy_publisher = TRUE;\n\t\t\t/* Check if we only need a subset of codecs, and&/or a specific fmtp */\n\t\t\tGHashTable *dummy_streams = NULL;\n\t\t\tif(dummy_str != NULL && json_array_size(dummy_str) > 0) {\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(dummy_str); i++) {\n\t\t\t\t\tjson_t *m = json_array_get(dummy_str, i);\n\t\t\t\t\tjson_t *c = json_object_get(m, \"codec\");\n\t\t\t\t\tif(c == NULL || !json_is_string(c) || json_is_null(c)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid dummy stream codec, skipping in '%s'...\\n\",\n\t\t\t\t\t\t\tvideoroom->room_id_str);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst char *codec = json_string_value(c);\n\t\t\t\t\tjson_t *f = json_object_get(m, \"fmtp\");\n\t\t\t\t\tif(f != NULL && (!json_is_string(f) || json_is_null(f))) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- Invalid dummy stream fmtp, skipping in '%s'...\\n\",\n\t\t\t\t\t\t\tvideoroom->room_id_str);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst char *fmtp = f ? json_string_value(f) : \"none\";\n\t\t\t\t\tif(dummy_streams == NULL)\n\t\t\t\t\t\tdummy_streams = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free);\n\t\t\t\t\tg_hash_table_insert(dummy_streams, g_strdup(codec), g_strdup(fmtp));\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Create the dummy publisher */\n\t\t\tgboolean e2ee = dummy_e2ee && json_is_true(dummy_e2ee);\n\t\t\tjanus_videoroom_create_dummy_publisher(videoroom, e2ee, dummy_streams);\n\t\t\tif(dummy_streams != NULL)\n\t\t\t\tg_hash_table_destroy(dummy_streams);\n\t\t}\n\t\t/* Compute a list of the supported codecs for the summary */\n\t\tchar audio_codecs[100], video_codecs[100];\n\t\tjanus_videoroom_codecstr(videoroom, audio_codecs, video_codecs, sizeof(audio_codecs), \"|\");\n\t\tJANUS_LOG(LOG_VERB, \"Created VideoRoom: %s (%s, %s, %s/%s codecs, secret: %s, pin: %s, pvtid: %s)\\n\",\n\t\t\tvideoroom->room_id_str, videoroom->room_name,\n\t\t\tvideoroom->is_private ? \"private\" : \"public\",\n\t\t\taudio_codecs, video_codecs,\n\t\t\tvideoroom->room_secret ? videoroom->room_secret : \"no secret\",\n\t\t\tvideoroom->room_pin ? videoroom->room_pin : \"no pin\",\n\t\t\tvideoroom->require_pvtid ? \"required\" : \"optional\");\n\t\tif(videoroom->record) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Room is going to be recorded in %s\\n\", videoroom->rec_dir ? videoroom->rec_dir : \"the current folder\");\n\t\t}\n\t\tif(videoroom->require_e2ee) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- All publishers MUST use end-to-end encryption\\n\");\n\t\t}\n\t\tif(videoroom->dummy_publisher) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- The room is going to have a dummy publisher for placeholder subscriptions\\n\");\n\t\t}\n\t\tif(save) {\n\t\t\t/* This room is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Saving room %s permanently in config file\\n\", videoroom->room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ], value[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", videoroom->room_id_str);\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, cat);\n\t\t\t/* Now for the values */\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", videoroom->room_name));\n\t\t\tif(videoroom->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tif(videoroom->require_pvtid)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"require_pvtid\", \"true\"));\n\t\t\tif(videoroom->signed_tokens)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"signed_tokens\", \"true\"));\n\t\t\tif(videoroom->require_e2ee)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"require_e2ee\", \"true\"));\n\t\t\tif(videoroom->dummy_publisher)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"dummy_publisher\", \"true\"));\n\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, videoroom->bitrate);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bitrate\", value));\n\t\t\tif(videoroom->bitrate_cap)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bitrate_cap\", \"true\"));\n\t\t\tg_snprintf(value, BUFSIZ, \"%d\", videoroom->max_publishers);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"publishers\", value));\n\t\t\tif(videoroom->fir_freq) {\n\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu16, videoroom->fir_freq);\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"fir_freq\", value));\n\t\t\t}\n\t\t\tchar video_codecs[100];\n\t\t\tchar audio_codecs[100];\n\t\t\tjanus_videoroom_codecstr(videoroom, audio_codecs, video_codecs, sizeof(audio_codecs), \",\");\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiocodec\", audio_codecs));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videocodec\", video_codecs));\n\t\t\tif(videoroom->vp9_profile)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"vp9_profile\", videoroom->vp9_profile));\n\t\t\tif(videoroom->h264_profile)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"h264_profile\", videoroom->h264_profile));\n\t\t\tif(videoroom->do_opusfec)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"opus_fec\", \"true\"));\n\t\t\tif(videoroom->do_opusdtx)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"opus_dtx\", \"true\"));\n\t\t\tif(videoroom->room_secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", videoroom->room_secret));\n\t\t\tif(videoroom->room_pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", videoroom->room_pin));\n\t\t\tif(videoroom->audiolevel_ext) {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_ext\", \"true\"));\n\t\t\t\tif(videoroom->audiolevel_event)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_event\", \"true\"));\n\t\t\t\tif(videoroom->audio_active_packets > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", videoroom->audio_active_packets);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_active_packets\", value));\n\t\t\t\t}\n\t\t\t\tif(videoroom->audio_level_average > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", videoroom->audio_level_average);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_level_average\", value));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_ext\", \"no\"));\n\t\t\t}\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videoorient_ext\", videoroom->videoorient_ext ? \"true\" : \"false\"));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"playoutdelay_ext\", videoroom->playoutdelay_ext ? \"true\" : \"false\"));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"transport_wide_cc_ext\", videoroom->transport_wide_cc_ext ? \"true\" : \"false\"));\n\t\t\tif(videoroom->notify_joining)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"notify_joining\", \"true\"));\n\t\t\tif(videoroom->record)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record\", \"true\"));\n\t\t\tif(videoroom->rec_dir)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rec_dir\", videoroom->rec_dir));\n\t\t\tif(videoroom->lock_record)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"lock_record\", \"true\"));\n\t\t\tif(videoroom->helper_threads > 0) {\n\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, videoroom->helper_threads);\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"threads\", value));\n\t\t\t}\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_VIDEOROOM_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\n\t\tg_hash_table_insert(rooms,\n\t\t\tstring_ids ? (gpointer)g_strdup(videoroom->room_id_str) : (gpointer)janus_uint64_dup(videoroom->room_id),\n\t\t\tvideoroom);\n\t\t/* Show updated rooms list */\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, rooms);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_videoroom *vr = value;\n\t\t\tJANUS_LOG(LOG_VERB, \"  ::: [%s][%s] %\"SCNu32\", max %d publishers, FIR frequency of %d seconds\\n\",\n\t\t\t\tvr->room_id_str, vr->room_name, vr->bitrate, vr->max_publishers, vr->fir_freq);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"created\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"created\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\t\tgateway->notify_event(&janus_videoroom_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"edit\")) {\n\t\t/* Edit the properties for an existing VideoRoom */\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to edit the properties of an existing VideoRoom room\\n\");\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, edit_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\t/* We only allow for a limited set of properties to be edited */\n\t\tjson_t *desc = json_object_get(root, \"new_description\");\n\t\tjson_t *is_private = json_object_get(root, \"new_is_private\");\n\t\tjson_t *req_pvtid = json_object_get(root, \"new_require_pvtid\");\n\t\tjson_t *secret = json_object_get(root, \"new_secret\");\n\t\tjson_t *pin = json_object_get(root, \"new_pin\");\n\t\tjson_t *bitrate = json_object_get(root, \"new_bitrate\");\n\t\tjson_t *fir_freq = json_object_get(root, \"new_fir_freq\");\n\t\tjson_t *publishers = json_object_get(root, \"new_publishers\");\n\t\tjson_t *lock_record = json_object_get(root, \"new_lock_record\");\n\t\tjson_t *rec_dir = json_object_get(root, \"new_rec_dir\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't edit room permanently\\n\");\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't edit room permanently\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Edit the room properties that were provided */\n\t\tif(desc != NULL && strlen(json_string_value(desc)) > 0) {\n\t\t\tchar *old_description = videoroom->room_name;\n\t\t\tchar *new_description = g_strdup(json_string_value(desc));\n\t\t\tvideoroom->room_name = new_description;\n\t\t\tg_free(old_description);\n\t\t}\n\t\tif(is_private)\n\t\t\tvideoroom->is_private = json_is_true(is_private);\n\t\tif(req_pvtid)\n\t\t\tvideoroom->require_pvtid = json_is_true(req_pvtid);\n\t\tif(publishers)\n\t\t\tvideoroom->max_publishers = json_integer_value(publishers);\n\t\tif(bitrate) {\n\t\t\tvideoroom->bitrate = json_integer_value(bitrate);\n\t\t\tif(videoroom->bitrate > 0 && videoroom->bitrate < 64000)\n\t\t\t\tvideoroom->bitrate = 64000;\t/* Don't go below 64k */\n\t\t}\n\t\tif(fir_freq)\n\t\t\tvideoroom->fir_freq = json_integer_value(fir_freq);\n\t\tif(secret && strlen(json_string_value(secret)) > 0) {\n\t\t\tchar *old_secret = videoroom->room_secret;\n\t\t\tchar *new_secret = g_strdup(json_string_value(secret));\n\t\t\tvideoroom->room_secret = new_secret;\n\t\t\tg_free(old_secret);\n\t\t}\n\t\tif(pin && strlen(json_string_value(pin)) > 0) {\n\t\t\tchar *old_pin = videoroom->room_pin;\n\t\t\tchar *new_pin = g_strdup(json_string_value(pin));\n\t\t\tvideoroom->room_pin = new_pin;\n\t\t\tg_free(old_pin);\n\t\t}\n\t\tif(lock_record)\n\t\t\tvideoroom->lock_record = json_is_true(lock_record);\n\t\tif(rec_dir) {\n\t\t\tchar *old_rec_dir = videoroom->rec_dir;\n\t\t\tchar *new_rec_dir = g_strdup(json_string_value(rec_dir));\n\t\t\tvideoroom->rec_dir = new_rec_dir;\n\t\t\tg_free(old_rec_dir);\n\t\t}\n\t\tif(save) {\n\t\t\t/* This room is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Modifying room %s permanently in config file\\n\", videoroom->room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ], value[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", videoroom->room_id_str);\n\t\t\t/* Remove the old category first */\n\t\t\tjanus_config_remove(config, NULL, cat);\n\t\t\t/* Now write the room details again */\n\t\t\tjanus_config_category *c = janus_config_get_create(config, NULL, janus_config_type_category, cat);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"description\", videoroom->room_name));\n\t\t\tif(videoroom->is_private)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"is_private\", \"true\"));\n\t\t\tif(videoroom->require_pvtid)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"require_pvtid\", \"true\"));\n\t\t\tif(videoroom->signed_tokens)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"signed_tokens\", \"true\"));\n\t\t\tif(videoroom->require_e2ee)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"require_e2ee\", \"true\"));\n\t\t\tif(videoroom->dummy_publisher)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"dummy_publisher\", \"true\"));\n\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, videoroom->bitrate);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bitrate\", value));\n\t\t\tif(videoroom->bitrate_cap)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"bitrate_cap\", \"true\"));\n\t\t\tg_snprintf(value, BUFSIZ, \"%d\", videoroom->max_publishers);\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"publishers\", value));\n\t\t\tif(videoroom->fir_freq) {\n\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu16, videoroom->fir_freq);\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"fir_freq\", value));\n\t\t\t}\n\t\t\tchar audio_codecs[100];\n\t\t\tchar video_codecs[100];\n\t\t\tjanus_videoroom_codecstr(videoroom, audio_codecs, video_codecs, sizeof(audio_codecs), \",\");\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiocodec\", audio_codecs));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videocodec\", video_codecs));\n\t\t\tif(videoroom->vp9_profile)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"vp9_profile\", videoroom->vp9_profile));\n\t\t\tif(videoroom->h264_profile)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"h264_profile\", videoroom->h264_profile));\n\t\t\tif(videoroom->do_opusfec)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"opus_fec\", \"true\"));\n\t\t\tif(videoroom->do_opusdtx)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"opus_dtx\", \"true\"));\n\t\t\tif(videoroom->room_secret)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"secret\", videoroom->room_secret));\n\t\t\tif(videoroom->room_pin)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"pin\", videoroom->room_pin));\n\t\t\tif(videoroom->audiolevel_ext) {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_ext\", \"true\"));\n\t\t\t\tif(videoroom->audiolevel_event)\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_event\", \"true\"));\n\t\t\t\tif(videoroom->audio_active_packets > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", videoroom->audio_active_packets);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_active_packets\", value));\n\t\t\t\t}\n\t\t\t\tif(videoroom->audio_level_average > 0) {\n\t\t\t\t\tg_snprintf(value, BUFSIZ, \"%d\", videoroom->audio_level_average);\n\t\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audio_level_average\", value));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"audiolevel_ext\", \"no\"));\n\t\t\t}\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"videoorient_ext\", videoroom->videoorient_ext ? \"true\" : \"false\"));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"playoutdelay_ext\", videoroom->playoutdelay_ext ? \"true\" : \"false\"));\n\t\t\tjanus_config_add(config, c, janus_config_item_create(\"transport_wide_cc_ext\", videoroom->transport_wide_cc_ext ? \"true\" : \"false\"));\n\t\t\tif(videoroom->notify_joining)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"notify_joining\", \"true\"));\n\t\t\tif(videoroom->record)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"record\", \"true\"));\n\t\t\tif(videoroom->rec_dir)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"rec_dir\", videoroom->rec_dir));\n\t\t\tif(videoroom->lock_record)\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"lock_record\", \"true\"));\n\t\t\tif(videoroom->helper_threads > 0) {\n\t\t\t\tg_snprintf(value, BUFSIZ, \"%\"SCNu32, videoroom->helper_threads);\n\t\t\t\tjanus_config_add(config, c, janus_config_item_create(\"threads\", value));\n\t\t\t}\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_VIDEOROOM_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room changes are not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t/* Send info back */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"edited\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"edited\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\t\tgateway->notify_event(&janus_videoroom_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"destroy\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to destroy an existing VideoRoom room\\n\");\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, destroy_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *permanent = json_object_get(root, \"permanent\");\n\t\tgboolean save = permanent ? json_is_true(permanent) : FALSE;\n\t\tif(save && config == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No configuration file, can't destroy room permanently\\n\");\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"No configuration file, can't destroy room permanently\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Remove room, but add a reference until we're done */\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tg_hash_table_remove(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\t/* Notify all participants that the fun is over, and that they'll be kicked */\n\t\tJANUS_LOG(LOG_VERB, \"Notifying all participants\\n\");\n\t\tjson_t *destroyed = json_object();\n\t\tjson_object_set_new(destroyed, \"videoroom\", json_string(\"destroyed\"));\n\t\tjson_object_set_new(destroyed, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tg_hash_table_iter_init(&iter, videoroom->participants);\n\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_videoroom_publisher *p = value;\n\t\t\tif(p && !g_atomic_int_get(&p->destroyed) && p->session && p->room && !p->dummy) {\n\t\t\t\tjanus_mutex_lock(&p->mutex);\n\t\t\t\tg_clear_pointer(&p->room, janus_videoroom_room_dereference);\n\t\t\t\tjanus_mutex_unlock(&p->mutex);\n\t\t\t\t/* Notify the user we're going to destroy the room... */\n\t\t\t\tint ret = gateway->push_event(p->session->handle, &janus_videoroom_plugin, NULL, destroyed, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\t/* ... and then ask the core to close the PeerConnection */\n\t\t\t\tgateway->close_pc(p->session->handle);\n\t\t\t}\n\t\t}\n\t\tjson_decref(destroyed);\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"destroyed\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tgateway->notify_event(&janus_videoroom_plugin, session ? session->handle : NULL, info);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tif(save) {\n\t\t\t/* This change is permanent: save to the configuration file too\n\t\t\t * FIXME: We should check if anything fails... */\n\t\t\tJANUS_LOG(LOG_VERB, \"Destroying room %s permanently in config file\\n\", room_id_str);\n\t\t\tjanus_mutex_lock(&config_mutex);\n\t\t\tchar cat[BUFSIZ];\n\t\t\t/* The room ID is the category (prefixed by \"room-\") */\n\t\t\tg_snprintf(cat, BUFSIZ, \"room-%s\", room_id_str);\n\t\t\tjanus_config_remove(config, NULL, cat);\n\t\t\t/* Save modified configuration */\n\t\t\tif(janus_config_save(config, config_folder, JANUS_VIDEOROOM_PACKAGE) < 0)\n\t\t\t\tsave = FALSE;\t/* This will notify the user the room destruction is not permanent */\n\t\t\tjanus_mutex_unlock(&config_mutex);\n\t\t}\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t/* Done */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"destroyed\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"permanent\", save ? json_true() : json_false());\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"list\")) {\n\t\t/* List all rooms (but private ones) and their details (except for the secret, of course...) */\n\t\tJANUS_LOG(LOG_VERB, \"Getting the list of VideoRoom rooms\\n\");\n\t\tgboolean lock_room_list = TRUE;\n\t\tif(admin_key != NULL) {\n\t\t\tjson_t *admin_key_json = json_object_get(root, \"admin_key\");\n\t\t\t/* Verify admin_key if it was provided */\n\t\t\tif(admin_key_json != NULL && json_is_string(admin_key_json) && strlen(json_string_value(admin_key_json)) > 0) {\n\t\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t} else {\n\t\t\t\t\tlock_room_list = FALSE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjson_t *list = json_array();\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, rooms);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_videoroom *room = value;\n\t\t\tif(!room)\n\t\t\t\tcontinue;\n\t\t\tjanus_refcount_increase(&room->ref);\n\t\t\tif(room->is_private && lock_room_list) {\n\t\t\t\t/* Skip private room if no valid admin_key was provided */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping private room '%s'\\n\", room->room_name);\n\t\t\t\tjanus_refcount_decrease(&room->ref);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(!g_atomic_int_get(&room->destroyed)) {\n\t\t\t\tjson_t *rl = json_object();\n\t\t\t\tjson_object_set_new(rl, \"room\", string_ids ? json_string(room->room_id_str) : json_integer(room->room_id));\n\t\t\t\tjson_object_set_new(rl, \"description\", json_string(room->room_name));\n\t\t\t\tjson_object_set_new(rl, \"pin_required\", room->room_pin ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"is_private\", room->is_private ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"max_publishers\", json_integer(room->max_publishers));\n\t\t\t\tjson_object_set_new(rl, \"bitrate\", json_integer(room->bitrate));\n\t\t\t\tif(room->bitrate_cap)\n\t\t\t\t\tjson_object_set_new(rl, \"bitrate_cap\", json_true());\n\t\t\t\tjson_object_set_new(rl, \"fir_freq\", json_integer(room->fir_freq));\n\t\t\t\tjson_object_set_new(rl, \"require_pvtid\", room->require_pvtid ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"require_e2ee\", room->require_e2ee ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"dummy_publisher\", room->dummy_publisher ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"notify_joining\", room->notify_joining ? json_true() : json_false());\n\t\t\t\tchar audio_codecs[100];\n\t\t\t\tchar video_codecs[100];\n\t\t\t\tjanus_videoroom_codecstr(room, audio_codecs, video_codecs, sizeof(audio_codecs), \",\");\n\t\t\t\tjson_object_set_new(rl, \"audiocodec\", json_string(audio_codecs));\n\t\t\t\tjson_object_set_new(rl, \"videocodec\", json_string(video_codecs));\n\t\t\t\tif(room->do_opusfec)\n\t\t\t\t\tjson_object_set_new(rl, \"opus_fec\", json_true());\n\t\t\t\tif(room->do_opusdtx)\n\t\t\t\t\tjson_object_set_new(rl, \"opus_dtx\", json_true());\n\t\t\t\tjson_object_set_new(rl, \"record\", room->record ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"rec_dir\", json_string(room->rec_dir));\n\t\t\t\tjson_object_set_new(rl, \"lock_record\", room->lock_record ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"num_participants\", json_integer(g_hash_table_size(room->participants)));\n\t\t\t\tjson_object_set_new(rl, \"audiolevel_ext\", room->audiolevel_ext ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"audiolevel_event\", room->audiolevel_event ? json_true() : json_false());\n\t\t\t\tif(room->audiolevel_event) {\n\t\t\t\t\tjson_object_set_new(rl, \"audio_active_packets\", json_integer(room->audio_active_packets));\n\t\t\t\t\tjson_object_set_new(rl, \"audio_level_average\", json_integer(room->audio_level_average));\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(rl, \"videoorient_ext\", room->videoorient_ext ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"playoutdelay_ext\", room->playoutdelay_ext ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(rl, \"transport_wide_cc_ext\", room->transport_wide_cc_ext ? json_true() : json_false());\n\t\t\t\tjson_array_append_new(list, rl);\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&room->ref);\n\t\t}\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"list\", list);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"rtp_forward\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, rtp_forward_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pid_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pidstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_rtpfwd && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *pub_id = json_object_get(root, \"publisher_id\");\n\t\tjson_t *json_host = json_object_get(root, \"host\");\n\t\tjson_t *json_host_family = json_object_get(root, \"host_family\");\n\t\tconst char *host_family = json_string_value(json_host_family);\n\t\tint family = 0;\n\t\tif(host_family) {\n\t\t\tif(!strcasecmp(host_family, \"ipv4\")) {\n\t\t\t\tfamily = AF_INET;\n\t\t\t} else if(!strcasecmp(host_family, \"ipv6\")) {\n\t\t\t\tfamily = AF_INET6;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported protocol family (%s)\\n\", host_family);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported protocol family (%s)\", host_family);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tguint64 publisher_id = 0;\n\t\tchar publisher_id_num[30], *publisher_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tpublisher_id = json_integer_value(pub_id);\n\t\t\tg_snprintf(publisher_id_num, sizeof(publisher_id_num), \"%\"SCNu64, publisher_id);\n\t\t\tpublisher_id_str = publisher_id_num;\n\t\t} else {\n\t\t\tpublisher_id_str = (char *)json_string_value(pub_id);\n\t\t}\n\t\tconst char *host = json_string_value(json_host), *resolved_host = NULL;\n\t\t/* Check if we need to resolve this host address */\n\t\tstruct addrinfo *res = NULL, *start = NULL;\n\t\tjanus_network_address addr;\n\t\tjanus_network_address_string_buffer addr_buf;\n\t\tstruct addrinfo hints;\n\t\tmemset(&hints, 0, sizeof(hints));\n\t\tif(family != 0)\n\t\t\thints.ai_family = family;\n\t\tif(getaddrinfo(host, NULL, family != 0 ? &hints : NULL, &res) == 0) {\n\t\t\tstart = res;\n\t\t\twhile(res != NULL) {\n\t\t\t\tif(janus_network_address_from_sockaddr(res->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t/* Resolved */\n\t\t\t\t\tresolved_host = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\tstart = NULL;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tres = res->ai_next;\n\t\t\t}\n\t\t}\n\t\tif(resolved_host == NULL) {\n\t\t\tif(start)\n\t\t\t\tfreeaddrinfo(start);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s)...\\n\", host);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Could not resolve address (%s)...\", host);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\thost = resolved_host;\n\t\t/* Iterate on the provided streams array */\n\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\tif(streams == NULL || json_array_size(streams) == 0) {\n\t\t\t/* No streams array, we'll use the legacy approach: make sure the host attribute was set */\n\t\t\tif(host == NULL) {\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Missing mandatory element host (deprecated API)\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t} else {\n\t\t\t/* Iterate on the streams objects and validate them all */\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, rtp_forward_stream_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t/* Make sure we have a host attribute, either global or stream-specific */\n\t\t\t\tjson_t *stream_host = json_object_get(s, \"host\");\n\t\t\t\tconst char *s_host = json_string_value(stream_host);\n\t\t\t\tif(host == NULL && s_host == NULL) {\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Missing mandatory element host (global or local)\");\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\tif(s_host != NULL) {\n\t\t\t\t\tjson_t *stream_host_family = json_object_get(s, \"host_family\");\n\t\t\t\t\tconst char *s_host_family = json_string_value(stream_host_family);\n\t\t\t\t\tint s_family = family;\n\t\t\t\t\tif(s_host_family) {\n\t\t\t\t\t\tif(!strcasecmp(s_host_family, \"ipv4\")) {\n\t\t\t\t\t\t\ts_family = AF_INET;\n\t\t\t\t\t\t} else if(!strcasecmp(s_host_family, \"ipv6\")) {\n\t\t\t\t\t\t\ts_family = AF_INET6;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported protocol family (%s)\\n\", s_host_family);\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported protocol family (%s)\", s_host_family);\n\t\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tmemset(&hints, 0, sizeof(hints));\n\t\t\t\t\tif(s_family != 0)\n\t\t\t\t\t\thints.ai_family = s_family;\n\t\t\t\t\tstart = NULL;\n\t\t\t\t\tres = NULL;\n\t\t\t\t\tif(getaddrinfo(s_host, NULL, s_family != 0 ? &hints : NULL, &res) == 0) {\n\t\t\t\t\t\tstart = res;\n\t\t\t\t\t\twhile(res != NULL) {\n\t\t\t\t\t\t\tif(janus_network_address_from_sockaddr(res->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t\t\t\t/* Resolved */\n\t\t\t\t\t\t\t\tresolved_host = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\t\t\t\tstart = NULL;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tres = res->ai_next;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(resolved_host == NULL) {\n\t\t\t\t\t\tif(start)\n\t\t\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s)...\\n\", s_host);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Could not resolve address (%s)...\", s_host);\n\t\t\t\t\t\tgoto prepare_response;\n\t\t\t\t\t}\n\t\t\t\t\t/* Add the resolved address to the JSON object, so that we can use it later */\n\t\t\t\t\tjson_object_set_new(s, \"host\", json_string(resolved_host));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* We may need to SRTP-encrypt this stream */\n\t\tint srtp_suite = 0;\n\t\tconst char *srtp_crypto = NULL;\n\t\tjson_t *s_suite = json_object_get(root, \"srtp_suite\");\n\t\tjson_t *s_crypto = json_object_get(root, \"srtp_crypto\");\n\t\tif(s_suite && s_crypto) {\n\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP suite (%d)\\n\", srtp_suite);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP suite (%d)\", srtp_suite);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t}\n\t\t/* Look for room and publisher */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)publisher_id_str : (gpointer)&publisher_id);\n\t\tif(publisher == NULL) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such publisher (%s)\\n\", publisher_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such feed (%s)\", publisher_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&publisher->ref);\t/* This is just to handle the request for now */\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\tjanus_mutex_lock(&publisher->rtp_forwarders_mutex);\n\t\tif(publisher->udp_sock <= 0) {\n\t\t\tpublisher->udp_sock = socket(!ipv6_disabled ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\tint v6only = 0;\n\t\t\tif(publisher->udp_sock <= 0 ||\n\t\t\t\t\t(!ipv6_disabled && setsockopt(publisher->udp_sock, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)) {\n\t\t\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not open UDP socket for RTP stream for publisher (%s), %d (%s)\\n\",\n\t\t\t\t\tpublisher_id_str, errno, g_strerror(errno));\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Could not open UDP socket for RTP stream\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\t/* Are we using the new approach, or the old deprecated one? */\n\t\tresponse = json_object();\n\t\tjanus_videoroom_publisher_stream *ps = NULL;\n\t\tjson_t *new_forwarders = NULL, *rtp_stream = NULL;\n\t\tif(streams != NULL) {\n\t\t\t/* New approach: iterate on all objects, and create the related forwarder(s) */\n\t\t\tnew_forwarders = json_array();\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tjson_t *stream_mid = json_object_get(s, \"mid\");\n\t\t\t\tconst char *mid = json_string_value(stream_mid);\n\t\t\t\tps = g_hash_table_lookup(publisher->streams_bymid, mid);\n\t\t\t\tif(ps == NULL) {\n\t\t\t\t\t/* FIXME Should we return an error instead? */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No such stream with mid '%s', skipping forwarder...\\n\", mid);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjanus_rtp_forwarder *f = NULL;\n\t\t\t\tjson_t *stream_host = json_object_get(s, \"host\");\n\t\t\t\thost = json_string_value(stream_host) ? json_string_value(stream_host) : json_string_value(json_host);\n\t\t\t\tjson_t *stream_port = json_object_get(s, \"port\");\n\t\t\t\tuint16_t port = json_integer_value(stream_port);\n\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t\t\t/* We have all we need */\n\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\thost, port, 0, 0, 0, FALSE, 0, NULL, 0, FALSE, TRUE);\n\t\t\t\t\tif(f) {\n\t\t\t\t\t\tjson_t *rtpf = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\tjson_array_append_new(new_forwarders, rtpf);\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"data\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* If we got here, it's RTP media, check the other properties too */\n\t\t\t\tjson_t *stream_pt = json_object_get(s, \"pt\");\n\t\t\t\tjson_t *stream_ssrc = json_object_get(s, \"ssrc\");\n\t\t\t\tjson_t *stream_rtcp_port = json_object_get(s, \"rtcp_port\");\n\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\thost, port, stream_rtcp_port ? json_integer_value(stream_rtcp_port) : -1,\n\t\t\t\t\t\tjson_integer_value(stream_pt), json_integer_value(stream_ssrc),\n\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 0, FALSE, FALSE);\n\t\t\t\t\tif(f) {\n\t\t\t\t\t\tjson_t *rtpf = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\tjson_array_append_new(new_forwarders, rtpf);\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"audio\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tjson_t *stream_simulcast = json_object_get(s, \"simulcast\");\n\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\thost, port, stream_rtcp_port ? json_integer_value(stream_rtcp_port) : -1,\n\t\t\t\t\t\tjson_integer_value(stream_pt), json_integer_value(stream_ssrc),\n\t\t\t\t\t\tjson_is_true(stream_simulcast), srtp_suite, srtp_crypto, 0, TRUE, FALSE);\n\t\t\t\t\tif(f) {\n\t\t\t\t\t\tjson_t *rtpf = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\tjson_array_append_new(new_forwarders, rtpf);\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(!json_is_true(stream_simulcast)) {\n\t\t\t\t\t\t/* Check if there's simulcast substreams we need to relay */\n\t\t\t\t\t\tstream_port = json_object_get(s, \"port_2\");\n\t\t\t\t\t\tport = json_integer_value(stream_port);\n\t\t\t\t\t\tstream_pt = json_object_get(s, \"pt_2\");\n\t\t\t\t\t\tstream_ssrc = json_object_get(s, \"ssrc_2\");\n\t\t\t\t\t\tif(json_integer_value(stream_port) > 0) {\n\t\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\t\t\thost, port, 0, json_integer_value(stream_pt), json_integer_value(stream_ssrc),\n\t\t\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 1, TRUE, FALSE);\n\t\t\t\t\t\t\tif(f) {\n\t\t\t\t\t\t\t\tjson_t *rtpf = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\t\tjson_array_append_new(new_forwarders, rtpf);\n\t\t\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\t\t\t\t\tstring_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"video_substream\", json_integer(1));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstream_port = json_object_get(s, \"port_3\");\n\t\t\t\t\t\tport = json_integer_value(stream_port);\n\t\t\t\t\t\tstream_pt = json_object_get(s, \"pt_3\");\n\t\t\t\t\t\tstream_ssrc = json_object_get(s, \"ssrc_3\");\n\t\t\t\t\t\tif(json_integer_value(stream_port) > 0) {\n\t\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\t\t\thost, port, 0, json_integer_value(stream_pt), json_integer_value(stream_ssrc),\n\t\t\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 2, TRUE, FALSE);\n\t\t\t\t\t\t\tif(f) {\n\t\t\t\t\t\t\t\tjson_t *rtpf = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\t\tjson_array_append_new(new_forwarders, rtpf);\n\t\t\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"room\",\n\t\t\t\t\t\t\t\t\t\tstring_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"video_substream\", json_integer(2));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t/* Old deprecated approach: return the legacy info as well */\n\t\t\tJANUS_LOG(LOG_WARN, \"Deprecated 'rtp_forward' API: please start looking into the new one for the future\\n\");\n\t\t\trtp_stream = json_object();\n\t\t\tint video_port[3] = {-1, -1, -1}, video_rtcp_port = -1, video_pt[3] = {0, 0, 0};\n\t\t\tuint32_t video_ssrc[3] = {0, 0, 0};\n\t\t\tint audio_port = -1, audio_rtcp_port = -1, audio_pt = 0;\n\t\t\tuint32_t audio_ssrc = 0;\n\t\t\tint data_port = -1;\n\t\t\t/* There may be multiple target video ports (e.g., publisher simulcasting) */\n\t\t\tjson_t *vid_port = json_object_get(root, \"video_port\");\n\t\t\tif(vid_port) {\n\t\t\t\tvideo_port[0] = json_integer_value(vid_port);\n\t\t\t\tjson_t *pt = json_object_get(root, \"video_pt\");\n\t\t\t\tif(pt)\n\t\t\t\t\tvideo_pt[0] = json_integer_value(pt);\n\t\t\t\tjson_t *ssrc = json_object_get(root, \"video_ssrc\");\n\t\t\t\tif(ssrc)\n\t\t\t\t\tvideo_ssrc[0] = json_integer_value(ssrc);\n\t\t\t}\n\t\t\tvid_port = json_object_get(root, \"video_port_2\");\n\t\t\tif(vid_port) {\n\t\t\t\tvideo_port[1] = json_integer_value(vid_port);\n\t\t\t\tjson_t *pt = json_object_get(root, \"video_pt_2\");\n\t\t\t\tif(pt)\n\t\t\t\t\tvideo_pt[1] = json_integer_value(pt);\n\t\t\t\tjson_t *ssrc = json_object_get(root, \"video_ssrc_2\");\n\t\t\t\tif(ssrc)\n\t\t\t\t\tvideo_ssrc[1] = json_integer_value(ssrc);\n\t\t\t}\n\t\t\tvid_port = json_object_get(root, \"video_port_3\");\n\t\t\tif(vid_port) {\n\t\t\t\tvideo_port[2] = json_integer_value(vid_port);\n\t\t\t\tjson_t *pt = json_object_get(root, \"video_pt_3\");\n\t\t\t\tif(pt)\n\t\t\t\t\tvideo_pt[2] = json_integer_value(pt);\n\t\t\t\tjson_t *ssrc = json_object_get(root, \"video_ssrc_3\");\n\t\t\t\tif(ssrc)\n\t\t\t\t\tvideo_ssrc[2] = json_integer_value(ssrc);\n\t\t\t}\n\t\t\tjson_t *vid_rtcp_port = json_object_get(root, \"video_rtcp_port\");\n\t\t\tif(vid_rtcp_port)\n\t\t\t\tvideo_rtcp_port = json_integer_value(vid_rtcp_port);\n\t\t\t/* Audio target */\n\t\t\tjson_t *au_port = json_object_get(root, \"audio_port\");\n\t\t\tif(au_port) {\n\t\t\t\taudio_port = json_integer_value(au_port);\n\t\t\t\tjson_t *pt = json_object_get(root, \"audio_pt\");\n\t\t\t\tif(pt)\n\t\t\t\t\taudio_pt = json_integer_value(pt);\n\t\t\t\tjson_t *ssrc = json_object_get(root, \"audio_ssrc\");\n\t\t\t\tif(ssrc)\n\t\t\t\t\taudio_ssrc = json_integer_value(ssrc);\n\t\t\t}\n\t\t\tjson_t *au_rtcp_port = json_object_get(root, \"audio_rtcp_port\");\n\t\t\tif(au_rtcp_port)\n\t\t\t\taudio_rtcp_port = json_integer_value(au_rtcp_port);\n\t\t\t/* Data target */\n\t\t\tjson_t *d_port = json_object_get(root, \"data_port\");\n\t\t\tif(d_port) {\n\t\t\t\tdata_port = json_integer_value(d_port);\n\t\t\t}\n\t\t\t/* Do we need to forward multiple simulcast streams to a single endpoint? */\n\t\t\tgboolean simulcast = FALSE;\n\t\t\tif(json_object_get(root, \"simulcast\") != NULL)\n\t\t\t\tsimulcast = json_is_true(json_object_get(root, \"simulcast\"));\n\t\t\tif(simulcast) {\n\t\t\t\t/* We do, disable the other video ports if they were requested */\n\t\t\t\tvideo_port[1] = -1;\n\t\t\t\tvideo_port[2] = -1;\n\t\t\t}\n\t\t\t/* Create all the forwarders we need */\n\t\t\tjanus_rtp_forwarder *f = NULL;\n\t\t\tguint32 audio_handle = 0;\n\t\t\tguint32 video_handle[3] = {0, 0, 0};\n\t\t\tguint32 data_handle = 0;\n\t\t\tif(audio_port > 0) {\n\t\t\t\t/* FIXME Find the audio stream */\n\t\t\t\tGList *temp = publisher->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\tif(ps && ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\t\t/* FIXME Found */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tps = NULL;\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tif(ps == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find any audio stream to forward, skipping...\\n\");\n\t\t\t\t} else {\n\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\thost, audio_port, audio_rtcp_port, audio_pt, audio_ssrc,\n\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 0, FALSE, FALSE);\n\t\t\t\t\taudio_handle = f ? f->stream_id : 0;\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(f != NULL && notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"audio\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(audio_port));\n\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(video_port[0] > 0 || video_port[1] > 0 || video_port[2] > 0) {\n\t\t\t\t/* FIXME Find the video stream */\n\t\t\t\tGList *temp = publisher->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\tif(ps && ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t/* FIXME Found */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tps = NULL;\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tif(ps == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find any video stream to forward, skipping...\\n\");\n\t\t\t\t} else {\n\t\t\t\t\tif(video_port[0] > 0) {\n\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\t\thost, video_port[0], video_rtcp_port, video_pt[0], video_ssrc[0],\n\t\t\t\t\t\t\tsimulcast, srtp_suite, srtp_crypto, 0, TRUE, FALSE);\n\t\t\t\t\t\tvideo_handle[0] = f ? f->stream_id : 0;\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(f != NULL && notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(video_port[0]));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(video_port[1] > 0) {\n\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\t\thost, video_port[1], 0, video_pt[1], video_ssrc[1],\n\t\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 1, TRUE, FALSE);\n\t\t\t\t\t\tvideo_handle[1] = f ? f->stream_id : 0;\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(f != NULL && notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"video_substream\", json_integer(1));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(video_port[1]));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(video_port[2] > 0) {\n\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\t\thost, video_port[2], 0, video_pt[2], video_ssrc[2],\n\t\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 2, TRUE, FALSE);\n\t\t\t\t\t\tvideo_handle[2] = f ? f->stream_id : 0;\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(f != NULL && notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"video\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"video_substream\", json_integer(2));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(video_port[2]));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjanus_videoroom_reqpli(ps, \"New RTP forward publisher\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(data_port > 0) {\n\t\t\t\t/* FIXME Find the data stream */\n\t\t\t\tGList *temp = publisher->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\tif(ps && ps->type == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t\t\t\t/* FIXME Found */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tps = NULL;\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tif(ps == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find any data stream to forward, skipping...\\n\");\n\t\t\t\t} else {\n\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\thost, data_port, 0, 0, 0, FALSE, 0, NULL, 0, FALSE, TRUE);\n\t\t\t\t\tdata_handle = f ? f->stream_id : 0;\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(f != NULL && notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = janus_videoroom_rtp_forwarder_summary(f);\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"rtp_forward\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"publisher_id\",\n\t\t\t\t\t\t\tstring_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"media\", json_string(\"data\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(f->stream_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"host\", json_string(host));\n\t\t\t\t\t\tjson_object_set_new(info, \"port\", json_integer(data_port));\n\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(audio_handle > 0) {\n\t\t\t\tjson_object_set_new(rtp_stream, \"audio_stream_id\", json_integer(audio_handle));\n\t\t\t\tjson_object_set_new(rtp_stream, \"audio\", json_integer(audio_port));\n\t\t\t}\n\t\t\tif(video_handle[0] > 0 || video_handle[1] > 0 || video_handle[2] > 0) {\n\t\t\t\t/* Done */\n\t\t\t\tif(video_handle[0] > 0) {\n\t\t\t\t\tjson_object_set_new(rtp_stream, \"video_stream_id\", json_integer(video_handle[0]));\n\t\t\t\t\tjson_object_set_new(rtp_stream, \"video\", json_integer(video_port[0]));\n\t\t\t\t\tif(video_rtcp_port > 0) {\n\t\t\t\t\t\tjson_object_set_new(rtp_stream, \"video_rtcp\", json_integer(video_rtcp_port));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(video_handle[1] > 0) {\n\t\t\t\t\tjson_object_set_new(rtp_stream, \"video_stream_id_2\", json_integer(video_handle[1]));\n\t\t\t\t\tjson_object_set_new(rtp_stream, \"video_2\", json_integer(video_port[1]));\n\t\t\t\t}\n\t\t\t\tif(video_handle[2] > 0) {\n\t\t\t\t\tjson_object_set_new(rtp_stream, \"video_stream_id_3\", json_integer(video_handle[2]));\n\t\t\t\t\tjson_object_set_new(rtp_stream, \"video_3\", json_integer(video_port[2]));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(data_handle > 0) {\n\t\t\t\tjson_object_set_new(rtp_stream, \"data_stream_id\", json_integer(data_handle));\n\t\t\t\tjson_object_set_new(rtp_stream, \"data\", json_integer(data_port));\n\t\t\t}\n\t\t\tjson_object_set_new(rtp_stream, \"warning\", json_string(\"deprecated_api\"));\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t/* These two unrefs are related to the message handling */\n\t\tjanus_refcount_decrease(&publisher->ref);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tjson_object_set_new(rtp_stream, \"host\", json_string(host));\n\t\tjson_object_set_new(response, \"publisher_id\", string_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\tif(new_forwarders != NULL)\n\t\t\tjson_object_set_new(response, \"forwarders\", new_forwarders);\n\t\tif(rtp_stream != NULL)\n\t\t\tjson_object_set_new(response, \"rtp_stream\", rtp_stream);\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"rtp_forward\"));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"stop_rtp_forward\")) {\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pid_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pidstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, stop_rtp_forward_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_rtpfwd && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *pub_id = json_object_get(root, \"publisher_id\");\n\t\tjson_t *id = json_object_get(root, \"stream_id\");\n\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tguint64 publisher_id = 0;\n\t\tchar publisher_id_num[30], *publisher_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tpublisher_id = json_integer_value(pub_id);\n\t\t\tg_snprintf(publisher_id_num, sizeof(publisher_id_num), \"%\"SCNu64, publisher_id);\n\t\t\tpublisher_id_str = publisher_id_num;\n\t\t} else {\n\t\t\tpublisher_id_str = (char *)json_string_value(pub_id);\n\t\t}\n\t\tguint32 stream_id = json_integer_value(id);\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)publisher_id_str : (gpointer)&publisher_id);\n\t\tif(publisher == NULL) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such publisher (%s)\\n\", publisher_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such feed (%s)\", publisher_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&publisher->ref);\t/* Just to handle the message now */\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\tjanus_mutex_lock(&publisher->rtp_forwarders_mutex);\n\t\t/* Find the forwarder by iterating on all the streams */\n\t\tgboolean found = FALSE;\n\t\tGList *temp = publisher->streams;\n\t\twhile(temp) {\n\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\t\t\tjanus_rtp_forwarder *f = g_hash_table_lookup(ps->rtp_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\tif(f != NULL) {\n\t\t\t\tif(f->metadata != NULL) {\n\t\t\t\t\t/* This belongs to a remotization, ignore */\n\t\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\t\tfound = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tg_hash_table_remove(ps->rtp_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\t/* Found, remove from global index too */\n\t\t\t\tg_hash_table_remove(publisher->rtp_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\t\tfound = TRUE;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\tjanus_refcount_decrease(&publisher->ref);\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tif(!found) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No such stream (%\"SCNu32\")\\n\", stream_id);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such stream (%\"SCNu32\")\", stream_id);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"stop_rtp_forward\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"publisher_id\", string_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\tjson_object_set_new(response, \"stream_id\", json_integer(stream_id));\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"stop_rtp_forward\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\tjson_object_set_new(info, \"publisher_id\", string_ids ? json_string(publisher_id_str) : json_integer(publisher_id));\n\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(stream_id));\n\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t}\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"exists\")) {\n\t\t/* Check whether a given room exists or not, returns true/false */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tgboolean room_exists = g_hash_table_contains(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"exists\", room_exists ? json_true() : json_false());\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"allowed\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to edit the list of allowed participants in an existing VideoRoom room\\n\");\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, allowed_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *action = json_object_get(root, \"action\");\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *allowed = json_object_get(root, \"allowed\");\n\t\tconst char *action_text = json_string_value(action);\n\t\tif(strcasecmp(action_text, \"enable\") && strcasecmp(action_text, \"disable\") &&\n\t\t\t\tstrcasecmp(action_text, \"add\") && strcasecmp(action_text, \"remove\")) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported action '%s' (allowed)\\n\", action_text);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Unsupported action '%s' (allowed)\", action_text);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tif(!strcasecmp(action_text, \"enable\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Enabling the check on allowed authorization tokens for room %s\\n\", room_id_str);\n\t\t\tvideoroom->check_allowed = TRUE;\n\t\t} else if(!strcasecmp(action_text, \"disable\")) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Disabling the check on allowed authorization tokens for room %s (free entry)\\n\", room_id_str);\n\t\t\tvideoroom->check_allowed = FALSE;\n\t\t} else {\n\t\t\tgboolean add = !strcasecmp(action_text, \"add\");\n\t\t\tif(allowed) {\n\t\t\t\t/* Make sure the \"allowed\" array only contains strings */\n\t\t\t\tgboolean ok = TRUE;\n\t\t\t\tif(json_array_size(allowed) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\t\tjson_t *a = json_array_get(allowed, i);\n\t\t\t\t\t\tif(!a || !json_is_string(a)) {\n\t\t\t\t\t\t\tok = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!ok) {\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element in the allowed array (not a string)\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element in the allowed array (not a string)\");\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tgoto prepare_response;\n\t\t\t\t}\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(allowed); i++) {\n\t\t\t\t\tconst char *token = json_string_value(json_array_get(allowed, i));\n\t\t\t\t\tif(add) {\n\t\t\t\t\t\tif(!g_hash_table_lookup(videoroom->allowed, token))\n\t\t\t\t\t\t\tg_hash_table_insert(videoroom->allowed, g_strdup(token), GINT_TO_POINTER(TRUE));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tg_hash_table_remove(videoroom->allowed, token);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\tjson_t *list = json_array();\n\t\tif(strcasecmp(action_text, \"disable\")) {\n\t\t\tif(g_hash_table_size(videoroom->allowed) > 0) {\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer key;\n\t\t\t\tg_hash_table_iter_init(&iter, videoroom->allowed);\n\t\t\t\twhile(g_hash_table_iter_next(&iter, &key, NULL)) {\n\t\t\t\t\tchar *token = key;\n\t\t\t\t\tjson_array_append_new(list, json_string(token));\n\t\t\t\t}\n\t\t\t}\n\t\t\tjson_object_set_new(response, \"allowed\", list);\n\t\t}\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tJANUS_LOG(LOG_VERB, \"VideoRoom room allowed list updated\\n\");\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"kick\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to kick a participant from an existing VideoRoom room\\n\");\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, kick_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tguint64 user_id = 0;\n\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tuser_id = json_integer_value(id);\n\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\tuser_id_str = user_id_num;\n\t\t} else {\n\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_videoroom_publisher *participant = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such user %s in room %s\\n\", user_id_str, room_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such user %s in room %s\", user_id_str, room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(participant->dummy) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't kick dummy users\\n\");\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"Can't kick dummy users\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&participant->ref);\n\t\tif(participant->kicked) {\n\t\t\t/* Already kicked */\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\tresponse = json_object();\n\t\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\t\t/* Done */\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tparticipant->kicked = TRUE;\n\t\tg_atomic_int_set(&participant->session->started, 0);\n\t\t/* Prepare an event for this */\n\t\tjson_t *kicked = json_object();\n\t\tjson_object_set_new(kicked, \"videoroom\", json_string(\"event\"));\n\t\tjson_object_set_new(kicked, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\t\tjson_object_set_new(kicked, \"leaving\", json_string(\"ok\"));\n\t\tjson_object_set_new(kicked, \"reason\", json_string(\"kicked\"));\n\t\tint ret = gateway->push_event(participant->session->handle, &janus_videoroom_plugin, NULL, kicked, NULL);\n\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\tjson_decref(kicked);\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t/* If this room requires valid private_id values, we can kick subscriptions too */\n\t\tif(videoroom->require_pvtid && participant->subscriptions != NULL) {\n\t\t\t/* Iterate on the subscriptions we know this user has */\n\t\t\tjanus_mutex_lock(&participant->own_subscriptions_mutex);\n\t\t\tGSList *s = participant->subscriptions;\n\t\t\twhile(s) {\n\t\t\t\tjanus_videoroom_subscriber *subscriber = (janus_videoroom_subscriber *)s->data;\n\t\t\t\tif(subscriber) {\n\t\t\t\t\tsubscriber->kicked = TRUE;\n\t\t\t\t\t/* FIXME We should also close the PeerConnection, but we risk race conditions if we do it here,\n\t\t\t\t\t * so for now we mark the subscriber as kicked and prevent it from getting any media after this */\n\t\t\t\t}\n\t\t\t\ts = s->next;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&participant->own_subscriptions_mutex);\n\t\t}\n\t\t/* This publisher is leaving, tell everybody */\n\t\tjanus_videoroom_leave_or_unpublish(participant, TRUE, TRUE);\n\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\tif(participant && !g_atomic_int_get(&participant->destroyed) && participant->session)\n\t\t\tgateway->close_pc(participant->session->handle);\n\t\tJANUS_LOG(LOG_INFO, \"Kicked user %s from room %s\\n\", user_id_str, room_id_str);\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\t/* Done */\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tjanus_refcount_decrease(&participant->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"moderate\")) {\n\t\tJANUS_LOG(LOG_VERB, \"Attempt to moderate a participant as a moderator in an existing VideoRoom room\\n\");\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, moderate_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tguint64 user_id = 0;\n\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tuser_id = json_integer_value(id);\n\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\tuser_id_str = user_id_num;\n\t\t} else {\n\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_videoroom_publisher *participant = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id);\n\t\tif(participant == NULL) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such user %s in room %s\\n\", user_id_str, room_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such user %s in room %s\", user_id_str, room_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&participant->ref);\n\t\t/* Check if there's any media delivery to change */\n\t\tconst char *mid = json_string_value(json_object_get(root, \"mid\"));\n\t\tgboolean muted = json_is_true(json_object_get(root, \"mute\"));\n\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t/* Subscribe to a specific mid */\n\t\tjanus_videoroom_publisher_stream *ps = g_hash_table_lookup(participant->streams_bymid, mid);\n\t\tif(ps == NULL) {\n\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such stream %s\\n\", mid);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such stream %s\", mid);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO || ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\tif(participant->session && g_atomic_int_get(&participant->session->started) &&\n\t\t\t\t\t!muted && ps->active && ps->muted) {\n\t\t\t\t/* Audio/Video was just resumed, try resetting the RTP headers for viewers */\n\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\t\tGSList *temp = ps->subscribers;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_videoroom_subscriber_stream *ss = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\t\tif(ss)\n\t\t\t\t\t\tss->context.seq_reset = TRUE;\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t}\n\t\t}\n\t\tps->muted = muted;\n\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t/* Prepare an event for this */\n\t\tjson_t *event = json_object();\n\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\t\tjson_object_set_new(event, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\tjson_object_set_new(event, \"mid\", json_string(mid));\n\t\tjson_object_set_new(event, \"moderation\", muted ? json_string(\"muted\") : json_string(\"unmuted\"));\n\t\t/* Notify the speaker this event is related to as well */\n\t\tjanus_videoroom_notify_participants(participant, event, TRUE);\n\t\tjson_decref(event);\n\t\t/* Also notify event handlers */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"videoroom\", json_string(\"moderated\"));\n\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\tjson_object_set_new(info, \"mid\", json_string(mid));\n\t\t\tjson_object_set_new(info, \"moderation\", muted ? json_string(\"muted\") : json_string(\"unmuted\"));\n\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t}\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t/* Prepare response */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\t/* Done */\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tjanus_refcount_decrease(&participant->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"listparticipants\")) {\n\t\t/* List all participants in a room, specifying whether they're publishers or just attendees */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, FALSE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t/* Return a list of all participants (whether they're publishing or not) */\n\t\tjson_t *list = json_array();\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tg_hash_table_iter_init(&iter, videoroom->participants);\n\t\twhile (!g_atomic_int_get(&videoroom->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_videoroom_publisher *p = value;\n\t\t\tjson_t *pl = json_object();\n\t\t\tjson_object_set_new(pl, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\tif(p->display)\n\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\tif(p->metadata)\n\t\t\t\tjson_object_set_new(pl, \"metadata\", json_deep_copy(p->metadata));\n\t\t\tif(p->dummy)\n\t\t\t\tjson_object_set_new(pl, \"dummy\", json_true());\n\t\t\tif(p->remote)\n\t\t\t\tjson_object_set_new(pl, \"remote\", json_true());\n\t\t\tjson_object_set_new(pl, \"publisher\", g_atomic_int_get(&p->session->started) ? json_true() : json_false());\n\t\t\t/* To see if the participant is talking, we need to find the audio stream(s) */\n\t\t\tif(g_atomic_int_get(&p->session->started)) {\n\t\t\t\tgboolean found = FALSE, talking = FALSE;\n\t\t\t\tjanus_mutex_lock(&p->streams_mutex);\n\t\t\t\tGList *temp = p->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\tif(ps && ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO &&\n\t\t\t\t\t\t\tps->audio_level_extmap_id > 0) {\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\ttalking |= ps->talking;\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t\t\t\tif(found)\n\t\t\t\t\tjson_object_set_new(pl, \"talking\", talking ? json_true() : json_false());\n\t\t\t}\n\t\t\tjson_array_append_new(list, pl);\n\t\t}\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"participants\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"participants\", list);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"listforwarders\")) {\n\t\t/* List all forwarders in a room */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *room = json_object_get(root, \"room\");\n\t\tguint64 room_id = 0;\n\t\tchar room_id_num[30], *room_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\troom_id = json_integer_value(room);\n\t\t\tg_snprintf(room_id_num, sizeof(room_id_num), \"%\"SCNu64, room_id);\n\t\t\troom_id_str = room_id_num;\n\t\t} else {\n\t\t\troom_id_str = (char *)json_string_value(room);\n\t\t}\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t/* Return a list of all forwarders */\n\t\tjson_t *list = json_array();\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tg_hash_table_iter_init(&iter, videoroom->participants);\n\t\twhile (!g_atomic_int_get(&videoroom->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_videoroom_publisher *p = value;\n\t\t\tjanus_mutex_lock(&p->streams_mutex);\n\t\t\tjanus_mutex_lock(&p->rtp_forwarders_mutex);\n\t\t\tif(g_hash_table_size(p->rtp_forwarders) == 0) {\n\t\t\t\tjanus_mutex_unlock(&p->rtp_forwarders_mutex);\n\t\t\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjson_t *pl = json_object();\n\t\t\tjson_object_set_new(pl, \"publisher_id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\tif(p->display)\n\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\tif(p->metadata)\n\t\t\t\tjson_object_set_new(pl, \"metadata\", json_deep_copy(p->metadata));\n\t\t\tjson_t *flist = json_array();\n\t\t\t/* Iterate on all media streams to see what's being forwarded */\n\t\t\tjanus_videoroom_publisher_stream *ps = NULL;\n\t\t\tGList *temp = p->streams;\n\t\t\twhile(temp) {\n\t\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\t\t\t\tif(g_hash_table_size(ps->rtp_forwarders) == 0) {\n\t\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tGHashTableIter iter_f;\n\t\t\t\tgpointer key_f, value_f;\n\t\t\t\tg_hash_table_iter_init(&iter_f, ps->rtp_forwarders);\n\t\t\t\twhile(g_hash_table_iter_next(&iter_f, &key_f, &value_f)) {\n\t\t\t\t\tjanus_rtp_forwarder *rpv = value_f;\n\t\t\t\t\t/* If this belongs to a remotization, skip it */\n\t\t\t\t\tif(rpv->metadata != NULL)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t/* Return a different, media-agnostic, format */\n\t\t\t\t\tjson_t *fl = janus_videoroom_rtp_forwarder_summary(rpv);\n\t\t\t\t\tjson_array_append_new(flist, fl);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&p->rtp_forwarders_mutex);\n\t\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t\t\tjson_object_set_new(pl, \"forwarders\", flist);\n\t\t\tjson_array_append_new(list, pl);\n\t\t}\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"forwarders\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\tjson_object_set_new(response, \"publishers\", list);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"enable_recording\")) {\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, record_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjson_t *record = json_object_get(root, \"record\");\n\t\tgboolean recording_active = json_is_true(record);\n\t\tJANUS_LOG(LOG_VERB, \"Enable Recording: %d\\n\", (recording_active ? 1 : 0));\n\t\t/* Lookup room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\t/* Set recording status */\n\t\tgboolean room_new_recording_active = recording_active;\n\t\tif (room_new_recording_active != videoroom->record) {\n\t\t\t/* Room recording state has changed */\n\t\t\tvideoroom->record = room_new_recording_active;\n\t\t\t/* Iterate over all participants */\n\t\t\tgpointer value;\n\t\t\tGHashTableIter iter;\n\t\t\tg_hash_table_iter_init(&iter, videoroom->participants);\n\t\t\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_videoroom_publisher *participant = value;\n\t\t\t\tif(participant && participant->session) {\n\t\t\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\t\t\tgboolean prev_recording_active = participant->recording_active;\n\t\t\t\t\tparticipant->recording_active = recording_active;\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting record property: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->recording_active ? \"true\" : \"false\", participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t/* Do we need to do something with the recordings right now? */\n\t\t\t\t\tif(participant->recording_active != prev_recording_active) {\n\t\t\t\t\t\t/* Something changed */\n\t\t\t\t\t\tif(!participant->recording_active) {\n\t\t\t\t\t\t\t/* Not recording (anymore?) */\n\t\t\t\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\t\t\t\tjanus_videoroom_recorder_close(participant);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\t\t} else if(participant->recording_active && g_atomic_int_get(&participant->session->started)) {\n\t\t\t\t\t\t\t/* We've started recording, send a PLI and go on */\n\t\t\t\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\t\t\t\tGList *temp = participant->streams;\n\t\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\t\t\tjanus_videoroom_recorder_create(ps);\n\t\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Recording video\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"record\", json_boolean(recording_active));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"publish_remotely\")) {\n\t\t/* Configure a local publisher to restream to a remote VideoRomm instance as well */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, publish_remotely_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pid_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pidstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(lock_rtpfwd && admin_key != NULL) {\n\t\t\t/* An admin key was specified: make sure it was provided, and that it's valid */\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t\tJANUS_CHECK_SECRET(admin_key, root, \"admin_key\", error_code, error_cause,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* We may need to SRTP-encrypt this stream */\n\t\tint srtp_suite = 0;\n\t\tconst char *srtp_crypto = NULL;\n\t\tjson_t *s_suite = json_object_get(root, \"srtp_suite\");\n\t\tjson_t *s_crypto = json_object_get(root, \"srtp_crypto\");\n\t\tif(s_suite && s_crypto) {\n\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP suite (%d)\\n\", srtp_suite);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP suite (%d)\", srtp_suite);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t\tJANUS_LOG(LOG_VERB, \"SRTP setting s_suite (%d) and s_crypto (%s) on publish_remotely\\n\", srtp_suite, srtp_crypto);\n\t\t}\n\t\tconst char *remote_id = json_string_value(json_object_get(root, \"remote_id\"));\n\t\tjson_t *pub_id = json_object_get(root, \"publisher_id\");\n\t\tjson_t *json_host = json_object_get(root, \"host\");\n\t\tjson_t *json_host_family = json_object_get(root, \"host_family\");\n\t\tconst char *host_family = json_string_value(json_host_family);\n\t\tuint16_t port = json_integer_value(json_object_get(root, \"port\"));\n\t\tuint16_t rtcp_port = json_integer_value(json_object_get(root, \"rtcp_port\"));\n\t\tif(port == 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (port must be a non-zero positive integer)\\n\");\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Invalid element (port must be a non-zero positive integer)\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tint family = 0;\n\t\tif(host_family) {\n\t\t\tif(!strcasecmp(host_family, \"ipv4\")) {\n\t\t\t\tfamily = AF_INET;\n\t\t\t} else if(!strcasecmp(host_family, \"ipv6\")) {\n\t\t\t\tfamily = AF_INET6;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported protocol family (%s)\\n\", host_family);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unsupported protocol family (%s)\", host_family);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tguint64 publisher_id = 0;\n\t\tchar publisher_id_num[30], *publisher_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tpublisher_id = json_integer_value(pub_id);\n\t\t\tg_snprintf(publisher_id_num, sizeof(publisher_id_num), \"%\"SCNu64, publisher_id);\n\t\t\tpublisher_id_str = publisher_id_num;\n\t\t} else {\n\t\t\tpublisher_id_str = (char *)json_string_value(pub_id);\n\t\t}\n\t\tconst char *host = json_string_value(json_host), *resolved_host = NULL;\n\t\t/* Check if we need to resolve this host address */\n\t\tstruct addrinfo *res = NULL, *start = NULL;\n\t\tjanus_network_address addr;\n\t\tjanus_network_address_string_buffer addr_buf;\n\t\tstruct addrinfo hints;\n\t\tmemset(&hints, 0, sizeof(hints));\n\t\tif(family != 0)\n\t\t\thints.ai_family = family;\n\t\tif(getaddrinfo(host, NULL, family != 0 ? &hints : NULL, &res) == 0) {\n\t\t\tstart = res;\n\t\t\twhile(res != NULL) {\n\t\t\t\tif(janus_network_address_from_sockaddr(res->ai_addr, &addr) == 0 &&\n\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\t/* Resolved */\n\t\t\t\t\tresolved_host = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\tfreeaddrinfo(start);\n\t\t\t\t\tstart = NULL;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tres = res->ai_next;\n\t\t\t}\n\t\t}\n\t\tif(resolved_host == NULL) {\n\t\t\tif(start)\n\t\t\t\tfreeaddrinfo(start);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not resolve address (%s)...\\n\", host);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Could not resolve address (%s)...\", host);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\thost = resolved_host;\n\t\t/* Look for room and publisher */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)publisher_id_str : (gpointer)&publisher_id);\n\t\tif(publisher == NULL) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such publisher (%s)\\n\", publisher_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such feed (%s)\", publisher_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&publisher->ref);\t/* This is just to handle the request for now */\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t/* FIXME At the moment, we only allow for the remotization of\n\t\t * local publishers, not remote ones: it may make sense to allow\n\t\t * the remotization of remote publishers as well in the future\n\t\t * (e.g., for cascading beyond the source), but that's something\n\t\t * that in case we'll work on in subsequent code changes */\n\t\tif(publisher->remote) {\n\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Only local publishers can be remotized\\n\");\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\tg_snprintf(error_cause, 512, \"Only local publishers can be remotized\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\tjanus_mutex_lock(&publisher->rtp_forwarders_mutex);\n\t\tif(g_hash_table_lookup(publisher->remote_recipients, remote_id) != NULL) {\n\t\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Remotization already exists (%s)\\n\", remote_id);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ID_EXISTS;\n\t\t\tg_snprintf(error_cause, 512, \"Remotization already exists (%s)\", remote_id);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tif(publisher->udp_sock <= 0) {\n\t\t\tpublisher->udp_sock = socket(!ipv6_disabled ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\tint v6only = 0;\n\t\t\tif(publisher->udp_sock <= 0 ||\n\t\t\t\t\t(!ipv6_disabled && setsockopt(publisher->udp_sock, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)) {\n\t\t\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Could not open UDP socket for RTP stream for publisher (%s), %d (%s)\\n\",\n\t\t\t\t\tpublisher_id_str, errno, g_strerror(errno));\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Could not open UDP socket for RTP stream\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\t/* Add a new RTP forwarder for each of the publisher streams */\n\t\tjanus_videoroom_publisher_stream *ps = NULL;\n\t\tjanus_rtp_forwarder *f = NULL;\n\t\tgboolean rtcp_added = FALSE, add_rtcp = FALSE;\n\t\tGList *temp = publisher->streams;\n\t\twhile(temp) {\n\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\tif(ps == NULL || g_atomic_int_get(&ps->destroyed)) {\n\t\t\t\ttemp = temp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t/* Audio stream */\n\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\thost, port, -1, 0,\n\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP),\n\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 0, FALSE, FALSE);\n\t\t\t\tif(f != NULL)\n\t\t\t\t\tf->metadata = g_strdup(remote_id);\n\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t/* Video stream */\n\t\t\t\tadd_rtcp = (!rtcp_added && rtcp_port > 0);\n\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\thost, port, add_rtcp ? rtcp_port : -1, 0,\n\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP),\n\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 0, TRUE, FALSE);\n\t\t\t\tif(f != NULL)\n\t\t\t\t\tf->metadata = g_strdup(remote_id);\n\t\t\t\tif(add_rtcp)\n\t\t\t\t\trtcp_added = TRUE;\n\t\t\t\t/* Check if there's simulcast substreams we need to relay too */\n\t\t\t\tif(ps->vssrc[1] || ps->rid[1]) {\n\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\thost, port, -1, 0,\n\t\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP + 1),\n\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 1, TRUE, FALSE);\n\t\t\t\t\tif(f != NULL)\n\t\t\t\t\t\tf->metadata = g_strdup(remote_id);\n\t\t\t\t}\n\t\t\t\tif(ps->vssrc[2] || ps->rid[2]) {\n\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\t\thost, port, -1, 0,\n\t\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP + 2),\n\t\t\t\t\t\tFALSE, srtp_suite, srtp_crypto, 2, TRUE, FALSE);\n\t\t\t\t\tif(f != NULL)\n\t\t\t\t\t\tf->metadata = g_strdup(remote_id);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Data stream */\n\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(publisher, ps,\n\t\t\t\t\thost, port, -1, 0,\n\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP),\n\t\t\t\t\tFALSE, 0, NULL, 0, FALSE, TRUE);\n\t\t\t\tif(f != NULL)\n\t\t\t\t\tf->metadata = g_strdup(remote_id);\n\t\t\t}\n\t\t\ttemp = temp->next;\n\t\t}\n\t\t/* Keep track of this remotization */\n\t\tjanus_videoroom_remote_recipient *recipient = g_malloc(sizeof(janus_videoroom_remote_recipient));\n\t\trecipient->remote_id = g_strdup(remote_id);\n\t\trecipient->host = g_strdup(host);\n\t\trecipient->port = port;\n\t\trecipient->rtcp_port = rtcp_port;\n\t\trecipient->rtcp_added = rtcp_added;\n\t\trecipient->srtp_suite = srtp_suite;\n\t\trecipient->srtp_crypto = srtp_crypto ? g_strdup(srtp_crypto) : NULL;\n\t\tg_hash_table_insert(publisher->remote_recipients, g_strdup(remote_id), recipient);\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(publisher->room_id_str) : json_integer(publisher->room_id));\n\t\tjson_object_set_new(response, \"id\", string_ids ? json_string(publisher->user_id_str) : json_integer(publisher->user_id));\n\t\tjson_object_set_new(response, \"remote_id\", json_string(remote_id));\n\t\tjanus_refcount_decrease(&publisher->ref);\t/* This is just to handle the request for now */\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"unpublish_remotely\")) {\n\t\t/* Configure a local publisher to stop restreaming to a remote VideoRomm instance */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, unpublish_remotely_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pid_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pidstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tconst char *remote_id = json_string_value(json_object_get(root, \"remote_id\"));\n\t\tjson_t *pub_id = json_object_get(root, \"publisher_id\");\n\t\tguint64 publisher_id = 0;\n\t\tchar publisher_id_num[30], *publisher_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tpublisher_id = json_integer_value(pub_id);\n\t\t\tg_snprintf(publisher_id_num, sizeof(publisher_id_num), \"%\"SCNu64, publisher_id);\n\t\t\tpublisher_id_str = publisher_id_num;\n\t\t} else {\n\t\t\tpublisher_id_str = (char *)json_string_value(pub_id);\n\t\t}\n\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)publisher_id_str : (gpointer)&publisher_id);\n\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed)) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such publisher (%s)\\n\", publisher_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such publisher (%s)\", publisher_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&publisher->ref);\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\tjanus_mutex_lock(&publisher->rtp_forwarders_mutex);\n\t\t/* Check if we know of this remotization */\n\t\tif(g_hash_table_remove(publisher->remote_recipients, remote_id) == FALSE) {\n\t\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such remotization (%s)\\n\", remote_id);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such remotization (%s)\", remote_id);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Now get rid of all RTP forwarders with that ID */\n\t\tGList *temp = publisher->streams;\n\t\twhile(temp) {\n\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, ps->rtp_forwarders);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_rtp_forwarder *f = (janus_rtp_forwarder *)value;\n\t\t\t\tif(f->metadata != NULL && !strcmp((char *)f->metadata, remote_id)) {\n\t\t\t\t\t/* We found one, get rid of it */\n\t\t\t\t\tuint32_t stream_id = f->stream_id;\n\t\t\t\t\tg_hash_table_iter_remove(&iter);\n\t\t\t\t\t/* Remove from global index too */\n\t\t\t\t\tg_hash_table_remove(publisher->rtp_forwarders, GUINT_TO_POINTER(stream_id));\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t/* Done */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(publisher->room_id_str) : json_integer(publisher->room_id));\n\t\tjson_object_set_new(response, \"id\", string_ids ? json_string(publisher->user_id_str) : json_integer(publisher->user_id));\n\t\tjanus_refcount_decrease(&publisher->ref);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"listremotes\")) {\n\t\t/* List all the remote restreams a local publisher is configured with;\n\t\t * notice that this is different from RTP forwarders, since this is\n\t\t * explicitly related to the concept of remote publishers */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pid_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, pidstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tjson_t *id = json_object_get(root, \"publisher_id\");\n\t\tguint64 publisher_id = 0;\n\t\tchar publisher_id_num[30], *publisher_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tpublisher_id = json_integer_value(id);\n\t\t\tg_snprintf(publisher_id_num, sizeof(publisher_id_num), \"%\"SCNu64, publisher_id);\n\t\t\tpublisher_id_str = publisher_id_num;\n\t\t} else {\n\t\t\tpublisher_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)publisher_id_str : (gpointer)&publisher_id);\n\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed)) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such publisher (%s)\\n\", publisher_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such publisher (%s)\", publisher_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&publisher->ref);\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_mutex_lock(&publisher->rtp_forwarders_mutex);\n\t\t/* Return a list of all remotizations for this publisher */\n\t\tjson_t *list = json_array();\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, publisher->remote_recipients);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_videoroom_remote_recipient *r = (janus_videoroom_remote_recipient *)value;\n\t\t\tif(r) {\n\t\t\t\tjson_t *pr = json_object();\n\t\t\t\tjson_object_set_new(pr, \"remote_id\", json_string(r->remote_id));\n\t\t\t\tjson_object_set_new(pr, \"host\", json_string(r->host));\n\t\t\t\tjson_object_set_new(pr, \"port\", json_integer(r->port));\n\t\t\t\tif(r->rtcp_port > 0)\n\t\t\t\t\tjson_object_set_new(pr, \"rtcp_port\", json_integer(r->rtcp_port));\n\t\t\t\tjson_array_append_new(list, pr);\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->rtp_forwarders_mutex);\n\t\t/* Done */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(publisher->room_id_str) : json_integer(publisher->room_id));\n\t\tjson_object_set_new(response, \"id\", string_ids ? json_string(publisher->user_id_str) : json_integer(publisher->user_id));\n\t\tjson_object_set_new(response, \"list\", list);\n\t\tjanus_refcount_decrease(&publisher->ref);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"add_remote_publisher\")) {\n\t\t/* Add a new remote publisher */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idopt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstropt_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, remote_publisher_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\t/* Validate the stream parameters too */\n\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\tif(json_array_size(streams) == 0) {\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element value (streams can't be empty)\\n\");\n\t\t\tg_snprintf(error_cause, 512, \"Invalid element value (streams can't be empty)\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tsize_t i = 0;\n\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, remote_publisher_stream_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tbreak;\n\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\tjanus_videoroom_media mtype = janus_videoroom_media_from_str(type);\n\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_NONE) {\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element value (type)\\n\");\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element value (type)\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_AUDIO || mtype == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\tconst char *codec = json_string_value(json_object_get(s, \"codec\"));\n\t\t\t\tif(codec == NULL) {\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing mandatory element (codec)\\n\");\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Missing mandatory element (codec)\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif((mtype == JANUS_VIDEOROOM_MEDIA_AUDIO && janus_audiocodec_from_name(codec) == JANUS_AUDIOCODEC_NONE) ||\n\t\t\t\t\t\t(mtype == JANUS_VIDEOROOM_MEDIA_VIDEO && janus_videocodec_from_name(codec) == JANUS_VIDEOCODEC_NONE)) {\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element value (unsupported codec)\\n\");\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element value (unsupported codec)\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* We may need to SRTP-decrypt this stream */\n\t\tint srtp_suite = 0;\n\t\tconst char *srtp_crypto = NULL;\n\t\tjson_t *s_suite = json_object_get(root, \"srtp_suite\");\n\t\tjson_t *s_crypto = json_object_get(root, \"srtp_crypto\");\n\t\tif(s_suite && s_crypto) {\n\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP suite (%d)\\n\", srtp_suite);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP suite (%d)\", srtp_suite);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t\tJANUS_LOG(LOG_VERB, \"SRTP setting s_suite (%d) and s_crypto (%s) on add_remote_publisher\\n\", srtp_suite, srtp_crypto);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\t/* Now access the room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\t/* Prepare a new fake publisher on behalf of the remote one */\n\t\tjson_t *display = json_object_get(root, \"display\");\n\t\tconst char *display_text = display ? json_string_value(display) : NULL;\n\t\tguint64 user_id = 0;\n\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\tgboolean user_id_allocated = FALSE;\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tjson_t *metadata = json_object_get(root, \"metadata\");\n\t\tif(id) {\n\t\t\tif(!string_ids) {\n\t\t\t\tuser_id = json_integer_value(id);\n\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\tuser_id_str = user_id_num;\n\t\t\t} else {\n\t\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t\t}\n\t\t\tif(g_hash_table_lookup(videoroom->participants,\n\t\t\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id) != NULL) {\n\t\t\t\t/* User ID already taken */\n\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ID_EXISTS;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"User ID %s already exists\\n\", user_id_str);\n\t\t\t\tg_snprintf(error_cause, 512, \"User ID %s already exists\", user_id_str);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t}\n\t\tif(!string_ids) {\n\t\t\tif(user_id == 0) {\n\t\t\t\t/* Generate a random ID */\n\t\t\t\twhile(user_id == 0) {\n\t\t\t\t\tuser_id = janus_random_uint64();\n\t\t\t\t\tif(g_hash_table_lookup(videoroom->participants, &user_id) != NULL) {\n\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\tuser_id = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\tuser_id_str = user_id_num;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID: %\"SCNu64\"\\n\", user_id);\n\t\t} else {\n\t\t\tif(user_id_str == NULL) {\n\t\t\t\t/* Generate a random ID */\n\t\t\t\twhile(user_id_str == NULL) {\n\t\t\t\t\tuser_id_str = janus_random_uuid();\n\t\t\t\t\tif(g_hash_table_lookup(videoroom->participants, user_id_str) != NULL) {\n\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\tg_clear_pointer(&user_id_str, g_free);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tuser_id_allocated = TRUE;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID: %s\\n\", user_id_str);\n\t\t}\n\t\t/* Create the socket we'll need for this remote publisher */\n\t\tconst char *mcast = json_string_value(json_object_get(root, \"mcast\"));\n\t\tconst char *iface = json_string_value(json_object_get(root, \"iface\"));\n\t\tjanus_network_address miface;\n\t\tif(iface) {\n\t\t\tstruct ifaddrs *ifas = NULL;\n\t\t\tif(getifaddrs(&ifas) == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unable to acquire list of network devices/interfaces; remote publishers may not work as expected... %d (%s)\\n\",\n\t\t\t\t\terrno, g_strerror(errno));\n\t\t\t}\n\t\t\tif(janus_network_lookup_interface(ifas, iface, &miface) != 0) {\n\t\t\t\tif(user_id_allocated)\n\t\t\t\t\tg_free(user_id_str);\n\t\t\t\tif(ifas)\n\t\t\t\t\tfreeifaddrs(ifas);\n\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid network interface configuration for remote publisher...\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, ifas ? \"Invalid network interface configuration for remote publisher\" : \"Unable to query network device information\");\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tif(ifas)\n\t\t\t\tfreeifaddrs(ifas);\n\t\t} else {\n\t\t\tjanus_network_address_nullify(&miface);\n\t\t}\n\t\tuint16_t port = json_integer_value(json_object_get(root, \"port\"));\n\t\tuint16_t rtcp_port = json_integer_value(json_object_get(root, \"rtcp_port\"));\n\t\tchar host[46];\n\t\thost[0] = '\\0';\n\t\tint fd = janus_videoroom_create_fd(port, mcast ? inet_addr(mcast) : INADDR_ANY, &miface, host, sizeof(host));\n\t\tif(fd < 0) {\n\t\t\tif(user_id_allocated)\n\t\t\t\tg_free(user_id_str);\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not open UDP socket for RTP stream for remote publisher, %d (%s)\\n\",\n\t\t\t\terrno, g_strerror(errno));\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Could not open UDP socket for RTP stream\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tport = janus_videoroom_get_fd_port(fd);\n\t\tint rtcp_fd = janus_videoroom_create_fd(rtcp_port, mcast ? inet_addr(mcast) : INADDR_ANY, &miface, host, sizeof(host));\n\t\tif(rtcp_fd < 0) {\n\t\t\tclose(fd);\n\t\t\tif(user_id_allocated)\n\t\t\t\tg_free(user_id_str);\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not open UDP socket for remote publisher RTCP, %d (%s)\\n\",\n\t\t\t\terrno, g_strerror(errno));\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Could not open UDP socket for RTP stream\");\n\t\t\tgoto prepare_response;\n\t\t}\n\t\trtcp_port = janus_videoroom_get_fd_port(rtcp_fd);\n\t\t/* We create a dummy session first, that's not actually bound to anything */\n\t\tjanus_videoroom_session *session = g_malloc0(sizeof(janus_videoroom_session));\n\t\tsession->handle = NULL;\n\t\tsession->participant_type = janus_videoroom_p_type_publisher;\n\t\tg_atomic_int_set(&session->started, 1);\n\t\tjanus_mutex_init(&session->mutex);\n\t\tjanus_refcount_init(&session->ref, janus_videoroom_session_free);\n\t\t/* We actually create a publisher instance, which has no associated session but looks like it's publishing */\n\t\tjanus_videoroom_publisher *publisher = g_malloc0(sizeof(janus_videoroom_publisher));\n\t\tpublisher->session = session;\n\t\tsession->participant = publisher;\n\t\tpublisher->room_id = videoroom->room_id;\n\t\tpublisher->room_id_str = videoroom->room_id_str ? g_strdup(videoroom->room_id_str) : NULL;\n\t\tpublisher->room = videoroom;\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tpublisher->user_id = user_id;\n\t\tpublisher->user_id_str = user_id_allocated ? user_id_str : g_strdup(user_id_str);\n\t\tpublisher->display = display_text ? g_strdup(display_text) : NULL;\n\t\tpublisher->acodec = JANUS_AUDIOCODEC_NONE;\n\t\tpublisher->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\tpublisher->data_mindex = -1;\n\t\tpublisher->remote = TRUE;\n\t\tpublisher->remote_ssrc_offset = janus_random_uint32();\n\t\tpublisher->remote_fd = fd;\n\t\tpublisher->remote_rtcp_fd = rtcp_fd;\n\t\tpublisher->metadata = metadata ? json_deep_copy(metadata) : NULL;\n\t\tpipe(publisher->pipefd);\n\t\tjanus_mutex_init(&publisher->subscribers_mutex);\n\t\tjanus_mutex_init(&publisher->own_subscriptions_mutex);\n\t\tpublisher->streams_byid = g_hash_table_new_full(NULL, NULL,\n\t\t\tNULL, (GDestroyNotify)janus_videoroom_publisher_stream_destroy);\n\t\tpublisher->streams_bymid = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_publisher_stream_unref);\n\t\tjanus_mutex_init(&publisher->streams_mutex);\n\t\tjanus_mutex_init(&publisher->rtp_forwarders_mutex);\n\t\tpublisher->remote_recipients = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_remote_recipient_free);\n\t\tpublisher->rtp_forwarders = g_hash_table_new(NULL, NULL);\n\t\tpublisher->udp_sock = -1;\n\t\tg_atomic_int_set(&publisher->destroyed, 0);\n\t\tjanus_mutex_init(&publisher->mutex);\n\t\tjanus_refcount_init(&publisher->ref, janus_videoroom_publisher_free);\n\t\t/* Create publisher streams for all the things that the remote publisher is sending */\n\t\tjanus_videoroom_publisher_stream *ps = NULL;\n\t\tint mindex = 0;\n\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\tjanus_videoroom_media mtype = janus_videoroom_media_from_str(type);\n\t\t\tconst char *codec = json_string_value(json_object_get(s, \"codec\"));\n\t\t\tconst char *desc = json_string_value(json_object_get(s, \"description\"));\n\t\t\tgboolean disabled = json_is_true(json_object_get(s, \"disabled\"));\n\t\t\t/* Create a publisher stream */\n\t\t\tps = g_malloc0(sizeof(janus_videoroom_publisher_stream));\n\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_AUDIO || mtype == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t/* First of all, let's check if we need to setup an SRTP for remote publisher */\n\t\t\t\tif(srtp_suite > 0 && srtp_crypto != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"enabling SRTP crypto (%s) for stream.\\n\", srtp_crypto);\n\t\t\t\t\tgsize len = 0;\n\t\t\t\t\tguchar *srtp_crypto_decoded = g_base64_decode(srtp_crypto, &len);\n\t\t\t\t\tif(len < SRTP_MASTER_LENGTH) {\n\t\t\t\t\t\t/* Something went wrong */\n\t\t\t\t\t\tg_free(srtp_crypto_decoded);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP crypto (%s), disabling stream\\n\", srtp_crypto);\n\t\t\t\t\t\tps->is_srtp = FALSE;\n\t\t\t\t\t\tdisabled = TRUE;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Set SRTP policy */\n\t\t\t\t\t\tsrtp_policy_t *policy = &ps->srtp_policy;\n\t\t\t\t\t\tsrtp_crypto_policy_set_rtp_default(&policy->rtp);\n\t\t\t\t\t\tif(srtp_suite == 32) {\n\t\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&policy->rtp);\n\t\t\t\t\t\t} else if(srtp_suite == 80) {\n\t\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy->rtp);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpolicy->ssrc.type = ssrc_any_inbound;\n\t\t\t\t\t\tpolicy->key = srtp_crypto_decoded;\n\t\t\t\t\t\tpolicy->next = NULL;\n\t\t\t\t\t\t/* Create SRTP context */\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_create(&ps->srtp_ctx, policy);\n\t\t\t\t\t\tif(res == srtp_err_status_ok) {\n\t\t\t\t\t\t\tps->is_srtp = TRUE;\n\t\t\t\t\t\t\tps->srtp_suite = srtp_suite;\n\t\t\t\t\t\t\tps->srtp_crypto = g_strdup(srtp_crypto);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Something went wrong... */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating SRTP context: %d (%s), disabling stream\\n\", res, janus_srtp_error_str(res));\n\t\t\t\t\t\t\tps->is_srtp = FALSE;\n\t\t\t\t\t\t\tdisabled = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tps->type = mtype;\n\t\t\tps->mindex = mindex;\n\t\t\tchar mid[5];\n\t\t\tg_snprintf(mid, sizeof(mid), \"%d\", mindex);\n\t\t\tps->mid = g_strdup(mid);\n\t\t\tps->publisher = publisher;\n\t\t\tjanus_refcount_increase(&publisher->ref);\t/* Add a reference to the publisher */\n\t\t\tps->description = desc ? g_strdup(desc) : NULL;\n\t\t\tps->active = TRUE;\n\t\t\tps->disabled = disabled;\n\t\t\tps->acodec = JANUS_AUDIOCODEC_NONE;\n\t\t\tps->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\t\tps->min_delay = -1;\n\t\t\tps->max_delay = -1;\n\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\tps->acodec = janus_audiocodec_from_name(codec);\n\t\t\t\tps->pt = janus_audiocodec_pt(ps->acodec);\n\t\t\t\tgboolean found = FALSE;\n\t\t\t\tint j = 0;\n\t\t\t\tfor(j=0; j<5; j++) {\n\t\t\t\t\tif(videoroom->acodec[j] == ps->acodec) {\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!found) {\n\t\t\t\t\t/* Codec not allowed in this room */\n\t\t\t\t\tps->disabled = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tps->opusstereo = json_is_true(json_object_get(s, \"stereo\"));\n\t\t\t\t\tps->opusfec = json_is_true(json_object_get(s, \"fec\")) && videoroom->do_opusfec;\n\t\t\t\t\tps->opusdtx = json_is_true(json_object_get(s, \"dtx\")) && videoroom->do_opusdtx;\n\t\t\t\t}\n\t\t\t\tint audio_level_extmap_id = json_integer_value(json_object_get(s, \"audiolevel_ext_id\"));\n\t\t\t\tif(audio_level_extmap_id > 0)\n\t\t\t\t\tps->audio_level_extmap_id = audio_level_extmap_id;\n\t\t\t} else if(mtype == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\tps->vcodec = janus_videocodec_from_name(codec);\n\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\tgboolean found = FALSE;\n\t\t\t\tint j = 0;\n\t\t\t\tfor(j=0; j<5; j++) {\n\t\t\t\t\tif(videoroom->vcodec[j] == ps->vcodec) {\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!found) {\n\t\t\t\t\t/* Codec not allowed in this room */\n\t\t\t\t\tps->disabled = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264) {\n\t\t\t\t\t\tconst char *h264_profile = json_string_value(json_object_get(s, \"h264_profile\"));\n\t\t\t\t\t\tif(h264_profile)\n\t\t\t\t\t\t\tps->h264_profile = g_strdup(h264_profile);\n\t\t\t\t\t\telse if(videoroom->h264_profile)\n\t\t\t\t\t\t\tps->h264_profile = g_strdup(videoroom->h264_profile);\n\t\t\t\t\t} else if(ps->vcodec == JANUS_VIDEOCODEC_VP9) {\n\t\t\t\t\t\tconst char *vp9_profile = json_string_value(json_object_get(s, \"vp9_profile\"));\n\t\t\t\t\t\tif(vp9_profile)\n\t\t\t\t\t\t\tps->vp9_profile = g_strdup(vp9_profile);\n\t\t\t\t\t\telse if(videoroom->vp9_profile)\n\t\t\t\t\t\t\tps->vp9_profile = g_strdup(videoroom->vp9_profile);\n\t\t\t\t\t}\n\t\t\t\t\tps->simulcast = json_is_true(json_object_get(s, \"simulcast\"));\n\t\t\t\t\tps->svc = json_is_true(json_object_get(s, \"svc\"));\n\t\t\t\t\tif(ps->simulcast) {\n\t\t\t\t\t\tps->vssrc[0] = publisher->remote_ssrc_offset + REMOTE_PUBLISHER_BASE_SSRC + (mindex*REMOTE_PUBLISHER_SSRC_STEP);\n\t\t\t\t\t\tps->vssrc[1] = publisher->remote_ssrc_offset + REMOTE_PUBLISHER_BASE_SSRC + (mindex*REMOTE_PUBLISHER_SSRC_STEP) + 1;\n\t\t\t\t\t\tps->vssrc[2] = publisher->remote_ssrc_offset + REMOTE_PUBLISHER_BASE_SSRC + (mindex*REMOTE_PUBLISHER_SSRC_STEP) + 2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tint video_orient_extmap_id = json_integer_value(json_object_get(s, \"videoorient_ext_id\"));\n\t\t\t\tif(video_orient_extmap_id > 0)\n\t\t\t\t\tps->video_orient_extmap_id = video_orient_extmap_id;\n\t\t\t\tint playout_delay_extmap_id = json_integer_value(json_object_get(s, \"playoutdelay_ext_id\"));\n\t\t\t\tif(playout_delay_extmap_id > 0)\n\t\t\t\t\tps->playout_delay_extmap_id = playout_delay_extmap_id;\n\t\t\t} else if(mtype == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t\tif(publisher->data_mindex == -1) {\n\t\t\t\t\tpublisher->data_mindex = ps->mindex;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring extra data channel m-line from remote publisher\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_atomic_int_set(&ps->destroyed, 0);\n\t\t\tjanus_refcount_init(&ps->ref, janus_videoroom_publisher_stream_free);\n\t\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the id-indexed hashtable */\n\t\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the mid-indexed hashtable */\n\t\t\tjanus_mutex_init(&ps->subscribers_mutex);\n\t\t\tjanus_mutex_init(&ps->rtp_forwarders_mutex);\n\t\t\tps->rtp_forwarders = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\tpublisher->streams = g_list_append(publisher->streams, ps);\n\t\t\tg_hash_table_insert(publisher->streams_byid, GINT_TO_POINTER(ps->mindex), ps);\n\t\t\tg_hash_table_insert(publisher->streams_bymid, g_strdup(ps->mid), ps);\n\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\tmindex++;\n\t\t}\n\t\t/* Done, spawn a thread for this remote publisher */\n\t\tGError *error = NULL;\n\t\tchar tname[16];\n\t\tg_snprintf(tname, sizeof(tname), \"vremote %s\", publisher->user_id_str);\n\t\tpublisher->remote_thread = g_thread_try_new(tname, janus_videoroom_remote_publisher_thread, publisher, &error);\n\t\tif(error != NULL) {\n\t\t\t/* Something went wrong */\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\tg_list_free_full(publisher->streams, (GDestroyNotify)(janus_videoroom_publisher_stream_unref));\n\t\t\tpublisher->streams = NULL;\n\t\t\tg_hash_table_remove_all(publisher->streams_byid);\n\t\t\tg_hash_table_remove_all(publisher->streams_bymid);\n\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\tjanus_videoroom_leave_or_unpublish(publisher, TRUE, FALSE);\n\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\tjanus_videoroom_publisher_destroy(publisher);\n\t\t\tJANUS_LOG(LOG_ERR, \"Could not spawn thread for remote publisher, %d (%s)\\n\",\n\t\t\t\terrno, g_strerror(errno));\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\tg_snprintf(error_cause, 512, \"Could not spawn thread for remote publisher\");\n\t\t\tgoto prepare_response;\n\t\t}\n\n\t\tjanus_mutex_lock(&publisher->rec_mutex);\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t/* Check if we need to start recording */\n\t\tif((publisher->room && publisher->room->record) || publisher->recording_active) {\n\t\t\tGList *temp = publisher->streams;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\tjanus_videoroom_recorder_create(ps);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tpublisher->recording_active = TRUE;\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\tjanus_mutex_unlock(&publisher->rec_mutex);\n\n\t\t/* Done */\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tjson_object_set_new(response, \"room\", string_ids ? json_string(publisher->room_id_str) : json_integer(publisher->room_id));\n\t\tjson_object_set_new(response, \"id\", string_ids ? json_string(publisher->user_id_str) : json_integer(publisher->user_id));\n\t\t/* Return connectivity information */\n\t\tif(strlen(host) > 0)\n\t\t\tjson_object_set_new(response, \"ip\", json_string(host));\n\t\tjson_object_set_new(response, \"port\", json_integer(port));\n\t\tjson_object_set_new(response, \"rtcp_port\", json_integer(rtcp_port));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"update_remote_publisher\")) {\n\t\t/* Update an existing remote publisher */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, remote_publisher_update_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\t/* Validate the stream parameters too */\n\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\tif(streams && json_array_size(streams) > 0) {\n\t\t\tsize_t i = 0;\n\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, remote_publisher_stream_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0)\n\t\t\t\t\tbreak;\n\t\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\t\tjanus_videoroom_media mtype = janus_videoroom_media_from_str(type);\n\t\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_NONE) {\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element value (type)\\n\");\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element value (type)\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_AUDIO || mtype == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\tconst char *codec = json_string_value(json_object_get(s, \"codec\"));\n\t\t\t\t\tif(codec == NULL) {\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing mandatory element (codec)\\n\");\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Missing mandatory element (codec)\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif((mtype == JANUS_VIDEOROOM_MEDIA_AUDIO && janus_audiocodec_from_name(codec) == JANUS_AUDIOCODEC_NONE) ||\n\t\t\t\t\t\t\t(mtype == JANUS_VIDEOROOM_MEDIA_VIDEO && janus_videocodec_from_name(codec) == JANUS_VIDEOCODEC_NONE)) {\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element value (unsupported codec)\\n\");\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element value (unsupported codec)\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* We may need to SRTP-decrypt this stream */\n\t\tint srtp_suite = 0;\n\t\tconst char *srtp_crypto = NULL;\n\t\tjson_t *s_suite = json_object_get(root, \"srtp_suite\");\n\t\tjson_t *s_crypto = json_object_get(root, \"srtp_crypto\");\n\t\tif(s_suite && s_crypto) {\n\t\t\tsrtp_suite = json_integer_value(s_suite);\n\t\t\tif(srtp_suite != 32 && srtp_suite != 80) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP suite (%d)\\n\", srtp_suite);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid SRTP suite (%d)\", srtp_suite);\n\t\t\t\tgoto prepare_response;\n\t\t\t}\n\t\t\tsrtp_crypto = json_string_value(s_crypto);\n\t\t\tJANUS_LOG(LOG_VERB, \"SRTP setting s_suite (%d) and s_crypto (%s) on add_remote_publisher\\n\", srtp_suite, srtp_crypto);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\t/* Now access the room */\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 publisher_id = 0;\n\t\tchar publisher_id_num[30], *publisher_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tpublisher_id = json_integer_value(id);\n\t\t\tg_snprintf(publisher_id_num, sizeof(publisher_id_num), \"%\"SCNu64, publisher_id);\n\t\t\tpublisher_id_str = publisher_id_num;\n\t\t} else {\n\t\t\tpublisher_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)publisher_id_str : (gpointer)&publisher_id);\n\t\tif(publisher == NULL || !publisher->remote || g_atomic_int_get(&publisher->remote_leaving)) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such remote publisher (%s)\\n\", publisher_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such remote publisher (%s)\", publisher_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&publisher->ref);\n\t\t/* Check if there's a new display, new metadata, new streams, or changes to existing ones */\n\t\tjson_t *display = json_object_get(root, \"display\");\n\t\tif(display) {\n\t\t\tchar *old_display = publisher->display;\n\t\t\tchar *new_display = g_strdup(json_string_value(display));\n\t\t\tpublisher->display = new_display;\n\t\t\tg_free(old_display);\n\t\t}\n\t\tjson_t *metadata = json_object_get(root, \"metadata\");\n\t\tif(metadata) {\n\t\t\tjson_t *old_metadata = publisher->metadata;\n\t\t\tjson_t *new_metadata = json_deep_copy(metadata);\n\t\t\tpublisher->metadata = new_metadata;\n\t\t\tif(old_metadata)\n\t\t\t\tjson_decref(old_metadata);\n\t\t}\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\tjanus_videoroom_publisher_stream *ps = NULL;\n\t\tint changes = FALSE;\n\t\tsize_t i = 0;\n\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\tint mindex = json_integer_value(json_object_get(s, \"mindex\"));\n\t\t\tps = g_hash_table_lookup(publisher->streams_bymid, mid);\n\t\t\tif(ps != NULL) {\n\t\t\t\t/* Update an existing stream */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Updating existing stream (mid %s)\\n\", mid);\n\t\t\t\tconst char *desc = json_string_value(json_object_get(s, \"description\"));\n\t\t\t\tif(ps->description == NULL || (desc && strcmp(ps->description, desc))) {\n\t\t\t\t\tg_free(ps->description);\n\t\t\t\t\tps->description = desc ? g_strdup(desc) : NULL;\n\t\t\t\t\tchanges = TRUE;\n\t\t\t\t}\n\t\t\t\tjson_t *disabled = json_object_get(s, \"disabled\");\n\t\t\t\tif(disabled && ps->disabled != json_is_true(disabled)) {\n\t\t\t\t\tps->disabled = json_is_true(disabled);\n\t\t\t\t\tchanges = TRUE;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* If we're here, we need to create a new stream */\n\t\t\tif(mindex - g_list_length(publisher->streams) > 1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Not adding new stream with mindex %d (missing indexes)\\n\", mindex);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst char *type = json_string_value(json_object_get(s, \"type\"));\n\t\t\tjanus_videoroom_media mtype = janus_videoroom_media_from_str(type);\n\t\t\tconst char *codec = json_string_value(json_object_get(s, \"codec\"));\n\t\t\tconst char *desc = json_string_value(json_object_get(s, \"description\"));\n\t\t\tgboolean disabled = json_is_true(json_object_get(s, \"disabled\"));\n\t\t\t/* Create a publisher stream */\n\t\t\tps = g_malloc0(sizeof(janus_videoroom_publisher_stream));\n\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_AUDIO || mtype == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t/* First of all, let's check if we need to setup an SRTP for remote publisher */\n\t\t\t\tif(srtp_suite > 0 && srtp_crypto != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Enabling SRTP crypto (%s) for stream\\n\", srtp_crypto);\n\t\t\t\t\tgsize len = 0;\n\t\t\t\t\tguchar *srtp_crypto_decoded = g_base64_decode(srtp_crypto, &len);\n\t\t\t\t\tif(len < SRTP_MASTER_LENGTH) {\n\t\t\t\t\t\t/* Something went wrong */\n\t\t\t\t\t\tg_free(srtp_crypto_decoded);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP crypto (%s), disabling stream\\n\", srtp_crypto);\n\t\t\t\t\t\tdisabled = TRUE;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Set SRTP policy */\n\t\t\t\t\t\tsrtp_policy_t *policy = &ps->srtp_policy;\n\t\t\t\t\t\tsrtp_crypto_policy_set_rtp_default(&policy->rtp);\n\t\t\t\t\t\tif(srtp_suite == 32) {\n\t\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&policy->rtp);\n\t\t\t\t\t\t} else if(srtp_suite == 80) {\n\t\t\t\t\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy->rtp);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpolicy->ssrc.type = ssrc_any_inbound;\n\t\t\t\t\t\tpolicy->key = srtp_crypto_decoded;\n\t\t\t\t\t\tpolicy->next = NULL;\n\t\t\t\t\t\t/* Create SRTP context */\n\t\t\t\t\t\tsrtp_err_status_t res = srtp_create(&ps->srtp_ctx, policy);\n\t\t\t\t\t\tif(res == srtp_err_status_ok) {\n\t\t\t\t\t\t\tps->is_srtp = TRUE;\n\t\t\t\t\t\t\tps->srtp_suite = srtp_suite;\n\t\t\t\t\t\t\tps->srtp_crypto = g_strdup(srtp_crypto);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Something went wrong... */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating SRTP context: %d (%s), disabling stream\\n\", res, janus_srtp_error_str(res));\n\t\t\t\t\t\t\tps->is_srtp = FALSE;\n\t\t\t\t\t\t\tdisabled = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"SRTP crypto (%d) (%s) not enabled for stream\\n\", srtp_suite, srtp_crypto);\n\t\t\t\t}\n\t\t\t}\n\t\t\tps->type = mtype;\n\t\t\tps->mindex = mindex;\n\t\t\tchar pmid[5];\n\t\t\tg_snprintf(pmid, sizeof(pmid), \"%d\", mindex);\n\t\t\tps->mid = g_strdup(pmid);\n\t\t\tps->publisher = publisher;\n\t\t\tjanus_refcount_increase(&publisher->ref);\t/* Add a reference to the publisher */\n\t\t\tps->description = desc ? g_strdup(desc) : NULL;\n\t\t\tps->active = TRUE;\n\t\t\tps->disabled = disabled;\n\t\t\tps->acodec = JANUS_AUDIOCODEC_NONE;\n\t\t\tps->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\t\tps->min_delay = -1;\n\t\t\tps->max_delay = -1;\n\t\t\tif(mtype == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\tps->acodec = janus_audiocodec_from_name(codec);\n\t\t\t\tps->pt = janus_audiocodec_pt(ps->acodec);\n\t\t\t\tgboolean found = FALSE;\n\t\t\t\tint j = 0;\n\t\t\t\tfor(j=0; j<5; j++) {\n\t\t\t\t\tif(videoroom->acodec[j] == ps->acodec) {\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!found) {\n\t\t\t\t\t/* Codec not allowed in this room */\n\t\t\t\t\tps->disabled = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tps->opusstereo = json_is_true(json_object_get(s, \"stereo\"));\n\t\t\t\t\tps->opusfec = json_is_true(json_object_get(s, \"fec\")) && videoroom->do_opusfec;\n\t\t\t\t\tps->opusdtx = json_is_true(json_object_get(s, \"dtx\")) && videoroom->do_opusdtx;\n\t\t\t\t}\n\t\t\t\tint audio_level_extmap_id = json_integer_value(json_object_get(s, \"audiolevel_ext_id\"));\n\t\t\t\tif(audio_level_extmap_id > 0)\n\t\t\t\t\tps->audio_level_extmap_id = audio_level_extmap_id;\n\t\t\t} else if(mtype == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\tps->vcodec = janus_videocodec_from_name(codec);\n\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\tgboolean found = FALSE;\n\t\t\t\tint j = 0;\n\t\t\t\tfor(j=0; j<5; j++) {\n\t\t\t\t\tif(videoroom->vcodec[j] == ps->vcodec) {\n\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!found) {\n\t\t\t\t\t/* Codec not allowed in this room */\n\t\t\t\t\tps->disabled = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264) {\n\t\t\t\t\t\tconst char *h264_profile = json_string_value(json_object_get(s, \"h264_profile\"));\n\t\t\t\t\t\tif(h264_profile)\n\t\t\t\t\t\t\tps->h264_profile = g_strdup(h264_profile);\n\t\t\t\t\t\telse if(videoroom->h264_profile)\n\t\t\t\t\t\t\tps->h264_profile = g_strdup(videoroom->h264_profile);\n\t\t\t\t\t} else if(ps->vcodec == JANUS_VIDEOCODEC_VP9) {\n\t\t\t\t\t\tconst char *vp9_profile = json_string_value(json_object_get(s, \"vp9_profile\"));\n\t\t\t\t\t\tif(vp9_profile)\n\t\t\t\t\t\t\tps->vp9_profile = g_strdup(vp9_profile);\n\t\t\t\t\t\telse if(videoroom->vp9_profile)\n\t\t\t\t\t\t\tps->vp9_profile = g_strdup(videoroom->vp9_profile);\n\t\t\t\t\t}\n\t\t\t\t\tps->simulcast = json_is_true(json_object_get(s, \"simulcast\"));\n\t\t\t\t\tps->svc = json_is_true(json_object_get(s, \"svc\"));\n\t\t\t\t\tif(ps->simulcast) {\n\t\t\t\t\t\tps->vssrc[0] = publisher->remote_ssrc_offset + REMOTE_PUBLISHER_BASE_SSRC + (mindex*REMOTE_PUBLISHER_SSRC_STEP);\n\t\t\t\t\t\tps->vssrc[1] = publisher->remote_ssrc_offset + REMOTE_PUBLISHER_BASE_SSRC + (mindex*REMOTE_PUBLISHER_SSRC_STEP) + 1;\n\t\t\t\t\t\tps->vssrc[2] = publisher->remote_ssrc_offset + REMOTE_PUBLISHER_BASE_SSRC + (mindex*REMOTE_PUBLISHER_SSRC_STEP) + 2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tint video_orient_extmap_id = json_integer_value(json_object_get(s, \"videoorient_ext_id\"));\n\t\t\t\tif(video_orient_extmap_id > 0)\n\t\t\t\t\tps->video_orient_extmap_id = video_orient_extmap_id;\n\t\t\t\tint playout_delay_extmap_id = json_integer_value(json_object_get(s, \"playoutdelay_ext_id\"));\n\t\t\t\tif(playout_delay_extmap_id > 0)\n\t\t\t\t\tps->playout_delay_extmap_id = playout_delay_extmap_id;\n\t\t\t} else if(mtype == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t\tif(publisher->data_mindex == -1) {\n\t\t\t\t\tpublisher->data_mindex = ps->mindex;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring extra data channel m-line from remote publisher\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_atomic_int_set(&ps->destroyed, 0);\n\t\t\tjanus_refcount_init(&ps->ref, janus_videoroom_publisher_stream_free);\n\t\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the id-indexed hashtable */\n\t\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the mid-indexed hashtable */\n\t\t\tjanus_mutex_init(&ps->subscribers_mutex);\n\t\t\tjanus_mutex_init(&ps->rtp_forwarders_mutex);\n\t\t\tps->rtp_forwarders = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\t\t\tpublisher->streams = g_list_append(publisher->streams, ps);\n\t\t\tg_hash_table_insert(publisher->streams_byid, GINT_TO_POINTER(ps->mindex), ps);\n\t\t\tg_hash_table_insert(publisher->streams_bymid, g_strdup(ps->mid), ps);\n\t\t\tchanges = TRUE;\n\t\t}\n\t\tif(changes) {\n\t\t\t/* Notify all other participants this publisher's media has changed */\n\t\t\tjanus_videoroom_notify_about_publisher(publisher, TRUE);\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\n\t\tjanus_mutex_lock(&publisher->rec_mutex);\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t/* Check if we need to start recording */\n\t\tif((publisher->room && publisher->room->record) || publisher->recording_active) {\n\t\t\tGList *temp = publisher->streams;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\tjanus_videoroom_recorder_create(ps);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tpublisher->recording_active = TRUE;\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\tjanus_mutex_unlock(&publisher->rec_mutex);\n\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t/* Done */\n\t\tjanus_refcount_decrease(&publisher->ref);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tgoto prepare_response;\n\t} else if(!strcasecmp(request_text, \"remove_remote_publisher\")) {\n\t\t/* Get rid an existing remote publisher */\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tif(!string_ids) {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, id_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t} else {\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstr_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t}\n\t\tif(error_code != 0)\n\t\t\tgoto prepare_response;\n\t\tjanus_mutex_lock(&rooms_mutex);\n\t\tjanus_videoroom *videoroom = NULL;\n\t\terror_code = janus_videoroom_access_room(root, TRUE, FALSE, &videoroom, error_cause, sizeof(error_cause));\n\t\tif(error_code != 0) {\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\tjanus_refcount_increase(&videoroom->ref);\n\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\tjson_t *id = json_object_get(root, \"id\");\n\t\tguint64 publisher_id = 0;\n\t\tchar publisher_id_num[30], *publisher_id_str = NULL;\n\t\tif(!string_ids) {\n\t\t\tpublisher_id = json_integer_value(id);\n\t\t\tg_snprintf(publisher_id_num, sizeof(publisher_id_num), \"%\"SCNu64, publisher_id);\n\t\t\tpublisher_id_str = publisher_id_num;\n\t\t} else {\n\t\t\tpublisher_id_str = (char *)json_string_value(id);\n\t\t}\n\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\tstring_ids ? (gpointer)publisher_id_str : (gpointer)&publisher_id);\n\t\tif(publisher == NULL || !publisher->remote || !g_atomic_int_compare_and_exchange(&publisher->remote_leaving, 0, 1)) {\n\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\tJANUS_LOG(LOG_ERR, \"No such remote publisher (%s)\\n\", publisher_id_str);\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\tg_snprintf(error_cause, 512, \"No such remote publisher (%s)\", publisher_id_str);\n\t\t\tgoto prepare_response;\n\t\t}\n\t\t/* Mark the remote publisher as leaving, the thread will do the cleanup */\n\t\tg_atomic_int_set(&publisher->remote_leaving, 1);\n\t\t/* Notify the thread that it's time to go */\n\t\tif(publisher->pipefd[1] > 0) {\n\t\t\tint code = 1;\n\t\t\tssize_t res = 0;\n\t\t\tdo {\n\t\t\t\tres = write(publisher->pipefd[1], &code, sizeof(int));\n\t\t\t} while(res == -1 && errno == EINTR);\n\t\t}\n\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t/* Done */\n\t\tresponse = json_object();\n\t\tjson_object_set_new(response, \"videoroom\", json_string(\"success\"));\n\t\tgoto prepare_response;\n\t} else {\n\t\t/* Not a request we recognize, don't do anything */\n\t\treturn NULL;\n\t}\n\nprepare_response:\n\t\t{\n\t\t\tif(error_code == 0 && !response) {\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nstruct janus_plugin_result *janus_videoroom_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_ERROR, g_atomic_int_get(&stopping) ? \"Shutting down\" : \"Plugin not initialized\", NULL);\n\n\t/* Pre-parse the message */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = message;\n\tjson_t *response = NULL;\n\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videoroom_session *session = janus_videoroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No session associated with this handle...\");\n\t\tgoto plugin_response;\n\t}\n\t/* Increase the reference counter for this session: we'll decrease it after we handle the message */\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tJANUS_LOG(LOG_ERR, \"Session has already been marked as destroyed...\\n\");\n\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"Session has already been marked as destroyed...\");\n\t\tgoto plugin_response;\n\t}\n\n\tif(message == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_MESSAGE;\n\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\tgoto plugin_response;\n\t}\n\tif(!json_is_object(root)) {\n\t\tJANUS_LOG(LOG_ERR, \"JSON error: not an object\\n\");\n\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_JSON;\n\t\tg_snprintf(error_cause, 512, \"JSON error: not an object\");\n\t\tgoto plugin_response;\n\t}\n\t/* Get the request first */\n\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\tjson_t *request = json_object_get(root, \"request\");\n\t/* Some requests ('create', 'destroy', 'exists', 'list') can be handled synchronously */\n\tconst char *request_text = json_string_value(request);\n\t/* We have a separate method to process synchronous requests, as those may\n\t * arrive from the Admin API as well, and so we handle them the same way */\n\tresponse = janus_videoroom_process_synchronous_request(session, root);\n\tif(response != NULL) {\n\t\t/* We got a response, send it back */\n\t\tgoto plugin_response;\n\t} else if(!strcasecmp(request_text, \"join\") || !strcasecmp(request_text, \"joinandconfigure\") || !strcasecmp(request_text, \"update\")\n\t\t\t|| !strcasecmp(request_text, \"configure\") || !strcasecmp(request_text, \"publish\") || !strcasecmp(request_text, \"unpublish\")\n\t\t\t|| !strcasecmp(request_text, \"start\") || !strcasecmp(request_text, \"pause\") || !strcasecmp(request_text, \"switch\")\n\t\t\t|| !strcasecmp(request_text, \"subscribe\") || !strcasecmp(request_text, \"unsubscribe\") || !strcasecmp(request_text, \"leave\")) {\n\t\t/* These messages are handled asynchronously */\n\n\t\tjanus_videoroom_message *msg = g_malloc(sizeof(janus_videoroom_message));\n\t\tmsg->handle = handle;\n\t\tmsg->transaction = transaction;\n\t\tmsg->message = root;\n\t\tmsg->jsep = jsep;\n\t\tg_async_queue_push(messages, msg);\n\n\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL, NULL);\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code == 0 && !response) {\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid response\");\n\t\t\t}\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\t\tresponse = event;\n\t\t\t}\n\t\t\tif(root != NULL)\n\t\t\t\tjson_decref(root);\n\t\t\tif(jsep != NULL)\n\t\t\t\tjson_decref(jsep);\n\t\t\tg_free(transaction);\n\n\t\t\tif(session != NULL)\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\treturn janus_plugin_result_new(JANUS_PLUGIN_OK, NULL, response);\n\t\t}\n\n}\n\njson_t *janus_videoroom_handle_admin_message(json_t *message) {\n\t/* Some requests (e.g., 'create' and 'destroy') can be handled via Admin API */\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *response = NULL;\n\n\tJANUS_VALIDATE_JSON_OBJECT(message, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto admin_response;\n\tjson_t *request = json_object_get(message, \"request\");\n\tconst char *request_text = json_string_value(request);\n\tif((response = janus_videoroom_process_synchronous_request(NULL, message)) != NULL) {\n\t\t/* We got a response, send it back */\n\t\tgoto admin_response;\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nadmin_response:\n\t\t{\n\t\t\tif(!response) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tresponse = json_object();\n\t\t\t\tjson_object_set_new(response, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n\n}\n\nvoid janus_videoroom_setup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] WebRTC media is now available\\n\", JANUS_VIDEOROOM_PACKAGE, handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videoroom_session *session = janus_videoroom_lookup_session(handle);\n\tif(!session) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tg_atomic_int_set(&session->hangingup, 0);\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\t/* Media relaying can start now */\n\tg_atomic_int_set(&session->started, 1);\n\tif(session->participant) {\n\t\t/* If this is a publisher, notify all subscribers about the fact they can\n\t\t * now subscribe; if this is a subscriber, instead, ask the publisher a FIR */\n\t\tif(session->participant_type == janus_videoroom_p_type_publisher) {\n\t\t\tjanus_videoroom_publisher *participant = janus_videoroom_session_get_publisher(session);\n\t\t\t/* Notify all other participants that there's a new boy in town */\n\t\t\tjanus_videoroom *room = participant->room;\n\t\t\tif(room && !g_atomic_int_get(&room->destroyed)) {\n\t\t\t\tjanus_refcount_increase(&room->ref);\n\t\t\t\tjanus_mutex_lock(&room->mutex);\n\t\t\t}\n\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\tif(room) {\n\t\t\t\tjanus_videoroom_notify_about_publisher(participant, FALSE);\n\t\t\t}\n\t\t\t/* Check if we need to start recording */\n\t\t\tif((participant->room && participant->room->record) || participant->recording_active) {\n\t\t\t\tGList *temp = participant->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\tjanus_videoroom_recorder_create(ps);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tparticipant->recording_active = TRUE;\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\tif(room) {\n\t\t\t\tjanus_mutex_unlock(&room->mutex);\n\t\t\t\tjanus_refcount_decrease(&room->ref);\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t} else if(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\t\tjanus_videoroom_subscriber *s = janus_videoroom_session_get_subscriber(session);\n\t\t\tif(s && s->streams) {\n\t\t\t\t/* Send a PLI for all the video streams we subscribed to */\n\t\t\t\tGList *temp = s->streams;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_videoroom_subscriber_stream *ss = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\t\tjanus_videoroom_publisher_stream *ps = ss->publisher_streams ? ss->publisher_streams->data : NULL;\n\t\t\t\t\tif(ps && ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && ps->publisher && ps->publisher->session) {\n\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"New subscriber available\");\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"subscribed\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\", json_integer(s->room_id));\n\t\t\t\t\t/* TODO Fix the event to event handlers, we don't have a single feed anymore */\n\t\t\t\t\t//~ json_object_set_new(info, \"feed\", json_integer(p->user_id));\n\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(s)\n\t\t\t\tjanus_refcount_decrease(&s->ref);\n\t\t}\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_videoroom_incoming_rtp_internal(janus_videoroom_session *session, janus_videoroom_publisher *participant, janus_plugin_rtp *pkt);\nvoid janus_videoroom_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *pkt) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_videoroom_session *session = (janus_videoroom_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || session->participant_type != janus_videoroom_p_type_publisher)\n\t\treturn;\n\tjanus_videoroom_publisher *participant = janus_videoroom_session_get_publisher_nodebug(session);\n\tif(participant == NULL)\n\t\treturn;\n\tjanus_videoroom_incoming_rtp_internal(session, participant, pkt);\n}\nstatic void janus_videoroom_incoming_rtp_internal(janus_videoroom_session *session, janus_videoroom_publisher *participant, janus_plugin_rtp *pkt) {\n\tif(g_atomic_int_get(&participant->destroyed) || participant->kicked || !participant->streams) {\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&participant->mutex);\n\tjanus_videoroom *videoroom = participant->room;\n\tif(videoroom == NULL) {\n\t\tjanus_mutex_unlock(&participant->mutex);\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\treturn;\n\t}\n\tjanus_refcount_increase_nodebug(&videoroom->ref);\n\tjanus_mutex_unlock(&participant->mutex);\n\n\t/* Find the stream this packet belongs to */\n\tjanus_mutex_lock(&participant->streams_mutex);\n\tjanus_videoroom_publisher_stream *ps = g_hash_table_lookup(participant->streams_byid, GINT_TO_POINTER(pkt->mindex));\n\tif(ps != NULL)\n\t\tjanus_refcount_increase_nodebug(&ps->ref);\n\tjanus_mutex_unlock(&participant->streams_mutex);\n\tif(ps == NULL || ps->disabled || g_atomic_int_get(&ps->destroyed)) {\n\t\t/* No stream..? */\n\t\tif(ps != NULL)\n\t\t\tjanus_refcount_decrease_nodebug(&ps->ref);\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\tjanus_refcount_decrease_nodebug(&videoroom->ref);\n\t\treturn;\n\t}\n\n\tgboolean video = pkt->video;\n\tchar *buf = pkt->buffer;\n\tuint16_t len = pkt->length;\n\t/* In case this is an audio packet and we're doing talk detection, check the audio level extension */\n\tif(!video && videoroom->audiolevel_event && ps->active && !ps->muted && ps->audio_level_extmap_id > 0) {\n\t\tint level = pkt->extensions.audio_level;\n\t\tif(level != -1) {\n\t\t\tps->audio_dBov_sum += level;\n\t\t\tps->audio_active_packets++;\n\t\t\tps->audio_dBov_level = level;\n\t\t\tint audio_active_packets = participant->user_audio_active_packets ? participant->user_audio_active_packets : videoroom->audio_active_packets;\n\t\t\tint audio_level_average = participant->user_audio_level_average ? participant->user_audio_level_average : videoroom->audio_level_average;\n\t\t\tif(ps->audio_active_packets > 0 && ps->audio_active_packets == audio_active_packets) {\n\t\t\t\tgboolean notify_talk_event = FALSE;\n\t\t\t\tfloat audio_dBov_avg = (float)ps->audio_dBov_sum/(float)ps->audio_active_packets;\n\t\t\t\tif(audio_dBov_avg < audio_level_average) {\n\t\t\t\t\t/* Participant talking, should we notify all participants? */\n\t\t\t\t\tif(!ps->talking)\n\t\t\t\t\t\tnotify_talk_event = TRUE;\n\t\t\t\t\tps->talking = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\t/* Participant not talking anymore, should we notify all participants? */\n\t\t\t\t\tif(ps->talking)\n\t\t\t\t\t\tnotify_talk_event = TRUE;\n\t\t\t\t\tps->talking = FALSE;\n\t\t\t\t}\n\t\t\t\tps->audio_active_packets = 0;\n\t\t\t\tps->audio_dBov_sum = 0;\n\t\t\t\t/* Only notify in case of state changes */\n\t\t\t\tif(notify_talk_event) {\n\t\t\t\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(ps->talking ? \"talking\" : \"stopped-talking\"));\n\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\t\t\t\tjson_object_set_new(event, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t\tjson_object_set_new(event, \"mindex\", json_integer(ps->mindex));\n\t\t\t\t\tjson_object_set_new(event, \"mid\", json_string(ps->mid));\n\t\t\t\t\tjson_object_set_new(event, \"audio-level-dBov-avg\", json_real(audio_dBov_avg));\n\t\t\t\t\t/* Notify the speaker this event is related to as well */\n\t\t\t\t\tjanus_videoroom_notify_participants(participant, event, TRUE);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"videoroom\", json_string(ps->talking ? \"talking\" : \"stopped-talking\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(videoroom->room_id_str) : json_integer(videoroom->room_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"mindex\", json_integer(ps->mindex));\n\t\t\t\t\t\tjson_object_set_new(info, \"mid\", json_string(ps->mid));\n\t\t\t\t\t\tjson_object_set_new(info, \"audio-level-dBov-avg\", json_real(audio_dBov_avg));\n\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif(ps->active && !ps->muted) {\n\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\t\tint sc = video ? 0 : -1;\n\t\t/* Check if we're simulcasting, and if so, keep track of the \"layer\" */\n\t\tif(video && ps->simulcast) {\n\t\t\tuint32_t ssrc = ntohl(rtp->ssrc);\n\t\t\tif(ssrc == ps->vssrc[0])\n\t\t\t\tsc = 0;\n\t\t\telse if(ssrc == ps->vssrc[1])\n\t\t\t\tsc = 1;\n\t\t\telse if(ssrc == ps->vssrc[2])\n\t\t\t\tsc = 2;\n\t\t\telse if(ps->rid_extmap_id > 0) {\n\t\t\t\t/* We may not know the SSRC yet, try the rid RTP extension */\n\t\t\t\tchar sdes_item[16];\n\t\t\t\tjanus_mutex_lock(&ps->rid_mutex);\n\t\t\t\tif(janus_rtp_header_extension_parse_rid(buf, len, ps->rid_extmap_id, sdes_item, sizeof(sdes_item)) == 0) {\n\t\t\t\t\tif(ps->rid[0] != NULL && !strcmp(ps->rid[0], sdes_item)) {\n\t\t\t\t\t\tps->vssrc[0] = ssrc;\n\t\t\t\t\t\tsc = 0;\n\t\t\t\t\t} else if(ps->rid[1] != NULL && !strcmp(ps->rid[1], sdes_item)) {\n\t\t\t\t\t\tps->vssrc[1] = ssrc;\n\t\t\t\t\t\tsc = 1;\n\t\t\t\t\t} else if(ps->rid[2] != NULL && !strcmp(ps->rid[2], sdes_item)) {\n\t\t\t\t\t\tps->vssrc[2] = ssrc;\n\t\t\t\t\t\tsc = 2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&ps->rid_mutex);\n\t\t\t}\n\t\t}\n\t\t/* Forward RTP to the appropriate port for the rtp_forwarders associated with this publisher, if there are any */\n\t\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, ps->rtp_forwarders);\n\t\twhile(participant->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\t\tif(rtp_forward->is_data || (video && !rtp_forward->is_video) || (!video && rtp_forward->is_video))\n\t\t\t\tcontinue;\n\t\t\tjanus_rtp_forwarder_send_rtp_full(rtp_forward, buf, len, sc,\n\t\t\t\tps->vssrc, ps->rid, ps->vcodec, &ps->rid_mutex);\n\t\t}\n\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t/* Set the payload type of the publisher */\n\t\trtp->type = ps->pt;\n\t\t/* Save the frame if we're recording */\n\t\tif(!video || !ps->simulcast) {\n\t\t\tjanus_recorder_save_frame(ps->rc, buf, len);\n\t\t} else {\n\t\t\t/* We're simulcasting, save the best video quality */\n\t\t\tgboolean save = janus_rtp_simulcasting_context_process_rtp(&ps->rec_simctx,\n\t\t\t\tbuf, len, pkt->extensions.dd_content, pkt->extensions.dd_len,\n\t\t\t\tps->vssrc, ps->rid, ps->vcodec, &ps->rec_ctx, &ps->rid_mutex);\n\t\t\tif(save) {\n\t\t\t\tuint32_t seq_number = ntohs(rtp->seq_number);\n\t\t\t\tuint32_t timestamp = ntohl(rtp->timestamp);\n\t\t\t\tuint32_t ssrc = ntohl(rtp->ssrc);\n\t\t\t\tjanus_rtp_header_update(rtp, &ps->rec_ctx, TRUE, 0);\n\t\t\t\t/* We use a fixed SSRC for the whole recording */\n\t\t\t\trtp->ssrc = ps->vssrc[0];\n\t\t\t\tjanus_recorder_save_frame(ps->rc, buf, len);\n\t\t\t\t/* Restore the header, as it will be needed by subscribers */\n\t\t\t\trtp->ssrc = htonl(ssrc);\n\t\t\t\trtp->timestamp = htonl(timestamp);\n\t\t\t\trtp->seq_number = htons(seq_number);\n\t\t\t}\n\t\t}\n\t\t/* Done, relay it */\n\t\tjanus_videoroom_rtp_relay_packet packet = { 0 };\n\t\tpacket.source = ps;\n\t\tpacket.data = rtp;\n\t\tpacket.length = len;\n\t\tpacket.extensions = pkt->extensions;\n\t\tpacket.is_rtp = TRUE;\n\t\tpacket.is_video = video;\n\t\tpacket.svc = FALSE;\n\t\tif(video && ps->svc) {\n\t\t\t/* We're doing SVC: let's parse this packet to see which layers are there */\n\t\t\tint plen = 0;\n\t\t\tchar *payload = janus_rtp_payload(buf, len, &plen);\n\t\t\tif(payload == NULL) {\n\t\t\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\t\t\tjanus_refcount_decrease_nodebug(&videoroom->ref);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_VP9) {\n\t\t\t\tgboolean found = FALSE;\n\t\t\t\tmemset(&packet.svc_info, 0, sizeof(packet.svc_info));\n\t\t\t\tif(janus_vp9_parse_svc(payload, plen, &found, &packet.svc_info) == 0) {\n\t\t\t\t\tpacket.svc = found;\n\t\t\t\t}\n\t\t\t} else if(ps->vcodec == JANUS_VIDEOCODEC_AV1) {\n\t\t\t\tpacket.svc = (pkt->extensions.dd_len > 0);\n\t\t\t}\n\t\t}\n\t\tif(video && ps->simulcast)\n\t\t\tpacket.simulcast = TRUE;\n\t\tpacket.ssrc[0] = (sc != -1 ? ps->vssrc[0] : 0);\n\t\tpacket.ssrc[1] = (sc != -1 ? ps->vssrc[1] : 0);\n\t\tpacket.ssrc[2] = (sc != -1 ? ps->vssrc[2] : 0);\n\t\t/* Backup the actual timestamp and sequence number set by the publisher, in case switching is involved */\n\t\tpacket.timestamp = ntohl(packet.data->timestamp);\n\t\tpacket.seq_number = ntohs(packet.data->seq_number);\n\t\tif(ps->min_delay > -1 && ps->max_delay > -1) {\n\t\t\tpacket.extensions.min_delay = ps->min_delay;\n\t\t\tpacket.extensions.max_delay = ps->max_delay;\n\t\t}\n\t\t/* Go: some viewers may decide to drop the packet, but that's up to them */\n\t\tjanus_mutex_lock_nodebug(&ps->subscribers_mutex);\n\t\tif(videoroom->helper_threads > 0) {\n\t\t\tg_list_foreach(videoroom->threads, janus_videoroom_helper_rtpdata_packet, &packet);\n\t\t} else {\n\t\t\tg_slist_foreach(ps->subscribers, janus_videoroom_relay_rtp_packet, &packet);\n\t\t}\n\t\tjanus_mutex_unlock_nodebug(&ps->subscribers_mutex);\n\n\t\t/* Check if we need to send any REMB, FIR or PLI back to this publisher */\n\t\tif(video && ps->active && !ps->muted) {\n\t\t\t/* Did we send a REMB already, or is it time to send one? */\n\t\t\tgboolean send_remb = FALSE;\n\t\t\tif(participant->remb_latest == 0 && participant->remb_startup > 0) {\n\t\t\t\t/* Still in the starting phase, send the ramp-up REMB feedback */\n\t\t\t\tsend_remb = TRUE;\n\t\t\t} else if(participant->remb_latest > 0 && janus_get_monotonic_time()-participant->remb_latest >= 5*G_USEC_PER_SEC) {\n\t\t\t\t/* 5 seconds have passed since the last REMB, send a new one */\n\t\t\t\tsend_remb = TRUE;\n\t\t\t}\n\n\t\t\tif(send_remb && participant->bitrate) {\n\t\t\t\t/* We send a few incremental REMB messages at startup */\n\t\t\t\tuint32_t bitrate = participant->bitrate;\n\t\t\t\tif(participant->remb_startup > 0) {\n\t\t\t\t\tbitrate = bitrate/participant->remb_startup;\n\t\t\t\t\tparticipant->remb_startup--;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Sending REMB (%s, %\"SCNu32\")\\n\", participant->display, bitrate);\n\t\t\t\tif(!participant->remote) {\n\t\t\t\t\tgateway->send_remb(session->handle, bitrate);\n\t\t\t\t} else {\n\t\t\t\t\t/* TODO Forward back to the remote publisher */\n\t\t\t\t}\n\t\t\t\tif(participant->remb_startup == 0)\n\t\t\t\t\tparticipant->remb_latest = janus_get_monotonic_time();\n\t\t\t}\n\t\t\t/* Generate FIR/PLI too, if needed */\n\t\t\tif(video && ps->active && !ps->muted && (videoroom->fir_freq > 0)) {\n\t\t\t\t/* We generate RTCP every tot seconds/frames */\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\t/* First check if this is a keyframe, though: if so, we reset the timer */\n\t\t\t\tint plen = 0;\n\t\t\t\tchar *payload = janus_rtp_payload(buf, len, &plen);\n\t\t\t\tif(payload == NULL) {\n\t\t\t\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\t\t\t\tjanus_refcount_decrease_nodebug(&videoroom->ref);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t\t\tif(janus_vp8_is_keyframe(payload, plen))\n\t\t\t\t\t\tps->fir_latest = now;\n\t\t\t\t} else if(ps->vcodec == JANUS_VIDEOCODEC_VP9) {\n\t\t\t\t\tif(janus_vp9_is_keyframe(payload, plen))\n\t\t\t\t\t\tps->fir_latest = now;\n\t\t\t\t} else if(ps->vcodec == JANUS_VIDEOCODEC_H264) {\n\t\t\t\t\tif(janus_h264_is_keyframe(payload, plen))\n\t\t\t\t\t\tps->fir_latest = now;\n\t\t\t\t} else if(ps->vcodec == JANUS_VIDEOCODEC_AV1) {\n\t\t\t\t\tif(janus_av1_is_keyframe(payload, plen))\n\t\t\t\t\t\tps->fir_latest = now;\n\t\t\t\t} else if(ps->vcodec == JANUS_VIDEOCODEC_H265) {\n\t\t\t\t\tif(janus_h265_is_keyframe(payload, plen))\n\t\t\t\t\t\tps->fir_latest = now;\n\t\t\t\t}\n\t\t\t\tif((now-ps->fir_latest) >= ((gint64)videoroom->fir_freq*G_USEC_PER_SEC)) {\n\t\t\t\t\t/* FIXME We send a FIR every tot seconds */\n\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Regular keyframe request\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tjanus_refcount_decrease_nodebug(&ps->ref);\n\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\tjanus_refcount_decrease_nodebug(&videoroom->ref);\n}\n\nvoid janus_videoroom_incoming_rtcp(janus_plugin_session *handle, janus_plugin_rtcp *packet) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_videoroom_session *session = (janus_videoroom_session *)handle->plugin_handle;\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed))\n\t\treturn;\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\tif(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\t/* A subscriber sent some RTCP, check what it is and if we need to forward it to the publisher */\n\t\tjanus_videoroom_subscriber *s = janus_videoroom_session_get_subscriber_nodebug(session);\n\t\tif(s == NULL)\n\t\t\treturn;\n\t\tif(g_atomic_int_get(&s->destroyed)) {\n\t\t\tjanus_refcount_decrease_nodebug(&s->ref);\n\t\t\treturn;\n\t\t}\n\t\t/* Find the stream this packet belongs to */\n\t\tjanus_mutex_lock(&s->streams_mutex);\n\t\tjanus_videoroom_subscriber_stream *ss = g_hash_table_lookup(s->streams_byid, GINT_TO_POINTER(packet->mindex));\n\t\tif(ss == NULL || ss->publisher_streams == NULL) {\n\t\t\t/* No stream..? */\n\t\t\tjanus_mutex_unlock(&s->streams_mutex);\n\t\t\tjanus_refcount_decrease_nodebug(&s->ref);\n\t\t\treturn;\n\t\t}\n\t\tjanus_videoroom_publisher_stream *ps = ss->publisher_streams ? ss->publisher_streams->data : NULL;\n\t\tif(ps == NULL || ps->type != JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\tjanus_mutex_unlock(&s->streams_mutex);\n\t\t\tjanus_refcount_decrease_nodebug(&s->ref);\n\t\t\treturn;\t\t/* The only feedback we handle is video related anyway... */\n\t\t}\n\t\tjanus_refcount_increase_nodebug(&ps->ref);\n\t\tjanus_mutex_unlock(&s->streams_mutex);\n\t\tif(janus_rtcp_has_fir(buf, len) || janus_rtcp_has_pli(buf, len)) {\n\t\t\t/* We got a FIR or PLI, forward a PLI to the publisher */\n\t\t\tjanus_videoroom_publisher *p = ps->publisher;\n\t\t\tif(p && p->session)\n\t\t\t\tjanus_videoroom_reqpli(ps, \"PLI from subscriber\");\n\t\t}\n\t\tuint32_t bitrate = janus_rtcp_get_remb(buf, len);\n\t\tif(bitrate > 0) {\n\t\t\t/* FIXME We got a REMB from this subscriber, should we do something about it? */\n\t\t}\n\t\tjanus_refcount_decrease_nodebug(&ps->ref);\n\t\tjanus_refcount_decrease_nodebug(&s->ref);\n\t}\n}\n\nstatic void janus_videoroom_incoming_data_internal(janus_videoroom_session *session, janus_videoroom_publisher *participant, janus_plugin_data *packet);\nvoid janus_videoroom_incoming_data(janus_plugin_session *handle, janus_plugin_data *packet) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_videoroom_session *session = (janus_videoroom_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || session->participant_type != janus_videoroom_p_type_publisher)\n\t\treturn;\n\tjanus_videoroom_publisher *participant = janus_videoroom_session_get_publisher_nodebug(session);\n\tif(participant == NULL)\n\t\treturn;\n\tjanus_videoroom_incoming_data_internal(session, participant, packet);\n}\nstatic void janus_videoroom_incoming_data_internal(janus_videoroom_session *session, janus_videoroom_publisher *participant, janus_plugin_data *packet) {\n\tif(packet->buffer == NULL || packet->length == 0) {\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&participant->destroyed) || participant->kicked || !participant->streams) {\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\treturn;\n\t}\n\tjanus_mutex_lock(&participant->mutex);\n\tjanus_videoroom *videoroom = participant->room;\n\tif(videoroom == NULL) {\n\t\tjanus_mutex_unlock(&participant->mutex);\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\treturn;\n\t}\n\tjanus_refcount_increase_nodebug(&videoroom->ref);\n\tjanus_mutex_unlock(&participant->mutex);\n\tif(g_atomic_int_get(&participant->destroyed) || participant->data_mindex < 0 || !participant->streams || participant->kicked) {\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\tjanus_refcount_decrease_nodebug(&videoroom->ref);\n\t\treturn;\n\t}\n\tchar *buf = packet->buffer;\n\tuint16_t len = packet->length;\n\n\t/* Find the stream this packet belongs to */\n\tjanus_mutex_lock(&participant->streams_mutex);\n\tjanus_videoroom_publisher_stream *ps = g_hash_table_lookup(participant->streams_byid, GINT_TO_POINTER(participant->data_mindex));\n\tif(ps != NULL)\n\t\tjanus_refcount_increase_nodebug(&ps->ref);\n\tjanus_mutex_unlock(&participant->streams_mutex);\n\tif(ps == NULL || !ps->active || ps->muted || g_atomic_int_get(&ps->destroyed)) {\n\t\t/* No or inactive stream..? */\n\t\tif(ps != NULL)\n\t\t\tjanus_refcount_decrease_nodebug(&ps->ref);\n\t\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\t\treturn;\n\t}\n\n\t/* Any forwarder involved? */\n\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\t/* Forward RTP to the appropriate port for the rtp_forwarders associated with this publisher, if there are any */\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, ps->rtp_forwarders);\n\twhile(participant->udp_sock > 0 && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_rtp_forwarder *rtp_forward = (janus_rtp_forwarder *)value;\n\t\tif(rtp_forward->is_data) {\n\t\t\tstruct sockaddr *address = (rtp_forward->serv_addr.sin_family == AF_INET ?\n\t\t\t\t(struct sockaddr *)&rtp_forward->serv_addr : (struct sockaddr *)&rtp_forward->serv_addr6);\n\t\t\tsize_t addrlen = (rtp_forward->serv_addr.sin_family == AF_INET ? sizeof(rtp_forward->serv_addr) : sizeof(rtp_forward->serv_addr6));\n\t\t\t/* Check if this is a regular RTP forwarder, or a publisher remotization */\n\t\t\tif(rtp_forward->metadata == NULL) {\n\t\t\t\t/* Regular forwarder, send the payload as it is */\n\t\t\t\tif(sendto(participant->udp_sock, buf, len, 0, address, addrlen) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Error forwarding data packet for %s... %s (len=%d)...\\n\",\n\t\t\t\t\t\tparticipant->display, g_strerror(errno), len);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Remotization, prefix with a fake RTP header so that we can\n\t\t\t\t * set an SRRC (and use the payload type for binary vs. text) */\n\t\t\t\tchar buffer[1500];\n\t\t\t\tmemset(buffer, 0, sizeof(buffer));\n\t\t\t\tint buflen = len + 12;\n\t\t\t\tif(buflen > (int)sizeof(buffer))\t/* FIXME We're going to truncate */\n\t\t\t\t\tbuflen = sizeof(buffer);\n\t\t\t\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t\t\t\trtp->version = 2;\n\t\t\t\trtp->ssrc = htonl(rtp_forward->ssrc);\n\t\t\t\trtp->type = packet->binary ? 1 : 0;\n\t\t\t\tmemcpy(buffer + 12, buf, buflen - 12);\n\t\t\t\tif(sendto(participant->udp_sock, buffer, buflen, 0, address, addrlen) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Error forwarding data packet for %s... %s (len=%d)...\\n\",\n\t\t\t\t\t\tparticipant->display, g_strerror(errno), len);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\tJANUS_LOG(LOG_VERB, \"Got a %s DataChannel message (%d bytes) to forward\\n\",\n\t\tpacket->binary ? \"binary\" : \"text\", len);\n\t/* Save the message if we're recording */\n\tjanus_recorder_save_frame(ps->rc, buf, len);\n\t/* Relay to all subscribers */\n\tjanus_videoroom_rtp_relay_packet pkt = { 0 };\n\tpkt.source = ps;\n\tpkt.data = (struct rtp_header *)buf;\n\tpkt.length = len;\n\tpkt.is_rtp = FALSE;\n\tpkt.textdata = !packet->binary;\n\tjanus_mutex_lock_nodebug(&ps->subscribers_mutex);\n\tif(videoroom->helper_threads > 0) {\n\t\tg_list_foreach(videoroom->threads, janus_videoroom_helper_rtpdata_packet, &pkt);\n\t} else {\n\t\tg_slist_foreach(ps->subscribers, janus_videoroom_relay_data_packet, &pkt);\n\t}\n\tjanus_mutex_unlock_nodebug(&ps->subscribers_mutex);\n\tjanus_refcount_decrease_nodebug(&ps->ref);\n\tjanus_videoroom_publisher_dereference_nodebug(participant);\n\tjanus_refcount_decrease_nodebug(&videoroom->ref);\n}\n\nvoid janus_videoroom_data_ready(janus_plugin_session *handle) {\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) ||\n\t\t\tg_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized) || !gateway)\n\t\treturn;\n\t/* Data channels are writable */\n\tjanus_videoroom_session *session = (janus_videoroom_session *)handle->plugin_handle;\n\tif(!session || g_atomic_int_get(&session->destroyed) || g_atomic_int_get(&session->hangingup))\n\t\treturn;\n\tif(g_atomic_int_compare_and_exchange(&session->dataready, 0, 1)) {\n\t\tJANUS_LOG(LOG_INFO, \"[%s-%p] Data channel available\\n\", JANUS_VIDEOROOM_PACKAGE, handle);\n\t}\n}\n\nvoid janus_videoroom_slow_link(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink) {\n\t/* The core is informing us that our peer got too many NACKs, are we pushing media too hard? */\n\tif(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videoroom_session *session = janus_videoroom_lookup_session(handle);\n\tif(!session || g_atomic_int_get(&session->destroyed) || !session->participant) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\t/* Check if it's an uplink (publisher) or downlink (viewer) issue */\n\tif(session->participant_type == janus_videoroom_p_type_publisher) {\n\t\tif(!uplink) {\n\t\t\tjanus_videoroom_publisher *publisher = janus_videoroom_session_get_publisher(session);\n\t\t\tif(publisher == NULL) {\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(g_atomic_int_get(&publisher->destroyed)) {\n\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t/* Send an event on the handle to notify the application: it's\n\t\t\t * up to the application to then choose a policy and enforce it */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"slow_link\"));\n\t\t\t/* Also add info on what the current bitrate cap is */\n\t\t\tuint32_t bitrate = publisher->bitrate;\n\t\t\tjson_object_set_new(event, \"current-bitrate\", json_integer(bitrate));\n\t\t\tgateway->push_event(session->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\tjson_decref(event);\n\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Got a slow uplink on a VideoRoom publisher? Weird, because it doesn't receive media...\\n\");\n\t\t}\n\t} else if(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\tif(uplink) {\n\t\t\tjanus_videoroom_subscriber *subscriber = janus_videoroom_session_get_subscriber(session);\n\t\t\tif(subscriber == NULL) {\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(g_atomic_int_get(&subscriber->destroyed)) {\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t/* Send an event on the handle to notify the application: it's\n\t\t\t * up to the application to then choose a policy and enforce it */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"slow_link\"));\n\t\t\tgateway->push_event(session->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\tjson_decref(event);\n\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Got a slow downlink on a VideoRoom viewer? Weird, because it doesn't send media...\\n\");\n\t\t}\n\t}\n\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_videoroom_recorder_create(janus_videoroom_publisher_stream *ps) {\n\tchar filename[255];\n\tjanus_recorder *rc = NULL;\n\tgint64 now = janus_get_real_time();\n\tif(ps->publisher && ps->rc == NULL) {\n\t\tjanus_videoroom_publisher *participant = ps->publisher;\n\t\tconst char *type = NULL;\n\t\tswitch(ps->type) {\n\t\t\tcase JANUS_VIDEOROOM_MEDIA_AUDIO:\n\t\t\t\ttype = janus_audiocodec_name(ps->acodec);\n\t\t\t\tbreak;\n\t\t\tcase JANUS_VIDEOROOM_MEDIA_VIDEO:\n\t\t\t\ttype = janus_videocodec_name(ps->vcodec);\n\t\t\t\tbreak;\n\t\t\tcase JANUS_VIDEOROOM_MEDIA_DATA:\n\t\t\t\ttype = \"text\";\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\treturn;\n\t\t}\n\t\tjanus_rtp_switching_context_reset(&ps->rec_ctx);\n\t\tjanus_rtp_simulcasting_context_reset(&ps->rec_simctx);\n\t\tps->rec_simctx.substream_target = 2;\n\t\tps->rec_simctx.templayer_target = 2;\n\t\tmemset(filename, 0, 255);\n\t\tif(participant->recording_base) {\n\t\t\t/* Use the filename and path we have been provided */\n\t\t\tg_snprintf(filename, 255, \"%s-%s-%d\", participant->recording_base,\n\t\t\t\tjanus_videoroom_media_str(ps->type), ps->mindex);\n\t\t\trc = janus_recorder_create_full(participant->room->rec_dir, type, ps->fmtp, filename);\n\t\t\tif(rc == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open a %s recording file for this publisher!\\n\",\n\t\t\t\t\tjanus_videoroom_media_str(ps->type));\n\t\t\t}\n\t\t} else {\n\t\t\t/* Build a filename */\n\t\t\tg_snprintf(filename, 255, \"videoroom-%s-user-%s-%\"SCNi64\"-%s-%d\",\n\t\t\t\tparticipant->room_id_str, participant->user_id_str, now,\n\t\t\t\tjanus_videoroom_media_str(ps->type), ps->mindex);\n\t\t\trc = janus_recorder_create_full(participant->room->rec_dir, type, ps->fmtp, filename);\n\t\t\tif(rc == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't open an %s recording file for this publisher!\\n\",\n\t\t\t\t\tjanus_videoroom_media_str(ps->type));\n\t\t\t}\n\t\t}\n\t\t/* If the stream has a description, store it in the recording */\n\t\tif(ps->description)\n\t\t\tjanus_recorder_description(rc, ps->description);\n\t\t/* If the video-orientation extension has been negotiated, mark it in the recording */\n\t\tif(ps->video_orient_extmap_id > 0)\n\t\t\tjanus_recorder_add_extmap(rc, ps->video_orient_extmap_id, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION);\n\t\t/* If media is encrypted, mark it in the recording */\n\t\tif(ps->type != JANUS_VIDEOROOM_MEDIA_DATA && participant->e2ee)\n\t\t\tjanus_recorder_encrypted(rc);\n\t\tps->rc = rc;\n\t}\n}\n\nstatic void janus_videoroom_recorder_close(janus_videoroom_publisher *participant) {\n\tGList *temp = participant->streams;\n\twhile(temp) {\n\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\tif(ps->rc) {\n\t\t\tjanus_recorder *rc = ps->rc;\n\t\t\tps->rc = NULL;\n\t\t\tjanus_recorder_close(rc);\n\t\t\tJANUS_LOG(LOG_INFO, \"Closed %s recording %s\\n\", janus_videoroom_media_str(ps->type),\n\t\t\t\trc->filename ? rc->filename : \"??\");\n\t\t\tjanus_recorder_destroy(rc);\n\t\t}\n\t\ttemp = temp->next;\n\t}\n}\n\nvoid janus_videoroom_hangup_media(janus_plugin_session *handle) {\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] No WebRTC media anymore; %p %p\\n\", JANUS_VIDEOROOM_PACKAGE, handle, handle->gateway_handle, handle->plugin_handle);\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))\n\t\treturn;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_videoroom_session *session = janus_videoroom_lookup_session(handle);\n\tif(!session) {\n\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tif(g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tjanus_videoroom_hangup_media_internal(session);\n\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_videoroom_hangup_media_internal(gpointer session_data) {\n\tjanus_videoroom_session *session = (janus_videoroom_session *)session_data;\n\tg_atomic_int_set(&session->started, 0);\n\tif(!g_atomic_int_compare_and_exchange(&session->hangingup, 0, 1)) {\n\t\treturn;\n\t}\n\tg_atomic_int_set(&session->dataready, 0);\n\t/* Send an event to the browser and tell the PeerConnection is over */\n\tif(session->participant_type == janus_videoroom_p_type_publisher) {\n\t\t/* This publisher just 'unpublished' */\n\t\tjanus_videoroom_publisher *participant = janus_videoroom_session_get_publisher(session);\n\t\t/* Get rid of the recorders, if available */\n\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\tg_free(participant->recording_base);\n\t\tparticipant->recording_base = NULL;\n\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\tjanus_videoroom_recorder_close(participant);\n\t\tjanus_mutex_unlock(&participant->streams_mutex)\n\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\tparticipant->acodec = JANUS_AUDIOCODEC_NONE;\n\t\tparticipant->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\tparticipant->firefox = FALSE;\n\t\tparticipant->e2ee = FALSE;\n\t\t/* Get rid of streams */\n\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\tGList *subscribers = NULL, *mappings = NULL;\n\t\tGList *temp = participant->streams;\n\t\twhile(temp) {\n\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t/* Close all subscriptions to this stream */\n\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\tGSList *temp2 = ps->subscribers;\n\t\t\twhile(temp2) {\n\t\t\t\tjanus_videoroom_subscriber_stream *ss = (janus_videoroom_subscriber_stream *)temp2->data;\n\t\t\t\ttemp2 = temp2->next;\n\t\t\t\tif(ss) {\n\t\t\t\t\t/* Take note of the subscriber, so that we can send an updated offer */\n\t\t\t\t\tif(ss->type != JANUS_VIDEOROOM_MEDIA_DATA && g_list_find(subscribers, ss->subscriber) == NULL) {\n\t\t\t\t\t\tjanus_refcount_increase(&ss->subscriber->ref);\n\t\t\t\t\t\tjanus_refcount_increase(&ss->subscriber->session->ref);\n\t\t\t\t\t\tsubscribers = g_list_append(subscribers, ss->subscriber);\n\t\t\t\t\t}\n\t\t\t\t\t/* Take note of the subscription to remove */\n\t\t\t\t\tjanus_videoroom_stream_mapping *m = g_malloc(sizeof(janus_videoroom_stream_mapping));\n\t\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t\tjanus_refcount_increase(&ss->ref);\n\t\t\t\t\tjanus_refcount_increase(&ss->subscriber->ref);\n\t\t\t\t\tm->ps = ps;\n\t\t\t\t\tm->ss = ss;\n\t\t\t\t\tm->unref_ss = (g_slist_find(ps->subscribers, ss) != NULL);\n\t\t\t\t\tm->subscriber = ss->subscriber;\n\t\t\t\t\tmappings = g_list_append(mappings, m);\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_slist_free(ps->subscribers);\n\t\t\tps->subscribers = NULL;\n\t\t\tjanus_rtp_simulcasting_cleanup(&ps->rid_extmap_id, ps->vssrc, ps->rid, &ps->rid_mutex);\n\t\t\tg_free(ps->fmtp);\n\t\t\tps->fmtp = NULL;\n\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tif(mappings) {\n\t\t\ttemp = mappings;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_videoroom_stream_mapping *m = (janus_videoroom_stream_mapping *)temp->data;\n\t\t\t\t/* Remove the subscription (turns the m-line to inactive) */\n\t\t\t\tjanus_videoroom_publisher_stream *ps = m->ps;\n\t\t\t\tjanus_videoroom_subscriber *subscriber = m->subscriber;\n\t\t\t\tjanus_videoroom_subscriber_stream *ss = m->ss;\n\t\t\t\tif(subscriber) {\n\t\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\t\tjanus_videoroom_subscriber_stream_remove(ss, ps, TRUE);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\tif(m->unref_ss)\n\t\t\t\t\t\tjanus_refcount_decrease(&ss->ref);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t}\n\t\t\t\tjanus_refcount_decrease(&ss->ref);\n\t\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t\tg_list_free_full(mappings, (GDestroyNotify)g_free);\n\t\t}\n\t\t/* Any subscriber session to update? */\n\t\tjanus_mutex_lock(&participant->mutex);\n\t\tjanus_videoroom *room = participant->room;\n\t\tif(room)\n\t\t\tjanus_refcount_increase_nodebug(&room->ref);\n\t\tjanus_mutex_unlock(&participant->mutex);\n\t\tif(subscribers != NULL) {\n\t\t\ttemp = subscribers;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_videoroom_subscriber *subscriber = (janus_videoroom_subscriber *)temp->data;\n\t\t\t\t/* Send (or schedule) a new offer */\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\tif(!subscriber->autoupdate || room == NULL || g_atomic_int_get(&room->destroyed)) {\n\t\t\t\t\t/* ... unless we've been asked not to, or there's no room (anymore) */\n\t\t\t\t\tg_atomic_int_set(&subscriber->skipped_autoupdate, 1);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(!g_atomic_int_get(&subscriber->answered)) {\n\t\t\t\t\t/* We're still waiting for an answer to a previous offer, postpone this */\n\t\t\t\t\tg_atomic_int_set(&subscriber->pending_offer, 1);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t} else {\n\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"updated\"));\n\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(subscriber, FALSE, NULL);\n\t\t\t\t\tjson_t *media_event = NULL;\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled())\n\t\t\t\t\t\tmedia_event = json_deep_copy(media);\n\t\t\t\t\tjson_object_set_new(event, \"streams\", media);\n\t\t\t\t\t/* Generate a new offer */\n\t\t\t\t\tjson_t *jsep = janus_videoroom_subscriber_offer(subscriber);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\t\tint res = gateway->push_event(subscriber->session->handle, &janus_videoroom_plugin, NULL, event, jsep);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tjson_decref(jsep);\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"updated\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"streams\", media_event);\n\t\t\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(subscriber->pvt_id));\n\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_refcount_decrease(&subscriber->session->ref);\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\ttemp = temp->next;\n\t\t\t}\n\t\t}\n\t\tg_list_free(subscribers);\n\t\t/* Free streams */\n\t\tg_list_free(participant->streams);\n\t\tparticipant->streams = NULL;\n\t\tg_hash_table_remove_all(participant->streams_byid);\n\t\tg_hash_table_remove_all(participant->streams_bymid);\n\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\tjanus_videoroom_leave_or_unpublish(participant, FALSE, FALSE);\n\t\tjanus_refcount_decrease(&participant->ref);\n\t\tif(room)\n\t\t\tjanus_refcount_decrease_nodebug(&room->ref);\n\t} else if(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\t/* Get rid of subscriber */\n\t\tjanus_videoroom_subscriber *subscriber = janus_videoroom_session_get_subscriber(session);\n\t\tif(subscriber) {\n\t\t\tsubscriber->paused = TRUE;\n\t\t\tsubscriber->e2ee = FALSE;\n\t\t\tg_atomic_int_set(&subscriber->answered, 0);\n\t\t\tg_atomic_int_set(&subscriber->pending_offer, 0);\n\t\t\tg_atomic_int_set(&subscriber->pending_restart, 0);\n\t\t\t/* Get rid of streams */\n\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\tGList *temp = subscriber->streams;\n\t\t\twhile(temp) {\n\t\t\t\tjanus_videoroom_subscriber_stream *s = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\tGSList *list = s->publisher_streams;\n\t\t\t\twhile(list) {\n\t\t\t\t\tjanus_videoroom_publisher_stream *ps = list->data;\n\t\t\t\t\tif(ps && ps->publisher != NULL) {\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"unsubscribed\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\t\t\tjson_string(ps->publisher->room_id_str) : json_integer(ps->publisher->room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"feed\",  string_ids ?\n\t\t\t\t\t\t\t\tjson_string(ps->publisher->user_id_str) : json_integer(ps->publisher->user_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"mid\", json_string(ps->mid));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tlist = list->next;\n\t\t\t\t}\n\t\t\t\ttemp = temp->next;\n\t\t\t\tjanus_videoroom_subscriber_stream_remove(s, NULL, TRUE);\n\t\t\t}\n\t\t\t/* Free streams */\n\t\t\tg_list_free(subscriber->streams);\n\t\t\tsubscriber->streams = NULL;\n\t\t\tg_hash_table_remove_all(subscriber->streams_byid);\n\t\t\tg_hash_table_remove_all(subscriber->streams_bymid);\n\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t}\n\t\t/* TODO Should we close the handle as well? */\n\t}\n\tg_atomic_int_set(&session->hangingup, 0);\n}\n\n/* Thread to handle incoming messages */\nstatic void *janus_videoroom_handler(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining VideoRoom handler thread\\n\");\n\tjanus_videoroom_message *msg = NULL;\n\tint error_code = 0;\n\tchar error_cause[512];\n\tjson_t *root = NULL;\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\tmsg = g_async_queue_pop(messages);\n\t\tif(msg == &exit_message)\n\t\t\tbreak;\n\t\tif(msg->handle == NULL) {\n\t\t\tjanus_videoroom_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_videoroom *videoroom = NULL;\n\t\tjanus_videoroom_publisher *participant = NULL;\n\t\tjanus_videoroom_subscriber *subscriber = NULL;\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_videoroom_session *session = janus_videoroom_lookup_session(msg->handle);\n\t\tif(!session) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\tjanus_videoroom_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjanus_videoroom_message_free(msg);\n\t\t\tcontinue;\n\t\t}\n\t\tif(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\t\tsubscriber = janus_videoroom_session_get_subscriber(session);\n\t\t\tif(subscriber == NULL || g_atomic_int_get(&subscriber->destroyed)) {\n\t\t\t\tif(subscriber != NULL)\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid subscriber instance\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid subscriber instance\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(subscriber->room == NULL) {\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"No such room\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\t\t\tg_snprintf(error_cause, 512, \"No such room\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Handle request */\n\t\terror_code = 0;\n\t\troot = NULL;\n\t\tif(msg->message == NULL) {\n\t\t\tif(subscriber != NULL) {\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"No message??\\n\");\n\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_MESSAGE;\n\t\t\tg_snprintf(error_cause, 512, \"%s\", \"No message??\");\n\t\t\tgoto error;\n\t\t}\n\t\troot = msg->message;\n\t\t/* Get the request first */\n\t\tJANUS_VALIDATE_JSON_OBJECT(root, request_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\tif(error_code != 0) {\n\t\t\tif(subscriber != NULL) {\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t}\n\t\t\tgoto error;\n\t\t}\n\t\tjson_t *request = json_object_get(root, \"request\");\n\t\tconst char *request_text = json_string_value(request);\n\t\tjson_t *event = NULL;\n\t\tgboolean sdp_update = FALSE;\n\t\tif(json_object_get(msg->jsep, \"update\") != NULL)\n\t\t\tsdp_update = json_is_true(json_object_get(msg->jsep, \"update\"));\n\t\t/* 'create' and 'destroy' are handled synchronously: what kind of participant is this session referring to? */\n\t\tif(session->participant_type == janus_videoroom_p_type_none) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Configuring new participant\\n\");\n\t\t\t/* Not configured yet, we need to do this now */\n\t\t\tif(strcasecmp(request_text, \"join\") && strcasecmp(request_text, \"joinandconfigure\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid request \\\"%s\\\" on unconfigured participant\\n\", request_text);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_JOIN_FIRST;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid request on unconfigured participant\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!string_ids) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, room_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t} else {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t}\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, join_parameters,\n\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\tif(error_code != 0)\n\t\t\t\tgoto error;\n\t\t\tjanus_mutex_lock(&rooms_mutex);\n\t\t\terror_code = janus_videoroom_access_room(root, FALSE, TRUE, &videoroom, error_cause, sizeof(error_cause));\n\t\t\tif(error_code != 0) {\n\t\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_increase(&videoroom->ref);\n\t\t\tjanus_mutex_unlock(&rooms_mutex);\n\t\t\tjanus_mutex_lock(&sessions_mutex);\n\t\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\t\tjson_t *ptype = json_object_get(root, \"ptype\");\n\t\t\tconst char *ptype_text = json_string_value(ptype);\n\t\t\tif(!strcasecmp(ptype_text, \"publisher\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Configuring new publisher\\n\");\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, publisher_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(!string_ids) {\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idopt_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, idstropt_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t}\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjson_t *descriptions = json_object_get(root, \"descriptions\");\n\t\t\t\tif(descriptions != NULL && json_array_size(descriptions) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(descriptions); i++) {\n\t\t\t\t\t\tjson_t *d = json_array_get(descriptions, i);\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(d, publish_desc_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\t\tgoto error;\n\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* A token might be required to join */\n\t\t\t\tif(videoroom->check_allowed) {\n\t\t\t\t\tjson_t *token = json_object_get(root, \"token\");\n\t\t\t\t\tconst char *token_text = token ? json_string_value(token) : NULL;\n\t\t\t\t\tif(token_text == NULL || g_hash_table_lookup(videoroom->allowed, token_text) == NULL) {\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized (not in the allowed list)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Unauthorized (not in the allowed list)\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjson_t *display = json_object_get(root, \"display\");\n\t\t\t\tjson_t *metadata= json_object_get(root, \"metadata\");\n\t\t\t\tconst char *display_text = display ? json_string_value(display) : NULL;\n\t\t\t\tguint64 user_id = 0;\n\t\t\t\tchar user_id_num[30], *user_id_str = NULL;\n\t\t\t\tgboolean user_id_allocated = FALSE;\n\t\t\t\tjson_t *id = json_object_get(root, \"id\");\n\t\t\t\tif(id) {\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tuser_id = json_integer_value(id);\n\t\t\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\t\t\tuser_id_str = user_id_num;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tuser_id_str = (char *)json_string_value(id);\n\t\t\t\t\t}\n\t\t\t\t\tif(g_hash_table_lookup(videoroom->participants,\n\t\t\t\t\t\t\tstring_ids ? (gpointer)user_id_str : (gpointer)&user_id) != NULL) {\n\t\t\t\t\t\t/* User ID already taken */\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ID_EXISTS;\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"User ID %s already exists\\n\", user_id_str);\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"User ID %s already exists\", user_id_str);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!string_ids) {\n\t\t\t\t\tif(user_id == 0) {\n\t\t\t\t\t\t/* Generate a random ID */\n\t\t\t\t\t\twhile(user_id == 0) {\n\t\t\t\t\t\t\tuser_id = janus_random_uint64();\n\t\t\t\t\t\t\tif(g_hash_table_lookup(videoroom->participants, &user_id) != NULL) {\n\t\t\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\t\t\tuser_id = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg_snprintf(user_id_num, sizeof(user_id_num), \"%\"SCNu64, user_id);\n\t\t\t\t\t\tuser_id_str = user_id_num;\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID: %\"SCNu64\"\\n\", user_id);\n\t\t\t\t} else {\n\t\t\t\t\tif(user_id_str == NULL) {\n\t\t\t\t\t\t/* Generate a random ID */\n\t\t\t\t\t\twhile(user_id_str == NULL) {\n\t\t\t\t\t\t\tuser_id_str = janus_random_uuid();\n\t\t\t\t\t\t\tif(g_hash_table_lookup(videoroom->participants, user_id_str) != NULL) {\n\t\t\t\t\t\t\t\t/* User ID already taken, try another one */\n\t\t\t\t\t\t\t\tg_clear_pointer(&user_id_str, g_free);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tuser_id_allocated = TRUE;\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Participant ID: %s\\n\", user_id_str);\n\t\t\t\t}\n\t\t\t\t/* Process the request */\n\t\t\t\tjson_t *bitrate = NULL, *record = NULL, *recfile = NULL,\n\t\t\t\t\t*audiocodec = NULL, *videocodec = NULL,\n\t\t\t\t\t*user_audio_active_packets = NULL, *user_audio_level_average = NULL;\n\t\t\t\tif(!strcasecmp(request_text, \"joinandconfigure\")) {\n\t\t\t\t\t/* Also configure (or publish a new feed) audio/video/bitrate for this new publisher */\n\t\t\t\t\t/* join_parameters were validated earlier. */\n\t\t\t\t\taudiocodec = json_object_get(root, \"audiocodec\");\n\t\t\t\t\tvideocodec = json_object_get(root, \"videocodec\");\n\t\t\t\t\tbitrate = json_object_get(root, \"bitrate\");\n\t\t\t\t\trecord = json_object_get(root, \"record\");\n\t\t\t\t\trecfile = json_object_get(root, \"filename\");\n\t\t\t\t}\n\t\t\t\tuser_audio_active_packets = json_object_get(root, \"audio_active_packets\");\n\t\t\t\tuser_audio_level_average = json_object_get(root, \"audio_level_average\");\n\t\t\t\tjanus_videoroom_publisher *publisher = g_malloc0(sizeof(janus_videoroom_publisher));\n\t\t\t\tpublisher->session = session;\n\t\t\t\tpublisher->room_id = videoroom->room_id;\n\t\t\t\tpublisher->room_id_str = videoroom->room_id_str ? g_strdup(videoroom->room_id_str) : NULL;\n\t\t\t\tpublisher->room = videoroom;\n\t\t\t\tvideoroom = NULL;\n\t\t\t\tpublisher->user_id = user_id;\n\t\t\t\tpublisher->user_id_str = user_id_str ? g_strdup(user_id_str) : NULL;\n\t\t\t\tpublisher->display = display_text ? g_strdup(display_text) : NULL;\n\t\t\t\tpublisher->metadata = NULL;\n\t\t\t\tpublisher->recording_active = FALSE;\n\t\t\t\tpublisher->recording_base = NULL;\n\t\t\t\tpublisher->firefox = FALSE;\n\t\t\t\tpublisher->bitrate = publisher->room->bitrate;\n\t\t\t\tpublisher->subscriptions = NULL;\n\t\t\t\tpublisher->acodec = JANUS_AUDIOCODEC_NONE;\n\t\t\t\tpublisher->vcodec = JANUS_VIDEOCODEC_NONE;\n\t\t\t\tjanus_mutex_init(&publisher->subscribers_mutex);\n\t\t\t\tjanus_mutex_init(&publisher->own_subscriptions_mutex);\n\t\t\t\tpublisher->streams_byid = g_hash_table_new_full(NULL, NULL,\n\t\t\t\t\tNULL, (GDestroyNotify)janus_videoroom_publisher_stream_destroy);\n\t\t\t\tpublisher->streams_bymid = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_publisher_stream_unref);\n\t\t\t\tjanus_mutex_init(&publisher->streams_mutex);\n\t\t\t\tpublisher->remb_startup = 4;\n\t\t\t\tpublisher->remb_latest = 0;\n\t\t\t\tjanus_mutex_init(&publisher->rtp_forwarders_mutex);\n\t\t\t\tpublisher->remote_recipients = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_remote_recipient_free);\n\t\t\t\tpublisher->rtp_forwarders = g_hash_table_new(NULL, NULL);\n\t\t\t\tpublisher->udp_sock = -1;\n\t\t\t\t/* Finally, generate a private ID: this is only needed in case the participant\n\t\t\t\t * wants to allow the plugin to know which subscriptions belong to them */\n\t\t\t\tpublisher->pvt_id = 0;\n\t\t\t\twhile(publisher->pvt_id == 0) {\n\t\t\t\t\tpublisher->pvt_id = janus_random_uint32();\n\t\t\t\t\tif(g_hash_table_lookup(publisher->room->private_ids, GUINT_TO_POINTER(publisher->pvt_id)) != NULL) {\n\t\t\t\t\t\t/* Private ID already taken, try another one */\n\t\t\t\t\t\tpublisher->pvt_id = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tg_atomic_int_set(&publisher->destroyed, 0);\n\t\t\t\tjanus_mutex_init(&publisher->mutex);\n\t\t\t\tjanus_refcount_init(&publisher->ref, janus_videoroom_publisher_free);\n\t\t\t\t/* In case we also wanted to configure */\n\t\t\t\tif(audiocodec && json_string_value(json_object_get(msg->jsep, \"sdp\")) != NULL) {\n\t\t\t\t\tjanus_audiocodec acodec = janus_audiocodec_from_name(json_string_value(audiocodec));\n\t\t\t\t\tif(acodec == JANUS_AUDIOCODEC_NONE ||\n\t\t\t\t\t\t\t(acodec != publisher->room->acodec[0] &&\n\t\t\t\t\t\t\tacodec != publisher->room->acodec[1] &&\n\t\t\t\t\t\t\tacodec != publisher->room->acodec[2] &&\n\t\t\t\t\t\t\tacodec != publisher->room->acodec[3] &&\n\t\t\t\t\t\t\tacodec != publisher->room->acodec[4])) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Participant asked for audio codec '%s', but it's not allowed (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tjson_string_value(audiocodec), publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t\t\tjanus_mutex_unlock(&publisher->room->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->room->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Audio codec unavailable in this room\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Participant asked for audio codec '%s' (room %s, user %s)\\n\",\n\t\t\t\t\t\tjson_string_value(audiocodec), publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t\tpublisher->acodec = acodec;\n\t\t\t\t}\n\t\t\t\tif(videocodec && json_string_value(json_object_get(msg->jsep, \"sdp\")) != NULL) {\n\t\t\t\t\t/* The publisher would like to use a video codec in particular */\n\t\t\t\t\tjanus_videocodec vcodec = janus_videocodec_from_name(json_string_value(videocodec));\n\t\t\t\t\tif(vcodec == JANUS_VIDEOCODEC_NONE ||\n\t\t\t\t\t\t\t(vcodec != publisher->room->vcodec[0] &&\n\t\t\t\t\t\t\tvcodec != publisher->room->vcodec[1] &&\n\t\t\t\t\t\t\tvcodec != publisher->room->vcodec[2] &&\n\t\t\t\t\t\t\tvcodec != publisher->room->vcodec[3] &&\n\t\t\t\t\t\t\tvcodec != publisher->room->vcodec[4])) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Participant asked for video codec '%s', but it's not allowed (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tjson_string_value(videocodec), publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t\t\tjanus_mutex_unlock(&publisher->room->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->room->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Video codec unavailable in this room\");\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Participant asked for video codec '%s' (room %s, user %s)\\n\",\n\t\t\t\t\t\tjson_string_value(videocodec), publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t\tpublisher->vcodec = vcodec;\n\t\t\t\t}\n\t\t\t\tif(bitrate) {\n\t\t\t\t\tpublisher->bitrate = json_integer_value(bitrate);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video bitrate: %\"SCNu32\" (room %s, user %s)\\n\",\n\t\t\t\t\t\tpublisher->bitrate, publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(record) {\n\t\t\t\t\tpublisher->recording_active = json_is_true(record);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting record property: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\tpublisher->recording_active ? \"true\" : \"false\", publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(recfile) {\n\t\t\t\t\tpublisher->recording_base = g_strdup(json_string_value(recfile));\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting recording basename: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\tpublisher->recording_base, publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(user_audio_active_packets) {\n\t\t\t\t\tpublisher->user_audio_active_packets = json_integer_value(user_audio_active_packets);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting user audio_active_packets: %d (room %s, user %s)\\n\",\n\t\t\t\t\t\tpublisher->user_audio_active_packets, publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(user_audio_level_average) {\n\t\t\t\t\tpublisher->user_audio_level_average = json_integer_value(user_audio_level_average);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting user audio_level_average: %d (room %s, user %s)\\n\",\n\t\t\t\t\t\tpublisher->user_audio_level_average, publisher->room_id_str, publisher->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(metadata) {\n\t\t\t\t\tpublisher->metadata = json_deep_copy(metadata);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting metadata: (room %s, user %s)\\n\",\n\t\t\t\t\t\tpublisher->room_id_str, publisher->user_id_str);\n\t\t\t\t}\n\t\t\t\t/* Done */\n\t\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\t\t/* Make sure the session has not been destroyed in the meanwhile */\n\t\t\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&publisher->room->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&publisher->room->ref);\n\t\t\t\t\tjanus_videoroom_publisher_destroy(publisher);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Session destroyed, invalidating new publisher\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Session destroyed, invalidating new publisher\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tsession->participant_type = janus_videoroom_p_type_publisher;\n\t\t\t\tsession->participant = publisher;\n\t\t\t\t/* Return a list of all available publishers (those with an SDP available, that is) */\n\t\t\t\tjson_t *list = json_array(), *attendees = NULL;\n\t\t\t\tif(publisher->room->notify_joining)\n\t\t\t\t\tattendees = json_array();\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tjanus_refcount_increase(&publisher->ref);\n\t\t\t\tjanus_refcount_increase(&publisher->session->ref);\n\t\t\t\tg_hash_table_insert(publisher->room->participants,\n\t\t\t\t\tstring_ids ? (gpointer)g_strdup(publisher->user_id_str) : (gpointer)janus_uint64_dup(publisher->user_id),\n\t\t\t\t\tpublisher);\n\t\t\t\tg_hash_table_insert(publisher->room->private_ids, GUINT_TO_POINTER(publisher->pvt_id), publisher);\n\t\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\t\tg_hash_table_iter_init(&iter, publisher->room->participants);\n\t\t\t\twhile (!g_atomic_int_get(&publisher->room->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\tjanus_videoroom_publisher *p = value;\n\t\t\t\t\tif(p == publisher || !p->streams || !g_atomic_int_get(&p->session->started)) {\n\t\t\t\t\t\t/* Check if we're also notifying normal joins and not just publishers */\n\t\t\t\t\t\tif(p != publisher && publisher->room->notify_joining) {\n\t\t\t\t\t\t\tjson_t *al = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(al, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\t\t\t\t\tif(p->display)\n\t\t\t\t\t\t\t\tjson_object_set_new(al, \"display\", json_string(p->display));\n\t\t\t\t\t\t\tif(p->metadata)\n\t\t\t\t\t\t\t\tjson_object_set_new(al, \"metadata\", json_deep_copy(p->metadata));\n\t\t\t\t\t\t\tjson_array_append_new(attendees, al);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *pl = json_object();\n\t\t\t\t\tjson_object_set_new(pl, \"id\", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id));\n\t\t\t\t\tif(p->display)\n\t\t\t\t\t\tjson_object_set_new(pl, \"display\", json_string(p->display));\n\t\t\t\t\tif(p->metadata)\n\t\t\t\t\t\tjson_object_set_new(pl, \"metadata\", json_deep_copy(p->metadata));\n\t\t\t\t\tif(p->dummy)\n\t\t\t\t\t\tjson_object_set_new(pl, \"dummy\", json_true());\n\t\t\t\t\t/* Add proper info on all the streams */\n\t\t\t\t\tgboolean audio_added = FALSE, video_added = FALSE, talking_found = FALSE, talking = FALSE;\n\t\t\t\t\tjson_t *media = json_array();\n\t\t\t\t\tjanus_mutex_lock(&p->streams_mutex);\n\t\t\t\t\tGList *temp = p->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(janus_videoroom_media_str(ps->type)));\n\t\t\t\t\t\tjson_object_set_new(info, \"mindex\", json_integer(ps->mindex));\n\t\t\t\t\t\tjson_object_set_new(info, \"mid\", json_string(ps->mid));\n\n\t\t\t\t\t\tif(ps->disabled) {\n\t\t\t\t\t\t\tjson_object_set_new(info, \"disabled\", json_true());\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif(ps->description)\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"description\", json_string(ps->description));\n\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t\t\t\t\t/* FIXME For backwards compatibility, we need audio_codec in the global info */\n\t\t\t\t\t\t\t\tif(!audio_added) {\n\t\t\t\t\t\t\t\t\taudio_added = TRUE;\n\t\t\t\t\t\t\t\t\tjson_object_set_new(pl, \"audio_codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(ps->acodec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\t\t\t\t\tif(ps->opusstereo)\n\t\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"stereo\", json_true());\n\t\t\t\t\t\t\t\t\tif(ps->opusfec)\n\t\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"fec\", json_true());\n\t\t\t\t\t\t\t\t\tif(ps->opusdtx)\n\t\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"dtx\", json_true());\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(ps->audio_level_extmap_id > 0) {\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"talking\", talking ? json_true() : json_false());\n\t\t\t\t\t\t\t\t\t/* FIXME For backwards compatibility, we also need talking in the global info */\n\t\t\t\t\t\t\t\t\ttalking_found = TRUE;\n\t\t\t\t\t\t\t\t\ttalking |= ps->talking;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\t/* FIXME For backwards compatibility, we need video_codec in the global info */\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\t\t\t\tif(!video_added) {\n\t\t\t\t\t\t\t\t\tvideo_added = TRUE;\n\t\t\t\t\t\t\t\t\tjson_object_set_new(pl, \"video_codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264 && ps->h264_profile != NULL)\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"h264_profile\", json_string(ps->h264_profile));\n\t\t\t\t\t\t\t\telse if(ps->vcodec == JANUS_VIDEOCODEC_VP9 && ps->vp9_profile != NULL)\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"vp9_profile\", json_string(ps->vp9_profile));\n\t\t\t\t\t\t\t\tif(ps->simulcast)\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"simulcast\", json_true());\n\t\t\t\t\t\t\t\tif(ps->svc)\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"svc\", json_true());\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(ps->muted)\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"moderated\", json_true());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_array_append_new(media, info);\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t\t\t\t\tjson_object_set_new(pl, \"streams\", media);\n\t\t\t\t\tif(talking_found)\n\t\t\t\t\t\tjson_object_set_new(pl, \"talking\", talking ? json_true() : json_false());\n\t\t\t\t\tjson_array_append_new(list, pl);\n\t\t\t\t}\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"joined\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(publisher->room->room_id_str) :\n\t\t\t\t\tjson_integer(publisher->room->room_id));\n\t\t\t\tjson_object_set_new(event, \"description\", json_string(publisher->room->room_name));\n\t\t\t\tjson_object_set_new(event, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\t\tjson_object_set_new(event, \"private_id\", json_integer(publisher->pvt_id));\n\t\t\t\tjson_object_set_new(event, \"publishers\", list);\n\t\t\t\tif(publisher->user_audio_active_packets)\n\t\t\t\t\tjson_object_set_new(event, \"audio_active_packets\", json_integer(publisher->user_audio_active_packets));\n\t\t\t\tif(publisher->user_audio_level_average)\n\t\t\t\t\tjson_object_set_new(event, \"audio_level_average\", json_integer(publisher->user_audio_level_average));\n\t\t\t\tif(attendees != NULL)\n\t\t\t\t\tjson_object_set_new(event, \"attendees\", attendees);\n\t\t\t\t/* See if we need to notify about a new participant joined the room (by default, we don't). */\n\t\t\t\tjanus_videoroom_participant_joining(publisher);\n\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"joined\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(publisher->room->room_id_str) :\n\t\t\t\t\tjson_integer(publisher->room->room_id));\n\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(user_id_str) : json_integer(user_id));\n\t\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(publisher->pvt_id));\n\t\t\t\t\tif(publisher->room->check_allowed) {\n\t\t\t\t\t\tconst char *token = json_string_value(json_object_get(root, \"token\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"token\", json_string(token));\n\t\t\t\t\t}\n\t\t\t\t\tif(display_text != NULL)\n\t\t\t\t\t\tjson_object_set_new(info, \"display\", json_string(display_text));\n\t\t\t\t\tif(publisher->metadata)\n\t\t\t\t\t\tjson_object_set_new(info, \"metadata\", json_deep_copy(publisher->metadata));\n\t\t\t\t\tif(publisher->user_audio_active_packets)\n\t\t\t\t\t\tjson_object_set_new(info, \"audio_active_packets\", json_integer(publisher->user_audio_active_packets));\n\t\t\t\t\tif(publisher->user_audio_level_average)\n\t\t\t\t\t\tjson_object_set_new(info, \"audio_level_average\", json_integer(publisher->user_audio_level_average));\n\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&publisher->room->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tif(user_id_allocated)\n\t\t\t\t\tg_free(user_id_str);\n\t\t\t} else if(!strcasecmp(ptype_text, \"subscriber\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Configuring new subscriber\\n\");\n\t\t\t\t/* This is a new subscriber */\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, subscriber_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tsession = janus_videoroom_lookup_session(msg->handle);\n\t\t\t\tif(!session) {\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No session associated with this handle...\\n\");\n\t\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(g_atomic_int_get(&session->destroyed)) {\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Make sure there's no SDP attached here */\n\t\t\t\tif(json_string_value(json_object_get(msg->jsep, \"sdp\")) != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't send an offer to create subscribers\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't send an offer to create subscribers\");\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Who does this subscription belong to? */\n\t\t\t\tguint64 feed_id = 0;\n\t\t\t\tchar feed_id_num[30], *feed_id_str = NULL;\n\t\t\t\tjson_t *pvt = json_object_get(root, \"private_id\");\n\t\t\t\tguint64 pvt_id = json_integer_value(pvt);\n\t\t\t\t/* The new way of subscribing is specifying the streams we're interested in */\n\t\t\t\tjson_t *feeds = json_object_get(root, \"streams\");\n\t\t\t\tgboolean legacy = FALSE;\n\t\t\t\tif(feeds == NULL || json_array_size(feeds) == 0) {\n\t\t\t\t\t/* For backwards compatibility, we still support the old \"feed\" property, which means\n\t\t\t\t\t * \"subscribe to all the feeds from this publisher\" (depending on offer_audio, etc.) */\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, feed_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, feedstr_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t}\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *feed = json_object_get(root, \"feed\");\n\t\t\t\t\tif(!feed) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"At least one between 'streams' and 'feed' must be specified\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"At least one between 'streams' and 'feed' must be specified\");\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t}\n\t\t\t\t\t/* Create a fake \"streams\" array and put the only feed there */\n\t\t\t\t\tjson_t *m = json_array();\n\t\t\t\t\tjson_t *s = json_object();\n\t\t\t\t\tjson_object_set_new(s, \"feed\", string_ids ? json_string(feed_id_str) : json_integer(feed_id));\n\t\t\t\t\tjson_array_append_new(m, s);\n\t\t\t\t\tjson_object_set_new(root, \"streams\", m);\n\t\t\t\t\tfeeds = json_object_get(root, \"streams\");\n\t\t\t\t\tlegacy = TRUE;\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Deprecated subscriber 'join' API: please start looking into the new one for the future\\n\");\n\t\t\t\t}\n\t\t\t\tjson_t *msid = json_object_get(root, \"use_msid\");\n\t\t\t\tgboolean use_msid  = json_is_true(msid);\n\t\t\t\tjson_t *au = json_object_get(root, \"autoupdate\");\n\t\t\t\tgboolean autoupdate  = au ? json_is_true(au) : TRUE;\n\t\t\t\t/* Make sure all the feeds we're subscribing to exist */\n\t\t\t\tGList *publishers = NULL;\n\t\t\t\tgboolean e2ee = videoroom->require_e2ee, sub_e2ee = FALSE, first = TRUE;\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(feeds); i++) {\n\t\t\t\t\tjson_t *s = json_array_get(feeds, i);\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, subscriber_stream_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feed_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feedstr_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t}\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *feed = json_object_get(s, \"feed\");\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(videoroom->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed) ||\n\t\t\t\t\t\t\t!g_atomic_int_get(&publisher->session->started)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such feed (%s)\\n\", feed_id_str);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such feed (%s)\", feed_id_str);\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tsub_e2ee = publisher->e2ee;\n\t\t\t\t\tif(e2ee && !sub_e2ee) {\n\t\t\t\t\t\t/* Attempt to subscribe to non-end-to-end encrypted\n\t\t\t\t\t\t * publisher in an end-to-end encrypted subscription */\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't have not end-to-end encrypted feed in this subscription (%s)\\n\", feed_id_str);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't have not end-to-end encrypted feed in this subscription (%s)\", feed_id_str);\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t} else if(!e2ee && sub_e2ee) {\n\t\t\t\t\t\tif(first) {\n\t\t\t\t\t\t\t/* This subscription will use end-to-end encryption */\n\t\t\t\t\t\t\te2ee = TRUE;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Attempt to subscribe to end-to-end encrypted\n\t\t\t\t\t\t\t * publisher in a non-end-to-end encrypted subscription */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't have end-to-end encrypted feed in this subscription (%s)\\n\", feed_id_str);\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_FEED;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't have end-to-end encrypted feed in this subscription (%s)\", feed_id_str);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(first)\n\t\t\t\t\t\tfirst = FALSE;\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\tif(mid != NULL) {\n\t\t\t\t\t\t/* Check the mid too */\n\t\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\t\tif(g_hash_table_lookup(publisher->streams_bymid, mid) == NULL) {\n\t\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such mid '%s' in feed (%s)\\n\", mid, feed_id_str);\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such mid '%s' in feed (%s)\", mid, feed_id_str);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\t\tjson_t *sc_substream = json_object_get(s, \"substream\");\n\t\t\t\t\tif(json_integer_value(spatial) < 0 || json_integer_value(spatial) > 2 ||\n\t\t\t\t\t\t\tjson_integer_value(sc_substream) < 0 || json_integer_value(sc_substream) > 2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (substream/spatial_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (substream/spatial_layer should be 0, 1 or 2)\");\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\t\tjson_t *sc_temporal = json_object_get(s, \"temporal\");\n\t\t\t\t\tif(json_integer_value(temporal) < 0 || json_integer_value(temporal) > 2 ||\n\t\t\t\t\t\t\tjson_integer_value(sc_temporal) < 0 || json_integer_value(sc_temporal) > 2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (temporal/temporal_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (temporal/temporal_layer should be 0, 1 or 2)\");\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\t/* Increase the refcount before unlocking so that nobody can remove and free the publisher in the meantime. */\n\t\t\t\t\tjanus_refcount_increase(&publisher->ref);\n\t\t\t\t\tjanus_refcount_increase(&publisher->session->ref);\n\t\t\t\t\tpublishers = g_list_append(publishers, publisher);\n\t\t\t\t}\n\t\t\t\t/* FIXME These properties are only there for backwards compatibility */\n\t\t\t\tjson_t *offer_audio = json_object_get(root, \"offer_audio\");\n\t\t\t\tjson_t *offer_video = json_object_get(root, \"offer_video\");\n\t\t\t\tjson_t *offer_data = json_object_get(root, \"offer_data\");\n\t\t\t\tjanus_videoroom_publisher *owner = NULL;\n\t\t\t\t/* Let's check if this room requires valid private_id values */\n\t\t\t\tif(videoroom->require_pvtid) {\n\t\t\t\t\t/* It does, let's make sure this subscription complies */\n\t\t\t\t\towner = g_hash_table_lookup(videoroom->private_ids, GUINT_TO_POINTER(pvt_id));\n\t\t\t\t\tif(pvt_id == 0 || owner == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized (this room requires a valid private_id)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Unauthorized (this room requires a valid private_id)\");\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t/* Unref publishers */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_refcount_increase(&owner->ref);\n\t\t\t\t\tjanus_refcount_increase(&owner->session->ref);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t/* Allocate a new subscriber instance */\n\t\t\t\tjanus_videoroom_subscriber *subscriber = g_malloc0(sizeof(janus_videoroom_subscriber));\n\t\t\t\tsubscriber->session = session;\n\t\t\t\tsubscriber->room_id = videoroom->room_id;\n\t\t\t\tsubscriber->room_id_str = videoroom->room_id_str ? g_strdup(videoroom->room_id_str) : NULL;\n\t\t\t\tsubscriber->room = videoroom;\n\t\t\t\tsubscriber->e2ee = e2ee;\n\t\t\t\tvideoroom = NULL;\n\t\t\t\tsubscriber->pvt_id = pvt_id;\n\t\t\t\tsubscriber->use_msid = use_msid;\n\t\t\t\tsubscriber->autoupdate = autoupdate;\n\t\t\t\tsubscriber->paused = TRUE;\t/* We need an explicit start from the stream */\n\t\t\t\tsubscriber->streams_byid = g_hash_table_new_full(NULL, NULL,\n\t\t\t\t\tNULL, (GDestroyNotify)janus_videoroom_subscriber_stream_destroy);\n\t\t\t\tsubscriber->streams_bymid = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t\t\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_videoroom_subscriber_stream_unref);\n\t\t\t\tjanus_mutex_init(&subscriber->streams_mutex);\n\t\t\t\tg_atomic_int_set(&subscriber->destroyed, 0);\n\t\t\t\tjanus_refcount_init(&subscriber->ref, janus_videoroom_subscriber_free);\n\t\t\t\tjanus_refcount_increase(&subscriber->ref);\n\t\t\t\t/* FIXME backwards compatibility */\n\t\t\t\tgboolean do_audio = offer_audio ? json_is_true(offer_audio) : TRUE;\n\t\t\t\tgboolean do_video = offer_video ? json_is_true(offer_video) : TRUE;\n\t\t\t\tgboolean do_data = offer_data ? json_is_true(offer_data) : TRUE;\n\t\t\t\t/* Initialize the subscriber streams */\n\t\t\t\tgboolean data_added = FALSE;\n\t\t\t\tjanus_videoroom_subscriber_stream *data_stream = NULL;\n\t\t\t\tfor(i=0; i<json_array_size(feeds); i++) {\n\t\t\t\t\tjson_t *s = json_array_get(feeds, i);\n\t\t\t\t\tjson_t *feed = json_object_get(s, \"feed\");\n\t\t\t\t\tguint64 feed_id = 0;\n\t\t\t\t\tchar *feed_id_str = NULL;\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(subscriber->room->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\tif(publisher == NULL) {\n\t\t\t\t\t\t/* TODO We shouldn't let this happen... */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping feed %s...\\n\", feed_id_str);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\tconst char *crossrefid = json_string_value(json_object_get(s, \"crossrefid\"));\n\t\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\t\tjson_t *sc_substream = json_object_get(s, \"substream\");\n\t\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\t\tjson_t *sc_temporal = json_object_get(s, \"temporal\");\n\t\t\t\t\tjson_t *sc_fallback = json_object_get(s, \"fallback\");\n\t\t\t\t\tjson_t *min_delay = json_object_get(s, \"min_delay\");\n\t\t\t\t\tjson_t *max_delay = json_object_get(s, \"max_delay\");\n\t\t\t\t\tif(mid) {\n\t\t\t\t\t\t/* Subscribe to a specific mid */\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = g_hash_table_lookup(publisher->streams_bymid, mid);\n\t\t\t\t\t\tif(ps == NULL) {\n\t\t\t\t\t\t\t/* TODO We shouldn't let this happen either... */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping mid %s in feed %s...\\n\", mid, feed_id_str);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_DATA && data_added) {\n\t\t\t\t\t\t\t/* We already have a datachannel m-line, no need for others: just update the subscribers list */\n\t\t\t\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\t\t\t\t\tif(g_slist_find(ps->subscribers, data_stream) == NULL && g_slist_find(data_stream->publisher_streams, ps) == NULL) {\n\t\t\t\t\t\t\t\tps->subscribers = g_slist_append(ps->subscribers, data_stream);\n\t\t\t\t\t\t\t\tdata_stream->publisher_streams = g_slist_append(data_stream->publisher_streams, ps);\n\t\t\t\t\t\t\t\t/* If we're using helper threads, add the subscriber to one of those */\n\t\t\t\t\t\t\t\tif(subscriber->room && subscriber->room->helper_threads > 0) {\n\t\t\t\t\t\t\t\t\tint subscribers = -1;\n\t\t\t\t\t\t\t\t\tjanus_videoroom_helper *helper = NULL;\n\t\t\t\t\t\t\t\t\tGList *l = subscriber->room->threads;\n\t\t\t\t\t\t\t\t\twhile(l) {\n\t\t\t\t\t\t\t\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\t\t\t\t\t\t\t\tif(subscribers == -1 || (helper == NULL && ht->num_subscribers == 0) || ht->num_subscribers < subscribers) {\n\t\t\t\t\t\t\t\t\t\t\tsubscribers = ht->num_subscribers;\n\t\t\t\t\t\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\t\t\t\t\t\tGList *list = g_hash_table_lookup(helper->subscribers, ps);\n\t\t\t\t\t\t\t\t\tlist = g_list_append(list, data_stream);\n\t\t\t\t\t\t\t\t\tg_hash_table_insert(helper->subscribers, ps, list);\n\t\t\t\t\t\t\t\t\thelper->num_subscribers++;\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Added subscriber stream to helper thread #%d (%d subscribers)\\n\",\n\t\t\t\t\t\t\t\t\t\thelper->id, helper->num_subscribers);\n\t\t\t\t\t\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/* The two streams reference each other */\n\t\t\t\t\t\t\t\tjanus_refcount_increase(&data_stream->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = janus_videoroom_subscriber_stream_add(subscriber,\n\t\t\t\t\t\t\tps, crossrefid, legacy, do_audio, do_video, do_data);\n\t\t\t\t\t\tif(stream && ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO &&\n\t\t\t\t\t\t\t\t(spatial || sc_substream || temporal || sc_temporal || sc_fallback)) {\n\t\t\t\t\t\t\t/* Override the default spatial/substream/temporal targets */\n\t\t\t\t\t\t\tint substream_target = sc_substream ? json_integer_value(sc_substream) : -1;\n\t\t\t\t\t\t\tif(sc_substream && substream_target >= 0 && substream_target <= 2)\n\t\t\t\t\t\t\t\tstream->sim_context.substream_target = substream_target;\n\t\t\t\t\t\t\tif(sc_temporal)\n\t\t\t\t\t\t\t\tstream->sim_context.templayer_target = json_integer_value(sc_temporal);\n\t\t\t\t\t\t\tif(sc_fallback)\n\t\t\t\t\t\t\t\tstream->sim_context.drop_trigger = json_integer_value(sc_fallback);\n\t\t\t\t\t\t\tif(spatial)\n\t\t\t\t\t\t\t\tstream->svc_context.spatial_target = json_integer_value(spatial);\n\t\t\t\t\t\t\tif(temporal)\n\t\t\t\t\t\t\t\tstream->svc_context.temporal_target = json_integer_value(temporal);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream && ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t/* Override the playout-delay properties */\n\t\t\t\t\t\t\tif(min_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tstream->min_delay = md;\n\t\t\t\t\t\t\t\t\tif(stream->min_delay > stream->max_delay)\n\t\t\t\t\t\t\t\t\t\tstream->max_delay = stream->min_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(max_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tstream->max_delay = md;\n\t\t\t\t\t\t\t\t\tif(stream->max_delay < stream->min_delay)\n\t\t\t\t\t\t\t\t\t\tstream->min_delay = stream->max_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t\t\t\t\tdata_added = TRUE;\n\t\t\t\t\t\t\tdata_stream = stream;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Subscribe to all streams */\n\t\t\t\t\t\tGList *temp = publisher->streams;\n\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_DATA && data_added) {\n\t\t\t\t\t\t\t\t/* We already have a datachannel m-line, no need for others: just update the subscribers list */\n\t\t\t\t\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\t\t\t\t\t\tif(g_slist_find(ps->subscribers, data_stream) == NULL && g_slist_find(data_stream->publisher_streams, ps) == NULL) {\n\t\t\t\t\t\t\t\t\tps->subscribers = g_slist_append(ps->subscribers, data_stream);\n\t\t\t\t\t\t\t\t\tdata_stream->publisher_streams = g_slist_append(data_stream->publisher_streams, ps);\n\t\t\t\t\t\t\t\t\t/* If we're using helper threads, add the subscriber to one of those */\n\t\t\t\t\t\t\t\t\tif(subscriber->room && subscriber->room->helper_threads > 0) {\n\t\t\t\t\t\t\t\t\t\tint subscribers = -1;\n\t\t\t\t\t\t\t\t\t\tjanus_videoroom_helper *helper = NULL;\n\t\t\t\t\t\t\t\t\t\tGList *l = subscriber->room->threads;\n\t\t\t\t\t\t\t\t\t\twhile(l) {\n\t\t\t\t\t\t\t\t\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\t\t\t\t\t\t\t\t\tif(subscribers == -1 || (helper == NULL && ht->num_subscribers == 0) || ht->num_subscribers < subscribers) {\n\t\t\t\t\t\t\t\t\t\t\t\tsubscribers = ht->num_subscribers;\n\t\t\t\t\t\t\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\t\t\t\t\t\t\tGList *list = g_hash_table_lookup(helper->subscribers, ps);\n\t\t\t\t\t\t\t\t\t\tlist = g_list_append(list, data_stream);\n\t\t\t\t\t\t\t\t\t\tg_hash_table_insert(helper->subscribers, ps, list);\n\t\t\t\t\t\t\t\t\t\thelper->num_subscribers++;\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Added subscriber stream to helper thread #%d (%d subscribers)\\n\",\n\t\t\t\t\t\t\t\t\t\t\thelper->id, helper->num_subscribers);\n\t\t\t\t\t\t\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/* The two streams reference each other */\n\t\t\t\t\t\t\t\t\tjanus_refcount_increase(&data_stream->ref);\n\t\t\t\t\t\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = janus_videoroom_subscriber_stream_add(subscriber,\n\t\t\t\t\t\t\t\tps, crossrefid, legacy, do_audio, do_video, do_data);\n\t\t\t\t\t\t\tif(stream && ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO &&\n\t\t\t\t\t\t\t\t\t(spatial || sc_substream || temporal || sc_temporal)) {\n\t\t\t\t\t\t\t\t/* Override the default spatial/substream/temporal targets */\n\t\t\t\t\t\t\t\tint substream_target = sc_substream ? json_integer_value(sc_substream) : -1;\n\t\t\t\t\t\t\t\tif(sc_substream && substream_target >= 0 && substream_target <= 2)\n\t\t\t\t\t\t\t\t\tstream->sim_context.substream_target = substream_target;\n\t\t\t\t\t\t\t\tif(sc_temporal)\n\t\t\t\t\t\t\t\t\tstream->sim_context.templayer_target = json_integer_value(sc_temporal);\n\t\t\t\t\t\t\t\tif(spatial)\n\t\t\t\t\t\t\t\t\tstream->svc_context.spatial_target = json_integer_value(spatial);\n\t\t\t\t\t\t\t\tif(temporal)\n\t\t\t\t\t\t\t\t\tstream->svc_context.temporal_target = json_integer_value(temporal);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(stream && ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\t/* Override the playout-delay properties */\n\t\t\t\t\t\t\t\tif(min_delay) {\n\t\t\t\t\t\t\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tstream->min_delay = md;\n\t\t\t\t\t\t\t\t\t\tif(stream->min_delay > stream->max_delay)\n\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = stream->min_delay;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(max_delay) {\n\t\t\t\t\t\t\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tstream->max_delay = md;\n\t\t\t\t\t\t\t\t\t\tif(stream->max_delay < stream->min_delay)\n\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = stream->max_delay;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t\t\t\t\t\tdata_added = TRUE;\n\t\t\t\t\t\t\t\tdata_stream = stream;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t}\n\t\t\t\t/* Make sure we subscribed to at least something */\n\t\t\t\tif(subscriber->streams == NULL) {\n\t\t\t\t\t/* No subscription created? Unref publishers */\n\t\t\t\t\tif(owner) {\n\t\t\t\t\t\tjanus_refcount_decrease(&owner->session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&owner->ref);\n\t\t\t\t\t}\n\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't offer an SDP with no stream\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't offer an SDP with no stream\");\n\t\t\t\t\tjanus_videoroom_subscriber_destroy(subscriber);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tsession->participant = subscriber;\n\t\t\t\tif(owner != NULL) {\n\t\t\t\t\t/* Note: we should refcount these subscription-publisher mappings as well */\n\t\t\t\t\tjanus_mutex_lock(&owner->subscribers_mutex);\n\t\t\t\t\towner->subscriptions = g_slist_append(owner->subscriptions, subscriber);\n\t\t\t\t\tjanus_mutex_unlock(&owner->subscribers_mutex);\n\t\t\t\t}\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"attached\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t/* If this is a legacy subscription, put the feed ID too */\n\t\t\t\tif(legacy) {\n\t\t\t\t\tjson_object_set_new(event, \"id\", string_ids ? json_string(feed_id_str) : json_integer(feed_id));\n\t\t\t\t\tjson_object_set_new(event, \"warning\", json_string(\"deprecated_api\"));\n\t\t\t\t}\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(subscriber, legacy, event);\n\t\t\t\tjson_t *media_event = NULL;\n\t\t\t\tif(notify_events && gateway->events_is_enabled())\n\t\t\t\t\tmedia_event = json_deep_copy(media);\n\t\t\t\tjson_object_set_new(event, \"streams\", media);\n\t\t\t\tsession->participant_type = janus_videoroom_p_type_subscriber;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Preparing JSON event as a reply\\n\");\n\t\t\t\t/* Negotiate by crafting a new SDP matching the subscriptions */\n\t\t\t\tjson_t *jsep = janus_videoroom_subscriber_offer(subscriber);\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\tint res = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, jsep);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\tjson_decref(event);\n\t\t\t\tjson_decref(jsep);\n\t\t\t\tif(res < 0) {\n\t\t\t\t\t/* Something went wrong, get rid of the subscription */\n\t\t\t\t\tif(media_event)\n\t\t\t\t\t\tjson_decref(media_event);\n\t\t\t\t\tif(owner) {\n\t\t\t\t\t\tjanus_mutex_lock(&owner->subscribers_mutex);\n\t\t\t\t\t\towner->subscriptions = g_slist_remove(owner->subscriptions, subscriber);\n\t\t\t\t\t\tjanus_mutex_unlock(&owner->subscribers_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&owner->session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&owner->ref);\n\t\t\t\t\t}\n\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error pushing event to new subscriber\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error pushing event\");\n\t\t\t\t\tjanus_mutex_lock(&session->mutex)\n\t\t\t\t\tsession->participant = NULL;\n\t\t\t\t\tjanus_mutex_unlock(&session->mutex)\n\t\t\t\t\t/* Get rid of streams */\n\t\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\t\tGList *temp = subscriber->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream *s = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream_remove(s, NULL, TRUE);\n\t\t\t\t\t}\n\t\t\t\t\tg_list_free(subscriber->streams);\n\t\t\t\t\tsubscriber->streams = NULL;\n\t\t\t\t\tg_hash_table_remove_all(subscriber->streams_byid);\n\t\t\t\t\tg_hash_table_remove_all(subscriber->streams_bymid);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\tjanus_videoroom_subscriber_destroy(subscriber);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"subscribing\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\tjson_object_set_new(info, \"streams\", media_event);\n\t\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(pvt_id));\n\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\t/* Decrease the references we took before */\n\t\t\t\twhile(publishers) {\n\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t}\n\t\t\t\tif(owner) {\n\t\t\t\t\t/* Done adding the subscription, owner is safe to be released */\n\t\t\t\t\tjanus_refcount_decrease(&owner->session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&owner->ref);\n\t\t\t\t}\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\tcontinue;\n\t\t\t} else {\n\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (ptype)\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid element (ptype)\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t} else if(session->participant_type == janus_videoroom_p_type_publisher) {\n\t\t\t/* Handle this publisher */\n\t\t\tparticipant = janus_videoroom_session_get_publisher(session);\n\t\t\tif(participant == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid participant instance\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\tg_snprintf(error_cause, 512, \"Invalid participant instance\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(participant->room == NULL) {\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\tif(!strcasecmp(request_text, \"join\") || !strcasecmp(request_text, \"joinandconfigure\")) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Not in a room (create a new handle)\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ALREADY_JOINED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Not in a room (create a new handle)\");\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such room\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such room\");\n\t\t\t\t}\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(!strcasecmp(request_text, \"join\") || !strcasecmp(request_text, \"joinandconfigure\")) {\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already in as a publisher on this handle\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ALREADY_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already in as a publisher on this handle\");\n\t\t\t\tgoto error;\n\t\t\t} else if(!strcasecmp(request_text, \"configure\") || !strcasecmp(request_text, \"publish\")) {\n\t\t\t\tif(!strcasecmp(request_text, \"publish\") && g_atomic_int_get(&participant->session->started)) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't publish, already published\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ALREADY_PUBLISHED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't publish, already published\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(participant->kicked) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized, you have been kicked\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Unauthorized, you have been kicked\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Configure (or publish a new feed) audio/video/bitrate for this publisher */\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, publish_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjson_t *descriptions = json_object_get(root, \"descriptions\");\n\t\t\t\tif(descriptions != NULL && json_array_size(descriptions) > 0) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(descriptions); i++) {\n\t\t\t\t\t\tjson_t *d = json_array_get(descriptions, i);\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(d, publish_desc_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjson_t *audiocodec = json_object_get(root, \"audiocodec\");\n\t\t\t\tjson_t *videocodec = json_object_get(root, \"videocodec\");\n\t\t\t\tjson_t *bitrate = json_object_get(root, \"bitrate\");\n\t\t\t\tjson_t *record = json_object_get(root, \"record\");\n\t\t\t\tjson_t *recfile = json_object_get(root, \"filename\");\n\t\t\t\tjson_t *display = json_object_get(root, \"display\");\n\t\t\t\tjson_t *metadata = json_object_get(root, \"metadata\");\n\t\t\t\tjson_t *update = json_object_get(root, \"update\");\n\t\t\t\tjson_t *user_audio_active_packets = json_object_get(root, \"audio_active_packets\");\n\t\t\t\tjson_t *user_audio_level_average = json_object_get(root, \"audio_level_average\");\n\t\t\t\t/* Audio, video and data are deprecated properties */\n\t\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\t\tjson_t *data = json_object_get(root, \"data\");\n\t\t\t\t/* We use an array of streams to state the changes we want to make,\n\t\t\t\t * were for each stream we specify the 'mid' to impact (e.g., send) */\n\t\t\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\t\t\tif(streams == NULL) {\n\t\t\t\t\t/* No streams object, check if the properties have been\n\t\t\t\t\t * provided globally, which is how we handled this\n\t\t\t\t\t * request before: if so, create a new fake streams\n\t\t\t\t\t * array, and move the parsed options there */\n\t\t\t\t\tstreams = json_array();\n\t\t\t\t\tjson_t *stream = json_object();\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(root, \"mid\"));\n\t\t\t\t\tif(mid != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"mid\", json_string(mid));\n\t\t\t\t\tjson_t *send = json_object_get(root, \"send\");\n\t\t\t\t\tif(send != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"send\", json_is_true(send) ? json_true() : json_false());\n\t\t\t\t\tjson_t *keyframe = json_object_get(root, \"keyframe\");\n\t\t\t\t\tif(keyframe != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"keyframe\", json_is_true(keyframe) ? json_true() : json_false());\n\t\t\t\t\tjson_t *min_delay = json_object_get(root, \"min_delay\");\n\t\t\t\t\tif(min_delay != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"min_delay\", json_integer(json_integer_value(min_delay)));\n\t\t\t\t\tjson_t *max_delay = json_object_get(root, \"max_delay\");\n\t\t\t\t\tif(max_delay != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"max_delay\", json_integer(json_integer_value(max_delay)));\n\t\t\t\t\tjson_array_append_new(streams, stream);\n\t\t\t\t\tjson_object_set_new(root, \"streams\", streams);\n\t\t\t\t}\n\t\t\t\t/* Validate all the streams we need to configure */\n\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\tsize_t i = 0;\n\t\t\t\tsize_t streams_size = json_array_size(streams);\n\t\t\t\tfor(i=0; i<streams_size; i++) {\n\t\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, publish_stream_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\tif(mid == NULL && streams_size > 1) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (mid can't be null in a streams array)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (mid can't be null in a streams array)\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(mid != NULL && g_hash_table_lookup(participant->streams_bymid, mid) == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such mid '%s' published\\n\", mid);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such mid '%s' published\", mid);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(mid != NULL) {\n\t\t\t\t\t\tjson_object_del(root, \"audio\");\n\t\t\t\t\t\taudio = NULL;\n\t\t\t\t\t\tjson_object_del(root, \"video\");\n\t\t\t\t\t\tvideo = NULL;\n\t\t\t\t\t\tjson_object_del(root, \"data\");\n\t\t\t\t\t\tdata = NULL;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* A renegotiation may be taking place */\n\t\t\t\tgboolean do_update = update ? json_is_true(update) : FALSE;\n\t\t\t\tif(do_update && !sdp_update) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Got an 'update' request, but no SDP update? Ignoring...\\n\");\n\t\t\t\t\tdo_update = FALSE;\n\t\t\t\t}\n\t\t\t\t/* Check if there's an SDP to take into account */\n\t\t\t\tif(json_string_value(json_object_get(msg->jsep, \"sdp\"))) {\n\t\t\t\t\tif(audiocodec) {\n\t\t\t\t\t\t/* The participant would like to use an audio codec in particular */\n\t\t\t\t\t\tjanus_audiocodec acodec = janus_audiocodec_from_name(json_string_value(audiocodec));\n\t\t\t\t\t\tif(acodec == JANUS_AUDIOCODEC_NONE ||\n\t\t\t\t\t\t\t\t(acodec != participant->room->acodec[0] &&\n\t\t\t\t\t\t\t\tacodec != participant->room->acodec[1] &&\n\t\t\t\t\t\t\t\tacodec != participant->room->acodec[2] &&\n\t\t\t\t\t\t\t\tacodec != participant->room->acodec[3] &&\n\t\t\t\t\t\t\t\tacodec != participant->room->acodec[4])) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Participant asked for audio codec '%s', but it's not allowed (room %s, user %s)\\n\",\n\t\t\t\t\t\t\t\tjson_string_value(audiocodec), participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Audio codec unavailable in this room\");\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Participant asked for audio codec '%s' (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tjson_string_value(audiocodec), participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t\tparticipant->acodec = acodec;\n\t\t\t\t\t}\n\t\t\t\t\tif(videocodec) {\n\t\t\t\t\t\t/* The participant would like to use a video codec in particular */\n\t\t\t\t\t\tjanus_videocodec vcodec = janus_videocodec_from_name(json_string_value(videocodec));\n\t\t\t\t\t\tif(vcodec == JANUS_VIDEOCODEC_NONE ||\n\t\t\t\t\t\t\t\t(vcodec != participant->room->vcodec[0] &&\n\t\t\t\t\t\t\t\tvcodec != participant->room->vcodec[1] &&\n\t\t\t\t\t\t\t\tvcodec != participant->room->vcodec[2] &&\n\t\t\t\t\t\t\t\tvcodec != participant->room->vcodec[3] &&\n\t\t\t\t\t\t\t\tvcodec != participant->room->vcodec[4])) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Participant asked for video codec '%s', but it's not allowed (room %s, user %s)\\n\",\n\t\t\t\t\t\t\t\tjson_string_value(videocodec), participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Video codec unavailable in this room\");\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Participant asked for video codec '%s' (room %s, user %s)\\n\",\n\t\t\t\t\t\t\tjson_string_value(videocodec), participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t\tparticipant->vcodec = vcodec;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Enforce the requested changes (if configuring) */\n\t\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\t\t/* Get the stream we need to tweak */\n\t\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\t\t/* Check which properties we need to tweak */\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\tjson_t *send = json_object_get(s, \"send\");\n\t\t\t\t\tjson_t *keyframe = json_object_get(s, \"keyframe\");\n\t\t\t\t\tjson_t *min_delay = json_object_get(s, \"min_delay\");\n\t\t\t\t\tjson_t *max_delay = json_object_get(s, \"max_delay\");\n\t\t\t\t\tGList *temp = participant->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\tgboolean mid_found = (mid && !strcasecmp(ps->mid, mid));\n\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO && (audio || (send && mid_found))) {\n\t\t\t\t\t\t\tgboolean audio_active = mid_found ? json_is_true(send) : json_is_true(audio);\n\t\t\t\t\t\t\tif(!ps->active && !ps->muted && audio_active) {\n\t\t\t\t\t\t\t\t/* Audio was just resumed, try resetting the RTP headers for viewers */\n\t\t\t\t\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\t\t\t\t\t\tGSList *slist = ps->subscribers;\n\t\t\t\t\t\t\t\twhile(slist) {\n\t\t\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream *s = (janus_videoroom_subscriber_stream *)slist->data;\n\t\t\t\t\t\t\t\t\tif(s)\n\t\t\t\t\t\t\t\t\t\ts->context.seq_reset = TRUE;\n\t\t\t\t\t\t\t\t\tslist = slist->next;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tps->active = audio_active;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting audio property (%s): %s (room %s, user %s)\\n\",\n\t\t\t\t\t\t\t\tps->mid, ps->active ? \"true\" : \"false\", participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && (video || (send && mid_found))) {\n\t\t\t\t\t\t\tgboolean video_active = mid_found ? json_is_true(send) : json_is_true(video);\n\t\t\t\t\t\t\tif(!ps->active && !ps->muted && video_active) {\n\t\t\t\t\t\t\t\t/* Video was just resumed, try resetting the RTP headers for viewers */\n\t\t\t\t\t\t\t\tjanus_mutex_lock(&participant->subscribers_mutex);\n\t\t\t\t\t\t\t\tGSList *slist = ps->subscribers;\n\t\t\t\t\t\t\t\twhile(slist) {\n\t\t\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream *s = (janus_videoroom_subscriber_stream *)slist->data;\n\t\t\t\t\t\t\t\t\tif(s)\n\t\t\t\t\t\t\t\t\t\ts->context.seq_reset = TRUE;\n\t\t\t\t\t\t\t\t\tslist = slist->next;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&participant->subscribers_mutex);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tps->active = video_active;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video property (%s): %s (room %s, user %s)\\n\",\n\t\t\t\t\t\t\t\tps->mid, ps->active ? \"true\" : \"false\", participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_DATA && (data || (send && mid_found))) {\n\t\t\t\t\t\t\tgboolean data_active = mid_found ? json_is_true(send) : json_is_true(data);\n\t\t\t\t\t\t\tps->active = data_active;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting data property (%s): %s (room %s, user %s)\\n\",\n\t\t\t\t\t\t\t\tps->mid, ps->active ? \"true\" : \"false\", participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && (mid_found || mid == NULL) &&\n\t\t\t\t\t\t\t\tkeyframe && json_is_true(keyframe)) {\n\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Keyframe request\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && (mid_found || mid == NULL)) {\n\t\t\t\t\t\t\tif(min_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\tps->min_delay = -1;\n\t\t\t\t\t\t\t\t\tps->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tps->min_delay = md;\n\t\t\t\t\t\t\t\t\tif(ps->min_delay > ps->max_delay)\n\t\t\t\t\t\t\t\t\t\tps->max_delay = ps->min_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(max_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\tps->min_delay = -1;\n\t\t\t\t\t\t\t\t\tps->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tps->max_delay = md;\n\t\t\t\t\t\t\t\t\tif(ps->max_delay < ps->min_delay)\n\t\t\t\t\t\t\t\t\t\tps->min_delay = ps->max_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\tif(bitrate) {\n\t\t\t\t\tparticipant->bitrate = json_integer_value(bitrate);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video bitrate: %\"SCNu32\" (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->bitrate, participant->room_id_str, participant->user_id_str);\n\t\t\t\t\t/* Send a new REMB */\n\t\t\t\t\tif(g_atomic_int_get(&session->started))\n\t\t\t\t\t\tparticipant->remb_latest = janus_get_monotonic_time();\n\t\t\t\t\tgateway->send_remb(msg->handle, participant->bitrate);\n\t\t\t\t}\n\t\t\t\tif(user_audio_active_packets) {\n\t\t\t\t\tparticipant->user_audio_active_packets = json_integer_value(user_audio_active_packets);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting user audio_active_packets: %d (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->user_audio_active_packets, participant->room_id_str, participant->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(user_audio_level_average) {\n\t\t\t\t\tparticipant->user_audio_level_average = json_integer_value(user_audio_level_average);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting user audio_level_average: %d (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->user_audio_level_average, participant->room_id_str, participant->user_id_str);\n\t\t\t\t}\n\t\t\t\tgboolean record_locked = FALSE;\n\t\t\t\tif((record || recfile) && participant->room->lock_record && participant->room->room_secret) {\n\t\t\t\t\tJANUS_CHECK_SECRET(participant->room->room_secret, root, \"secret\", error_code, error_cause,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t/* Wrong secret provided, we'll prevent the recording state from being changed */\n\t\t\t\t\t\trecord_locked = TRUE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\t\tgboolean prev_recording_active = participant->recording_active;\n\t\t\t\tif(record && !record_locked) {\n\t\t\t\t\tparticipant->recording_active = json_is_true(record);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting record property: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->recording_active ? \"true\" : \"false\", participant->room_id_str, participant->user_id_str);\n\t\t\t\t}\n\t\t\t\tif(recfile && !record_locked) {\n\t\t\t\t\tparticipant->recording_base = g_strdup(json_string_value(recfile));\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting recording basename: %s (room %s, user %s)\\n\",\n\t\t\t\t\t\tparticipant->recording_base, participant->room_id_str, participant->user_id_str);\n\t\t\t\t}\n\t\t\t\t/* Do we need to do something with the recordings right now? */\n\t\t\t\tif(participant->recording_active != prev_recording_active) {\n\t\t\t\t\t/* Something changed */\n\t\t\t\t\tif(!participant->recording_active) {\n\t\t\t\t\t\t/* Not recording (anymore?) */\n\t\t\t\t\t\tjanus_mutex_lock(&participant->streams_mutex)\n\t\t\t\t\t\tjanus_videoroom_recorder_close(participant);\n\t\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex)\n\t\t\t\t\t} else if(participant->recording_active && g_atomic_int_get(&participant->session->started)) {\n\t\t\t\t\t\t/* We've started recording, send a PLI/FIR and go on */\n\t\t\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\t\t\tGList *temp = participant->streams;\n\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\t\tjanus_videoroom_recorder_create(ps);\n\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Recording video\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t\tif(display) {\n\t\t\t\t\tjanus_mutex_lock(&participant->room->mutex);\n\t\t\t\t\tchar *old_display = participant->display;\n\t\t\t\t\tchar *new_display = g_strdup(json_string_value(display));\n\t\t\t\t\tparticipant->display = new_display;\n\t\t\t\t\tif(old_display != NULL) {\n\t\t\t\t\t\t/* The display name changed, notify this */\n\t\t\t\t\t\tjson_t *display_event = json_object();\n\t\t\t\t\t\tjson_object_set_new(display_event, \"videoroom\", json_string(\"event\"));\n\t\t\t\t\t\tjson_object_set_new(display_event, \"id\", string_ids ?\n\t\t\t\t\t\t\tjson_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t\t\tjson_object_set_new(display_event, \"display\", json_string(participant->display));\n\t\t\t\t\t\tif(participant->room && !g_atomic_int_get(&participant->room->destroyed)) {\n\t\t\t\t\t\t\tjanus_videoroom_notify_participants(participant, display_event, FALSE);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_decref(display_event);\n\t\t\t\t\t}\n\t\t\t\t\tg_free(old_display);\n\t\t\t\t\tjanus_mutex_unlock(&participant->room->mutex);\n\t\t\t\t}\n\t\t\t\tif(metadata) {\n\t\t\t\t\tjanus_mutex_lock(&participant->room->mutex);\n\t\t\t\t\tjson_t *old_metadata = participant->metadata;\n\t\t\t\t\tjson_t *new_metadata = json_deep_copy(metadata);\n\t\t\t\t\tparticipant->metadata = new_metadata;\n\t\t\t\t\tif(old_metadata != NULL) {\n\t\t\t\t\t\t/* The metadata changed, notify this */\n\t\t\t\t\t\tjson_t *metadata_event = json_object();\n\t\t\t\t\t\tjson_object_set_new(metadata_event, \"videoroom\", json_string(\"event\"));\n\t\t\t\t\t\tjson_object_set_new(metadata_event, \"id\", string_ids ?\n\t\t\t\t\t\t\tjson_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t\t\tjson_object_set_new(metadata_event, \"metadata\", json_deep_copy(participant->metadata));\n\t\t\t\t\t\tif(participant->room && !g_atomic_int_get(&participant->room->destroyed)) {\n\t\t\t\t\t\t\tjanus_videoroom_notify_participants(participant, metadata_event, FALSE);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_decref(metadata_event);\n\t\t\t\t\t\tjson_decref(old_metadata);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&participant->room->mutex);\n\t\t\t\t}\n\t\t\t\t/* Are we updating the description? */\n\t\t\t\tif(descriptions != NULL && json_array_size(descriptions) > 0 && json_string_value(json_object_get(msg->jsep, \"sdp\")) == NULL) {\n\t\t\t\t\t/* We only do this here if this is an SDP-less configure: in case\n\t\t\t\t\t * a renegotiation is involved, descriptions are updated later */\n\t\t\t\t\tgboolean desc_updated = FALSE;\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tjanus_mutex_lock(&participant->room->mutex);\n\t\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\t\tfor(i=0; i<json_array_size(descriptions); i++) {\n\t\t\t\t\t\tjson_t *d = json_array_get(descriptions, i);\n\t\t\t\t\t\tconst char *d_mid = json_string_value(json_object_get(d, \"mid\"));\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = d_mid ? g_hash_table_lookup(participant->streams_bymid, d_mid) : NULL;\n\t\t\t\t\t\tif(ps != NULL) {\n\t\t\t\t\t\t\tconst char *d_desc = json_string_value(json_object_get(d, \"description\"));\n\t\t\t\t\t\t\tif(d_desc) {\n\t\t\t\t\t\t\t\tdesc_updated = TRUE;\n\t\t\t\t\t\t\t\tg_free(ps->description);\n\t\t\t\t\t\t\t\tps->description = g_strdup(d_desc);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* If at least a description changed, notify everyone else about the publisher details */\n\t\t\t\t\tif(desc_updated)\n\t\t\t\t\t\tjanus_videoroom_notify_about_publisher(participant, TRUE);\n\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\tjanus_mutex_unlock(&participant->room->mutex);\n\t\t\t\t}\n\t\t\t\t/* Done */\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\t\t\t\tjson_object_set_new(event, \"configured\", json_string(\"ok\"));\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"configured\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\t\t\t\t\tjson_object_set_new(info, \"id\", string_ids ? json_string(participant->user_id_str) : json_integer(participant->user_id));\n\t\t\t\t\t\t/* TODO Add info on all the configured stuff here, here */\n\t\t\t\t\tjson_object_set_new(info, \"bitrate\", json_integer(participant->bitrate));\n\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(request_text, \"unpublish\")) {\n\t\t\t\t/* This participant wants to unpublish */\n\t\t\t\tif(!g_atomic_int_get(&participant->session->started)) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't unpublish, not published\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NOT_PUBLISHED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't unpublish, not published\");\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\t\t\tjanus_videoroom_hangup_media(session->handle);\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\t/* Done */\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\t\t\t\tjson_object_set_new(event, \"unpublished\", json_string(\"ok\"));\n\t\t\t} else if(!strcasecmp(request_text, \"leave\")) {\n\t\t\t\t/* Prepare an event to confirm the request */\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(participant->room_id_str) : json_integer(participant->room_id));\n\t\t\t\tjson_object_set_new(event, \"leaving\", json_string(\"ok\"));\n\t\t\t\t/* This publisher is leaving, tell everybody */\n\t\t\t\tjanus_videoroom_leave_or_unpublish(participant, TRUE, FALSE);\n\t\t\t\t/* Done */\n\t\t\t\tg_atomic_int_set(&session->started, 0);\n\t\t\t\t//~ session->destroy = TRUE;\n\t\t\t} else {\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unknown request '%s'\\n\", request_text);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t} else if(session->participant_type == janus_videoroom_p_type_subscriber) {\n\t\t\t/* Handle this subscriber */\n\t\t\tif(!strcasecmp(request_text, \"join\")) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Already in as a subscriber on this handle\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_ALREADY_JOINED;\n\t\t\t\tg_snprintf(error_cause, 512, \"Already in as a subscriber on this handle\");\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tgoto error;\n\t\t\t} else if(!strcasecmp(request_text, \"start\")) {\n\t\t\t\t/* Start/restart receiving the publisher streams */\n\t\t\t\tif(subscriber->paused && msg->jsep == NULL) {\n\t\t\t\t\t/* This is just resuming a paused subscription, reset the RTP sequence numbers on all streams */\n\t\t\t\t\tGList *temp = subscriber->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\t\t\tstream->context.seq_reset = TRUE;\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = stream->publisher_streams ? stream->publisher_streams->data : NULL;\n\t\t\t\t\t\tif(ps && ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && ps->publisher && ps->publisher->session) {\n\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Subscriber start\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsubscriber->paused = FALSE;\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"started\", json_string(\"ok\"));\n\t\t\t} else if(!strcasecmp(request_text, \"subscribe\") || !strcasecmp(request_text, \"unsubscribe\") ||\n\t\t\t\t\t!strcasecmp(request_text, \"update\")) {\n\t\t\t\t/* Update a subscription by adding and/or removing new streams */\n\t\t\t\tgboolean update = !strcasecmp(request_text, \"update\");\n\t\t\t\tgboolean subscribe = update || !strcasecmp(request_text, \"subscribe\");\n\t\t\t\tgboolean unsubscribe = update || !strcasecmp(request_text, \"unsubscribe\");\n\t\t\t\tif(unsubscribe)\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Removing subscriber streams\\n\");\n\t\t\t\tif(subscribe)\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Adding new subscriber streams\\n\");\n\t\t\t\t/* Validate the request first */\n\t\t\t\tif(update) {\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, subscriber_combined_update_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, subscriber_update_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t}\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjson_t *sub_feeds = NULL, *unsub_feeds = NULL;\n\t\t\t\tif(subscribe) {\n\t\t\t\t\tsub_feeds = json_object_get(root, update ? \"subscribe\" : \"streams\");\n\t\t\t\t\tif(sub_feeds && json_array_size(sub_feeds) == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Empty subscription list\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Empty subscription list\");\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(unsubscribe) {\n\t\t\t\t\tunsub_feeds = json_object_get(root, update ? \"unsubscribe\" : \"streams\");\n\t\t\t\t\tif(unsub_feeds && json_array_size(unsub_feeds) == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Empty unsubscription list\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Empty unsubscription list\");\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(unsub_feeds); i++) {\n\t\t\t\t\t\tjson_t *s = json_array_get(unsub_feeds, i);\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, subscriber_remove_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feedopt_parameters,\n\t\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feedstropt_parameters,\n\t\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(update && sub_feeds == NULL && unsub_feeds == NULL) {\n\t\t\t\t\t/* We require at least one array when handling an \"update\" request */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"At least one of either 'subscribe' or 'unsubscribe' must be present\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"At least one of either 'subscribe' or 'unsubscribe' must be present\");\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* If we're subscribing, make sure all the feeds we're subscribing to exist */\n\t\t\t\tGList *publishers = NULL;\n\t\t\t\tif(subscribe) {\n\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\tfor(i=0; i<json_array_size(sub_feeds); i++) {\n\t\t\t\t\t\tjson_t *s = json_array_get(sub_feeds, i);\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, subscriber_stream_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feed_parameters,\n\t\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feedstr_parameters,\n\t\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *feed = json_object_get(s, \"feed\");\n\t\t\t\t\t\tguint64 feed_id = 0;\n\t\t\t\t\t\tchar feed_id_num[30], *feed_id_str = NULL;\n\t\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_lock(&subscriber->room->mutex);\n\t\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(subscriber->room->participants,\n\t\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\t\tjanus_mutex_unlock(&subscriber->room->mutex);\n\t\t\t\t\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed) ||\n\t\t\t\t\t\t\t\t!g_atomic_int_get(&publisher->session->started)) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such feed (%s)\\n\", feed_id_str);\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such feed (%s)\", feed_id_str);\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(publisher->e2ee != subscriber->e2ee) {\n\t\t\t\t\t\t\t/* Attempt to mix normal and end-to-end encrypted subscriptions */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't mix normal and end-to-end encrypted subscriptions\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_FEED;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't mix normal and end-to-end encrypted subscriptions\");\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\t\tif(mid != NULL) {\n\t\t\t\t\t\t\t/* Check the mid too */\n\t\t\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\t\t\tif(g_hash_table_lookup(publisher->streams_bymid, mid) == NULL) {\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such mid '%s' in feed (%s)\\n\", mid, feed_id_str);\n\t\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such mid '%s' in feed (%s)\", mid, feed_id_str);\n\t\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\t\t\tjson_t *sc_substream = json_object_get(s, \"substream\");\n\t\t\t\t\t\tif(json_integer_value(spatial) < 0 || json_integer_value(spatial) > 2 ||\n\t\t\t\t\t\t\t\tjson_integer_value(sc_substream) < 0 || json_integer_value(sc_substream) > 2) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (substream/spatial_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (substream/spatial_layer should be 0, 1 or 2)\");\n\t\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\t\t\tjson_t *sc_temporal = json_object_get(s, \"temporal\");\n\t\t\t\t\t\tif(json_integer_value(temporal) < 0 || json_integer_value(temporal) > 2 ||\n\t\t\t\t\t\t\t\tjson_integer_value(sc_temporal) < 0 || json_integer_value(sc_temporal) > 2) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (temporal/temporal_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (temporal/temporal_layer should be 0, 1 or 2)\");\n\t\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\t\tgoto error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Increase the refcount before unlocking so that nobody can remove and free the publisher in the meantime. */\n\t\t\t\t\t\tjanus_refcount_increase(&publisher->ref);\n\t\t\t\t\t\tjanus_refcount_increase(&publisher->session->ref);\n\t\t\t\t\t\tpublishers = g_list_append(publishers, publisher);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Update the subscription, now: if this is a combined request, always\n\t\t\t\t * handle the unsubscribe first, and the subscribe only after that */\n\t\t\t\tint changes = 0;\n\t\t\t\tsize_t i = 0;\n\t\t\t\tjanus_mutex_lock(&subscriber->room->mutex);\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\tif(unsubscribe) {\n\t\t\t\t\t/* Remove the specified subscriptions */\n\t\t\t\t\tfor(i=0; i<json_array_size(unsub_feeds); i++) {\n\t\t\t\t\t\tjson_t *s = json_array_get(unsub_feeds, i);\n\t\t\t\t\t\tjson_t *feed = json_object_get(s, \"feed\");\n\t\t\t\t\t\tguint64 feed_id = 0;\n\t\t\t\t\t\tchar feed_id_num[30], *feed_id_str = NULL;\n\t\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst char *sub_mid = json_string_value(json_object_get(s, \"sub_mid\"));\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = NULL;\n\t\t\t\t\t\tif(sub_mid) {\n\t\t\t\t\t\t\t/* A specific subscription mid has been provided */\n\t\t\t\t\t\t\tstream = g_hash_table_lookup(subscriber->streams_bymid, sub_mid);\n\t\t\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Subscriber stream with mid '%s' not found, not unsubscribing...\\n\", sub_mid);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream_remove(stream, NULL, TRUE);\n\t\t\t\t\t\t\tchanges++;\n\t\t\t\t\t\t} else if(feed_id_str != NULL) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(subscriber->room->participants,\n\t\t\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\t\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed) ||\n\t\t\t\t\t\t\t\t\t!g_atomic_int_get(&publisher->session->started)) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Publisher '%s' not found, not unsubscribing...\\n\", feed_id_str);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/* Are we unsubscribing from the publisher as a whole or only a single stream? */\n\t\t\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\t\t\t/* Iterate on all subscriptions, and remove those that don't match */\n\t\t\t\t\t\t\tGList *temp = subscriber->streams;\n\t\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\t\t/* We need more fine grained mechanisms for changing streaming properties */\n\t\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = NULL;\n\t\t\t\t\t\t\t\tGSList *list = stream->publisher_streams;\n\t\t\t\t\t\t\t\twhile(list) {\n\t\t\t\t\t\t\t\t\tps = list->data;\n\t\t\t\t\t\t\t\t\tif(ps == NULL || ps->publisher != publisher) {\n\t\t\t\t\t\t\t\t\t\t/* Not the publisher we're interested in */\n\t\t\t\t\t\t\t\t\t\tlist = list->next;\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(mid && ps->mid && strcasecmp(ps->mid, mid)) {\n\t\t\t\t\t\t\t\t\t\t/* Not the mid we're interested in */\n\t\t\t\t\t\t\t\t\t\tlist = list->next;\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(stream->type != JANUS_VIDEOROOM_MEDIA_DATA)\n\t\t\t\t\t\t\t\t\t\tchanges++;\n\t\t\t\t\t\t\t\t\tlist = list->next;\n\t\t\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream_remove(stream, ps, TRUE);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(subscribe) {\n\t\t\t\t\t/* Add streams, or replace existing and inactive ones */\n\t\t\t\t\tfor(i=0; i<json_array_size(sub_feeds); i++) {\n\t\t\t\t\t\tjson_t *s = json_array_get(sub_feeds, i);\n\t\t\t\t\t\tjson_t *feed = json_object_get(s, \"feed\");\n\t\t\t\t\t\tguint64 feed_id = 0;\n\t\t\t\t\t\tchar feed_id_num[30], *feed_id_str = NULL;\n\t\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(subscriber->room->participants,\n\t\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed) ||\n\t\t\t\t\t\t\t\t!g_atomic_int_get(&publisher->session->started)) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Publisher '%s' not found, not subscribing...\\n\", feed_id_str);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Are we subscribing to this publisher as a whole or only to a single stream? */\n\t\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\t\tconst char *crossrefid = json_string_value(json_object_get(s, \"crossrefid\"));\n\t\t\t\t\t\tjson_t *send = json_object_get(s, \"send\");\n\t\t\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\t\t\tjson_t *sc_substream = json_object_get(s, \"substream\");\n\t\t\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\t\t\tjson_t *sc_temporal = json_object_get(s, \"temporal\");\n\t\t\t\t\t\tjson_t *sc_fallback = json_object_get(s, \"fallback\");\n\t\t\t\t\t\tjson_t *min_delay = json_object_get(s, \"min_delay\");\n\t\t\t\t\t\tjson_t *max_delay = json_object_get(s, \"max_delay\");\n\t\t\t\t\t\tif(mid != NULL) {\n\t\t\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = g_hash_table_lookup(publisher->streams_bymid, mid);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\t\tif(ps == NULL) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No mid '%s' in publisher '%s', not subscribing...\\n\", mid, feed_id_str);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(ps->disabled) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping disabled m-line...\\n\");\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif((ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO && ps->acodec == JANUS_AUDIOCODEC_NONE) ||\n\t\t\t\t\t\t\t\t\t(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && ps->vcodec == JANUS_VIDEOCODEC_NONE)) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping rejected publisher stream...\\n\");\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = janus_videoroom_subscriber_stream_add_or_replace(subscriber, ps, crossrefid);\n\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\tchanges++;\n\t\t\t\t\t\t\t\tif(send) {\n\t\t\t\t\t\t\t\t\tgboolean oldsend = stream->send;\n\t\t\t\t\t\t\t\t\tgboolean newsend = json_is_true(send);\n\t\t\t\t\t\t\t\t\tif(!oldsend && newsend) {\n\t\t\t\t\t\t\t\t\t\t/* Medium just resumed, reset the RTP sequence numbers */\n\t\t\t\t\t\t\t\t\t\tstream->context.seq_reset = TRUE;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tstream->send = json_is_true(send);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO &&\n\t\t\t\t\t\t\t\t\t\t(spatial || sc_substream || temporal || sc_temporal)) {\n\t\t\t\t\t\t\t\t\t/* Override the default spatial/substream/temporal targets */\n\t\t\t\t\t\t\t\t\tint substream_target = sc_substream ? json_integer_value(sc_substream) : -1;\n\t\t\t\t\t\t\t\t\tif(sc_substream && substream_target >= 0 && substream_target <= 2)\n\t\t\t\t\t\t\t\t\t\tstream->sim_context.substream_target = substream_target;\n\t\t\t\t\t\t\t\t\tif(sc_temporal)\n\t\t\t\t\t\t\t\t\t\tstream->sim_context.templayer_target = json_integer_value(sc_temporal);\n\t\t\t\t\t\t\t\t\tif(sc_fallback)\n\t\t\t\t\t\t\t\t\t\tstream->sim_context.drop_trigger = json_integer_value(sc_fallback);\n\t\t\t\t\t\t\t\t\tif(spatial)\n\t\t\t\t\t\t\t\t\t\tstream->svc_context.spatial_target = json_integer_value(spatial);\n\t\t\t\t\t\t\t\t\tif(temporal)\n\t\t\t\t\t\t\t\t\t\tstream->svc_context.temporal_target = json_integer_value(temporal);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\t\t/* Override the playout-delay properties */\n\t\t\t\t\t\t\t\t\tif(min_delay) {\n\t\t\t\t\t\t\t\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = md;\n\t\t\t\t\t\t\t\t\t\t\tif(stream->min_delay > stream->max_delay)\n\t\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = stream->min_delay;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(max_delay) {\n\t\t\t\t\t\t\t\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = md;\n\t\t\t\t\t\t\t\t\t\t\tif(stream->max_delay < stream->min_delay)\n\t\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = stream->max_delay;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\t\t\tGList *temp = publisher->streams;\n\t\t\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\t\t\tif(ps->disabled) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping disabled m-line...\\n\");\n\t\t\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif((ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO && ps->acodec == JANUS_AUDIOCODEC_NONE) ||\n\t\t\t\t\t\t\t\t\t\t(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && ps->vcodec == JANUS_VIDEOCODEC_NONE)) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping rejected publisher stream...\\n\");\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = janus_videoroom_subscriber_stream_add_or_replace(subscriber, ps, crossrefid);\n\t\t\t\t\t\t\t\tif(stream) {\n\t\t\t\t\t\t\t\t\tchanges++;\n\t\t\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO &&\n\t\t\t\t\t\t\t\t\t\t\t(spatial || sc_substream || temporal || sc_temporal)) {\n\t\t\t\t\t\t\t\t\t\t/* Override the default spatial/substream/temporal targets */\n\t\t\t\t\t\t\t\t\t\tint substream_target = sc_substream ? json_integer_value(sc_substream) : -1;\n\t\t\t\t\t\t\t\t\t\tif(sc_substream && substream_target >= 0 && substream_target <= 2)\n\t\t\t\t\t\t\t\t\t\t\tstream->sim_context.substream_target = substream_target;\n\t\t\t\t\t\t\t\t\t\tif(sc_temporal)\n\t\t\t\t\t\t\t\t\t\t\tstream->sim_context.templayer_target = json_integer_value(sc_temporal);\n\t\t\t\t\t\t\t\t\t\tif(sc_fallback)\n\t\t\t\t\t\t\t\t\t\t\tstream->sim_context.drop_trigger = json_integer_value(sc_fallback);\n\t\t\t\t\t\t\t\t\t\tif(spatial)\n\t\t\t\t\t\t\t\t\t\t\tstream->svc_context.spatial_target = json_integer_value(spatial);\n\t\t\t\t\t\t\t\t\t\tif(temporal)\n\t\t\t\t\t\t\t\t\t\t\tstream->svc_context.temporal_target = json_integer_value(temporal);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\t\t\t/* Override the playout-delay properties */\n\t\t\t\t\t\t\t\t\t\tif(min_delay) {\n\t\t\t\t\t\t\t\t\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\t\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = md;\n\t\t\t\t\t\t\t\t\t\t\t\tif(stream->min_delay > stream->max_delay)\n\t\t\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = stream->min_delay;\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif(max_delay) {\n\t\t\t\t\t\t\t\t\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\t\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\tstream->max_delay = md;\n\t\t\t\t\t\t\t\t\t\t\t\tif(stream->max_delay < stream->min_delay)\n\t\t\t\t\t\t\t\t\t\t\t\t\tstream->min_delay = stream->max_delay;\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* We're done: check if this resulted in any actual change */\n\t\t\t\tif(g_atomic_int_compare_and_exchange(&subscriber->skipped_autoupdate, 1, 0))\n\t\t\t\t\tchanges++;\n\t\t\t\tif(changes == 0) {\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->room->mutex);\n\t\t\t\t\t/* Nothing changed, just ack and don't do anything else */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"No change made, skipping renegotiation\\n\");\n\t\t\t\t\tevent = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"updated\"));\n\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\t\tint res = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, NULL);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t/* Decrease the references we took before */\n\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t/* Done */\n\t\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif(!g_atomic_int_get(&subscriber->answered)) {\n\t\t\t\t\t/* We're still waiting for an answer to a previous offer, postpone this */\n\t\t\t\t\tg_atomic_int_set(&subscriber->pending_offer, 1);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->room->mutex);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Post-poning new offer, waiting for previous answer\\n\");\n\t\t\t\t\t/* Send a temporary event */\n\t\t\t\t\tevent = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"updating\"));\n\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\tgateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, NULL);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t/* Decrease the references we took before, if any */\n\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"updated\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(subscriber, FALSE, NULL);\n\t\t\t\tjson_t *media_event = NULL;\n\t\t\t\tif(notify_events && gateway->events_is_enabled())\n\t\t\t\t\tmedia_event = json_deep_copy(media);\n\t\t\t\tjson_object_set_new(event, \"streams\", media);\n\t\t\t\t/* Generate a new offer */\n\t\t\t\tjson_t *jsep = janus_videoroom_subscriber_offer(subscriber);\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\tjanus_mutex_unlock(&subscriber->room->mutex);\n\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\tint res = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, jsep);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\tjson_decref(event);\n\t\t\t\tjson_decref(jsep);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"updated\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\tjson_object_set_new(info, \"streams\", media_event);\n\t\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(subscriber->pvt_id));\n\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\t/* Decrease the references we took before, if any */\n\t\t\t\twhile(publishers) {\n\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t}\n\t\t\t\t/* Done */\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\tcontinue;\n\t\t\t} else if(!strcasecmp(request_text, \"configure\")) {\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, configure_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(subscriber->kicked) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized, you have been kicked\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Unauthorized, you have been kicked\");\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjson_t *restart = json_object_get(root, \"restart\");\n\t\t\t\tjson_t *update = json_object_get(root, \"update\");\n\t\t\t\t/* Audio, video and data are deprecated properties */\n\t\t\t\tjson_t *audio = json_object_get(root, \"audio\");\n\t\t\t\tjson_t *video = json_object_get(root, \"video\");\n\t\t\t\tjson_t *data = json_object_get(root, \"data\");\n\t\t\t\t/* We use an array of streams to state the changes we want to make,\n\t\t\t\t * were for each stream we specify the 'mid' to impact (e.g., send) */\n\t\t\t\tjson_t *streams = json_object_get(root, \"streams\");\n\t\t\t\tif(streams == NULL) {\n\t\t\t\t\t/* No streams object, check if the properties have been\n\t\t\t\t\t * provided globally, which is how we handled this\n\t\t\t\t\t * request before: if so, create a new fake streams\n\t\t\t\t\t * array, and move the parsed options there */\n\t\t\t\t\tstreams = json_array();\n\t\t\t\t\tjson_t *stream = json_object();\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(root, \"mid\"));\n\t\t\t\t\tif(mid != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"mid\", json_string(mid));\n\t\t\t\t\tjson_t *send = json_object_get(root, \"send\");\n\t\t\t\t\tif(send != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"send\", json_is_true(send) ? json_true() : json_false());\n\t\t\t\t\tjson_t *spatial = json_object_get(root, \"spatial_layer\");\n\t\t\t\t\tif(spatial != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"spatial_layer\", json_integer(json_integer_value(spatial)));\n\t\t\t\t\tjson_t *sc_substream = json_object_get(root, \"substream\");\n\t\t\t\t\tif(sc_substream != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"substream\", json_integer(json_integer_value(sc_substream)));\n\t\t\t\t\tjson_t *temporal = json_object_get(root, \"temporal_layer\");\n\t\t\t\t\tif(temporal != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"temporal_layer\", json_integer(json_integer_value(temporal)));\n\t\t\t\t\tjson_t *sc_temporal = json_object_get(root, \"temporal\");\n\t\t\t\t\tif(sc_temporal != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"temporal\", json_integer(json_integer_value(sc_temporal)));\n\t\t\t\t\tjson_t *sc_fallback = json_object_get(root, \"fallback\");\n\t\t\t\t\tif(sc_fallback != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"fallback\", json_integer(json_integer_value(sc_fallback)));\n\t\t\t\t\tjson_t *min_delay = json_object_get(root, \"min_delay\");\n\t\t\t\t\tif(min_delay != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"min_delay\", json_integer(json_integer_value(min_delay)));\n\t\t\t\t\tjson_t *max_delay = json_object_get(root, \"max_delay\");\n\t\t\t\t\tif(max_delay != NULL)\n\t\t\t\t\t\tjson_object_set_new(stream, \"max_delay\", json_integer(json_integer_value(max_delay)));\n\t\t\t\t\tjson_array_append_new(streams, stream);\n\t\t\t\t\tjson_object_set_new(root, \"streams\", streams);\n\t\t\t\t}\n\t\t\t\t/* Validate all the streams we need to configure */\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\tsize_t i = 0;\n\t\t\t\tsize_t streams_size = json_array_size(streams);\n\t\t\t\tfor(i=0; i<streams_size; i++) {\n\t\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, configure_stream_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\tif(mid == NULL && streams_size > 1) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (mid can't be null in a streams array)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (mid can't be null in a streams array)\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(mid != NULL && g_hash_table_lookup(subscriber->streams_bymid, mid) == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such mid '%s' in subscription\\n\", mid);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such mid '%s' in subscription\", mid);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(mid != NULL) {\n\t\t\t\t\t\tjson_object_del(root, \"audio\");\n\t\t\t\t\t\taudio = NULL;\n\t\t\t\t\t\tjson_object_del(root, \"video\");\n\t\t\t\t\t\tvideo = NULL;\n\t\t\t\t\t\tjson_object_del(root, \"data\");\n\t\t\t\t\t\tdata = NULL;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\t\tjson_t *sc_substream = json_object_get(s, \"substream\");\n\t\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\t\tjson_t *sc_temporal = json_object_get(s, \"temporal\");\n\t\t\t\t\tif(json_integer_value(spatial) < 0 || json_integer_value(spatial) > 2 ||\n\t\t\t\t\t\t\tjson_integer_value(sc_substream) < 0 || json_integer_value(sc_substream) > 2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (substream/spatial_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (substream/spatial_layer should be 0, 1 or 2)\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(json_integer_value(temporal) < 0 || json_integer_value(temporal) > 2 ||\n\t\t\t\t\t\t\tjson_integer_value(sc_temporal) < 0 || json_integer_value(sc_temporal) > 2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid element (temporal/temporal_layer should be 0, 1 or 2)\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid value (temporal/temporal_layer should be 0, 1 or 2)\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Enforce the requested changes */\n\t\t\t\tfor(i=0; i<json_array_size(streams); i++) {\n\t\t\t\t\t/* Get the stream we need to tweak */\n\t\t\t\t\tjson_t *s = json_array_get(streams, i);\n\t\t\t\t\t/* Check which properties we need to tweak */\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\tjson_t *send = json_object_get(s, \"send\");\n\t\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\t\tjson_t *sc_substream = json_object_get(s, \"substream\");\n\t\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\t\tjson_t *sc_temporal = json_object_get(s, \"temporal\");\n\t\t\t\t\tjson_t *sc_fallback = json_object_get(s, \"fallback\");\n\t\t\t\t\tjson_t *min_delay = json_object_get(s, \"min_delay\");\n\t\t\t\t\tjson_t *max_delay = json_object_get(s, \"max_delay\");\n\t\t\t\t\tGList *temp = subscriber->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\t/* We need more fine grained mechanisms for changing streaming properties */\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = stream->publisher_streams ? stream->publisher_streams->data : NULL;\n\t\t\t\t\t\tif(audio && stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\t\t\tgboolean oldaudio = stream->send;\n\t\t\t\t\t\t\tgboolean newaudio = json_is_true(audio);\n\t\t\t\t\t\t\tif(!oldaudio && newaudio) {\n\t\t\t\t\t\t\t\t/* Audio just resumed, reset the RTP sequence numbers */\n\t\t\t\t\t\t\t\tstream->context.seq_reset = TRUE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tstream->send = newaudio;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(video && stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\tgboolean oldvideo = stream->send;\n\t\t\t\t\t\t\tgboolean newvideo = json_is_true(video);\n\t\t\t\t\t\t\tif(!oldvideo && newvideo) {\n\t\t\t\t\t\t\t\t/* Video just resumed, reset the RTP sequence numbers */\n\t\t\t\t\t\t\t\tstream->context.seq_reset = TRUE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tstream->send = newvideo;\n\t\t\t\t\t\t\tif(newvideo) {\n\t\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Restoring video for subscriber\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(data && stream->type == JANUS_VIDEOROOM_MEDIA_DATA)\n\t\t\t\t\t\t\tstream->send = json_is_true(data);\n\t\t\t\t\t\t/* Let's also see if this is the right mid */\n\t\t\t\t\t\tif(mid && strcasecmp(stream->mid, mid)) {\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(send) {\n\t\t\t\t\t\t\tgboolean oldsend = stream->send;\n\t\t\t\t\t\t\tgboolean newsend = json_is_true(send);\n\t\t\t\t\t\t\tif(!oldsend && newsend) {\n\t\t\t\t\t\t\t\t/* Medium just resumed, reset the RTP sequence numbers */\n\t\t\t\t\t\t\t\tstream->context.seq_reset = TRUE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tstream->send = json_is_true(send);\n\t\t\t\t\t\t\tif(newsend) {\n\t\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Restoring video for subscriber\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Next properties are for video only */\n\t\t\t\t\t\tif(stream->type != JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Check if a simulcasting-related request is involved */\n\t\t\t\t\t\tif(ps && ps->simulcast) {\n\t\t\t\t\t\t\tint substream_target = sc_substream ? json_integer_value(sc_substream) : -1;\n\t\t\t\t\t\t\tif(sc_substream && substream_target >= 0 && substream_target <= 2) {\n\t\t\t\t\t\t\t\tstream->sim_context.substream_target = substream_target;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video SSRC to let through (simulcast): %\"SCNu32\" (index %d, was %d)\\n\",\n\t\t\t\t\t\t\t\t\tps->vssrc[stream->sim_context.substream_target],\n\t\t\t\t\t\t\t\t\tstream->sim_context.substream_target,\n\t\t\t\t\t\t\t\t\tstream->sim_context.substream);\n\t\t\t\t\t\t\t\tif(stream->sim_context.substream_target == stream->sim_context.substream) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right substream, so notify the user */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"substream\", json_integer(stream->sim_context.substream));\n\t\t\t\t\t\t\t\t\tgateway->push_event(msg->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Simulcasting substream change\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(ps->simulcast && sc_temporal) {\n\t\t\t\t\t\t\t\tstream->sim_context.templayer_target = json_integer_value(sc_temporal);\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Setting video temporal layer to let through (simulcast): %d (was %d)\\n\",\n\t\t\t\t\t\t\t\t\tstream->sim_context.templayer_target, stream->sim_context.templayer);\n\t\t\t\t\t\t\t\tif(stream->sim_context.templayer_target == stream->sim_context.templayer) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right temporal, so notify the user */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"temporal\", json_integer(stream->sim_context.templayer));\n\t\t\t\t\t\t\t\t\tgateway->push_event(msg->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t/* Send a PLI */\n\t\t\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Simulcasting temporal layer change\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(sc_fallback) {\n\t\t\t\t\t\t\t\tstream->sim_context.drop_trigger = json_integer_value(sc_fallback);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if(ps && ps->svc) {\n\t\t\t\t\t\t\t/* Also check if the viewer is trying to configure a layer change */\n\t\t\t\t\t\t\tif(spatial) {\n\t\t\t\t\t\t\t\tint spatial_layer = json_integer_value(spatial);\n\t\t\t\t\t\t\t\tif(spatial_layer == stream->svc_context.spatial) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right spatial layer, so notify the user */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"spatial_layer\", json_integer(stream->svc_context.spatial));\n\t\t\t\t\t\t\t\t\tgateway->push_event(msg->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t} else if(spatial_layer != stream->svc_context.spatial_target) {\n\t\t\t\t\t\t\t\t\t/* Send a PLI to the new RTP forward publisher */\n\t\t\t\t\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Need to downscale spatially\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tstream->svc_context.spatial_target = spatial_layer;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(temporal) {\n\t\t\t\t\t\t\t\tint temporal_layer = json_integer_value(temporal);\n\t\t\t\t\t\t\t\tif(temporal_layer > 2) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Temporal layer higher than 2, will probably be ignored\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(temporal_layer == stream->svc_context.temporal) {\n\t\t\t\t\t\t\t\t\t/* No need to do anything, we're already getting the right temporal layer, so notify the user */\n\t\t\t\t\t\t\t\t\tjson_t *event = json_object();\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\t\t\t\t\t\tjson_object_set_new(event, \"temporal_layer\", json_integer(stream->svc_context.temporal_target));\n\t\t\t\t\t\t\t\t\tgateway->push_event(msg->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tstream->svc_context.temporal_target = temporal_layer;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\tif(min_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(min_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tstream->min_delay = md;\n\t\t\t\t\t\t\t\t\tif(stream->min_delay > stream->max_delay)\n\t\t\t\t\t\t\t\t\t\tstream->max_delay = stream->min_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif(max_delay) {\n\t\t\t\t\t\t\t\tint16_t md = json_integer_value(max_delay);\n\t\t\t\t\t\t\t\tif(md < 0) {\n\t\t\t\t\t\t\t\t\tstream->min_delay = -1;\n\t\t\t\t\t\t\t\t\tstream->max_delay = -1;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tstream->max_delay = md;\n\t\t\t\t\t\t\t\t\tif(stream->max_delay < stream->min_delay)\n\t\t\t\t\t\t\t\t\t\tstream->min_delay = stream->max_delay;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"configured\", json_string(\"ok\"));\n\t\t\t\t/* The user may be interested in an ICE restart */\n\t\t\t\tgboolean do_restart = restart ? json_is_true(restart) : FALSE;\n\t\t\t\tgboolean do_update = update ? json_is_true(update) : FALSE;\n\t\t\t\tif(sdp_update || do_restart || do_update) {\n\t\t\t\t\t/* Negotiate by sending the selected publisher SDP back, and/or force an ICE restart */\n\t\t\t\t\tif(!g_atomic_int_get(&subscriber->answered)) {\n\t\t\t\t\t\t/* We're still waiting for an answer to a previous offer, postpone this */\n\t\t\t\t\t\tg_atomic_int_set(&subscriber->pending_offer, 1);\n\t\t\t\t\t\tg_atomic_int_set(&subscriber->pending_restart, 1);\n\t\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Post-poning new ICE restart offer, waiting for previous answer\\n\");\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *jsep = janus_videoroom_subscriber_offer(subscriber);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\tif(do_restart)\n\t\t\t\t\t\tjson_object_set_new(jsep, \"restart\", json_true());\n\t\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\t\tint res = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, jsep);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tjson_decref(jsep);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t/* Done */\n\t\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t} else if(!strcasecmp(request_text, \"pause\")) {\n\t\t\t\t/* Stop receiving the publisher streams for a while */\n\t\t\t\tsubscriber->paused = TRUE;\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"paused\", json_string(\"ok\"));\n\t\t\t} else if(!strcasecmp(request_text, \"switch\")) {\n\t\t\t\t/* This subscriber wants to switch to a different publisher */\n\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(root, switch_parameters,\n\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\tif(error_code != 0) {\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(!subscriber->room || g_atomic_int_get(&subscriber->room->destroyed)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Room Destroyed \\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such room \");\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(g_atomic_int_get(&subscriber->destroyed)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Room Destroyed (%\"SCNu64\")\\n\", subscriber->room_id);\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"No such room (%\"SCNu64\")\", subscriber->room_id);\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* While the legacy way of switching by just providing a feed ID is\n\t\t\t\t * still supported (at least for now), it isn't flexible enough: the\n\t\t\t\t * new proper way of doing that is providing the list of changes that\n\t\t\t\t * need to be done, in terms of which stream to switch to, and which\n\t\t\t\t * subscription mid to attach it to. This allows for partial switches\n\t\t\t\t * (e.g., change the second video source to Bob's camera), while the\n\t\t\t\t * old approach simply forces a single publisher as the new source. */\n\t\t\t\tjson_t *feeds = json_object_get(root, \"streams\");\n\t\t\t\tjson_t *feed = json_object_get(root, \"feed\");\n\t\t\t\tGList *publishers = NULL;\n\t\t\t\tif(feeds == NULL || json_array_size(feeds) == 0) {\n\t\t\t\t\t/* For backwards compatibility, we still support the old \"feed\" property, which means\n\t\t\t\t\t * \"switch to all the feeds from this publisher\" (much less sophisticated, though) */\n\t\t\t\t\tguint64 feed_id = 0;\n\t\t\t\t\tchar feed_id_num[30], *feed_id_str = NULL;\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t}\n\t\t\t\t\tif(feed_id_str == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"At least one between 'streams' and 'feed' must be specified\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"At least one between 'streams' and 'feed' must be specified\");\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&subscriber->room->mutex);\n\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(subscriber->room->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->room->mutex);\n\t\t\t\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed) ||\n\t\t\t\t\t\t\t!g_atomic_int_get(&publisher->session->started)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such feed (%s)\\n\", feed_id_str);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such feed (%s)\", feed_id_str);\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\t/* Create a fake \"streams\" list out of this publisher */\n\t\t\t\t\tfeeds = json_array();\n\t\t\t\t\tjson_object_set_new(root, \"streams\", feeds);\n\t\t\t\t\tjanus_refcount_increase(&publisher->ref);\n\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\tGList *temp = publisher->streams, *touched_already = NULL;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\t/* Look for a subscriber stream compatible with this publisher stream */\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = NULL;\n\t\t\t\t\t\tGList *temp2 = subscriber->streams;\n\t\t\t\t\t\twhile(temp2) {\n\t\t\t\t\t\t\tstream = (janus_videoroom_subscriber_stream *)temp->data;\n\t\t\t\t\t\t\tif(stream->type == ps->type && !g_list_find(touched_already, stream) &&\n\t\t\t\t\t\t\t\t\t((stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO && stream->acodec == ps->acodec) ||\n\t\t\t\t\t\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO && stream->vcodec == ps->vcodec))) {\n\t\t\t\t\t\t\t\t/* This streams looks right */\n\t\t\t\t\t\t\t\ttouched_already = g_list_append(touched_already, stream);\n\t\t\t\t\t\t\t\tjson_t *s = json_object();\n\t\t\t\t\t\t\t\tjson_object_set_new(s, \"feed\",  string_ids ?\n\t\t\t\t\t\t\t\t\tjson_string(publisher->user_id_str) : json_integer(publisher->user_id));\n\t\t\t\t\t\t\t\tjson_object_set_new(s, \"mid\", json_string(ps->mid));\n\t\t\t\t\t\t\t\tjson_object_set_new(s, \"sub_mid\", json_string(stream->mid));\n\t\t\t\t\t\t\t\tjson_array_append_new(feeds, s);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping %\"SCNu64\" stream '%s' legacy switch: no compliant subscriber stream\\n\",\n\t\t\t\t\t\t\t\t\tpublisher->user_id, ps->mid);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttemp2 = temp2->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\tg_list_free(touched_already);\n\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t/* Take note of the fact this is a legacy request */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Deprecated VideoRoom 'switch' API: please start looking into the new one for the future\\n\");\n\t\t\t\t}\n\t\t\t\t/* If we got here, we have a feeds list: make sure we have everything we need */\n\t\t\t\tif(json_array_size(feeds) == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Empty switch list\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Empty switch list\");\n\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Make sure all the feeds we're subscribing to exist */\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor(i=0; i<json_array_size(feeds); i++) {\n\t\t\t\t\tjson_t *s = json_array_get(feeds, i);\n\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, switch_update_parameters,\n\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feed_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_VALIDATE_JSON_OBJECT(s, feedstr_parameters,\n\t\t\t\t\t\t\terror_code, error_cause, TRUE,\n\t\t\t\t\t\t\tJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT);\n\t\t\t\t\t}\n\t\t\t\t\tif(error_code != 0) {\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\t/* Look for the publisher stream to switch to */\n\t\t\t\t\tjson_t *feed = json_object_get(s, \"feed\");\n\t\t\t\t\tguint64 feed_id = 0;\n\t\t\t\t\tchar feed_id_num[30], *feed_id_str = NULL;\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&subscriber->room->mutex);\n\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(subscriber->room->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->room->mutex);\n\t\t\t\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed) ||\n\t\t\t\t\t\t\t!g_atomic_int_get(&publisher->session->started)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such feed (%s)\\n\", feed_id_str);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such feed (%s)\", feed_id_str);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tif(publisher->e2ee != subscriber->e2ee) {\n\t\t\t\t\t\t/* Attempt to mix normal and end-to-end encrypted subscriptions */\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't mix normal and end-to-end encrypted subscriptions\\n\");\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Can't mix normal and end-to-end encrypted subscriptions\");\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\t/* Check the mid too */\n\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\tif(g_hash_table_lookup(publisher->streams_bymid, mid) == NULL) {\n\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"No such mid '%s' in feed (%s)\\n\", mid, feed_id_str);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"No such mid '%s' in feed (%s)\", mid, feed_id_str);\n\t\t\t\t\t\t/* Unref publishers we may have taken note of so far */\n\t\t\t\t\t\twhile(publishers) {\n\t\t\t\t\t\t\tpublisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t/* Increase the refcount before unlocking so that nobody can remove and free the publisher in the meantime. */\n\t\t\t\t\tjanus_refcount_increase(&publisher->ref);\n\t\t\t\t\tjanus_refcount_increase(&publisher->session->ref);\n\t\t\t\t\tpublishers = g_list_append(publishers, publisher);\n\t\t\t\t}\n\t\t\t\tgboolean paused = subscriber->paused;\n\t\t\t\tsubscriber->paused = TRUE;\n\t\t\t\t/* Switch to the new streams, unsubscribing from the ones we replace:\n\t\t\t\t * notice that no renegotiation happens, we just switch the sources */\n\t\t\t\tint changes = 0;\n\t\t\t\tgboolean update = FALSE;\n\t\t\t\tjanus_mutex_lock(&subscriber->room->mutex);\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\tfor(i=0; i<json_array_size(feeds); i++) {\n\t\t\t\t\tjson_t *s = json_array_get(feeds, i);\n\t\t\t\t\t/* Look for the specific subscription mid to update */\n\t\t\t\t\tconst char *sub_mid = json_string_value(json_object_get(s, \"sub_mid\"));\n\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = g_hash_table_lookup(subscriber->streams_bymid, sub_mid);\n\t\t\t\t\tif(stream == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Subscriber stream with mid '%s' not found, not switching...\\n\", sub_mid);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Look for the publisher stream to switch to */\n\t\t\t\t\tjson_t *feed = json_object_get(s, \"feed\");\n\t\t\t\t\tguint64 feed_id = 0;\n\t\t\t\t\tchar feed_id_num[30], *feed_id_str = NULL;\n\t\t\t\t\tif(!string_ids) {\n\t\t\t\t\t\tfeed_id = json_integer_value(feed);\n\t\t\t\t\t\tg_snprintf(feed_id_num, sizeof(feed_id_num), \"%\"SCNu64, feed_id);\n\t\t\t\t\t\tfeed_id_str = feed_id_num;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfeed_id_str = (char *)json_string_value(feed);\n\t\t\t\t\t}\n\t\t\t\t\tconst char *mid = json_string_value(json_object_get(s, \"mid\"));\n\t\t\t\t\tjanus_videoroom_publisher *publisher = g_hash_table_lookup(subscriber->room->participants,\n\t\t\t\t\t\tstring_ids ? (gpointer)feed_id_str : (gpointer)&feed_id);\n\t\t\t\t\tif(publisher == NULL || g_atomic_int_get(&publisher->destroyed) ||\n\t\t\t\t\t\t\t!g_atomic_int_get(&publisher->session->started)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Publisher '%s' not found, not switching...\\n\", feed_id_str);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\t\tjanus_videoroom_publisher_stream *ps = g_hash_table_lookup(publisher->streams_bymid, mid);\n\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\tif(ps == NULL || g_atomic_int_get(&ps->destroyed)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Publisher '%s' doesn't have any mid '%s', not switching...\\n\", feed_id_str, mid);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* If this mapping already exists, do nothing */\n\t\t\t\t\tif(g_slist_find(stream->publisher_streams, ps) != NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Publisher '%s'/'%s' is already feeding mid '%s', not switching...\\n\",\n\t\t\t\t\t\t\tfeed_id_str, mid, sub_mid);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* If the streams are not of the same type, do nothing */\n\t\t\t\t\tif(stream->type != ps->type) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Publisher '%s'/'%s' is not the same type as subscription mid '%s', not switching...\\n\",\n\t\t\t\t\t\t\tfeed_id_str, mid, sub_mid);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* If the streams are not using the same codec, do nothing */\n\t\t\t\t\tif((stream->type == JANUS_VIDEOROOM_MEDIA_AUDIO && stream->acodec != ps->acodec) ||\n\t\t\t\t\t\t\t(stream->type == JANUS_VIDEOROOM_MEDIA_VIDEO && stream->vcodec != ps->vcodec)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Publisher '%s'/'%s' is not using same codec as subscription mid '%s', not switching...\\n\",\n\t\t\t\t\t\t\tfeed_id_str, mid, sub_mid);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Unsubscribe the old stream and update it: we don't replace streams like we\n\t\t\t\t\t * do when doing new subscriptions, as that might change payload type, etc. */\n\t\t\t\t\tchanges++;\n\t\t\t\t\t/* Unsubscribe from the previous source first */\n\t\t\t\t\tjanus_refcount_increase(&stream->ref);\n\t\t\t\t\tgboolean unref = FALSE;\n\t\t\t\t\tif(stream->publisher_streams == NULL) {\n\t\t\t\t\t\t/* This stream was inactive, we'll need a renegotiation */\n\t\t\t\t\t\tupdate = TRUE;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tunref = TRUE;\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *stream_ps = stream->publisher_streams->data;\n\t\t\t\t\t\tjanus_mutex_lock(&stream_ps->subscribers_mutex);\n\t\t\t\t\t\tstream_ps->subscribers = g_slist_remove(stream_ps->subscribers, stream);\n\t\t\t\t\t\tstream->publisher_streams = g_slist_remove(stream->publisher_streams, stream_ps);\n\t\t\t\t\t\t/* Remove the subscriber from the helper threads too, if any */\n\t\t\t\t\t\tif(subscriber->room && subscriber->room->helper_threads > 0) {\n\t\t\t\t\t\t\tGList *l = subscriber->room->threads;\n\t\t\t\t\t\t\twhile(l) {\n\t\t\t\t\t\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\t\t\t\t\t\tjanus_mutex_lock(&ht->mutex);\n\t\t\t\t\t\t\t\tGList *list = g_hash_table_lookup(ht->subscribers, ps);\n\t\t\t\t\t\t\t\tif(g_list_find(list, s) != NULL) {\n\t\t\t\t\t\t\t\t\tht->num_subscribers--;\n\t\t\t\t\t\t\t\t\tlist = g_list_remove_all(list, s);\n\t\t\t\t\t\t\t\t\tg_hash_table_insert(ht->subscribers, ps, list);\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Removing subscriber stream from helper thread #%d (%d subscribers)\\n\",\n\t\t\t\t\t\t\t\t\t\tht->id, ht->num_subscribers);\n\t\t\t\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tjanus_mutex_unlock(&ht->mutex);\n\t\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&stream_ps->subscribers_mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&stream_ps->ref);\n\t\t\t\t\t}\n\n\t\t\t\t\t/* Subscribe to the new one */\n\t\t\t\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\t\t\t\tstream->publisher_streams = g_slist_append(stream->publisher_streams, ps);\n\t\t\t\t\tps->subscribers = g_slist_append(ps->subscribers, stream);\n\t\t\t\t\t/* If we're using helper threads, add the subscriber to one of those */\n\t\t\t\t\tif(subscriber->room && subscriber->room->helper_threads > 0) {\n\t\t\t\t\t\tint subscribers = -1;\n\t\t\t\t\t\tjanus_videoroom_helper *helper = NULL;\n\t\t\t\t\t\tGList *l = subscriber->room->threads;\n\t\t\t\t\t\twhile(l) {\n\t\t\t\t\t\t\tjanus_videoroom_helper *ht = (janus_videoroom_helper *)l->data;\n\t\t\t\t\t\t\tif(subscribers == -1 || (helper == NULL && ht->num_subscribers == 0) || ht->num_subscribers < subscribers) {\n\t\t\t\t\t\t\t\tsubscribers = ht->num_subscribers;\n\t\t\t\t\t\t\t\thelper = ht;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tl = l->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_lock(&helper->mutex);\n\t\t\t\t\t\tGList *list = g_hash_table_lookup(helper->subscribers, ps);\n\t\t\t\t\t\tlist = g_list_append(list, stream);\n\t\t\t\t\t\tg_hash_table_insert(helper->subscribers, ps, list);\n\t\t\t\t\t\thelper->num_subscribers++;\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Added subscriber stream to helper thread #%d (%d subscribers) (switching)\\n\",\n\t\t\t\t\t\t\thelper->id, helper->num_subscribers);\n\t\t\t\t\t\tjanus_mutex_unlock(&helper->mutex);\n\t\t\t\t\t}\n\t\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t\tjanus_refcount_increase(&stream->ref);\n\t\t\t\t\t/* Reset simulcast and SVC properties too */\n\t\t\t\t\tjanus_rtp_simulcasting_context_reset(&stream->sim_context);\n\t\t\t\t\tjanus_mutex_lock(&ps->rid_mutex);\n\t\t\t\t\tstream->sim_context.rid_ext_id = ps->rid_extmap_id;\n\t\t\t\t\tjanus_mutex_unlock(&ps->rid_mutex);\n\t\t\t\t\tstream->send = TRUE;\n\t\t\t\t\tjson_t *substream = json_object_get(s, \"substream\");\n\t\t\t\t\tint substream_target = substream ? json_integer_value(substream) : 2;\n\t\t\t\t\tif(substream_target >= 0 && substream_target <= 2) {\n\t\t\t\t\t\t/* Override substream_target if valid */\n\t\t\t\t\t\tstream->sim_context.substream_target = substream_target;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Reset sustream_target to 2 */\n\t\t\t\t\t\tstream->sim_context.substream_target = 2;\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *temporal = json_object_get(s, \"temporal\");\n\t\t\t\t\tint templayer_target = temporal ? json_integer_value(temporal) : 2;\n\t\t\t\t\tif(templayer_target >= 0 && templayer_target <= 2) {\n\t\t\t\t\t\t/* Override templayer_target if valid */\n\t\t\t\t\t\tstream->sim_context.templayer_target = templayer_target;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Reset templayer_target to 2 */\n\t\t\t\t\t\tstream->sim_context.templayer_target = 2;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_rtp_svc_context_reset(&stream->svc_context);\n\t\t\t\t\tjson_t *spatial = json_object_get(s, \"spatial_layer\");\n\t\t\t\t\tint spatial_target = spatial ? json_integer_value(spatial) : 2;\n\t\t\t\t\tif(spatial_target >= 0 && spatial_target <= 2) {\n\t\t\t\t\t\t/* Override spatial_target if valid */\n\t\t\t\t\t\tstream->svc_context.spatial_target = spatial_target;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Reset sustream_target to 2 */\n\t\t\t\t\t\tstream->svc_context.spatial_target = 2;\n\t\t\t\t\t}\n\t\t\t\t\ttemporal = json_object_get(s, \"temporal_layer\");\n\t\t\t\t\ttemplayer_target = temporal ? json_integer_value(temporal) : 2;\n\t\t\t\t\tif(templayer_target >= 0 && templayer_target <= 2) {\n\t\t\t\t\t\t/* Override templayer_target if valid */\n\t\t\t\t\t\tstream->svc_context.temporal_target = templayer_target;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Reset templayer_target to 2 */\n\t\t\t\t\t\tstream->svc_context.temporal_target = 2;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\t\t\t\tjanus_videoroom_reqpli(ps, \"Subscriber switch\");\n\t\t\t\t\tif(unref)\n\t\t\t\t\t\tjanus_refcount_decrease(&stream->ref);\n\t\t\t\t\tjanus_refcount_decrease(&stream->ref);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\tjanus_mutex_unlock(&subscriber->room->mutex);\n\t\t\t\t/* Decrease the references we took before */\n\t\t\t\twhile(publishers) {\n\t\t\t\t\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)publishers->data;\n\t\t\t\t\tjanus_refcount_decrease(&publisher->session->ref);\n\t\t\t\t\tjanus_refcount_decrease(&publisher->ref);\n\t\t\t\t\tpublishers = g_list_remove(publishers, publisher);\n\t\t\t\t}\n\t\t\t\t/* Done */\n\t\t\t\tsubscriber->paused = paused;\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"switched\", json_string(\"ok\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"changes\", json_integer(changes));\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(subscriber, FALSE, NULL);\n\t\t\t\tjson_t *media_event = NULL;\n\t\t\t\tif(notify_events && gateway->events_is_enabled())\n\t\t\t\t\tmedia_event = json_deep_copy(media);\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\tjson_object_set_new(event, \"streams\", media);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"switched\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\tjson_object_set_new(event, \"changes\", json_integer(changes));\n\t\t\t\t\tjson_object_set_new(event, \"streams\", media_event);\n\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t}\n\t\t\t\t/* Check if we need a renegotiation as well */\n\t\t\t\tif(update) {\n\t\t\t\t\t/* We do */\n\t\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\t\tif(!g_atomic_int_get(&subscriber->answered)) {\n\t\t\t\t\t\t/* We're still waiting for an answer to a previous offer, postpone this */\n\t\t\t\t\t\tg_atomic_int_set(&subscriber->pending_offer, 1);\n\t\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Post-poning new offer, waiting for previous answer\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjson_t *revent = json_object();\n\t\t\t\t\t\tjson_object_set_new(revent, \"videoroom\", json_string(\"updated\"));\n\t\t\t\t\t\tjson_object_set_new(revent, \"room\", string_ids ?\n\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(subscriber, FALSE, NULL);\n\t\t\t\t\t\tjson_t *media_event = NULL;\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled())\n\t\t\t\t\t\t\tmedia_event = json_deep_copy(media);\n\t\t\t\t\t\tjson_object_set_new(revent, \"streams\", media);\n\t\t\t\t\t\t/* Generate a new offer */\n\t\t\t\t\t\tjson_t *jsep = janus_videoroom_subscriber_offer(subscriber);\n\t\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\t\t\tint res = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, revent, jsep);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\t\t\tjson_decref(revent);\n\t\t\t\t\t\tjson_decref(jsep);\n\t\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"updated\"));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"room\", json_integer(subscriber->room_id));\n\t\t\t\t\t\t\tjson_object_set_new(info, \"streams\", media_event);\n\t\t\t\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(subscriber->pvt_id));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(request_text, \"leave\")) {\n\t\t\t\tguint64 room_id = subscriber ? subscriber->room_id : 0;\n\t\t\t\tchar *room_id_str = subscriber ? subscriber->room_id_str : NULL;\n\t\t\t\t/* Tell the core to tear down the PeerConnection, hangup_media will do the rest */\n\t\t\t\tjanus_videoroom_hangup_media(session->handle);\n\t\t\t\tgateway->close_pc(session->handle);\n\t\t\t\t/* Send an event back */\n\t\t\t\tevent = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(room_id_str) : json_integer(room_id));\n\t\t\t\tjson_object_set_new(event, \"left\", json_string(\"ok\"));\n\t\t\t\tg_atomic_int_set(&session->started, 0);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unknown request '%s'\\n\", request_text);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t}\n\n\t\t/* Prepare JSON event */\n\t\tJANUS_LOG(LOG_VERB, \"Preparing JSON event as a reply\\n\");\n\t\t/* Any SDP or update to handle? */\n\t\tconst char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, \"type\"));\n\t\tconst char *msg_sdp = json_string_value(json_object_get(msg->jsep, \"sdp\"));\n\t\tjson_t *msg_simulcast = json_object_get(msg->jsep, \"simulcast\");\n\t\tjson_t *msg_svc = json_object_get(msg->jsep, \"svc\");\n\t\tgboolean e2ee = json_is_true(json_object_get(msg->jsep, \"e2ee\"));\n\t\tif(!msg_sdp) {\n\t\t\t/* No SDP to send */\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t} else {\n\t\t\t/* Generate offer or answer */\n\t\t\tJANUS_LOG(LOG_VERB, \"This is involving a negotiation (%s) as well:\\n%s\\n\", msg_sdp_type, msg_sdp);\n\t\t\tif(sdp_update) {\n\t\t\t\t/* Renegotiation: make sure the user provided an offer, and send answer */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- Updating existing publisher\\n\");\n\t\t\t\tsession->sdp_version++;\t\t/* This needs to be increased when it changes */\n\t\t\t} else {\n\t\t\t\t/* New PeerConnection */\n\t\t\t\tsession->sdp_version = 1;\t/* This needs to be increased when it changes */\n\t\t\t\tsession->sdp_sessid = janus_get_real_time();\n\t\t\t}\n\t\t\tconst char *type = NULL;\n\t\t\tif(!strcasecmp(msg_sdp_type, \"offer\")) {\n\t\t\t\t/* We need to answer */\n\t\t\t\ttype = \"answer\";\n\t\t\t} else if(!strcasecmp(msg_sdp_type, \"answer\")) {\n\t\t\t\t/* We got an answer (from a subscriber?), no need to negotiate */\n\t\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\t\tint ret = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, NULL);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\t\tjson_decref(event);\n\t\t\t\t/* Take note of the fact we got our answer */\n\t\t\t\tif(session->participant == NULL) {\n\t\t\t\t\t/* Shouldn't happen? */\n\t\t\t\t\tif(subscriber != NULL)\n\t\t\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\t/* Mark all streams that were answered to as ready */\n\t\t\t\tchar error_str[512];\n\t\t\t\tjanus_sdp *answer = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str));\n\t\t\t\tGList *temp = answer->m_lines;\n\t\t\t\twhile(temp) {\n\t\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\t\tif(m->direction != JANUS_SDP_INACTIVE) {\n\t\t\t\t\t\tjanus_videoroom_subscriber_stream *stream = g_hash_table_lookup(subscriber->streams_byid, GINT_TO_POINTER(m->index));\n\t\t\t\t\t\tif(stream)\n\t\t\t\t\t\t\tg_atomic_int_set(&stream->ready, 1);\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjanus_sdp_destroy(answer);\n\t\t\t\t/* Check if we have other pending offers to send for this subscriber */\n\t\t\t\tif(g_atomic_int_compare_and_exchange(&subscriber->pending_offer, 1, 0)) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Pending offer, sending it now\\n\");\n\t\t\t\t\tevent = json_object();\n\t\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"updated\"));\n\t\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(subscriber, FALSE, NULL);\n\t\t\t\t\tjson_t *media_event = NULL;\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled())\n\t\t\t\t\t\tmedia_event = json_deep_copy(media);\n\t\t\t\t\tjson_object_set_new(event, \"streams\", media);\n\t\t\t\t\t/* Generate a new offer */\n\t\t\t\t\tjson_t *jsep = janus_videoroom_subscriber_offer(subscriber);\n\t\t\t\t\t/* Do we need an ICE restart as well? */\n\t\t\t\t\tif(g_atomic_int_compare_and_exchange(&subscriber->pending_restart, 1, 0))\n\t\t\t\t\t\tjson_object_set_new(jsep, \"restart\", json_true());\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\t\tint res = gateway->push_event(session->handle, &janus_videoroom_plugin, NULL, event, jsep);\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tjson_decref(jsep);\n\t\t\t\t\t/* Also notify event handlers */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"updated\"));\n\t\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\t\tjson_object_set_new(info, \"streams\", media_event);\n\t\t\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(subscriber->pvt_id));\n\t\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, session->handle, info);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tg_atomic_int_set(&subscriber->answered, 1);\n\t\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t}\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t\tjanus_videoroom_message_free(msg);\n\t\t\t\tcontinue;\n\t\t\t} else {\n\t\t\t\t/* TODO We don't support anything else right now... */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unknown SDP type '%s'\\n\", msg_sdp_type);\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_SDP_TYPE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Unknown SDP type '%s'\", msg_sdp_type);\n\t\t\t\tjson_decref(event);\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tif(session->participant_type != janus_videoroom_p_type_publisher) {\n\t\t\t\t/* We shouldn't be here, we always offer ourselves */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Only publishers send offers\\n\");\n\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_SDP_TYPE;\n\t\t\t\tg_snprintf(error_cause, 512, \"Only publishers send offers\");\n\t\t\t\tjson_decref(event);\n\t\t\t\tgoto error;\n\t\t\t} else {\n\t\t\t\t/* This is a new publisher, or an updated one */\n\t\t\t\tparticipant = janus_videoroom_session_get_publisher(session);\n\t\t\t\tif(participant == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid participant instance\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Invalid participant instance\");\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_videoroom *videoroom = participant->room;\n\t\t\t\tint count = 0;\n\t\t\t\tGHashTableIter iter;\n\t\t\t\tgpointer value;\n\t\t\t\tif(!videoroom) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tif(g_atomic_int_get(&videoroom->destroyed)) {\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM;\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\tjanus_refcount_increase(&videoroom->ref);\n\t\t\t\tif(!sdp_update) {\n\t\t\t\t\t/* New publisher, is there room? */\n\t\t\t\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\t\t\t\tg_hash_table_iter_init(&iter, videoroom->participants);\n\t\t\t\t\twhile (!g_atomic_int_get(&videoroom->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\t\tjanus_videoroom_publisher *p = value;\n\t\t\t\t\t\tif(p != participant && g_atomic_int_get(&p->session->started) && !p->dummy)\n\t\t\t\t\t\t\tcount++;\n\t\t\t\t\t}\n\t\t\t\t\tif(count == videoroom->max_publishers) {\n\t\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Maximum number of publishers (%d) already reached\\n\", videoroom->max_publishers);\n\t\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_PUBLISHERS_FULL;\n\t\t\t\t\t\tg_snprintf(error_cause, 512, \"Maximum number of publishers (%d) already reached\", videoroom->max_publishers);\n\t\t\t\t\t\tjson_decref(event);\n\t\t\t\t\t\tgoto error;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t}\n\t\t\t\tif(videoroom->require_e2ee && !e2ee && !participant->e2ee) {\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Room requires end-to-end encrypted media\\n\");\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Room requires end-to-end encrypted media\");\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Now prepare the SDP to give back */\n\t\t\t\tif(strstr(msg_sdp, \"mozilla\") || strstr(msg_sdp, \"Mozilla\")) {\n\t\t\t\t\tparticipant->firefox = TRUE;\n\t\t\t\t}\n\t\t\t\t/* Start by parsing the offer */\n\t\t\t\tchar error_str[512];\n\t\t\t\tjanus_sdp *offer = janus_sdp_parse(msg_sdp, error_str, sizeof(error_str));\n\t\t\t\tif(offer == NULL) {\n\t\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing offer: %s\\n\", error_str);\n\t\t\t\t\terror_code = JANUS_VIDEOROOM_ERROR_INVALID_SDP;\n\t\t\t\t\tg_snprintf(error_cause, 512, \"Error parsing offer: %s\", error_str);\n\t\t\t\t\tjson_decref(event);\n\t\t\t\t\tgoto error;\n\t\t\t\t}\n\t\t\t\t/* Prepare an answer, by iterating on all m-lines */\n\t\t\t\tjanus_sdp *answer = janus_sdp_generate_answer(offer);\n\t\t\t\tjson_t *media = json_array();\n\t\t\t\tjson_t *descriptions = json_object_get(root, \"descriptions\");\n\t\t\t\tconst char *audiocodec = NULL, *videocodec = NULL;\n\t\t\t\tchar *vp9_profile = NULL, *h264_profile = NULL;\n\t\t\t\tGList *temp = offer->m_lines;\n\t\t\t\tjanus_mutex_lock(&participant->rtp_forwarders_mutex);\n\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\twhile(temp) {\n\t\t\t\t\t/* Which media are available? */\n\t\t\t\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t\t\t\t/* Check if we have a stream instance for this m-line */\n\t\t\t\t\tgboolean new_ps = FALSE;\n\t\t\t\t\tjanus_videoroom_publisher_stream *ps = g_hash_table_lookup(participant->streams_byid, GINT_TO_POINTER(m->index));\n\t\t\t\t\tif(ps == NULL) {\n\t\t\t\t\t\t/* Initialize a new publisher stream */\n\t\t\t\t\t\tnew_ps = TRUE;\n\t\t\t\t\t\tps = g_malloc0(sizeof(janus_videoroom_publisher_stream));\n\t\t\t\t\t\tps->type = JANUS_VIDEOROOM_MEDIA_NONE;\n\t\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO)\n\t\t\t\t\t\t\tps->type = JANUS_VIDEOROOM_MEDIA_AUDIO;\n\t\t\t\t\t\telse if(m->type == JANUS_SDP_VIDEO)\n\t\t\t\t\t\t\tps->type = JANUS_VIDEOROOM_MEDIA_VIDEO;\n\t\t\t\t\t\tif(m->type == JANUS_SDP_APPLICATION)\n\t\t\t\t\t\t\tps->type = JANUS_VIDEOROOM_MEDIA_DATA;\n\t\t\t\t\t\tps->mindex = g_list_length(participant->streams);\n\t\t\t\t\t\tps->publisher = participant;\n\t\t\t\t\t\tjanus_refcount_increase(&participant->ref);\t/* Add a reference to the publisher */\n\t\t\t\t\t\t/* Initialize the stream */\n\t\t\t\t\t\tps->active = TRUE;\n\t\t\t\t\t\tps->acodec = participant->acodec;\n\t\t\t\t\t\tps->vcodec = participant->vcodec;\n\t\t\t\t\t\tps->pt = -1;\n\t\t\t\t\t\tps->min_delay = -1;\n\t\t\t\t\t\tps->max_delay = -1;\n\t\t\t\t\t\tg_atomic_int_set(&ps->destroyed, 0);\n\t\t\t\t\t\tjanus_refcount_init(&ps->ref, janus_videoroom_publisher_stream_free);\n\t\t\t\t\t\tjanus_refcount_increase(&ps->ref);\t/* This is for the mid-indexed hashtable */\n\t\t\t\t\t\tjanus_mutex_init(&ps->subscribers_mutex);\n\t\t\t\t\t\tjanus_mutex_init(&ps->rtp_forwarders_mutex);\n\t\t\t\t\t\tjanus_mutex_init(&ps->rid_mutex);\n\t\t\t\t\t\tps->rtp_forwarders = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_rtp_forwarder_destroy);\n\t\t\t\t\t}\n\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\t\t\t\t/* Are the extmaps we care about there? */\n\t\t\t\t\t\tGList *ma = m->attributes;\n\t\t\t\t\t\twhile(ma) {\n\t\t\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\t\t\t\tif(a->name && a->value) {\n\t\t\t\t\t\t\t\tif(!strcasecmp(a->name, \"mid\")) {\n\t\t\t\t\t\t\t\t\tgboolean mid_changed = FALSE;\n\t\t\t\t\t\t\t\t\t/* Check if we're just discovering the mid or if it changed */\n\t\t\t\t\t\t\t\t\tif(ps->mid != NULL && strcasecmp(ps->mid, a->value))\n\t\t\t\t\t\t\t\t\t\tmid_changed = TRUE;\n\t\t\t\t\t\t\t\t\tchar *old_mid = mid_changed ? ps->mid : NULL;\n\t\t\t\t\t\t\t\t\tif(ps->mid == NULL || mid_changed) {\n\t\t\t\t\t\t\t\t\t\tps->mid = g_strdup(a->value);\n\t\t\t\t\t\t\t\t\t\tif(mid_changed) {\n\t\t\t\t\t\t\t\t\t\t\t/* Update the table here, since this is not a new stream */\n\t\t\t\t\t\t\t\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\t\t\t\t\t\t\t\tg_hash_table_insert(participant->streams_bymid, g_strdup(ps->mid), ps);\n\t\t\t\t\t\t\t\t\t\t\tif(old_mid != NULL)\n\t\t\t\t\t\t\t\t\t\t\t\tg_hash_table_remove(participant->streams_bymid, old_mid);\n\t\t\t\t\t\t\t\t\t\t\tg_free(old_mid);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if(videoroom->audiolevel_ext && m->type == JANUS_SDP_AUDIO &&\n\t\t\t\t\t\t\t\t\t\tps->audio_level_extmap_id == 0 && strstr(a->value, JANUS_RTP_EXTMAP_AUDIO_LEVEL)) {\n\t\t\t\t\t\t\t\t\tps->audio_level_extmap_id = atoi(a->value);\n\t\t\t\t\t\t\t\t} else if(videoroom->videoorient_ext && m->type == JANUS_SDP_VIDEO &&\n\t\t\t\t\t\t\t\t\t\tps->video_orient_extmap_id == 0 && strstr(a->value, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION)) {\n\t\t\t\t\t\t\t\t\tps->video_orient_extmap_id = atoi(a->value);\n\t\t\t\t\t\t\t\t} else if(videoroom->playoutdelay_ext && m->type == JANUS_SDP_VIDEO &&\n\t\t\t\t\t\t\t\t\t\tps->playout_delay_extmap_id == 0 && strstr(a->value, JANUS_RTP_EXTMAP_PLAYOUT_DELAY)) {\n\t\t\t\t\t\t\t\t\tps->playout_delay_extmap_id = atoi(a->value);\n\t\t\t\t\t\t\t\t} else if(videoroom->do_opusfec && m->type == JANUS_SDP_AUDIO && !strcasecmp(a->name, \"fmtp\")) {\n\t\t\t\t\t\t\t\t\tif(strstr(a->value, \"useinbandfec=1\") && videoroom->do_opusfec)\n\t\t\t\t\t\t\t\t\t\tps->opusfec = TRUE;\n\t\t\t\t\t\t\t\t\tif(strstr(a->value, \"usedtx=1\") && videoroom->do_opusdtx)\n\t\t\t\t\t\t\t\t\t\tps->opusdtx = TRUE;\n\t\t\t\t\t\t\t\t\tif(strstr(a->value, \"stereo=1\"))\n\t\t\t\t\t\t\t\t\t\tps->opusstereo = TRUE;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tma = ma->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* If this m-line is active, check the codecs we can use, or the ones we should */\n\t\t\t\t\tjanus_sdp_mdirection mdir = JANUS_SDP_INACTIVE;\n\t\t\t\t\tif(m->direction != JANUS_SDP_INACTIVE) {\n\t\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\t\t\tif(ps->acodec != JANUS_AUDIOCODEC_NONE) {\n\t\t\t\t\t\t\t\t/* We already know which codec we'll use */\n\t\t\t\t\t\t\t\tif(ps->pt == -1 && janus_sdp_get_codec_pt(offer, m->index, janus_audiocodec_name(ps->acodec)) != -1) {\n\t\t\t\t\t\t\t\t\tps->pt = janus_audiocodec_pt(ps->acodec);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t/* Check the codec priorities in the room configuration */\n\t\t\t\t\t\t\t\tint i=0;\n\t\t\t\t\t\t\t\tfor(i=0; i<5; i++) {\n\t\t\t\t\t\t\t\t\tif(videoroom->acodec[i] == JANUS_AUDIOCODEC_NONE)\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, m->index, janus_audiocodec_name(videoroom->acodec[i])) != -1) {\n\t\t\t\t\t\t\t\t\t\tps->acodec = videoroom->acodec[i];\n\t\t\t\t\t\t\t\t\t\tps->pt = janus_audiocodec_pt(ps->acodec);\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmdir = (ps->acodec != JANUS_AUDIOCODEC_NONE ? JANUS_SDP_RECVONLY : JANUS_SDP_INACTIVE);\n\t\t\t\t\t\t} else if(m->type == JANUS_SDP_VIDEO) {\n\t\t\t\t\t\t\tvp9_profile = videoroom->vp9_profile;\n\t\t\t\t\t\t\th264_profile = videoroom->h264_profile;\n\t\t\t\t\t\t\tif(ps->vcodec != JANUS_VIDEOCODEC_NONE) {\n\t\t\t\t\t\t\t\t/* We already know which codec we'll use */\n\t\t\t\t\t\t\t\tif(ps->pt == -1 && ps->vcodec == JANUS_VIDEOCODEC_VP9 && vp9_profile) {\n\t\t\t\t\t\t\t\t\t/* Check if this VP9 profile is available */\n\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt_full(offer, -1, janus_videocodec_name(ps->vcodec), vp9_profile) != -1) {\n\t\t\t\t\t\t\t\t\t\t/* It is */\n\t\t\t\t\t\t\t\t\t\th264_profile = NULL;\n\t\t\t\t\t\t\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\t\t\t\t\t\t\tg_free(ps->vp9_profile);\n\t\t\t\t\t\t\t\t\t\tps->vp9_profile = g_strdup(vp9_profile);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t/* It isn't, fallback to checking whether VP9 is available without the profile */\n\t\t\t\t\t\t\t\t\t\tvp9_profile = NULL;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if(ps->pt == -1 && ps->vcodec == JANUS_VIDEOCODEC_H264 && h264_profile) {\n\t\t\t\t\t\t\t\t\t/* Check if this H.264 profile is available */\n\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt_full(offer, -1, janus_videocodec_name(ps->vcodec), h264_profile) != -1) {\n\t\t\t\t\t\t\t\t\t\t/* It is */\n\t\t\t\t\t\t\t\t\t\tvp9_profile = NULL;\n\t\t\t\t\t\t\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\t\t\t\t\t\t\tg_free(ps->h264_profile);\n\t\t\t\t\t\t\t\t\t\tps->h264_profile = g_strdup(h264_profile);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t/* It isn't, fallback to checking whether H.264 is available without the profile */\n\t\t\t\t\t\t\t\t\t\th264_profile = NULL;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif(ps->pt == -1 && janus_sdp_get_codec_pt(offer, m->index, janus_videocodec_name(ps->vcodec)) != -1) {\n\t\t\t\t\t\t\t\t\t/* We'll only get the profile later, when we've generated an answer  */\n\t\t\t\t\t\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t/* Check the codec priorities in the room configuration */\n\t\t\t\t\t\t\t\tint i=0;\n\t\t\t\t\t\t\t\tfor(i=0; i<5; i++) {\n\t\t\t\t\t\t\t\t\tif(videoroom->vcodec[i] == JANUS_VIDEOCODEC_NONE)\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\tif(videoroom->vcodec[i] == JANUS_VIDEOCODEC_VP9 && vp9_profile) {\n\t\t\t\t\t\t\t\t\t\t/* Check if this VP9 profile is available */\n\t\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt_full(offer, -1, janus_videocodec_name(videoroom->vcodec[i]), vp9_profile) != -1) {\n\t\t\t\t\t\t\t\t\t\t\t/* It is */\n\t\t\t\t\t\t\t\t\t\t\th264_profile = NULL;\n\t\t\t\t\t\t\t\t\t\t\tps->vcodec = videoroom->vcodec[i];\n\t\t\t\t\t\t\t\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\t\t\t\t\t\t\t\tps->vp9_profile = g_strdup(vp9_profile);\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t/* It isn't, fallback to checking whether VP9 is available without the profile */\n\t\t\t\t\t\t\t\t\t\tvp9_profile = NULL;\n\t\t\t\t\t\t\t\t\t} else if(videoroom->vcodec[i] == JANUS_VIDEOCODEC_H264 && h264_profile) {\n\t\t\t\t\t\t\t\t\t\t/* Check if this H.264 profile is available */\n\t\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt_full(offer, -1, janus_videocodec_name(videoroom->vcodec[i]), h264_profile) != -1) {\n\t\t\t\t\t\t\t\t\t\t\t/* It is */\n\t\t\t\t\t\t\t\t\t\t\tvp9_profile = NULL;\n\t\t\t\t\t\t\t\t\t\t\tps->vcodec = videoroom->vcodec[i];\n\t\t\t\t\t\t\t\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\t\t\t\t\t\t\t\tps->h264_profile = g_strdup(h264_profile);\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t/* It isn't, fallback to checking whether H.264 is available without the profile */\n\t\t\t\t\t\t\t\t\t\th264_profile = NULL;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/* Check if the codec is available */\n\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, m->index, janus_videocodec_name(videoroom->vcodec[i])) != -1) {\n\t\t\t\t\t\t\t\t\t\t/* We'll only get the profile later, when we've generated an answer  */\n\t\t\t\t\t\t\t\t\t\tps->vcodec = videoroom->vcodec[i];\n\t\t\t\t\t\t\t\t\t\tps->pt = janus_videocodec_pt(ps->vcodec);\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/* Check if simulcast or SVC is in place */\n\t\t\t\t\t\t\tif(msg_simulcast != NULL && json_array_size(msg_simulcast) > 0) {\n\t\t\t\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\t\t\t\tfor(i=0; i<json_array_size(msg_simulcast); i++) {\n\t\t\t\t\t\t\t\t\tjson_t *s = json_array_get(msg_simulcast, i);\n\t\t\t\t\t\t\t\t\tint mindex = json_integer_value(json_object_get(s, \"mindex\"));\n\t\t\t\t\t\t\t\t\tif(mindex != ps->mindex)\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Publisher stream is going to do simulcasting (#%d, %s)\\n\", ps->mindex, ps->mid);\n\t\t\t\t\t\t\t\t\tps->simulcast = TRUE;\n\t\t\t\t\t\t\t\t\tjanus_mutex_lock(&ps->rid_mutex);\n\t\t\t\t\t\t\t\t\t/* Clear existing RIDs in case this is a renegotiation */\n\t\t\t\t\t\t\t\t\tjanus_rtp_simulcasting_cleanup(&ps->rid_extmap_id, NULL, ps->rid, NULL);\n\t\t\t\t\t\t\t\t\tjanus_rtp_simulcasting_prepare(s,\n\t\t\t\t\t\t\t\t\t\t&ps->rid_extmap_id,\n\t\t\t\t\t\t\t\t\t\tps->vssrc, ps->rid);\n\t\t\t\t\t\t\t\t\tjanus_mutex_unlock(&ps->rid_mutex);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if(msg_svc != NULL && json_array_size(msg_svc) > 0 &&\n\t\t\t\t\t\t\t\t\t(ps->vcodec == JANUS_VIDEOCODEC_VP9 || ps->vcodec == JANUS_VIDEOCODEC_AV1)) {\n\t\t\t\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\t\t\t\tfor(i=0; i<json_array_size(msg_svc); i++) {\n\t\t\t\t\t\t\t\t\tjson_t *s = json_array_get(msg_svc, i);\n\t\t\t\t\t\t\t\t\tint mindex = json_integer_value(json_object_get(s, \"mindex\"));\n\t\t\t\t\t\t\t\t\tif(mindex != ps->mindex)\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Publisher stream is going to do SVC (#%d, %s)\\n\", ps->mindex, ps->mid);\n\t\t\t\t\t\t\t\t\tps->svc = TRUE;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmdir = (ps->vcodec != JANUS_VIDEOCODEC_NONE ? JANUS_SDP_RECVONLY : JANUS_SDP_INACTIVE);\n\t\t\t\t\t\t} else if(m->type == JANUS_SDP_APPLICATION) {\n\t\t\t\t\t\t\tmdir = JANUS_SDP_RECVONLY;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tps->disabled = (m->direction == JANUS_SDP_RECVONLY || mdir == JANUS_SDP_INACTIVE);\n\t\t\t\t\t/* Add a new m-line to the answer */\n\t\t\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\t\tchar audio_fmtp[256];\n\t\t\t\t\t\taudio_fmtp[0] = '\\0';\n\t\t\t\t\t\tif(ps->opusfec)\n\t\t\t\t\t\t\tg_snprintf(audio_fmtp, sizeof(audio_fmtp), \"useinbandfec=1\");\n\t\t\t\t\t\tif(ps->opusdtx) {\n\t\t\t\t\t\t\tif(strlen(audio_fmtp) == 0) {\n\t\t\t\t\t\t\t\tg_snprintf(audio_fmtp, sizeof(audio_fmtp), \"usedtx=1\");\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjanus_strlcat(audio_fmtp, \";usedtx=1\", sizeof(audio_fmtp));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ps->opusstereo) {\n\t\t\t\t\t\t\tif(strlen(audio_fmtp) == 0) {\n\t\t\t\t\t\t\t\tg_snprintf(audio_fmtp, sizeof(audio_fmtp), \"stereo=1\");\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjanus_strlcat(audio_fmtp, \";stereo=1\", sizeof(audio_fmtp));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_sdp_generate_answer_mline(offer, answer, m,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, mdir,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_audiocodec_name(ps->acodec),\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_FMTP, (strlen(audio_fmtp) ? audio_fmtp : NULL),\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_RID,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_REPAIRED_RID,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, videoroom->audiolevel_ext ? JANUS_RTP_EXTMAP_AUDIO_LEVEL : NULL,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, videoroom->transport_wide_cc_ext ? JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC : NULL,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t\tjanus_sdp_mline *m_answer = janus_sdp_mline_find_by_index(answer, m->index);\n\t\t\t\t\t\tif(m_answer != NULL) {\n\t\t\t\t\t\t\t/* TODO Remove, this is just here for backwards compatibility */\n\t\t\t\t\t\t\tif(audiocodec == NULL)\n\t\t\t\t\t\t\t\taudiocodec = janus_audiocodec_name(ps->acodec);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(m->type == JANUS_SDP_VIDEO) {\n\t\t\t\t\t\tjanus_sdp_generate_answer_mline(offer, answer, m,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_VIDEO,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, mdir,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_CODEC, janus_videocodec_name(ps->vcodec),\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_VP9_PROFILE, vp9_profile,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_H264_PROFILE, h264_profile,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_RID,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_REPAIRED_RID,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_DEPENDENCY_DESC,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, videoroom->videoorient_ext ? JANUS_RTP_EXTMAP_VIDEO_ORIENTATION : NULL,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, videoroom->playoutdelay_ext ? JANUS_RTP_EXTMAP_PLAYOUT_DELAY : NULL,\n\t\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, videoroom->transport_wide_cc_ext ? JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC : NULL,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t\tjanus_sdp_mline *m_answer = janus_sdp_mline_find_by_index(answer, m->index);\n\t\t\t\t\t\tif(m_answer != NULL) {\n\t\t\t\t\t\t\t/* TODO Remove, this is just here for backwards compatibility */\n\t\t\t\t\t\t\tif(videocodec == NULL)\n\t\t\t\t\t\t\t\tvideocodec = janus_videocodec_name(ps->vcodec);\n\t\t\t\t\t\t\t/* Check if video profile has been set */\n\t\t\t\t\t\t\tif((ps->vcodec == JANUS_VIDEOCODEC_H264 && ps->h264_profile == NULL) || (ps->vcodec == JANUS_VIDEOCODEC_VP9 && ps->vp9_profile == NULL)) {\n\t\t\t\t\t\t\t\tint video_pt = janus_sdp_get_codec_pt(answer, m->index, janus_videocodec_name(ps->vcodec));\n\t\t\t\t\t\t\t\tconst char *vfmtp = janus_sdp_get_fmtp(answer, m->index, video_pt);\n\t\t\t\t\t\t\t\tif(vfmtp != NULL) {\n\t\t\t\t\t\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264)\n\t\t\t\t\t\t\t\t\t\tps->h264_profile = janus_sdp_get_video_profile(ps->vcodec, vfmtp);\n\t\t\t\t\t\t\t\t\telse if(ps->vcodec == JANUS_VIDEOCODEC_VP9)\n\t\t\t\t\t\t\t\t\t\tps->vp9_profile = janus_sdp_get_video_profile(ps->vcodec, vfmtp);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/* Also add a bandwidth SDP attribute if we're capping the bitrate in the room */\n\t\t\t\t\t\t\tif(videoroom->bitrate > 0 && videoroom->bitrate_cap) {\n\t\t\t\t\t\t\t\tif(participant->firefox) {\n\t\t\t\t\t\t\t\t\t/* Use TIAS (bps) instead of AS (kbps) for the b= attribute, as explained here:\n\t\t\t\t\t\t\t\t\t * https://github.com/meetecho/janus-gateway/issues/1277#issuecomment-397677746 */\n\t\t\t\t\t\t\t\t\tm->b_name = g_strdup(\"TIAS\");\n\t\t\t\t\t\t\t\t\tm->b_value = videoroom->bitrate;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tm->b_name = g_strdup(\"AS\");\n\t\t\t\t\t\t\t\t\tm->b_value = videoroom->bitrate/1000;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(m->type == JANUS_SDP_APPLICATION) {\n\t\t\t\t\t\tjanus_sdp_generate_answer_mline(offer, answer, m,\n\t\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_APPLICATION,\n\t\t\t\t\t\t\tJANUS_SDP_OA_ACCEPT_EXTMAP, JANUS_RTP_EXTMAP_MID,\n\t\t\t\t\t\t\tJANUS_SDP_OA_DONE);\n\t\t\t\t\t}\n\t\t\t\t\t/* Make sure we have a mid */\n\t\t\t\t\tif(ps->mid == NULL) {\n\t\t\t\t\t\tchar mid[5];\n\t\t\t\t\t\tg_snprintf(mid, sizeof(mid), \"%d\", ps->mindex);\n\t\t\t\t\t\tps->mid = g_strdup(mid);\n\t\t\t\t\t}\n\t\t\t\t\t/* Do we have a description as well? */\n\t\t\t\t\tif(descriptions != NULL && json_array_size(descriptions) > 0) {\n\t\t\t\t\t\tsize_t i = 0;\n\t\t\t\t\t\tfor(i=0; i<json_array_size(descriptions); i++) {\n\t\t\t\t\t\t\tjson_t *d = json_array_get(descriptions, i);\n\t\t\t\t\t\t\tconst char *d_mid = json_string_value(json_object_get(d, \"mid\"));\n\t\t\t\t\t\t\tconst char *d_desc = json_string_value(json_object_get(d, \"description\"));\n\t\t\t\t\t\t\tif(d_desc && d_mid && ps->mid && !strcasecmp(d_mid, ps->mid)) {\n\t\t\t\t\t\t\t\tps->description = g_strdup(d_desc);\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* Add the stream to the list, if it's new */\n\t\t\t\t\tif(new_ps) {\n\t\t\t\t\t\tparticipant->streams = g_list_append(participant->streams, ps);\n\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_DATA)\n\t\t\t\t\t\t\tparticipant->data_mindex = ps->mindex;\n\t\t\t\t\t\tg_hash_table_insert(participant->streams_byid, GINT_TO_POINTER(ps->mindex), ps);\n\t\t\t\t\t\tg_hash_table_insert(participant->streams_bymid, g_strdup(ps->mid), ps);\n\t\t\t\t\t\t/* Also check if this publisher is remotized, and in case\n\t\t\t\t\t\t * automatically create forwarders to the remote recipients */\n\t\t\t\t\t\tGHashTableIter iter;\n\t\t\t\t\t\tgpointer value;\n\t\t\t\t\t\tg_hash_table_iter_init(&iter, participant->remote_recipients);\n\t\t\t\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\t\t\t\tjanus_videoroom_remote_recipient *r = (janus_videoroom_remote_recipient *)value;\n\t\t\t\t\t\t\tjanus_rtp_forwarder *f = NULL;\n\t\t\t\t\t\t\tif(r) {\n\t\t\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\t\t\t\t\t/* Audio stream */\n\t\t\t\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(participant, ps,\n\t\t\t\t\t\t\t\t\t\tr->host, r->port, -1, 0,\n\t\t\t\t\t\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP),\n\t\t\t\t\t\t\t\t\t\tFALSE, r->srtp_suite, r->srtp_crypto, 0, FALSE, FALSE);\n\t\t\t\t\t\t\t\t\tif(f != NULL)\n\t\t\t\t\t\t\t\t\t\tf->metadata = g_strdup(r->remote_id);\n\t\t\t\t\t\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\t\t\t/* Video stream */\n\t\t\t\t\t\t\t\t\tgboolean add_rtcp = (!r->rtcp_added && r->rtcp_port > 0);\n\t\t\t\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(participant, ps,\n\t\t\t\t\t\t\t\t\t\tr->host, r->port, add_rtcp ? r->rtcp_port : -1, 0,\n\t\t\t\t\t\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP),\n\t\t\t\t\t\t\t\t\t\tFALSE, r->srtp_suite, r->srtp_crypto, 0, TRUE, FALSE);\n\t\t\t\t\t\t\t\t\tif(f != NULL)\n\t\t\t\t\t\t\t\t\t\tf->metadata = g_strdup(r->remote_id);\n\t\t\t\t\t\t\t\t\tif(add_rtcp)\n\t\t\t\t\t\t\t\t\t\tr->rtcp_added = TRUE;\n\t\t\t\t\t\t\t\t\t/* Check if there's simulcast substreams we need to relay too */\n\t\t\t\t\t\t\t\t\tif(ps->vssrc[1] || ps->rid[1]) {\n\t\t\t\t\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(participant, ps,\n\t\t\t\t\t\t\t\t\t\t\tr->host, r->port, -1, 0,\n\t\t\t\t\t\t\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP + 1),\n\t\t\t\t\t\t\t\t\t\t\tFALSE, r->srtp_suite, r->srtp_crypto, 1, TRUE, FALSE);\n\t\t\t\t\t\t\t\t\t\tif(f != NULL)\n\t\t\t\t\t\t\t\t\t\t\tf->metadata = g_strdup(r->remote_id);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif(ps->vssrc[2] || ps->rid[2]) {\n\t\t\t\t\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(participant, ps,\n\t\t\t\t\t\t\t\t\t\t\tr->host, r->port, -1, 0,\n\t\t\t\t\t\t\t\t\t\t\t(REMOTE_PUBLISHER_BASE_SSRC + ps->mindex*REMOTE_PUBLISHER_SSRC_STEP + 2),\n\t\t\t\t\t\t\t\t\t\t\tFALSE, r->srtp_suite, r->srtp_crypto, 2, TRUE, FALSE);\n\t\t\t\t\t\t\t\t\t\tif(f != NULL)\n\t\t\t\t\t\t\t\t\t\t\tf->metadata = g_strdup(r->remote_id);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t/* Data stream */\n\t\t\t\t\t\t\t\t\tf = janus_videoroom_rtp_forwarder_add_helper(participant, ps,\n\t\t\t\t\t\t\t\t\t\tr->host, r->port, 0, 0, 0, FALSE, 0, NULL, 0, FALSE, TRUE);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t/* Add to the info we send back to the publisher */\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(janus_videoroom_media_str(ps->type)));\n\t\t\t\t\tjson_object_set_new(info, \"mindex\", json_integer(ps->mindex));\n\t\t\t\t\tjson_object_set_new(info, \"mid\", json_string(ps->mid));\n\t\t\t\t\tif(ps->disabled) {\n\t\t\t\t\t\tjson_object_set_new(info, \"disabled\", json_true());\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif(ps->description)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"description\", json_string(ps->description));\n\t\t\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_AUDIO) {\n\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_audiocodec_name(ps->acodec)));\n\t\t\t\t\t\t\tif(ps->acodec == JANUS_AUDIOCODEC_OPUS) {\n\t\t\t\t\t\t\t\tif(ps->opusstereo)\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"stereo\", json_true());\n\t\t\t\t\t\t\t\tif(ps->opusfec)\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"fec\", json_true());\n\t\t\t\t\t\t\t\tif(ps->opusdtx)\n\t\t\t\t\t\t\t\t\tjson_object_set_new(info, \"dtx\", json_true());\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO) {\n\t\t\t\t\t\t\tjson_object_set_new(info, \"codec\", json_string(janus_videocodec_name(ps->vcodec)));\n\t\t\t\t\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_H264 && ps->h264_profile != NULL)\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"h264_profile\", json_string(ps->h264_profile));\n\t\t\t\t\t\t\telse if(ps->vcodec == JANUS_VIDEOCODEC_VP9 && ps->vp9_profile != NULL)\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"vp9_profile\", json_string(ps->vp9_profile));\n\t\t\t\t\t\t\tif(ps->simulcast)\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"simulcast\", json_true());\n\t\t\t\t\t\t\tif(ps->svc)\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"svc\", json_true());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ps->audio_level_extmap_id > 0)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"audiolevel_ext_id\", json_integer(ps->audio_level_extmap_id));\n\t\t\t\t\t\tif(ps->video_orient_extmap_id > 0)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"videoorient_ext_id\", json_integer(ps->video_orient_extmap_id));\n\t\t\t\t\t\tif(ps->playout_delay_extmap_id > 0)\n\t\t\t\t\t\t\tjson_object_set_new(info, \"playoutdelay_ext_id\", json_integer(ps->playout_delay_extmap_id));\n\t\t\t\t\t}\n\t\t\t\t\tjson_array_append_new(media, info);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\tjanus_mutex_unlock(&participant->rtp_forwarders_mutex);\n\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\t/* Replace the session name */\n\t\t\t\tg_free(answer->s_name);\n\t\t\t\tchar s_name[100];\n\t\t\t\tg_snprintf(s_name, sizeof(s_name), \"VideoRoom %s\", videoroom->room_id_str);\n\t\t\t\tanswer->s_name = g_strdup(s_name);\n\t\t\t\t/* Generate an SDP string we can send back to the publisher */\n\t\t\t\tchar *answer_sdp = janus_sdp_write(answer);\n\t\t\t\tjanus_sdp_destroy(answer);\n\t\t\t\t/* For backwards compatibility, update the event with info on the codecs that we'll be handling\n\t\t\t\t * TODO This will make no sense in the future, as different streams may use different codecs */\n\t\t\t\tif(event) {\n\t\t\t\t\tif(audiocodec)\n\t\t\t\t\t\tjson_object_set_new(event, \"audio_codec\", json_string(audiocodec));\n\t\t\t\t\tif(videocodec)\n\t\t\t\t\t\tjson_object_set_new(event, \"video_codec\", json_string(videocodec));\n\t\t\t\t}\n\t\t\t\tjson_object_set_new(event, \"streams\", media);\n\t\t\t\t/* Is this room recorded, or are we recording this publisher already? */\n\t\t\t\tjanus_mutex_lock(&participant->rec_mutex);\n\t\t\t\tif(videoroom->record || participant->recording_active) {\n\t\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\t\tGList *temp = participant->streams;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\t\tjanus_videoroom_recorder_create(ps);\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t\tparticipant->recording_active = TRUE;\n\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&participant->rec_mutex);\n\t\t\t\t/* Send the answer back to the publisher */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Handling publisher: turned this into an '%s':\\n%s\\n\", type, answer_sdp);\n\t\t\t\tjson_t *jsep = json_pack(\"{ssss}\", \"type\", type, \"sdp\", answer_sdp);\n\t\t\t\tg_free(answer_sdp);\n\t\t\t\tif(e2ee)\n\t\t\t\t\tparticipant->e2ee = TRUE;\n\t\t\t\tif(participant->e2ee) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Publisher is going to do end-to-end media encryption\\n\");\n\t\t\t\t\tjson_object_set_new(jsep, \"e2ee\", json_true());\n\t\t\t\t}\n\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\tg_atomic_int_set(&session->hangingup, 0);\n\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\tint res = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, jsep);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\t/* If this is an update/renegotiation, notify participants about this */\n\t\t\t\tif(sdp_update && g_atomic_int_get(&session->started)) {\n\t\t\t\t\t/* Notify all other participants this publisher's media has changed */\n\t\t\t\t\tjanus_mutex_lock(&videoroom->mutex);\n\t\t\t\t\tjanus_mutex_lock(&participant->streams_mutex);\n\t\t\t\t\tjanus_videoroom_notify_about_publisher(participant, TRUE);\n\t\t\t\t\tjanus_mutex_unlock(&participant->streams_mutex);\n\t\t\t\t\tjanus_mutex_unlock(&videoroom->mutex);\n\t\t\t\t}\n\t\t\t\t/* Done */\n\t\t\t\tif(res != JANUS_OK) {\n\t\t\t\t\t/* TODO Failed to negotiate? We should remove this publisher */\n\t\t\t\t} else {\n\t\t\t\t\t/* We'll wait for the setup_media event before actually telling subscribers */\n\t\t\t\t}\n\t\t\t\tjanus_refcount_decrease(&videoroom->ref);\n\t\t\t\tjson_decref(event);\n\t\t\t\tjson_decref(jsep);\n\t\t\t}\n\t\t\tif(participant != NULL)\n\t\t\t\tjanus_refcount_decrease(&participant->ref);\n\t\t}\n\t\tif(subscriber != NULL)\n\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\tjanus_videoroom_message_free(msg);\n\n\t\tcontinue;\n\nerror:\n\t\t{\n\t\t\t/* Prepare JSON error event */\n\t\t\tjson_t *event = json_object();\n\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\tjson_object_set_new(event, \"error_code\", json_integer(error_code));\n\t\t\tjson_object_set_new(event, \"error\", json_string(error_cause));\n\t\t\tint ret = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, NULL);\n\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (%s)\\n\", ret, janus_get_api_error(ret));\n\t\t\tjson_decref(event);\n\t\t\tjanus_videoroom_message_free(msg);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Leaving VideoRoom handler thread\\n\");\n\treturn NULL;\n}\n\n/* Helper to quickly relay RTP packets from publishers to subscribers */\nstatic void janus_videoroom_relay_rtp_packet(gpointer data, gpointer user_data) {\n\tjanus_videoroom_rtp_relay_packet *packet = (janus_videoroom_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_videoroom_subscriber_stream *stream = (janus_videoroom_subscriber_stream *)data;\n\tif(!stream || !g_atomic_int_get(&stream->ready) || g_atomic_int_get(&stream->destroyed) ||\n\t\t\t!stream->send || !stream->publisher_streams ||\n\t\t\t!stream->subscriber || stream->subscriber->paused || stream->subscriber->kicked ||\n\t\t\t!stream->subscriber->session || !stream->subscriber->session->handle ||\n\t\t\t!g_atomic_int_get(&stream->subscriber->session->started))\n\t\treturn;\n\tjanus_videoroom_publisher_stream *ps = stream->publisher_streams ?\n\t\tstream->publisher_streams->data : NULL;\n\tif(ps != packet->source || ps == NULL)\n\t\treturn;\n\tjanus_videoroom_subscriber *subscriber = stream->subscriber;\n\tjanus_videoroom_session *session = subscriber->session;\n\n\t/* Make sure there hasn't been a publisher switch by checking the SSRC */\n\tif(packet->is_video) {\n\t\t/* Check if there's any SVC info to take into account */\n\t\tif(packet->svc) {\n\t\t\t/* Handle SVC: make sure we have a payload to work with */\n\t\t\tint plen = 0;\n\t\t\tchar *payload = janus_rtp_payload((char *)packet->data, packet->length, &plen);\n\t\t\tif(payload == NULL)\n\t\t\t\treturn;\n\t\t\t/* Process this packet: don't relay if it's not the layer we wanted to handle */\n\t\t\tchar rtph[12];\n\t\t\tmemcpy(&rtph, packet->data, sizeof(rtph));\n\t\t\tgboolean relay = janus_rtp_svc_context_process_rtp(&stream->svc_context,\n\t\t\t\t(char *)packet->data, packet->length, packet->extensions.dd_content, packet->extensions.dd_len,\n\t\t\t\tps->vcodec, &packet->svc_info, &stream->context);\n\t\t\tif(stream->svc_context.need_pli) {\n\t\t\t\t/* Send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the SVC context\\n\");\n\t\t\t\tjanus_videoroom_reqpli(ps, \"SVC change\");\n\t\t\t}\n\t\t\t/* Do we need to drop this? */\n\t\t\tif(!relay)\n\t\t\t\treturn;\n\t\t\t/* Any event we should notify? */\n\t\t\tif(stream->svc_context.changed_spatial) {\n\t\t\t\t/* Notify the user about the spatial layer change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\tjson_object_set_new(event, \"spatial_layer\", json_integer(stream->svc_context.spatial));\n\t\t\t\tgateway->push_event(subscriber->session->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\tif(stream->svc_context.changed_temporal) {\n\t\t\t\t/* Notify the user about the temporal layer change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\tjson_object_set_new(event, \"temporal_layer\", json_integer(stream->svc_context.temporal));\n\t\t\t\tgateway->push_event(subscriber->session->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\t/* If we got here, update the RTP header and send the packet */\n\t\t\tjanus_rtp_header_update(packet->data, &stream->context, TRUE, 0);\n\t\t\t/* Send the packet */\n\t\t\tif(gateway != NULL) {\n\t\t\t\tjanus_plugin_rtp rtp = { .mindex = stream->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length,\n\t\t\t\t\t.extensions = packet->extensions };\n\t\t\t\tif(stream->min_delay > -1 && stream->max_delay > -1) {\n\t\t\t\t\trtp.extensions.min_delay = stream->min_delay;\n\t\t\t\t\trtp.extensions.max_delay = stream->max_delay;\n\t\t\t\t}\n\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t}\n\t\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\t\tmemcpy(packet->data, &rtph, sizeof(rtph));\n\t\t} else if(packet->simulcast) {\n\t\t\t/* Handle simulcast: make sure we have a payload to work with */\n\t\t\tint plen = 0;\n\t\t\tchar *payload = janus_rtp_payload((char *)packet->data, packet->length, &plen);\n\t\t\tif(payload == NULL)\n\t\t\t\treturn;\n\t\t\t/* Process this packet: don't relay if it's not the SSRC/layer we wanted to handle */\n\t\t\tgboolean relay = janus_rtp_simulcasting_context_process_rtp(&stream->sim_context,\n\t\t\t\t(char *)packet->data, packet->length, packet->extensions.dd_content, packet->extensions.dd_len,\n\t\t\t\tpacket->ssrc, NULL, ps->vcodec, &stream->context, &ps->rid_mutex);\n\t\t\tif(!relay) {\n\t\t\t\t/* Did a lot of time pass before we could relay a packet? */\n\t\t\t\tgint64 now = janus_get_monotonic_time();\n\t\t\t\tif((now - stream->sim_context.last_relayed) >= G_USEC_PER_SEC) {\n\t\t\t\t\tg_atomic_int_set(&stream->sim_context.need_pli, 1);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(stream->sim_context.need_pli) {\n\t\t\t\t/* Send a PLI */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"We need a PLI for the simulcast context\\n\");\n\t\t\t\tjanus_videoroom_reqpli(ps, \"Simulcast change\");\n\t\t\t}\n\t\t\t/* Do we need to drop this? */\n\t\t\tif(!relay)\n\t\t\t\treturn;\n\t\t\t/* Any event we should notify? */\n\t\t\tif(stream->sim_context.changed_substream) {\n\t\t\t\t/* Notify the user about the substream change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\tjson_object_set_new(event, \"substream\", json_integer(stream->sim_context.substream));\n\t\t\t\tgateway->push_event(subscriber->session->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\tif(stream->sim_context.changed_temporal) {\n\t\t\t\t/* Notify the user about the temporal layer change */\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"event\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ? json_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_object_set_new(event, \"mid\", json_string(stream->mid));\n\t\t\t\tjson_object_set_new(event, \"temporal\", json_integer(stream->sim_context.templayer));\n\t\t\t\tgateway->push_event(subscriber->session->handle, &janus_videoroom_plugin, NULL, event, NULL);\n\t\t\t\tjson_decref(event);\n\t\t\t}\n\t\t\t/* If we got here, update the RTP header and send the packet */\n\t\t\tjanus_rtp_header_update(packet->data, &stream->context, TRUE, 0);\n\t\t\tchar vp8pd[6];\n\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t\t/* For VP8, we save the original payload descriptor, to restore it after */\n\t\t\t\tmemcpy(vp8pd, payload, sizeof(vp8pd));\n\t\t\t\tjanus_vp8_simulcast_descriptor_update(payload, plen, &stream->vp8_context,\n\t\t\t\t\tstream->sim_context.changed_substream);\n\t\t\t}\n\t\t\t/* Send the packet */\n\t\t\tif(gateway != NULL) {\n\t\t\t\tjanus_plugin_rtp rtp = { .mindex = stream->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length,\n\t\t\t\t\t.extensions = packet->extensions };\n\t\t\t\tif(stream->min_delay > -1 && stream->max_delay > -1) {\n\t\t\t\t\trtp.extensions.min_delay = stream->min_delay;\n\t\t\t\t\trtp.extensions.max_delay = stream->max_delay;\n\t\t\t\t}\n\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t}\n\t\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\t\tif(ps->vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t\t\t/* Restore the original payload descriptor as well, as it will be needed by the next viewer */\n\t\t\t\tmemcpy(payload, vp8pd, sizeof(vp8pd));\n\t\t\t}\n\t\t} else {\n\t\t\t/* Fix sequence number and timestamp (publisher switching may be involved) */\n\t\t\tjanus_rtp_header_update(packet->data, &stream->context, TRUE, 0);\n\t\t\t/* Send the packet */\n\t\t\tif(gateway != NULL) {\n\t\t\t\tjanus_plugin_rtp rtp = { .mindex = stream->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length,\n\t\t\t\t\t.extensions = packet->extensions };\n\t\t\t\tif(stream->min_delay > -1 && stream->max_delay > -1) {\n\t\t\t\t\trtp.extensions.min_delay = stream->min_delay;\n\t\t\t\t\trtp.extensions.max_delay = stream->max_delay;\n\t\t\t\t}\n\t\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t\t}\n\t\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t\t}\n\t} else {\n\t\t/* Fix sequence number and timestamp (publisher switching may be involved) */\n\t\tjanus_rtp_header_update(packet->data, &stream->context, FALSE, 0);\n\t\t/* Send the packet */\n\t\tif(gateway != NULL) {\n\t\t\tjanus_plugin_rtp rtp = { .mindex = stream->mindex, .video = packet->is_video, .buffer = (char *)packet->data, .length = packet->length,\n\t\t\t\t.extensions = packet->extensions };\n\t\t\tgateway->relay_rtp(session->handle, &rtp);\n\t\t}\n\t\t/* Restore the timestamp and sequence number to what the publisher set them to */\n\t\tpacket->data->timestamp = htonl(packet->timestamp);\n\t\tpacket->data->seq_number = htons(packet->seq_number);\n\t}\n\n\treturn;\n}\n\nstatic void janus_videoroom_relay_data_packet(gpointer data, gpointer user_data) {\n\tjanus_videoroom_rtp_relay_packet *packet = (janus_videoroom_rtp_relay_packet *)user_data;\n\tif(!packet || packet->is_rtp || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_videoroom_subscriber_stream *stream = (janus_videoroom_subscriber_stream *)data;\n\tif(!stream || !g_atomic_int_get(&stream->ready) || g_atomic_int_get(&stream->destroyed) ||\n\t\t\t!stream->send || !stream->publisher_streams ||\n\t\t\t!stream->subscriber || stream->subscriber->paused || stream->subscriber->kicked ||\n\t\t\t!stream->subscriber->session || !stream->subscriber->session->handle ||\n\t\t\t!g_atomic_int_get(&stream->subscriber->session->started) ||\n\t\t\t!g_atomic_int_get(&stream->subscriber->session->dataready))\n\t\treturn;\n\tjanus_videoroom_publisher_stream *ps = packet->source;\n\tif(ps->publisher == NULL || g_slist_find(stream->publisher_streams, ps) == NULL)\n\t\treturn;\n\tjanus_videoroom_subscriber *subscriber = stream->subscriber;\n\tjanus_videoroom_session *session = subscriber->session;\n\n\tif(gateway != NULL && packet->data != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Forwarding %s DataChannel message (%d bytes) to viewer\\n\",\n\t\t\tpacket->textdata ? \"text\" : \"binary\", packet->length);\n\t\tjanus_plugin_data data = {\n\t\t\t.label = ps->publisher->user_id_str,\n\t\t\t.protocol = NULL,\n\t\t\t.binary = !packet->textdata,\n\t\t\t.buffer = (char *)packet->data,\n\t\t\t.length = packet->length\n\t\t};\n\t\tgateway->relay_data(session->handle, &data);\n\t}\n\treturn;\n}\n\n/* The following methods are only relevant if RTCP is used for RTP forwarders */\nstatic void janus_videoroom_rtp_forwarder_rtcp_receive(janus_rtp_forwarder *rf, char *buffer, int len) {\n\tif(len > 0 && janus_is_rtcp(buffer, len)) {\n\t\tJANUS_LOG(LOG_HUGE, \"Got %s RTCP packet: %d bytes\\n\", rf->is_video ? \"video\" : \"audio\", len);\n\t\t/* We only handle incoming video PLIs or FIR at the moment */\n\t\tif(!janus_rtcp_has_fir(buffer, len) && !janus_rtcp_has_pli(buffer, len))\n\t\t\treturn;\n\t\t/* Check if this is a regular RTP forwarder, or a publisher remotization */\n\t\tif(rf->metadata == NULL) {\n\t\t\t/* Regular forwarder, send the PLI to the stream associated with it */\n\t\t\tjanus_videoroom_reqpli((janus_videoroom_publisher_stream *)rf->source, \"RTCP from forwarder\");\n\t\t} else {\n\t\t\t/* Remotization, check the SSRC in the request so that we know\n\t\t\t * which publisher video stream we should send the PLI to */\n\t\t\tuint32_t ssrc = 0;\n\t\t\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)buffer;\n\t\t\tint total = len;\n\t\t\twhile(rtcp && ssrc == 0) {\n\t\t\t\tif(!janus_rtcp_check_len(rtcp, total))\n\t\t\t\t\treturn;\t\t/* Invalid RTCP packet */\n\t\t\t\tif(rtcp->version != 2)\n\t\t\t\t\treturn;\t\t/* Invalid RTCP packet */\n\t\t\t\tswitch(rtcp->type) {\n\t\t\t\t\tcase RTCP_PSFB: {\n\t\t\t\t\t\tgint fmt = rtcp->rc;\n\t\t\t\t\t\tif(fmt == 1) {\n\t\t\t\t\t\t\tif(!janus_rtcp_check_fci(rtcp, total, 0))\n\t\t\t\t\t\t\t\treturn;\t\t/* Invalid RTCP packet */\n\t\t\t\t\t\t\t/* TODO */\n\t\t\t\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\t\t\t\tssrc = ntohl(rtcpfb->media);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t/* Is this a compound packet? */\n\t\t\t\tint length = ntohs(rtcp->length);\n\t\t\t\tif(length == 0)\n\t\t\t\t\tbreak;\n\t\t\t\ttotal -= length*4+4;\n\t\t\t\tif(total <= 0)\n\t\t\t\t\tbreak;\n\t\t\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t\t\t}\n\t\t\tif(ssrc > 0) {\n\t\t\t\t/* Look for the right publisher stream instance */\n\t\t\t\tchar *remote_id = (char *)rf->metadata;\n\t\t\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)rf->source;\n\t\t\t\tif(ps == NULL)\n\t\t\t\t\treturn;\n\t\t\t\tjanus_videoroom_publisher *p = ps->publisher;\n\t\t\t\tif(p == NULL || g_atomic_int_get(&p->destroyed))\n\t\t\t\t\treturn;\n\t\t\t\tjanus_mutex_lock(&p->streams_mutex);\n\t\t\t\tjanus_mutex_lock(&p->rtp_forwarders_mutex);\n\t\t\t\tif(g_hash_table_size(p->rtp_forwarders) == 0) {\n\t\t\t\t\tjanus_mutex_unlock(&p->rtp_forwarders_mutex);\n\t\t\t\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tgboolean found = FALSE;\n\t\t\t\tGList *temp = p->streams;\n\t\t\t\twhile(temp && !found) {\n\t\t\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t\t\tjanus_mutex_lock(&ps->rtp_forwarders_mutex);\n\t\t\t\t\tif(g_hash_table_size(ps->rtp_forwarders) == 0) {\n\t\t\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tGHashTableIter iter_f;\n\t\t\t\t\tgpointer key_f, value_f;\n\t\t\t\t\tg_hash_table_iter_init(&iter_f, ps->rtp_forwarders);\n\t\t\t\t\twhile(g_hash_table_iter_next(&iter_f, &key_f, &value_f)) {\n\t\t\t\t\t\tjanus_rtp_forwarder *rpv = value_f;\n\t\t\t\t\t\t/* We only care about video forwarders used for the same remotization */\n\t\t\t\t\t\tif(!rpv->is_video || rpv->metadata == NULL || strcasecmp((char *)rpv->metadata, remote_id))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t/* Check the SSRC */\n\t\t\t\t\t\tif(rpv->ssrc == ssrc) {\n\t\t\t\t\t\t\tfound = TRUE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&ps->rtp_forwarders_mutex);\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&p->rtp_forwarders_mutex);\n\t\t\t\tjanus_mutex_unlock(&p->streams_mutex);\n\t\t\t\tif(found)\n\t\t\t\t\tjanus_videoroom_reqpli(ps, \"RTCP from remotized forwarder\");\n\t\t\t}\n\t\t}\n\t}\n}\n\n/* Helpers to create a listener filedescriptor */\nstatic int janus_videoroom_create_fd(int port, in_addr_t mcast, const janus_network_address *iface, char *host, size_t hostlen) {\n\tjanus_mutex_lock(&fd_mutex);\n\tstruct sockaddr_in address = { 0 };\n\tstruct sockaddr_in6 address6 = { 0 };\n\tjanus_network_address_string_buffer address_representation;\n\n\tuint16_t rtp_port_next = rtp_range_slider; \t\t\t\t\t/* Read global slider */\n\tuint16_t rtp_port_start = rtp_port_next;\n\tgboolean use_range = (port == 0), rtp_port_wrap = FALSE;\n\n\tint fd = -1, family = 0;\n\twhile(1) {\n\t\t/* By default, we bind to both IPv4 and IPv6, unless IPv6 is disabled */\n\t\tfamily = ipv6_disabled ? AF_INET : 0;\n\t\tif(use_range && rtp_port_wrap && rtp_port_next >= rtp_port_start) {\n\t\t\t/* Full range scanned */\n\t\t\tJANUS_LOG(LOG_ERR, \"No ports available for RTP/RTCP in range: %u -- %u\\n\",\n\t\t\t\t  rtp_range_min, rtp_range_max);\n\t\t\tbreak;\n\t\t}\n\t\tif(!use_range) {\n\t\t\t/* Use the port specified in the arguments */\n\t\t\tif(IN_MULTICAST(ntohl(mcast))) {\n\t\t\t\tfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\t\tif(fd < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Cannot create socket for remote publisher... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\t\tbreak;\n\t\t\t\t}\n#ifdef IP_MULTICAST_ALL\n\t\t\t\tint mc_all = 0;\n\t\t\t\tif((setsockopt(fd, IPPROTO_IP, IP_MULTICAST_ALL, (void*) &mc_all, sizeof(mc_all))) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"setsockopt IP_MULTICAST_ALL failed... %d (%s)\\n\",\n\t\t\t\t\t\terrno, g_strerror(errno));\n\t\t\t\t\tclose(fd);\n\t\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n#endif\n\t\t\t\tstruct ip_mreq mreq;\n\t\t\t\tmemset(&mreq, '\\0', sizeof(mreq));\n\t\t\t\tmreq.imr_multiaddr.s_addr = mcast;\n\t\t\t\tif(!janus_network_address_is_null(iface)) {\n\t\t\t\t\tfamily = AF_INET;\n\t\t\t\t\tif(iface->family == AF_INET) {\n\t\t\t\t\t\tmreq.imr_interface = iface->ipv4;\n\t\t\t\t\t\t(void) janus_network_address_to_string_buffer(iface, &address_representation); /* This is OK: if we get here iface must be non-NULL */\n\t\t\t\t\t\tchar *maddr = inet_ntoa(mreq.imr_multiaddr);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Remote publisher using interface address: %s (%s)\\n\",\n\t\t\t\t\t\t\tjanus_network_address_string_from_buffer(&address_representation), maddr);\n\t\t\t\t\t\tif(maddr && host && hostlen > 0)\n\t\t\t\t\t\t\tg_strlcpy(host, maddr, hostlen);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid multicast address type (only IPv4 multicast is currently supported by this plugin)\\n\");\n\t\t\t\t\t\tclose(fd);\n\t\t\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\t\t\treturn -1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No multicast interface: this may not work as expected if you have multiple network devices (NICs)\\n\");\n\t\t\t\t}\n\t\t\t\tif(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"IP_ADD_MEMBERSHIP failed... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\t\tclose(fd);\n\t\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t/* Pick a port in the configured range */\n\t\t\tport = rtp_port_next;\n\t\t\tif((uint32_t)(rtp_port_next) < rtp_range_max) {\n\t\t\t\trtp_port_next++;\n\t\t\t} else {\n\t\t\t\trtp_port_next = rtp_range_min;\n\t\t\t\trtp_port_wrap = TRUE;\n\t\t\t}\n\t\t}\n\t\taddress.sin_family = AF_INET;\n\t\taddress.sin_port = htons(port);\n\t\taddress.sin_addr.s_addr = INADDR_ANY;\n\t\taddress6.sin6_family = AF_INET6;\n\t\taddress6.sin6_port = htons(port);\n\t\taddress6.sin6_addr = in6addr_any;\n\t\t/* If this is multicast, allow a re-use of the same ports (different groups may be used) */\n\t\tif(!use_range && IN_MULTICAST(ntohl(mcast))) {\n\t\t\tint reuse = 1;\n\t\t\tif(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"setsockopt SO_REUSEADDR failed... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\tclose(fd);\n\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\t/* TODO IPv6 */\n\t\t\tfamily = AF_INET;\n\t\t\taddress.sin_addr.s_addr = mcast;\n\t\t} else {\n\t\t\tif(!IN_MULTICAST(ntohl(mcast)) && !janus_network_address_is_null(iface)) {\n\t\t\t\tfamily = iface->family;\n\t\t\t\tif(iface->family == AF_INET) {\n\t\t\t\t\taddress.sin_addr = iface->ipv4;\n\t\t\t\t\t(void) janus_network_address_to_string_buffer(iface, &address_representation); /* This is OK: if we get here iface must be non-NULL */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Remote publisher restricted to interface address: %s\\n\",\n\t\t\t\t\t\tjanus_network_address_string_from_buffer(&address_representation));\n\t\t\t\t\tif(host && hostlen > 0)\n\t\t\t\t\t\tg_strlcpy(host, janus_network_address_string_from_buffer(&address_representation), hostlen);\n\t\t\t\t} else if(iface->family == AF_INET6) {\n\t\t\t\t\tif(ipv6_disabled) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Can't bind remote publisher to IPv6 address, IPv6 is disabled\\n\");\n\t\t\t\t\t\tclose(fd);\n\t\t\t\t\t\tjanus_mutex_unlock(&fd_mutex);\n\t\t\t\t\t\treturn -1;\n\t\t\t\t\t}\n\t\t\t\t\tmemcpy(&address6.sin6_addr, &iface->ipv6, sizeof(iface->ipv6));\n\t\t\t\t\t(void) janus_network_address_to_string_buffer(iface, &address_representation); /* This is OK: if we get here iface must be non-NULL */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Remote publisher restricted to interface address: %s\\n\",\n\t\t\t\t\t\tjanus_network_address_string_from_buffer(&address_representation));\n\t\t\t\t\tif(host && hostlen > 0)\n\t\t\t\t\t\tg_strlcpy(host, janus_network_address_string_from_buffer(&address_representation), hostlen);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid address/restriction type\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Bind to the specified port */\n\t\tif(fd == -1) {\n\t\t\tfd = socket(family == AF_INET ? AF_INET : AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\t\t\tint v6only = 0;\n\t\t\tif(fd < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Cannot create socket for remote publisher... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(family != AF_INET && setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"setsockopt on socket failed... %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tsize_t addrlen = (family == AF_INET ? sizeof(address) : sizeof(address6));\n\t\tif(bind(fd, (family == AF_INET ? (struct sockaddr *)&address : (struct sockaddr *)&address6), addrlen) < 0) {\n\t\t\tclose(fd);\n\t\t\tfd = -1;\n\t\t\tif(!use_range) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Bind failed (port %d)... %d (%s)\\n\", port, errno, g_strerror(errno));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\tif(use_range)\n\t\t\t\trtp_range_slider = port;\t/* Update global slider */\n\t\t\tbreak;\n\t\t}\n\t}\n\tjanus_mutex_unlock(&fd_mutex);\n\treturn fd;\n}\n/* Helper to return fd port */\nstatic int janus_videoroom_get_fd_port(int fd) {\n\tstruct sockaddr_in6 server = { 0 };\n\tsocklen_t len = sizeof(server);\n\tif(getsockname(fd, (struct sockaddr *)&server, &len) == -1) {\n\t\treturn -1;\n\t}\n\treturn ntohs(server.sin6_port);\n}\n/* Thread responsible for a specific remote publisher */\nstatic void *janus_videoroom_remote_publisher_thread(void *user_data) {\n\tjanus_videoroom_publisher *publisher = (janus_videoroom_publisher *)user_data;\n\tif(publisher == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid publisher instance\\n\");\n\t\tg_thread_unref(g_thread_self());\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%s/%s] Joining remote publisher thread...\\n\",\n\t\tpublisher->room->room_id_str, publisher->user_id_str);\n\n\tjanus_videoroom *videoroom = publisher->room;\n\tjanus_refcount_increase(&videoroom->ref);\n\tjanus_refcount_increase(&publisher->ref);\n\tjanus_refcount_increase(&publisher->session->ref);\n\n\t/* File descriptors */\n\tsocklen_t addrlen;\n\tstruct sockaddr_storage remote = { 0 };\n\tint resfd = 0, bytes = 0;\n\tstruct pollfd fds[3];\n\tint pipe_fd = publisher->pipefd[0];\n\tchar buffer[1500];\n\tmemset(buffer, 0, 1500);\n\tif(pipe_fd == -1) {\n\t\t/* If the pipe file descriptor doesn't exist, it means we're done already,\n\t\t * and/or we may never be notified about sessions being closed, so give up */\n\t\tJANUS_LOG(LOG_WARN, \"[%s/%s] Leaving remote publisher thread, no pipe file descriptor...\\n\",\n\t\t\tpublisher->room->room_id_str, publisher->user_id_str);\n\t\tjanus_videoroom_publisher_dereference(publisher);\n\t\tgoto cleanup;\n\t}\n\n\t/* RTP stuff */\n\tjanus_rtp_header *rtp = NULL;\n\tuint32_t ssrc = 0, diff = 0;\n\tint mindex = 0, vindex = 0;\n\tjanus_videoroom_publisher_stream *ps = NULL;\n\tjanus_plugin_rtp pkt = { 0 };\n\tjanus_plugin_data data = { 0 };\n\tGList *temp = NULL;\n\n\t/* As the first thing, we add the remote publisher to the list */\n\tjanus_mutex_lock(&videoroom->mutex);\n\tg_hash_table_insert(videoroom->participants,\n\t\tstring_ids ? (gpointer)g_strdup(publisher->user_id_str) : (gpointer)janus_uint64_dup(publisher->user_id),\n\t\tpublisher);\n\t/* Let's also notify all other participants that the publisher is here */\n\tjanus_mutex_lock(&publisher->streams_mutex);\n\tjanus_videoroom_notify_about_publisher(publisher, FALSE);\n\tjanus_mutex_unlock(&publisher->streams_mutex);\n\tjanus_mutex_unlock(&videoroom->mutex);\n\n\t/* Loop */\n\tint num = 0, i = 0;\n\twhile(!g_atomic_int_get(&publisher->remote_leaving) && !g_atomic_int_get(&publisher->destroyed) && !g_atomic_int_get(&videoroom->destroyed)) {\n\t\t/* Prepare poll */\n\t\tnum = 0;\n\t\tif(publisher->remote_fd != -1) {\n\t\t\tfds[num].fd = publisher->remote_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tif(publisher->remote_rtcp_fd != -1) {\n\t\t\tfds[num].fd = publisher->remote_rtcp_fd;\n\t\t\tfds[num].events = POLLIN;\n\t\t\tfds[num].revents = 0;\n\t\t\tnum++;\n\t\t}\n\t\tpipe_fd = publisher->pipefd[0];\n\t\tif(pipe_fd == -1) {\n\t\t\t/* Pipe was closed? Means the call is over */\n\t\t\tbreak;\n\t\t}\n\t\tfds[num].fd = pipe_fd;\n\t\tfds[num].events = POLLIN;\n\t\tfds[num].revents = 0;\n\t\tnum++;\n\t\t/* Check if we need to send any PLI */\n\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\ttemp = publisher->streams;\n\t\twhile(temp) {\n\t\t\tps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t\t/* Any PLI and/or REMB we should send back to the source? */\n\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO && g_atomic_int_get(&ps->need_pli))\n\t\t\t\tjanus_videoroom_reqpli(ps, \"Delayed PLI request\");\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t/* Wait for some data */\n\t\tresfd = poll(fds, num, 1000);\n\t\tif(resfd < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s/%s] Got an EINTR (%s), ignoring...\\n\",\n\t\t\t\t\tvideoroom->room_id_str, publisher->user_id_str, g_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s/%s] Error polling...\\n\", videoroom->room_id_str, publisher->user_id_str);\n\t\t\tJANUS_LOG(LOG_ERR, \"[%s/%s]   -- %d (%s)\\n\",\n\t\t\t\tvideoroom->room_id_str, publisher->user_id_str, errno, g_strerror(errno));\n\t\t\tbreak;\n\t\t} else if(resfd == 0) {\n\t\t\t/* No data, keep going */\n\t\t\tcontinue;\n\t\t}\n\t\tif(g_atomic_int_get(&publisher->remote_leaving) || g_atomic_int_get(&publisher->destroyed))\n\t\t\tbreak;\n\t\tfor(i=0; i<num; i++) {\n\t\t\tif(fds[i].revents & (POLLERR | POLLHUP)) {\n\t\t\t\t/* Socket error? */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s/%s] Error polling: %s... %d (%s)\\n\",\n\t\t\t\t\tvideoroom->room_id_str, publisher->user_id_str,\n\t\t\t\t\tfds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\", errno, g_strerror(errno));\n\t\t\t\tbreak;\n\t\t\t} else if(fds[i].revents & POLLIN) {\n\t\t\t\tif(pipe_fd != -1 && fds[i].fd == pipe_fd) {\n\t\t\t\t\t/* Poll interrupted for a reason, go on */\n\t\t\t\t\tint code = 0;\n\t\t\t\t\t(void)read(pipe_fd, &code, sizeof(int));\n\t\t\t\t\tbreak;\n\t\t\t\t} else if(fds[i].fd == publisher->remote_rtcp_fd) {\n\t\t\t\t\t/* Got Something on the RTCP socket, we only use this for latching */\n\t\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\t\tif(bytes < 0 || (!janus_is_rtp(buffer, bytes) && !janus_is_rtcp(buffer, bytes))) {\n\t\t\t\t\t\t/* For latching we need an RTP or RTCP packet */\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tmemcpy(&publisher->rtcp_addr, &remote, addrlen);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Got an RTP/RTCP packet */\n\t\t\t\taddrlen = sizeof(remote);\n\t\t\t\tbytes = recvfrom(fds[i].fd, buffer, 1500, 0, (struct sockaddr *)&remote, &addrlen);\n\t\t\t\tif(bytes < 0) {\n\t\t\t\t\t/* Failed to read? */\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Handle packet: check SSRC and do relay_rtp accordingly */\n\t\t\t\tif(!janus_is_rtp(buffer, bytes)) {\n\t\t\t\t\t/* Not RTP, drop the packet */\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\trtp = (janus_rtp_header *)buffer;\n\t\t\t\tssrc = ntohl(rtp->ssrc);\n\t\t\t\tif(ssrc < REMOTE_PUBLISHER_BASE_SSRC) {\n\t\t\t\t\t/* Can't be one of the SSRCs we're waiting for, innore */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s/%s] Invalid SSRC (%\"SCNu32\")\\n\",\n\t\t\t\t\t\tvideoroom->room_id_str, publisher->user_id_str, ssrc);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tdiff = ssrc - REMOTE_PUBLISHER_BASE_SSRC;\n\t\t\t\tmindex = diff/REMOTE_PUBLISHER_SSRC_STEP;\n\t\t\t\tvindex = diff - (mindex*REMOTE_PUBLISHER_SSRC_STEP);\n\t\t\t\tjanus_mutex_lock(&publisher->streams_mutex);\n\t\t\t\tps = g_hash_table_lookup(publisher->streams_byid, GINT_TO_POINTER(mindex));\n\t\t\t\tif(ps == NULL) {\n\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s/%s] Invalid mindex %d\\n\",\n\t\t\t\t\t\tvideoroom->room_id_str, publisher->user_id_str, mindex);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif((!ps->simulcast && vindex > 0) || vindex > 2) {\n\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%s/%s] Invalid substream %d\\n\",\n\t\t\t\t\t\tvideoroom->room_id_str, publisher->user_id_str, vindex);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Check if this is an actual RTP packet, or an\n\t\t\t\t * envelope created to relay data channels */\n\t\t\t\tif(ps->type == JANUS_VIDEOROOM_MEDIA_DATA) {\n\t\t\t\t\t/* Handle as data channel, stripping the RTP header */\n\t\t\t\t\tjanus_refcount_increase_nodebug(&publisher->ref);\n\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\tdata.label = NULL;\n\t\t\t\t\tdata.protocol = NULL;\n\t\t\t\t\tdata.binary = rtp->type ? TRUE : FALSE;\n\t\t\t\t\tdata.buffer = buffer + 12;\n\t\t\t\t\tdata.length = bytes - 12;\n\t\t\t\t\t/* Now handle the packet as if coming from a regular publisher */\n\t\t\t\t\tjanus_videoroom_incoming_data_internal(publisher->session, publisher, &data);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t/* Is this SRTP? */\n\t\t\t\tif(ps->is_srtp) {\n\t\t\t\t\tint buflen = bytes;\n\t\t\t\t\tsrtp_err_status_t res = srtp_unprotect(ps->srtp_ctx, buffer, &buflen);\n\t\t\t\t\tif(res != srtp_err_status_ok) {\n\t\t\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t\t\tguint32 timestamp = ntohl(rtp->timestamp);\n\t\t\t\t\t\tguint16 seq = ntohs(rtp->seq_number);\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s] Publisher stream (#%d) SRTP unprotect error: %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")\\n\",\n\t\t\t\t\t\t\tpublisher->user_id_str, ps->mindex, janus_srtp_error_str(res), bytes, buflen, timestamp, seq);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tbytes = buflen;\n\t\t\t\t}\n\t\t\t\t/* Prepare the RTP packet */\n\t\t\t\tpkt.mindex = mindex;\n\t\t\t\tpkt.video = (ps->type == JANUS_VIDEOROOM_MEDIA_VIDEO);\n\t\t\t\tpkt.buffer = buffer;\n\t\t\t\tpkt.length = bytes;\n\t\t\t\tjanus_plugin_rtp_extensions_reset(&pkt.extensions);\n\t\t\t\tjanus_refcount_increase_nodebug(&publisher->ref);\n\t\t\t\tjanus_mutex_unlock(&publisher->streams_mutex);\n\t\t\t\t/* Parse RTP extensions before relaying the packet */\n\t\t\t\tif(!pkt.video && ps->audio_level_extmap_id > 0) {\n\t\t\t\t\tgboolean vad = FALSE;\n\t\t\t\t\tint level = -1;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_audio_level(buffer, bytes,\n\t\t\t\t\t\t\tps->audio_level_extmap_id, &vad, &level) == 0) {\n\t\t\t\t\t\tpkt.extensions.audio_level = level;\n\t\t\t\t\t\tpkt.extensions.audio_level_vad = vad;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(pkt.video && ps->video_orient_extmap_id > 0) {\n\t\t\t\t\tgboolean c = FALSE, f = FALSE, r1 = FALSE, r0 = FALSE;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_video_orientation(buffer, bytes,\n\t\t\t\t\t\t\tps->video_orient_extmap_id, &c, &f, &r1, &r0) == 0) {\n\t\t\t\t\t\tpkt.extensions.video_rotation = 0;\n\t\t\t\t\t\tif(r1 && r0)\n\t\t\t\t\t\t\tpkt.extensions.video_rotation = 270;\n\t\t\t\t\t\telse if(r1)\n\t\t\t\t\t\t\tpkt.extensions.video_rotation = 180;\n\t\t\t\t\t\telse if(r0)\n\t\t\t\t\t\t\tpkt.extensions.video_rotation = 90;\n\t\t\t\t\t\tpkt.extensions.video_back_camera = c;\n\t\t\t\t\t\tpkt.extensions.video_flipped = f;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(pkt.video && ps->playout_delay_extmap_id > 0) {\n\t\t\t\t\tuint16_t min = 0, max = 0;\n\t\t\t\t\tif(janus_rtp_header_extension_parse_playout_delay(buffer, bytes,\n\t\t\t\t\t\t\tps->playout_delay_extmap_id, &min, &max) == 0) {\n\t\t\t\t\t\tpkt.extensions.min_delay = min;\n\t\t\t\t\t\tpkt.extensions.max_delay = max;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Apply an SSRC offset to avoid issues when switching,\n\t\t\t\t * see https://github.com/meetecho/janus-gateway/issues/3444 */\n\t\t\t\trtp->ssrc = htonl(ntohl(rtp->ssrc) + publisher->remote_ssrc_offset);\n\t\t\t\t/* Now handle the packet as if coming from a regular publisher */\n\t\t\t\tjanus_videoroom_incoming_rtp_internal(publisher->session, publisher, &pkt);\n\t\t\t}\n\t\t}\n\t}\ncleanup:\n\t/* If we got here, the remote publisher has been removed from the\n\t * room: let's notify all other publishers in the room */\n\tjanus_mutex_lock(&publisher->rec_mutex);\n\tg_free(publisher->recording_base);\n\tpublisher->recording_base = NULL;\n\tjanus_mutex_lock(&publisher->streams_mutex)\n\tjanus_videoroom_recorder_close(publisher);\n\tjanus_mutex_unlock(&publisher->streams_mutex)\n\tjanus_mutex_unlock(&publisher->rec_mutex);\n\tpublisher->acodec = JANUS_AUDIOCODEC_NONE;\n\tpublisher->vcodec = JANUS_VIDEOCODEC_NONE;\n\tpublisher->firefox = FALSE;\n\tpublisher->e2ee = FALSE;\n\t/* Get rid of streams */\n\tjanus_mutex_lock(&publisher->streams_mutex);\n\tGList *subscribers = NULL, *mappings = NULL;\n\ttemp = publisher->streams;\n\twhile(temp) {\n\t\tjanus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;\n\t\t/* Close all subscriptions to this stream */\n\t\tjanus_mutex_lock(&ps->subscribers_mutex);\n\t\tGSList *temp2 = ps->subscribers;\n\t\twhile(temp2) {\n\t\t\tjanus_videoroom_subscriber_stream *ss = (janus_videoroom_subscriber_stream *)temp2->data;\n\t\t\ttemp2 = temp2->next;\n\t\t\tif(ss) {\n\t\t\t\t/* Take note of the subscriber, so that we can send an updated offer */\n\t\t\t\tif(ss->type != JANUS_VIDEOROOM_MEDIA_DATA && g_list_find(subscribers, ss->subscriber) == NULL) {\n\t\t\t\t\tjanus_refcount_increase(&ss->subscriber->ref);\n\t\t\t\t\tjanus_refcount_increase(&ss->subscriber->session->ref);\n\t\t\t\t\tsubscribers = g_list_append(subscribers, ss->subscriber);\n\t\t\t\t}\n\t\t\t\t/* Take note of the subscription to remove */\n\t\t\t\tjanus_videoroom_stream_mapping *m = g_malloc(sizeof(janus_videoroom_stream_mapping));\n\t\t\t\tjanus_refcount_increase(&ps->ref);\n\t\t\t\tjanus_refcount_increase(&ss->ref);\n\t\t\t\tjanus_refcount_increase(&ss->subscriber->ref);\n\t\t\t\tm->ps = ps;\n\t\t\t\tm->ss = ss;\n\t\t\t\tm->unref_ss = (g_slist_find(ps->subscribers, ss) != NULL);\n\t\t\t\tm->subscriber = ss->subscriber;\n\t\t\t\tmappings = g_list_append(mappings, m);\n\t\t\t}\n\t\t}\n\t\tg_slist_free(ps->subscribers);\n\t\tps->subscribers = NULL;\n\t\tint i=0;\n\t\tfor(i=0; i<3; i++) {\n\t\t\tps->vssrc[i] = 0;\n\t\t\tg_free(ps->rid[i]);\n\t\t\tps->rid[i] = NULL;\n\t\t}\n\t\tps->rid_extmap_id = 0;\n\t\tg_free(ps->fmtp);\n\t\tps->fmtp = NULL;\n\t\tjanus_mutex_unlock(&ps->subscribers_mutex);\n\t\ttemp = temp->next;\n\t}\n\tif(mappings) {\n\t\ttemp = mappings;\n\t\twhile(temp) {\n\t\t\tjanus_videoroom_stream_mapping *m = (janus_videoroom_stream_mapping *)temp->data;\n\t\t\t/* Remove the subscription (turns the m-line to inactive) */\n\t\t\tjanus_videoroom_publisher_stream *ps = m->ps;\n\t\t\tjanus_videoroom_subscriber *subscriber = m->subscriber;\n\t\t\tjanus_videoroom_subscriber_stream *ss = m->ss;\n\t\t\tif(subscriber) {\n\t\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\t\tjanus_videoroom_subscriber_stream_remove(ss, ps, TRUE);\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\tif(m->unref_ss)\n\t\t\t\t\tjanus_refcount_decrease(&ss->ref);\n\t\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&ss->ref);\n\t\t\tjanus_refcount_decrease(&ps->ref);\n\t\t\ttemp = temp->next;\n\t\t}\n\t\tg_list_free_full(mappings, (GDestroyNotify)g_free);\n\t}\n\t/* Any subscriber session to update? */\n\tif(subscribers != NULL) {\n\t\ttemp = subscribers;\n\t\twhile(temp) {\n\t\t\tjanus_videoroom_subscriber *subscriber = (janus_videoroom_subscriber *)temp->data;\n\t\t\t/* Send (or schedule) a new offer */\n\t\t\tjanus_mutex_lock(&subscriber->streams_mutex);\n\t\t\tif(!g_atomic_int_get(&subscriber->answered)) {\n\t\t\t\t/* We're still waiting for an answer to a previous offer, postpone this */\n\t\t\t\tg_atomic_int_set(&subscriber->pending_offer, 1);\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t} else {\n\t\t\t\tjson_t *event = json_object();\n\t\t\t\tjson_object_set_new(event, \"videoroom\", json_string(\"updated\"));\n\t\t\t\tjson_object_set_new(event, \"room\", string_ids ?\n\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\tjson_t *media = janus_videoroom_subscriber_streams_summary(subscriber, FALSE, NULL);\n\t\t\t\tjson_t *media_event = NULL;\n\t\t\t\tif(notify_events && gateway->events_is_enabled())\n\t\t\t\t\tmedia_event = json_deep_copy(media);\n\t\t\t\tjson_object_set_new(event, \"streams\", media);\n\t\t\t\t/* Generate a new offer */\n\t\t\t\tjson_t *jsep = janus_videoroom_subscriber_offer(subscriber);\n\t\t\t\tjanus_mutex_unlock(&subscriber->streams_mutex);\n\t\t\t\t/* How long will the Janus core take to push the event? */\n\t\t\t\tgint64 start = janus_get_monotonic_time();\n\t\t\t\tint res = gateway->push_event(subscriber->session->handle, &janus_videoroom_plugin, NULL, event, jsep);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  >> Pushing event: %d (took %\"SCNu64\" us)\\n\", res, janus_get_monotonic_time()-start);\n\t\t\t\tjson_decref(event);\n\t\t\t\tjson_decref(jsep);\n\t\t\t\t/* Also notify event handlers */\n\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"updated\"));\n\t\t\t\t\tjson_object_set_new(info, \"room\", string_ids ?\n\t\t\t\t\t\tjson_string(subscriber->room_id_str) : json_integer(subscriber->room_id));\n\t\t\t\t\tjson_object_set_new(info, \"streams\", media_event);\n\t\t\t\t\tjson_object_set_new(info, \"private_id\", json_integer(subscriber->pvt_id));\n\t\t\t\t\tgateway->notify_event(&janus_videoroom_plugin, NULL, info);\n\t\t\t\t}\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&subscriber->session->ref);\n\t\t\tjanus_refcount_decrease(&subscriber->ref);\n\t\t\ttemp = temp->next;\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%s/%s] Leaving remote publisher thread...\\n\",\n\t\tvideoroom->room_id_str, publisher->user_id_str);\n\tg_list_free(subscribers);\n\t/* Free streams */\n\tg_list_free_full(publisher->streams, (GDestroyNotify)(janus_videoroom_publisher_stream_unref));\n\tpublisher->streams = NULL;\n\tg_hash_table_remove_all(publisher->streams_byid);\n\tg_hash_table_remove_all(publisher->streams_bymid);\n\tjanus_mutex_unlock(&publisher->streams_mutex);\n\tjanus_videoroom_leave_or_unpublish(publisher, TRUE, FALSE);\n\tjanus_refcount_decrease(&publisher->session->ref);\n\tjanus_videoroom_publisher_destroy(publisher);\n\t/* Done */\n\tjanus_refcount_decrease(&videoroom->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n\nstatic void janus_videoroom_helper_rtpdata_packet(gpointer data, gpointer user_data) {\n\tjanus_videoroom_rtp_relay_packet *packet = (janus_videoroom_rtp_relay_packet *)user_data;\n\tif(!packet || !packet->data || packet->length < 1) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid packet...\\n\");\n\t\treturn;\n\t}\n\tjanus_videoroom_helper *helper = (janus_videoroom_helper *)data;\n\tif(!helper) {\n\t\t//~ JANUS_LOG(LOG_ERR, \"Invalid session...\\n\");\n\t\treturn;\n\t}\n\t/* Clone the packet and queue it for delivery on the helper thread */\n\tjanus_videoroom_rtp_relay_packet *copy = g_malloc0(sizeof(janus_videoroom_rtp_relay_packet));\n\tcopy->source = packet->source;\n\tcopy->data = g_malloc(packet->length);\n\tmemcpy(copy->data, packet->data, packet->length);\n\tcopy->length = packet->length;\n\tcopy->is_rtp = packet->is_rtp;\n\tcopy->textdata = packet->textdata;\n\tcopy->is_video = packet->is_video;\n\tcopy->simulcast = packet->simulcast;\n\tcopy->ssrc[0] = packet->ssrc[0];\n\tcopy->ssrc[1] = packet->ssrc[1];\n\tcopy->ssrc[2] = packet->ssrc[2];\n\tcopy->svc = packet->svc;\n\tcopy->svc_info = packet->svc_info;\n\tcopy->timestamp = packet->timestamp;\n\tcopy->seq_number = packet->seq_number;\n\tg_async_queue_push(helper->queued_packets, copy);\n}\n\nstatic void *janus_videoroom_helper_thread(void *data) {\n\tjanus_videoroom_helper *helper = (janus_videoroom_helper *)data;\n\tjanus_videoroom *room = helper->room;\n\tjanus_videoroom_publisher_stream *ps = NULL;\n\tGList *subscribers = NULL;\n\tJANUS_LOG(LOG_VERB, \"[%s/#%d] Joining VideoRoom helper thread\\n\", room->room_id_str, helper->id);\n\tjanus_videoroom_rtp_relay_packet *pkt = NULL;\n\twhile(!g_atomic_int_get(&stopping) && !g_atomic_int_get(&room->destroyed) && !g_atomic_int_get(&helper->destroyed)) {\n\t\tpkt = g_async_queue_pop(helper->queued_packets);\n\t\tif(pkt == &exit_packet)\n\t\t\tbreak;\n\t\tjanus_mutex_lock(&helper->mutex);\n\t\t/* FIXME */\n\t\tps = pkt->source;\n\t\tsubscribers = g_hash_table_lookup(helper->subscribers, ps);\n\t\tif(subscribers != NULL) {\n\t\t\tg_list_foreach(subscribers,\n\t\t\t\tpkt->is_rtp ? janus_videoroom_relay_rtp_packet : janus_videoroom_relay_data_packet,\n\t\t\t\tpkt);\n\t\t}\n\t\tjanus_mutex_unlock(&helper->mutex);\n\t\tjanus_videoroom_rtp_relay_packet_free(pkt);\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%s/#%d] Leaving VideoRoom helper thread\\n\", room->room_id_str, helper->id);\n\tjanus_refcount_decrease(&helper->ref);\n\tjanus_refcount_decrease(&room->ref);\n\tg_thread_unref(g_thread_self());\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/plugins/lua/echotest.lua",
    "content": "-- This is a simple example of an echo test application built in Lua,\n-- and conceived to be used in conjunction with the janus_lua.c plugin\n--\n-- Note: this example depends on lua-json to do JSON processing\n-- (http://luaforge.net/projects/luajson/)\njson = require('json')\n-- We also import our own SDP helper utilities: you may have better ones\nsdp = require('janus-sdp')\n-- Let's also use our ugly stdout logger just for the fun of it: to add\n-- some color to the text we use the lua-term library\n-- (https://github.com/hoelzro/lua-term)\ncolors = require('term.colors')\nlogger = require('janus-logger')\n\n-- Example details\nname = \"echotest.lua\"\nlogger.prefix(\"[\" .. colors.blue .. name .. colors.reset .. \"]\")\nlogger.print(\"Loading...\")\n\n-- State and properties\nsessions = {}\ntasks = {}\n\n-- Just for fun, let's override the plugin info with our own\nfunction getVersion()\n\treturn 12\nend\nfunction getVersionString()\n\treturn \"0.0.12\"\nend\nfunction getDescription()\n\treturn \"This is echotest.lua, a Lua based clone of janus.plugin.echotest\"\nend\nfunction getName()\n\treturn \"Lua based EchoTest\"\nend\nfunction getAuthor()\n\treturn \"Lorenzo Miniero\"\nend\nfunction getPackage()\n\treturn \"janus.plugin.echolua\"\nend\n\n-- Methods\nfunction init(config)\n\t-- This is where we initialize the plugin, for static properties\n\tlogger.print(\"Initializing...\")\n\tif config ~= nil then\n\t\tlogger.print(\"Configuration file provided (\" .. config .. \"), but we don't need it\")\n\tend\n\tlogger.print(\"Initialized\")\n\t-- Just for fun (and to showcase the feature), let's send an event to handlers:\n\t-- notice how the first argument is 0, meaning this event is not tied to any session\n\tlocal event = { event = \"loaded\", script = name }\n\tlocal eventjson = json.encode(event)\n\tnotifyEvent(0, eventjson)\nend\n\nfunction destroy()\n\t-- This is where we deinitialize the plugin, when Janus shuts down\n\tlogger.print(\"Deinitialized\")\nend\n\nfunction createSession(id)\n\t-- Keep track of a new session\n\tlogger.print(\"Created new session: \" .. id)\n\tsessions[id] = { id = id, lua = name }\nend\n\nfunction destroySession(id)\n\t-- A Janus plugin session has gone\n\tlogger.print(\"Destroyed session: \" .. id)\n\thangupMedia(id)\n\tsessions[id] = nil\nend\n\nfunction querySession(id)\n\t-- Return info on a session\n\tlogger.print(\"Queried session: \" .. id)\n\tlocal s = sessions[id]\n\tif s == nil then\n\t\treturn nil\n\tend\n\tlocal info = { script = s[\"lua\"], id = s[\"id\"] }\n\tlocal infojson = json.encode(info)\n\treturn infojson\nend\n\nfunction handleMessage(id, tr, msg, jsep)\n\t-- Handle a message, synchronously or asynchronously, and return\n\t-- something accordingly: if it's the latter, we'll do a coroutine\n\tlogger.print(\"Handling message for session: \" .. id)\n\tlocal s = sessions[id]\n\tif s == nil then\n\t\treturn -1, \"Session not found\"\n\tend\n\t-- Decode the message JSON string to a table\n\tlocal msgT = json.decode(msg)\n\t-- Let's return a synchronous response if there's no jsep, asynchronous otherwise\n\tif jsep == nil then\n\t\tprocessRequest(id, msgT)\n\t\tlocal response = { echotest = \"response\", result = \"ok\" }\n\t\tlocal responsejson = json.encode(response)\n\t\treturn 0, responsejson\n\telse\n\t\t-- Decode the JSEP JSON string to a table too\n\t\tlocal jsepT = json.decode(jsep)\n\t\t-- We need a new coroutine here\n\t\tlocal async = coroutine.create(function(id, tr, comsg, cojsep)\n\t\t\t-- We'll only execute this when the scheduler resumes the task\n\t\t\tlogger.print(\"Handling async message for session: \" .. id)\n\t\t\tlocal s = sessions[id]\n\t\t\tif s == nil then\n\t\t\t\tlogger.print(\"Can't handle async message: so such session\")\n\t\t\t\treturn\n\t\t\tend\n\t\t\tlocal offer = sdp.parse(cojsep.sdp)\n\t\t\tlogger.print(\"Got offer: \" .. sdp.render(offer))\n\t\t\tlocal answer = sdp.generateAnswer(offer, { audio = true, video = true, data = true,\n\t\t\t\taudioCodec = comsg[\"audiocodec\"], videoCodec = comsg[\"videocodec\"],\n\t\t\t\tvp9Profile = comsg[\"videoprofile\"], h264Profile = comsg[\"videoprofile\"] })\n\t\t\tlogger.print(\"Generated answer: \" .. sdp.render(answer))\n\t\t\tlogger.print(\"Processing request: \" .. dumpTable(comsg))\n\t\t\tprocessRequest(id, comsg)\n\t\t\tlogger.print(\"Pushing event:\")\n\t\t\tlocal event = { echotest = \"event\", result = \"ok\" }\n\t\t\tlocal jsonevent = json.encode(event)\n\t\t\tlogger.print(\"  -- \" .. jsonevent)\n\t\t\tlocal jsepanswer = { type = \"answer\", sdp = sdp.render(answer) }\n\t\t\tlocal jsonjsep = json.encode(jsepanswer)\n\t\t\tlogger.print(\"  -- \" .. jsonjsep)\n\t\t\tpushEvent(id, tr, jsonevent, jsonjsep)\n\t\t\t-- Just for fun (and to showcase the feature), let's send an event to handlers;\n\t\t\t-- notice how we pass the id now, meaning this event is tied to a specific session\n\t\t\tlocal event = { event = \"processed\", request = comsg }\n\t\t\tlocal eventjson = json.encode(event)\n\t\t\tnotifyEvent(id, eventjson)\n\t\tend)\n\t\t-- Enqueue it: the scheduler will resume it later\n\t\ttasks[#tasks+1] = { co = async, id = id, tr = tr, msg = msgT, jsep = jsepT }\n\t\t-- Return explaining that this is will be handled asynchronously\n\t\tpokeScheduler()\n\t\treturn 1, nil\n\tend\nend\n\nfunction handleAdminMessage(message)\n\t-- This is just to showcase how you can handle incoming messages\n\t-- coming from the Admin API: we return the same message as a test\n\tlogger.print(\"Got admin message: \" .. dumpTable(message))\n\treturn message;\nend\n\nfunction setupMedia(id)\n\t-- WebRTC is now available\n\tlogger.print(\"WebRTC PeerConnection is up for session: \" .. id)\n\t-- Attach the session's stream to itself (echo test)\n\taddRecipient(id, id)\nend\n\nfunction hangupMedia(id)\n\t-- WebRTC not available anymore\n\tlogger.print(\"WebRTC PeerConnection is down for session: \" .. id)\n\t-- Detach the stream\n\tremoveRecipient(id, id)\n\t-- Clear some flags\n\tlocal s = sessions[id]\n\tif s ~= nil then\n\t\ts.audioCodec = nil\n\t\ts.videoCodec = nil\n\tend\nend\n\nfunction incomingTextData(id, buf, len, label, protocol)\n\t-- Relaying RTP/RTCP in Lua makes no sense, but just for fun\n\t-- we handle text data channel messages ourselves to manipulate them\n\tlocal edit = \"[\" .. name .. \"] --> \" .. buf\n\trelayTextData(id, edit, string.len(edit), label, protocol);\nend\n\nfunction incomingBinaryData(id, buf, len, label, protocol)\n\t-- If the data we're getting is binary, send it back as it is\n\trelayBinaryData(id, buf, len, label, protocol);\nend\n\nfunction dataReady(id)\n\t-- This callback is invoked when the datachannel first becomes\n\t-- available (meaning you should never send data before it has been\n\t-- invoked at least once), but also when the datachannel is ready to\n\t-- receive more data (buffers are empty), which means it can be used\n\t-- to throttle outgoing data and not send too much at a time.\nend\n\nfunction substreamChanged(id, substream)\n\t-- If simulcast is used, this callback is invoked when the substream\n\t-- we're sending to this session changes: 0=low, 1=medium, 2=high\n\tlogger.print(\"Substream changed for session \" .. id .. \": \" .. substream);\n\t-- Let's send an event so that the user is aware\n\tlocal event = { echotest = \"event\", videocodec = \"vp8\", substream = substream }\n\tlocal jsonevent = json.encode(event)\n\tpushEvent(id, nil, jsonevent, nil)\nend\n\nfunction temporalLayerChanged(id, temporal)\n\t-- If simulcast is used, this callback is invoked when the temporal\n\t-- layer we're sending to this session changes: 0=lowfps, 1=maxfps\n\tlogger.print(\"Temporal layer changed for session \" .. id .. \": \" .. temporal);\n\t-- Let's send an event so that the user is aware\n\tlocal event = { echotest = \"event\", videocodec = \"vp8\", temporal = temporal }\n\tlocal jsonevent = json.encode(event)\n\tpushEvent(id, nil, jsonevent, nil)\nend\n\nfunction resumeScheduler()\n\t-- This is the function responsible for resuming coroutines associated\n\t-- with whatever is relevant to the Lua script, e.g., for this script,\n\t-- with asynchronous requests: if you're handling async stuff yourself,\n\t-- you're free not to use this and just return, but the C Lua plugin\n\t-- expects this method to exist so it MUST be present, even if empty\n\tlogger.print(\"Resuming coroutines\")\n\tfor index,task in ipairs(tasks) do\n\t\tlocal success, result = coroutine.resume(task.co, task.id, task.tr, task.msg, task.jsep)\n\t\tif not success then\n\t\t\tlogger.print(colors.red .. \" \" .. dumpTable(result) .. colors.reset)\n\t\tend\n\tend\n\tlogger.print(\"Coroutines resumed\")\n\ttasks = {}\nend\n\n-- We use this internal method to process an API request\nfunction processRequest(id, msg)\n\tif msg == nil then\n\t\treturn -1\n\tend\n\t-- We implement most of the existing EchoTest API messages, here\n\tif msg[\"audio\"] == true then\n\t\tconfigureMedium(id, \"audio\", \"in\", true)\n\t\tconfigureMedium(id, \"audio\", \"out\", true)\n\telseif msg[\"audio\"] == false then\n\t\tconfigureMedium(id, \"audio\", \"in\", false)\n\t\tconfigureMedium(id, \"audio\", \"out\", false)\n\tend\n\tif msg[\"video\"] == true then\n\t\tconfigureMedium(id, \"video\", \"in\", true)\n\t\tconfigureMedium(id, \"video\", \"out\", true)\n\t\tsendPli(id)\n\telseif msg[\"video\"] == false then\n\t\tconfigureMedium(id, \"video\", \"in\", false)\n\t\tconfigureMedium(id, \"video\", \"out\", false)\n\tend\n\tif msg[\"data\"] == true then\n\t\tconfigureMedium(id, \"data\", \"in\", true)\n\t\tconfigureMedium(id, \"data\", \"out\", true)\n\telseif msg[\"data\"] == false then\n\t\tconfigureMedium(id, \"data\", \"in\", false)\n\t\tconfigureMedium(id, \"data\", \"out\", false)\n\tend\n\tif msg[\"bitrate\"] ~= nil then\n\t\tsetBitrate(id, msg[\"bitrate\"])\n\tend\n\tif msg[\"substream\"] ~= nil then\n\t\tsetSubstream(id, msg[\"substream\"])\n\t\tsendPli(id)\n\tend\n\tif msg[\"temporal\"] ~= nil then\n\t\tsetTemporalLayer(id, msg[\"temporal\"])\n\t\tsendPli(id)\n\tend\n\tif msg[\"keyframe\"] ~= nil then\n\t\tsendPli(id)\n\tend\n\tif msg[\"record\"] == true then\n\t\tlocal fnbase = msg[\"filename\"]\n\t\tif fnbase == nil then\n\t\t\tfnbase = \"lua-echotest-\" .. id .. \"-\" .. require 'socket'.gettime()\n\t\tend\n\t\t-- For the sake of simplicity, we're assuming Opus/VP8 here; in\n\t\t-- practice, you'll need to check what was negotiated. If you\n\t\t-- want the codec-specific info to be saved to the .mjr file as\n\t\t-- well, you'll need to add the '/fmtp=<info>' to the codec name,\n\t\t-- e.g.:    \"vp9/fmtp=profile-id=2\"\n\t\tstartRecording(id,\n\t\t\t\"audio\", \"opus\", \"/tmp\", fnbase .. \"-audio\",\n\t\t\t\"video\", \"vp8\", \"/tmp\", fnbase .. \"-video\",\n\t\t\t\"data\", \"text\", \"/tmp\", fnbase .. \"-data\"\n\t\t)\n\telseif msg[\"record\"] == false then\n\t\tstopRecording(id, \"audio\", \"video\", \"data\")\n\tend\n\treturn 0\nend\n\n-- Helper for logging tables\n-- https://stackoverflow.com/a/27028488\nfunction dumpTable(o)\n\tif type(o) == 'table' then\n\t\tlocal s = '{ '\n\t\tfor k,v in pairs(o) do\n\t\t\tif type(k) ~= 'number' then k = '\"'..k..'\"' end\n\t\t\ts = s .. '['..k..'] = ' .. dumpTable(v) .. ','\n\t\tend\n\t\treturn s .. '} '\n\telse\n\t\treturn tostring(o)\n\tend\nend\n\n-- Done\nlogger.print(\"Loaded\")\n"
  },
  {
    "path": "src/plugins/lua/janus-logger.lua",
    "content": "-- Simple logger for Lua scripts, which at the moment simply wraps calls\n-- to print(..), with the ability to specify a custom prefix. In the\n-- future this may be made much more sophisticated, but we don't care now.\n\nlocal JANUSLOG = {}\n\nlogPrefix = \"\"\n\nfunction JANUSLOG.prefix(prefix)\n\tif prefix ~= nil then\n\t\tlogPrefix = prefix .. \" \"\n\tend\nend\n\nfunction JANUSLOG.print(text)\n\tif text ~= nil then\n\t\tjanusLog(4, logPrefix .. text)\n\telse\n\t\tjanusLog(4, logPrefix .. \"(nil)\")\n\tend\nend\n\nfunction JANUSLOG.verbose(text)\n\tif text ~= nil then\n\t\tjanusLog(5, logPrefix .. text)\n\telse\n\t\tjanusLog(5, logPrefix .. \"(nil)\")\n\tend\nend\n\nfunction JANUSLOG.warn(text)\n\tif text ~= nil then\n\t\tjanusLog(3, logPrefix .. text)\n\telse\n\t\tjanusLog(3, logPrefix .. \"(nil)\")\n\tend\nend\n\nfunction JANUSLOG.error(text)\n\tif text ~= nil then\n\t\tjanusLog(2, logPrefix .. text)\n\telse\n\t\tjanusLog(2, logPrefix .. \"(nil)\")\n\tend\nend\n\nreturn JANUSLOG\n"
  },
  {
    "path": "src/plugins/lua/janus-sdp.lua",
    "content": "-- Set of utilities for parsing, processing and managing Janus SDPs in Lua,\n-- as the C Janus SDP utils that Janus provides are unavailable otherwise\n\nlocal JANUSSDP = {}\n\nfunction JANUSSDP.parse(text)\n\tif text == nil then\n\t\treturn nil\n\tend\n\tlocal lines = {}\n\tlocal s = nil\n\tfor s in text:gmatch(\"[^\\r\\n]+\") do\n\t\ttable.insert(lines, s)\n\tend\n\tlocal sdp = {}\n\tlocal index = nil\n\tlocal line = nil\n\tfor index,line in pairs(lines) do\n\t\tlocal t = line:sub(1,1)\n\t\tlocal ll = line:sub(3)\n\t\tlocal sc = ll:find(\":\")\n\t\tlocal n, v\n\t\tif sc == nil then\n\t\t\tn = ll\n\t\telse\n\t\t\tn = ll:sub(1,sc-1)\n\t\t\tv = ll:sub(sc+1)\n\t\tend\n\t\ttable.insert(sdp, {type = t, name = n, value = v})\n\tend\n\treturn sdp\nend\n\nfunction JANUSSDP.render(sdp)\n\tif sdp == nil then\n\t\treturn nil\n\tend\n\tlocal sdpString = \"\"\n\tlocal index = nil\n\tlocal a = nil\n\tfor index,a in pairs(sdp) do\n\t\tif a.value == nil then\n\t\t\tsdpString = sdpString .. a.type .. \"=\" .. a.name .. \"\\r\\n\"\n\t\telse\n\t\t\tsdpString = sdpString .. a.type .. \"=\" .. a.name .. \":\" .. a.value .. \"\\r\\n\"\n\t\tend\n\tend\n\treturn sdpString\nend\n\nfunction JANUSSDP.findPayloadType(sdp, codec, profile)\n\tif sdp == nil or codec == nil then\n\t\treturn -1\n\tend\n\tlocal pt = -1\n\tlocal codecUpper = codec:upper()\n\tlocal codecLower = codec:lower()\n\tlocal index = nil\n\tlocal a = nil\n\tlocal checkProfile = false\n\tfor index,a in pairs(sdp) do\n\t\tif checkProfile and a.name == \"fmtp\" and a.value ~= nil then\n\t\t\tcheckProfile = false\n\t\t\tif codec == \"vp9\" then\n\t\t\t\tif a.value:find(\"profile%-id%=\" .. profile) ~= nil then\n\t\t\t\t\t-- Found\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\t\tpt = -1\n\t\t\telseif codec == \"h264\" then\n\t\t\t\tif a.value:find(\"profile%-level%-id%=\" .. profile:lower()) ~= nil then\n\t\t\t\t\t-- Found\n\t\t\t\t\tbreak\n\t\t\t\telseif a.value:find(\"profile%-level%-id%=\" .. profile:upper()) ~= nil then\n\t\t\t\t\t-- Found\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\t\tpt = -1\n\t\t\tend\n\t\telseif a.name == \"rtpmap\" and a.value ~= nil then\n\t\t\tif a.value:find(codecLower) ~= nil or a.value:find(codecUpper) ~= nil then\n\t\t\t\tlocal n = a.value:gmatch(\"[^ ]+\")\n\t\t\t\tpt = tonumber(n())\n\t\t\t\tif profile == nil then\n\t\t\t\t\t-- We're done\n\t\t\t\t\tbreak\n\t\t\t\telse\n\t\t\t\t\t-- We need to make sure the profile matches\n\t\t\t\t\tcheckProfile = true\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn pt\nend\n\nfunction JANUSSDP.findCodec(sdp, pt)\n\tif sdp == nil or pt == nil then\n\t\treturn -1\n\tend\n\tif pt == 0 then\n\t\treturn \"pcmu\"\n\telseif pt == 8 then\n\t\treturn \"pcma\"\n\telseif pt == 9 then\n\t\treturn \"g722\"\n\tend\n\tlocal codec = nil\n\tlocal index = nil\n\tlocal a = nil\n\tfor index,a in pairs(sdp) do\n\t\tif a.name == \"rtpmap\" and a.value ~= nil then\n\t\t\tlocal n = a.value:gmatch(\"[^ ]+\")\n\t\t\tif tonumber(n()) == pt then\n\t\t\t\tif a.value:find(\"vp8\") ~= nil or a.value:find(\"VP8\") ~= nil then\n\t\t\t\t\tcodec = \"vp8\"\n\t\t\t\telseif a.value:find(\"vp9\") ~= nil or a.value:find(\"VP9\") ~= nil then\n\t\t\t\t\tcodec = \"vp9\"\n\t\t\t\telseif a.value:find(\"h264\") ~= nil or a.value:find(\"H264\") ~= nil then\n\t\t\t\t\tcodec = \"h264\"\n\t\t\t\telseif a.value:find(\"opus\") ~= nil or a.value:find(\"OPUS\") ~= nil then\n\t\t\t\t\tcodec = \"opus\"\n\t\t\t\telseif a.value:find(\"multiopus\") ~= nil or a.value:find(\"MULTIOPUS\") ~= nil then\n\t\t\t\t\tcodec = \"multiopus\"\n\t\t\t\telseif a.value:find(\"pcmu\") ~= nil or a.value:find(\"PCMU\") ~= nil then\n\t\t\t\t\tcodec = \"pcmu\"\n\t\t\t\telseif a.value:find(\"pcma\") ~= nil or a.value:find(\"PCMA\") ~= nil then\n\t\t\t\t\tcodec = \"pcma\"\n\t\t\t\telseif a.value:find(\"isac16\") ~= nil or a.value:find(\"ISAC16\") ~= nil then\n\t\t\t\t\tcodec = \"isac16\"\n\t\t\t\telseif a.value:find(\"isac32\") ~= nil or a.value:find(\"ISAC32\") ~= nil then\n\t\t\t\t\tcodec = \"isac32\"\n\t\t\t\telseif a.value:find(\"telephone-event\") ~= nil or a.value:find(\"TELEPHONE-EVENT\") ~= nil then\n\t\t\t\t\tcodec = \"isac32\"\n\t\t\t\tend\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\tend\n\treturn codec\nend\n\nfunction JANUSSDP.removeMLine(sdp, type)\n\tif sdp == nil or type == nil then\n\t\treturn\n\tend\n\tlocal removelist = {}\n\tlocal index = nil\n\tlocal a = nil\n\tlocal removing = false\n\tfor index,a in pairs(sdp) do\n\t\tif a.type == \"m\" then\n\t\t\tif a.name:find(type) ~= nil then\n\t\t\t\tremoving = true\n\t\t\telse\n\t\t\t\tremoving = false\n\t\t\tend\n\t\tend\n\t\tif removing == true then\n\t\t\tremovelist[#removelist+1] = index\n\t\tend\n\tend\n\tlocal i = nil\n\tfor i=#removelist,1,-1 do\n\t\tif removelist[i] ~= nil then\n\t\t\ttable.remove(sdp, removelist[i])\n\t\tend\n\tend\nend\n\nfunction JANUSSDP.removePayloadType(sdp, pt)\n\tif sdp == nil or pt == nil then\n\t\treturn\n\tend\n\tlocal removelist = {}\n\tlocal index = nil\n\tlocal a = nil\n\tfor index,a in pairs(sdp) do\n\t\tif a.type == \"m\" then\n\t\t\tlocal m = a.name:gsub(\" \" .. pt .. \" \", \" \")\n\t\t\tif m ~= nil then a.name = m end\n\t\t\ta.name = a.name .. \"\\r\\n\"\n\t\t\tlocal m = a.name:gsub(\" \" .. pt .. \"\\r\\n\", \"\\r\\n\")\n\t\t\tif m ~= nil then a.name = m end\n\t\t\ta.name = a.name:gsub(\"\\r\\n\", \"\")\n\t\telseif a.type == \"a\" and a.value ~= nil then\n\t\t\tlocal n = a.value:gmatch(\"[^ ]+\")\n\t\t\tif tonumber(n()) == pt then\n\t\t\t\tremovelist[#removelist+1] = index\n\t\t\tend\n\t\tend\n\tend\n\tlocal i = nil\n\tfor i=#removelist,1,-1 do\n\t\tif removelist[i] ~= nil then\n\t\t\ttable.remove(sdp, removelist[i])\n\t\tend\n\tend\nend\n\nfunction JANUSSDP.generateOffer(options)\n\t-- Let's set some defaults for the options, in case none were given\n\tif options == nil then options = {} end\n\tif options.audio == nil then options.audio = true end\n\tif options.audio == true and options.audioPt == nil then options.audioPt = 111 end\n\tif options.audio == true and options.audioCodec == nil then\n\t\toptions.audioCodec = \"opus\"\n\tend\n\tif options.audio == true then\n\t\tif options.audioCodec == \"opus\" then\n\t\t\toptions.audioRtpmap = \"opus/48000/2\"\n\t\telseif options.audioCodec == \"multiopus\" then\n\t\t\toptions.audioRtpmap = \"multiopus/48000/6\"\n\t\telseif options.audioCodec == \"pcmu\" then\n\t\t\toptions.audioRtpmap = \"PCMU/8000\"\n\t\t\toptions.audioPt = 0\n\t\telseif options.audioCodec == \"pcma\" then\n\t\t\toptions.audioRtpmap = \"PCMA/8000\"\n\t\t\toptions.audioPt = 8\n\t\telseif options.audioCodec == \"g722\" then\n\t\t\toptions.audioRtpmap = \"G722/8000\"\n\t\t\toptions.audioPt = 9\n\t\telseif options.audioCodec == \"isac16\" then\n\t\t\toptions.audioRtpmap = \"ISAC/16000\"\n\t\telseif options.audioCodec == \"isac32\" then\n\t\t\toptions.audioRtpmap = \"ISAC/32000\"\n\t\telse\n\t\t\t-- Unsupported codec\n\t\t\toptions.audio = false\n\t\tend\n\tend\n\tif options.audioDir == nil then options.audioDir = \"sendrecv\" end\n\tif options.video == nil then options.video = true end\n\tif options.video == true and options.videoPt == nil then options.videoPt = 96 end\n\tif options.video == true and options.videoCodec == nil then\n\t\toptions.videoCodec = \"vp8\"\n\tend\n\tif options.video == true then\n\t\tif options.videoCodec == \"vp8\" then\n\t\t\toptions.videoRtpmap = \"VP8/90000\"\n\t\telseif options.videoCodec == \"vp9\" then\n\t\t\toptions.videoRtpmap = \"VP9/90000\"\n\t\telseif options.videoCodec == \"h264\" then\n\t\t\toptions.videoRtpmap = \"H264/90000\"\n\t\telse\n\t\t\t-- Unsupported codec\n\t\t\toptions.video = false\n\t\tend\n\tend\n\tif options.videoDir == nil then options.videoDir = \"sendrecv\" end\n\tif options.videoRtcpfb == nil then options.videoRtcpfb = true end\n\tif options.data == nil then options.data = false end\n\tif options.data == true then options.dataDir = \"sendrecv\" end\n\tlocal address = options.address\n\tif address == nil then address = \"127.0.0.1\" end\n\tlocal ipv6 = false\n\tif options.ipv6 == true then ipv6 = true end\n\tlocal sessionName = options.sessionName\n\tif options.sessionName == nil then options.sessionName = \"Janus Lua session\" end\n\t-- Do we have enough for an offer?\n\tif options.audio == false and options.video == false and options.data == false then return nil end\n\t-- Let's prepare the offer\n\tlocal offer = {}\n\t-- Let's start from the session-level attributes\n\toffer[#offer+1] = { type = \"v\", name = \"0\" }\n\toffer[#offer+1] = { type = \"o\", name = \"- \" .. math.floor(math.random(4294967296)) .. \" 1 IN \" ..\n\t\t(ipv6 == true and \"IP6 \" or \"IP4 \") .. address }\n\toffer[#offer+1] = { type = \"s\", name = options.sessionName }\n\toffer[#offer+1] = { type = \"t\", name = \"0 0\" }\n\toffer[#offer+1] = { type = \"c\", name = \"IN \" .. (ipv6 == true and \"IP6 \" or \"IP4 \") .. address }\n\t-- Now let's add the media lines\n\tif options.audio == true then\n\t\toffer[#offer+1] = { type = \"m\", name = \"audio 9 UDP/TLS/RTP/SAVPF \" .. options.audioPt }\n\t\toffer[#offer+1] = { type = \"c\", name = \"IN \" .. (ipv6 == true and \"IP6 \" or \"IP4 \") .. address }\n\t\toffer[#offer+1] = { type = \"a\", name = options.audioDir }\n\t\toffer[#offer+1] = { type = \"a\", name = \"rtpmap\", value = options.audioPt .. \" \" .. options.audioRtpmap }\n\t\tif options.audioFmtp ~= nil then\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"fmtp\", value = options.audioPt .. \" \" .. options.audioFmtp }\n\t\tend\n\tend\n\tif options.video == true then\n\t\toffer[#offer+1] = { type = \"m\", name = \"video 9 UDP/TLS/RTP/SAVPF \" .. options.videoPt }\n\t\toffer[#offer+1] = { type = \"c\", name = \"IN \" .. (ipv6 == true and \"IP6 \" or \"IP4 \") .. address }\n\t\toffer[#offer+1] = { type = \"a\", name = options.videoDir }\n\t\toffer[#offer+1] = { type = \"a\", name = \"rtpmap\", value = options.videoPt .. \" \" .. options.videoRtpmap }\n\t\tif options.videoRtcpfb == true then\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"rtcp-fb\", value = options.videoPt .. \" ccm fir\" }\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"rtcp-fb\", value = options.videoPt .. \" nack\" }\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"rtcp-fb\", value = options.videoPt .. \" nack pli\" }\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"rtcp-fb\", value = options.videoPt .. \" goog-remb\" }\n\t\tend\n\t\tif options.videoCodec == \"vp9\" and options.vp9Profile ~= nil then\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"fmtp\", value = options.videoPt .. \" profile-id=\" .. options.vp9Profile }\n\t\telseif options.videoCodec == \"h264\" and options.h264Profile then\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"fmtp\", value = options.videoPt .. \" profile-level-id=\" .. options.h264Profile .. \";packetization-mode=1\" }\n\t\telseif options.videoFmtp ~= nil then\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"fmtp\", value = options.videoPt .. \" \" .. options.videoFmtp }\n\t\telseif options.videoCodec == \"h264\" then\n\t\t\toffer[#offer+1] = { type = \"a\", name = \"fmtp\", value = options.videoPt .. \" profile-level-id=42e01f;packetization-mode=1\" }\n\t\tend\n\tend\n\tif options.data == true then\n\t\toffer[#offer+1] = { type = \"m\", name = \"application 9 DTLS/SCTP 5000\" }\n\t\toffer[#offer+1] = { type = \"c\", name = \"IN \" .. (ipv6 == true and \"IP6 \" or \"IP4 \") .. address }\n\t\toffer[#offer+1] = { type = \"a\", name = \"sendrecv\" }\n\t\toffer[#offer+1] = { type = \"a\", name = \"sctmap\", value = \"5000 webrtc-datachannel 16\" }\n\tend\n\t-- Done\n\treturn offer\nend\n\nfunction JANUSSDP.generateAnswer(offer, options)\n\tif offer == nil then\n\t\treturn nil\n\tend\n\t-- Let's set some defaults for the options, in case none were given\n\tif options == nil then options = {} end\n\tif options.audio == nil then options.audio = true end\n\tif options.audioCodec == nil then\n\t\tif JANUSSDP.findPayloadType(offer, \"opus\") > 0 then\n\t\t\toptions.audioCodec = \"opus\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"multiopus\") > 0 then\n\t\t\toptions.audioCodec = \"multiopus\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"pcmu\") > 0 then\n\t\t\toptions.audioCodec = \"pcmu\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"pcma\") > 0 then\n\t\t\toptions.audioCodec = \"pcma\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"g722\") > 0 then\n\t\t\toptions.audioCodec = \"g722\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"isac16\") > 0 then\n\t\t\toptions.audioCodec = \"isac16\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"isac32\") > 0 then\n\t\t\toptions.audioCodec = \"isac32\"\n\t\tend\n\tend\n\tif options.video == nil then options.video = true end\n\tif options.videoCodec == nil then\n\t\tif JANUSSDP.findPayloadType(offer, \"vp8\") > 0 then\n\t\t\toptions.videoCodec = \"vp8\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"vp9\", options.vp9Profile) > 0 then\n\t\t\toptions.videoCodec = \"vp9\"\n\t\telseif JANUSSDP.findPayloadType(offer, \"h264\", options.h264Profile) > 0 then\n\t\t\toptions.videoCodec = \"h264\"\n\t\tend\n\tend\n\tif options.data == nil then options.data = true end\n\tif options.disableTwcc == nil then options.disableTwcc = false end\n\t-- Let's prepare the answer\n\tlocal answer = {}\n\t-- Iterate on all lines\n\tlocal audio = 0\n\tlocal video = 0\n\tlocal data = 0\n\tlocal audioPt = -1\n\tlocal videoPt = -1\n\tlocal medium = nil\n\tlocal reject = false\n\tlocal index = nil\n\tlocal a = nil\n\tfor index,a in pairs(offer) do\n\t\tif medium == nil and a.type ~= \"m\" then\n\t\t\t-- We just copy all the session-level attributes\n\t\t\tif a.value == nil then\n\t\t\t\tanswer[#answer+1] = a\n\t\t\tend\n\t\tend\n\t\tif a.type == \"m\" then\n\t\t\t-- New m-line\n\t\t\treject = false\n\t\t\tif a.name:find(\"audio\") ~= nil then\n\t\t\t\tmedium = \"audio\"\n\t\t\t\taudio = audio+1\n\t\t\t\tif audioPt < 0 then\n\t\t\t\t\taudioPt = JANUSSDP.findPayloadType(offer, options.audioCodec)\n\t\t\t\tend\n\t\t\t\tif audioPt < 0 then\n\t\t\t\t\taudio = audio+1\n\t\t\t\tend\n\t\t\t\tif audio > 1 then\n\t\t\t\t\treject = true\n\t\t\t\t\tanswer[#answer+1] = { type = \"m\", name = \"audio 0 UDP/TLS/RTP/SAVPF 0\" }\n\t\t\t\telse\n\t\t\t\t\tanswer[#answer+1] = { type = \"m\", name = \"audio 9 UDP/TLS/RTP/SAVPF \" .. audioPt }\n\t\t\t\tend\n\t\t\telseif a.name:find(\"video\") ~= nil then\n\t\t\t\tmedium = \"video\"\n\t\t\t\tvideo = video+1\n\t\t\t\tif videoPt < 0 then\n\t\t\t\t\tif options.videoCodec == \"vp9\" then\n\t\t\t\t\t\tvideoPt = JANUSSDP.findPayloadType(offer, options.videoCodec, options.vp9Profile)\n\t\t\t\t\telseif options.videoCodec == \"h264\" then\n\t\t\t\t\t\tvideoPt = JANUSSDP.findPayloadType(offer, options.videoCodec, options.h264Profile)\n\t\t\t\t\telse\n\t\t\t\t\t\tvideoPt = JANUSSDP.findPayloadType(offer, options.videoCodec)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\tif videoPt < 0 then\n\t\t\t\t\tvideo = video+1\n\t\t\t\tend\n\t\t\t\tif video > 1 then\n\t\t\t\t\treject = true\n\t\t\t\t\tanswer[#answer+1] = { type = \"m\", name = \"video 0 UDP/TLS/RTP/SAVPF 0\" }\n\t\t\t\telse\n\t\t\t\t\tanswer[#answer+1] = { type = \"m\", name = \"video 9 UDP/TLS/RTP/SAVPF \" .. videoPt }\n\t\t\t\tend\n\t\t\telseif a.name:find(\"application\") ~= nil then\n\t\t\t\tmedium = \"application\"\n\t\t\t\tdata = data+1\n\t\t\t\tif data > 1 then\n\t\t\t\t\treject = true\n\t\t\t\t\tanswer[#answer+1] = { type = \"m\", name = \"application 0 DTLS/SCTP 5000\" }\n\t\t\t\telse\n\t\t\t\t\tanswer[#answer+1] = { type = \"m\", name = a.name }\n\t\t\t\tend\n\t\t\tend\n\t\telseif a.type == \"a\" then\n\t\t\tif a.name == \"sendonly\" then\n\t\t\t\tanswer[#answer+1] = { type = \"a\", name = \"recvonly\" }\n\t\t\telseif a.name == \"recvonly\" then\n\t\t\t\tanswer[#answer+1] = { type = \"a\", name = \"sendonly\" }\n\t\t\telseif a.value ~= nil then\n\t\t\t\tif a.name == \"rtpmap\" or a.name == \"fmtp\" or a.name == \"rtcp-fb\" then\n\t\t\t\t\t-- Drop attributes associated to payload types we're getting rid of\n\t\t\t\t\tlocal n = a.value:gmatch(\"[^ ]+\")\n\t\t\t\t\tif medium == \"audio\" and tonumber(n()) == audioPt then\n\t\t\t\t\t\tanswer[#answer+1] = a\n\t\t\t\t\telseif medium == \"video\" and tonumber(n()) == videoPt then\n\t\t\t\t\t\tanswer[#answer+1] = a\n\t\t\t\t\tend\n\t\t\t\telseif a.name == \"extmap\" then\n\t\t\t\t\t-- We do negotiate some RTP extensions: check if there's a direction first\n\t\t\t\t\tlocal value = a.value\n\t\t\t\t\tif a.value:find(\"/sendonly\", 1, true) then\n\t\t\t\t\t\tvalue = a.value:gsub(\"/sendonly\", \"/recvonly\")\n\t\t\t\t\telseif a.value:find(\"/recvonly\", 1, true) then\n\t\t\t\t\t\tvalue = a.value:gsub(\"/recvonly\", \"/sendonly\")\n\t\t\t\t\tend\n\t\t\t\t\t-- Now check if we have to negotiate the extension\n\t\t\t\t\tif a.value:find(\"urn:ietf:params:rtp-hdrext:sdes:mid\", 1, true) then\n\t\t\t\t\t\tanswer[#answer+1] = { type = \"a\", name = a.name, value = value }\n\t\t\t\t\telseif a.value:find(\"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\", 1, true) then\n\t\t\t\t\t\tanswer[#answer+1] = { type = \"a\", name = a.name, value = value }\n\t\t\t\t\telseif a.value:find(\"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\", 1, true) then\n\t\t\t\t\t\tanswer[#answer+1] = { type = \"a\", name = a.name, value = value }\n\t\t\t\t\telseif options.disableTwcc ~= true and a.value:find(\"draft-holmer-rmcat-transport-wide-cc-extensions-01\", 1, true) then\n\t\t\t\t\t\tanswer[#answer+1] = { type = \"a\", name = a.name, value = value }\n\t\t\t\t\telseif options.enableAudioLevel ~= false and a.value:find(\"urn:ietf:params:rtp-hdrext:ssrc-audio-level\", 1, true) then\n\t\t\t\t\t\tanswer[#answer+1] = a\n\t\t\t\t\telseif options.enableAudioLevel ~= false and a.value:find(\"dependency-descriptor-rtp-header-extension\", 1, true) then\n\t\t\t\t\t\tanswer[#answer+1] = a\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\telse\n\t\t\t\tanswer[#answer+1] = a\n\t\t\tend\n\t\t\t-- TODO Handle/filter other attributes\n\t\tend\n\tend\n\t-- Done\n\treturn answer\nend\n\nreturn JANUSSDP\n"
  },
  {
    "path": "src/plugins/lua/videoroom.lua",
    "content": "-- This is a simple example of an video room application built in Lua,\n-- and conceived to be used in conjunction with the janus_lua.c plugin\n--\n-- Note: this example depends on lua-json to do JSON processing\n-- (http://luaforge.net/projects/luajson/)\njson = require('json')\n-- We also import our own SDP helper utilities: you may have better ones\nsdp = require('janus-sdp')\n-- Let's also use our ugly stdout logger just for the fun of it: to add\n-- some color to the text we use the lua-term library\n-- (https://github.com/hoelzro/lua-term)\ncolors = require('term.colors')\nlogger = require('janus-logger')\n\n-- Example details\nname = \"videoroom.lua\"\nlogger.prefix(\"[\" .. colors.blue .. name .. colors.reset .. \"]\")\nlogger.print(\"Loading...\")\n\n-- State and properties\nsessions = {}\nrooms = {}\ntasks = {}\n\n-- Errors\nJANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR\t= 499\nJANUS_VIDEOROOM_ERROR_NO_MESSAGE = 421\nJANUS_VIDEOROOM_ERROR_INVALID_JSON = 422\nJANUS_VIDEOROOM_ERROR_INVALID_REQUEST = 423\nJANUS_VIDEOROOM_ERROR_JOIN_FIRST = 424\nJANUS_VIDEOROOM_ERROR_ALREADY_JOINED = 425\nJANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM = 426\nJANUS_VIDEOROOM_ERROR_ROOM_EXISTS = 427\nJANUS_VIDEOROOM_ERROR_NO_SUCH_FEED = 428\nJANUS_VIDEOROOM_ERROR_MISSING_ELEMENT = 429\nJANUS_VIDEOROOM_ERROR_INVALID_ELEMENT = 430\nJANUS_VIDEOROOM_ERROR_INVALID_SDP_TYPE = 431\nJANUS_VIDEOROOM_ERROR_PUBLISHERS_FULL = 432\nJANUS_VIDEOROOM_ERROR_UNAUTHORIZED = 433\nJANUS_VIDEOROOM_ERROR_ALREADY_PUBLISHED = 434\nJANUS_VIDEOROOM_ERROR_NOT_PUBLISHED = 435\nJANUS_VIDEOROOM_ERROR_ID_EXISTS = 436\nJANUS_VIDEOROOM_ERROR_INVALID_SDP = 437\n\n\n-- Methods\nfunction init(config)\n\t-- This is where we initialize the plugin, for static properties\n\tlogger.print(\"Initializing...\")\n\tif config ~= nil then\n\t\t-- TODO Should we actually have code to parse a janus.plugin.videoroom.cfg file here?\n\t\tlogger.print(\"Configuration file provided (\" .. config .. \"), but we don't need it\")\n\tend\n\tlogger.print(\"Initialized\")\nend\n\nfunction destroy()\n\t-- This is where we deinitialize the plugin, when Janus shuts down\n\tlogger.print(\"Deinitialized\")\nend\n\nfunction createSession(id)\n\t-- Keep track of a new session\n\tlogger.print(\"Created new session: \" .. id)\n\tsessions[id] = { id = id, lua = name }\nend\n\nfunction destroySession(id)\n\t-- A Janus plugin session has gone\n\tlogger.print(\"Destroyed session: \" .. id)\n\thangupMedia(id)\n\t-- Remove the user from the list of participants\n\tlocal s = sessions[id]\n\tif s~= nil and s.userId ~= nil then\n\t\tlocal room = nil\n\t\tif s.roomId ~= nil then\n\t\t\troom = rooms[s.roomId]\n\t\tend\n\t\tif room ~= nil then\n\t\t\t-- If this is a publisher, notify other participants that the user is leaving\n\t\t\tif(s[\"pType\"] == \"publisher\") then\n\t\t\t\tlocal event = { videoroom = \"event\", leaving = s.userId, room = room.roomId }\n\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\tfor index,partId in pairs(room.participants) do\n\t\t\t\t\tlocal p = sessions[partId]\n\t\t\t\t\tif p ~= nil and p.id ~= id then\n\t\t\t\t\t\tpushEvent(p.id, nil, eventjson, nil)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\t-- If private IDs are required to prevent lurking, get rid of the subscriptions as well\n\t\t\t\tif room.requirePvtId == true then\n\t\t\t\t\tfor index,sub in ipairs(s.subscriptions) do\n\t\t\t\t\t\tlogger.print(\"  -- Getting rid of publisher's subscription: \" .. sub)\n\t\t\t\t\t\tendSession(sub)\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\ts.subscriptions = {}\n\t\t\t\tif s.privateId ~= nil then\n\t\t\t\t\troom.privateIds[s.privateId] = nil\n\t\t\t\tend\n\t\t\t\troom.participants[s.userId] = nil\n\t\t\tend\n\t\tend\n\t\ts.userId = nil\n\tend\n\tsessions[id] = nil\nend\n\nfunction querySession(id)\n\t-- Return info on a session\n\tlogger.print(\"Queried session: \" .. id)\n\tlocal s = sessions[id]\n\tif s == nil then\n\t\treturn nil\n\tend\n\tlocal info = { script = s[\"lua\"], id = s[\"id\"], display = s[\"display\"],\n\t\troom = s[\"roomId\"], ptype = s[\"pType\"], user = s[\"userId\"], feed = s[\"feedId\"],\n\t\taudio = s[\"audio\"], audioCodec = s[\"audioCodec\"],\n\t\tvideo = s[\"video\"], videoCodec = s[\"videoCodec\"],\n\t\tdata = s[\"data\"], bitrate = s[\"bitrate\"] }\n\tlocal infojson = json.encode(info)\n\treturn infojson\nend\n\nfunction handleMessage(id, tr, msg, jsep)\n\t-- Handle a message, synchronously or asynchronously, and return\n\t-- something accordingly: if it's the latter, we'll do a coroutine\n\tlogger.print(\"Handling message for session: \" .. id)\n\tlocal s = sessions[id]\n\tif s == nil then\n\t\treturn -1, \"Session not found\"\n\tend\n\t-- Decode the message JSON string to a table\n\tif msg == nil then\n\t\treturn -1, \"Invalid message\"\n\tend\n\tlocal msgT = json.decode(msg)\n\tlocal jsepT = nil\n\tif jsep ~= nil then\n\t\tjsepT = json.decode(jsep)\n\tend\n\t-- Handle the request\n\tlocal request = msgT[\"request\"]\n\tif request == \"create\" then\n\t\t-- Create a new room\n\t\tlocal roomId = msgT[\"room\"]\n\t\tif roomId == nil then\n\t\t\troomId = math.random(4294967296)\n\t\tend\n\t\tlogger.print(\"Creating new room: \" .. roomId)\n\t\tlocal description = msgT[\"description\"]\n\t\tlocal secret = msgT[\"secret\"]\n\t\tlocal publishers = msgT[\"publishers\"]\n\t\tif(publishers == nil) then\n\t\t\tpublishers = 3\n\t\tend\n\t\tlocal audioCodec = msgT[\"audiocodec\"]\n\t\tif(audioCodec == nil) then\n\t\t\taudioCodec = \"opus\"\n\t\tend\n\t\tlocal videoCodec = msgT[\"videocodec\"]\n\t\tif(videoCodec == nil) then\n\t\t\tvideoCodec = \"vp8\"\n\t\tend\n\t\tlocal vp9Profile = msgT[\"vp9_profile\"]\n\t\tlocal h264Profile = msgT[\"h264_profile\"]\n\t\tlocal bitrate = msgT[\"bitrate\"]\n\t\tlocal pliFreq = msgT[\"fir_freq\"]\n\t\tlocal requirePvtId = msgT[\"require_pvtid\"]\n\t\tlocal notifyJoining = msgT[\"notify_joining\"]\n\t\tif rooms[roomId] ~= nil then\n\t\t\tlocal response = { videoroom = \"error\", error_code = JANUS_VIDEOROOM_ERROR_ROOM_EXISTS, error = \"Room exists\" }\n\t\t\tlocal responsejson = json.encode(response)\n\t\t\treturn 0, responsejson\n\t\tend\n\t\trooms[roomId] = {\n\t\t\troomId = roomId,\n\t\t\tdescription = description,\n\t\t\tsecret = secret,\n\t\t\tpublishers = publishers,\n\t\t\taudioCodec = split(audioCodec, \",\"),\n\t\t\tvideoCodec = split(videoCodec, \",\"),\n\t\t\tvp9Profile = vp9Profile,\n\t\t\th264Profile = h264Profile,\n\t\t\tbitrate = bitrate,\n\t\t\tpliFreq = pliFreq,\n\t\t\trequirePvtId = requirePvtId,\n\t\t\tnotifyJoining = notifyJoining,\n\t\t\tparticipants = {},\n\t\t\tprivateIds = {}\n\t\t}\n\t\tlocal response = { videoroom = \"created\", room = roomId }\n\t\tlocal responsejson = json.encode(response)\n\t\treturn 0, responsejson\n\telseif request == \"destroy\" then\n\t\t-- Destroy an existing room\n\t\tlocal roomId = msgT[\"room\"]\n\t\tlogger.print(\"Destroying room: \" .. roomId)\n\t\tlocal room = rooms[roomId]\n\t\tif room == nil then\n\t\t\tlocal error = { videoroom = \"error\", error_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, error = \"No such room\" }\n\t\t\tlocal responsejson = json.encode(error)\n\t\t\treturn 0, responsejson\n\t\tend\n\t\tif room.secret ~= nil and room.secret ~= msgT[\"secret\"] then\n\t\t\tlocal error = { videoroom = \"error\", error_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED, error = \"Unauthorized (wrong secret)\" }\n\t\t\tlocal responsejson = json.encode(error)\n\t\t\treturn 0, responsejson\n\t\tend\n\t\t-- Kick users\n\t\tlocal event = { videoroom = \"destroyed\", room = roomId }\n\t\tfor index,partId in pairs(room.participants) do\n\t\t\tlocal p = sessions[partId]\n\t\t\tif p ~= nil then\n\t\t\t\t-- Notify user\n\t\t\t\tlogger.print(\"Notifying user: \" .. p.id)\n\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\tpushEvent(p.id, nil, eventjson, nil)\n\t\t\t\t-- Close the PeerConnection, if any\n\t\t\t\tif p.started == true then\n\t\t\t\t\thangupMedia(p.id)\n\t\t\t\t\t--~ closePc(p.id)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\troom.participants = {}\n\t\troom.privateIds = {}\n\t\t-- Done\n\t\trooms[roomId] = nil\n\t\tlocal response = { videoroom = \"destroyed\", room = roomId }\n\t\tlocal responsejson = json.encode(response)\n\t\treturn 0, responsejson\n\telseif request == \"list\" then\n\t\t-- List existing rooms\n\t\tlogger.print(\"Listing rooms\")\n\t\tlocal response = { videoroom = \"success\", list = rooms }\n\t\tlocal responsejson = json.encode(response)\n\t\treturn 0, responsejson\n\telseif request == \"exists\" then\n\t\t-- Check if an existing room exists\n\t\tlocal exists = false\n\t\tlocal roomId = msgT[\"room\"]\n\t\tif roomId ~= nil then\n\t\t\tlogger.print(\"Checking if room exists: \" .. roomId)\n\t\t\tif rooms[roomId] ~= nil then\n\t\t\t\texists = true\n\t\t\tend\n\t\tend\n\t\tlocal response = { videoroom = \"success\", room = roomId, exists = exists }\n\t\tlocal responsejson = json.encode(response)\n\t\treturn 0, responsejson\n\telseif request == \"listparticipants\" then\n\t\t-- List participants in a room\n\t\tlocal roomId = msgT[\"room\"]\n\t\tlogger.print(\"Listing participants in room: \" .. roomId)\n\t\tlocal room = rooms[roomId]\n\t\tif room == nil then\n\t\t\tlocal error = { videoroom = \"error\", error_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, error = \"No such room\" }\n\t\t\tlocal responsejson = json.encode(error)\n\t\t\treturn 0, responsejson\n\t\tend\n\t\tlocal response = { videoroom = \"participants\", room = roomId, participants = {} }\n\t\tfor index,partId in pairs(room.participants) do\n\t\t\tlocal p = sessions[partId]\n\t\t\tif p ~= nil and p.id ~= id and p.sdp ~= nil and p.started == true then\n\t\t\t\tresponse.participants[#response.participants+1] = {\n\t\t\t\t\tid = p.userId,\n\t\t\t\t\tdisplay = p.display,\n\t\t\t\t\taudio_codec = p.audioCodec,\n\t\t\t\t\tvideo_codec = p.videoCodec\n\t\t\t\t}\n\t\t\tend\n\t\tend\n\t\tlocal responsejson = json.encode(response)\n\t\tlogger.print(responsejson)\n\t\tif responsejson:find(\"\\\"participants\\\":{}\") ~= nil then\n\t\t\t-- Ugly hack, as lua-json turns our empty array into an empty object\n\t\t\tresponsejson = string.gsub(responsejson, \"\\\"participants\\\":{}\", \"\\\"participants\\\":[]\")\n\t\tend\n\t\treturn 0, responsejson\n\telse\n\t\t-- Check if it's a request we can handle asynchronously\n\t\tif request == \"join\" or request == \"configure\" or request == \"publish\" or request == \"unpublish\"\n\t\t\t\tor request == \"start\" or request == \"switch\" or request == \"leave\" then\n\t\t\t-- We need a new coroutine here\n\t\t\tlocal async = coroutine.create(function(id, tr, comsg, cojsep)\n\t\t\t\t-- We'll only execute this when the scheduler resumes the task\n\t\t\t\tlogger.print(\"Handling async message for session: \" .. id)\n\t\t\t\tlogger.print(\"  -- \" .. dumpTable(comsg))\n\t\t\t\tlocal s = sessions[id]\n\t\t\t\tif s == nil then\n\t\t\t\t\tlogger.print(\"Can't handle async message: so such session\")\n\t\t\t\t\treturn\n\t\t\t\tend\n\t\t\t\tlocal request = comsg[\"request\"]\n\t\t\t\tlogger.print(\"Handling request: \" .. request)\n\t\t\t\tlogger.print(\"Session: \" .. dumpTable(s))\n\t\t\t\tif request == \"join\" then\n\t\t\t\t\t-- Join a room as publisher or subscriber\n\t\t\t\t\tlocal roomId = comsg[\"room\"]\n\t\t\t\t\tlocal room = rooms[roomId]\n\t\t\t\t\tif room == nil then\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, error = \"No such room\" }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\t\tlocal pType = comsg[\"ptype\"]\n\t\t\t\t\tlogger.print(\"Joining room as \" .. pType .. \": \" .. roomId)\n\t\t\t\t\tif pType == \"publisher\" then\n\t\t\t\t\t\t-- Setup new publisher\n\t\t\t\t\t\tlocal userId = comsg[\"id\"]\n\t\t\t\t\t\tlocal display = comsg[\"display\"]\n\t\t\t\t\t\tif userId == nil then\n\t\t\t\t\t\t\tuserId = math.random(4294967296)\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif room.participants[userId] ~= nil then\n\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_ID_EXISTS, error = \"UserID already exists\" }\n\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\tend\n\t\t\t\t\t\ts[\"pType\"] = pType\n\t\t\t\t\t\ts[\"roomId\"] = roomId\n\t\t\t\t\t\ts[\"userId\"] = userId\n\t\t\t\t\t\tlocal privateId = math.random(4294967296)\n\t\t\t\t\t\ts.subscriptions = {}\n\t\t\t\t\t\ts[\"privateId\"] = privateId\n\t\t\t\t\t\ts[\"display\"] = display\n\t\t\t\t\t\ts[\"subscribers\"] = {}\n\t\t\t\t\t\troom.participants[userId] = id\n\t\t\t\t\t\troom.privateIds[privateId] = id\n\t\t\t\t\t\t-- Import the room settings\n\t\t\t\t\t\ts[\"audioCodec\"] = nil\t-- We'll figure out this later\n\t\t\t\t\t\ts[\"videoCodec\"] = nil\t-- We'll figure out this later\n\t\t\t\t\t\tif room.bitrate ~= nil then\n\t\t\t\t\t\t\tlogger.print(\"Setting bitrate: \" .. room.bitrate)\n\t\t\t\t\t\t\tsetBitrate(id, room.bitrate)\n\t\t\t\t\t\t\ts[\"bitrate\"] = room.bitrate\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif room.pliFreq ~= nil then\n\t\t\t\t\t\t\tlogger.print(\"Setting PLI frequency: \" .. room.pliFreq)\n\t\t\t\t\t\t\tsetPliFreq(id, room.pliFreq)\n\t\t\t\t\t\t\ts[\"pliFreq\"] = room.pliFreq\n\t\t\t\t\t\tend\n\t\t\t\t\t\t-- Publishers can only send media\n\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"out\", true)\n\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"in\", false)\n\t\t\t\t\t\tconfigureMedium(id, \"video\", \"out\", true)\n\t\t\t\t\t\tconfigureMedium(id, \"video\", \"in\", false)\n\t\t\t\t\t\tconfigureMedium(id, \"data\", \"out\", true)\n\t\t\t\t\t\tconfigureMedium(id, \"data\", \"in\", false)\n\t\t\t\t\t\t-- Send event back with a list of active publishers (and possibly other attendees)\n\t\t\t\t\t\tlocal event = { videoroom = \"joined\", room = roomId, description = room.description,\n\t\t\t\t\t\t\tid = userId, private_id = privateId, publishers = {} }\n\t\t\t\t\t\tif room.notifyJoining then\n\t\t\t\t\t\t\tevent.attendees = {}\n\t\t\t\t\t\tend\n\t\t\t\t\t\tfor index,partId in pairs(room.participants) do\n\t\t\t\t\t\t\tlocal p = sessions[partId]\n\t\t\t\t\t\t\t-- Publishers first\n\t\t\t\t\t\t\tif p ~= nil and p.id ~= id and p.sdp ~= nil and p.started == true then\n\t\t\t\t\t\t\t\tevent.publishers[#event.publishers+1] = {\n\t\t\t\t\t\t\t\t\tid = p.userId,\n\t\t\t\t\t\t\t\t\tdisplay = p.display,\n\t\t\t\t\t\t\t\t\taudio_codec = p.audioCodec,\n\t\t\t\t\t\t\t\t\tvideo_codec = p.videoCodec\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t-- If notify_joining=true, send a list of attendees as well\n\t\t\t\t\t\t\tif room.notifyJoining and p ~= nil and p.id ~= id then\n\t\t\t\t\t\t\t\tevent.attendees[#event.attendees+1] = {\n\t\t\t\t\t\t\t\t\tid = p.userId,\n\t\t\t\t\t\t\t\t\tdisplay = p.display\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tif eventjson:find(\"\\\"publishers\\\":{}\") ~= nil then\n\t\t\t\t\t\t\t-- Ugly hack, as lua-json turns our empty array into an empty object\n\t\t\t\t\t\t\teventjson = string.gsub(eventjson, \"\\\"publishers\\\":{}\", \"\\\"publishers\\\":[]\")\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif room.notifyJoining and eventjson:find(\"\\\"attendees\\\":{}\") ~= nil then\n\t\t\t\t\t\t\t-- Ugly hack, as lua-json turns our empty array into an empty object\n\t\t\t\t\t\t\teventjson = string.gsub(eventjson, \"\\\"attendees\\\":{}\", \"\\\"attendees\\\":[]\")\n\t\t\t\t\t\tend\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t-- If notify_joining=true, notify other participants as well\n\t\t\t\t\t\tif room.notifyJoining then\n\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", event = \"joining\",\n\t\t\t\t\t\t\t\troom = room.roomId, id = s.userId, display = s.display }\n\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\tfor index,partId in pairs(room.participants) do\n\t\t\t\t\t\t\t\tlocal p = sessions[partId]\n\t\t\t\t\t\t\t\tif p ~= nil and p.id ~= id then\n\t\t\t\t\t\t\t\t\tpushEvent(p.id, nil, eventjson, nil)\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\telseif pType == \"subscriber\" then\n\t\t\t\t\t\t-- Setup new subscriber\n\t\t\t\t\t\tlocal feedId = comsg[\"feed\"]\n\t\t\t\t\t\tlogger.print(\"Subscribing to feed: \" .. feedId)\n\t\t\t\t\t\tif room.participants[feedId] == nil then\n\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED, error = \"No such feed\" }\n\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\tend\n\t\t\t\t\t\tlocal f = sessions[room.participants[feedId]]\n\t\t\t\t\t\tif f == nil or f.started ~= true then\n\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED, error = \"No such feed\" }\n\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\tend\n\t\t\t\t\t\tlocal privateId = comsg[\"private_id\"]\n\t\t\t\t\t\tif room.requirePvtId == true then\n\t\t\t\t\t\t\t-- Make sure a valid private ID was provided\n\t\t\t\t\t\t\tlocal owner = room.privateIds[privateId]\n\t\t\t\t\t\t\tif owner == nil then\n\t\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_UNAUTHORIZED, error = \"Unauthorized (this room requires a valid private_id)\" }\n\t\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tlocal o = sessions[owner];\n\t\t\t\t\t\t\t-- Add this session to the owner's subscriptions\n\t\t\t\t\t\t\to.subscriptions[#o.subscriptions+1] = id\n\t\t\t\t\t\tend\n\t\t\t\t\t\ts[\"privateId\"] = privateId\n\t\t\t\t\t\ts[\"pType\"] = pType\n\t\t\t\t\t\ts[\"roomId\"] = roomId\n\t\t\t\t\t\ts[\"feedId\"] = feedId\n\t\t\t\t\t\ts[\"feedSessionId\"] = f.id\n\t\t\t\t\t\tf[\"subscribers\"][id] = id\n\t\t\t\t\t\t-- Subscribers can only receive media\n\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"in\", true)\n\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"out\", false)\n\t\t\t\t\t\ts[\"audio\"] = true\n\t\t\t\t\t\tconfigureMedium(id, \"video\", \"in\", true)\n\t\t\t\t\t\tconfigureMedium(id, \"video\", \"out\", false)\n\t\t\t\t\t\ts[\"video\"] = true\n\t\t\t\t\t\tconfigureMedium(id, \"data\", \"in\", true)\n\t\t\t\t\t\tconfigureMedium(id, \"data\", \"out\", false)\n\t\t\t\t\t\ts[\"data\"] = true\n\t\t\t\t\t\t-- Check if we need to drop anything\n\t\t\t\t\t\tif comsg[\"audio\"] == true then\n\t\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"in\", true)\n\t\t\t\t\t\t\ts[\"audio\"] = true\n\t\t\t\t\t\telseif comsg[\"audio\"] == false then\n\t\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"in\", false)\n\t\t\t\t\t\t\ts[\"audio\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"video\"] == true then\n\t\t\t\t\t\t\tconfigureMedium(id, \"video\", \"in\", true)\n\t\t\t\t\t\t\ts[\"video\"] = true\n\t\t\t\t\t\telseif comsg[\"video\"] == false then\n\t\t\t\t\t\t\tconfigureMedium(id, \"video\", \"in\", false)\n\t\t\t\t\t\t\ts[\"video\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"data\"] == true then\n\t\t\t\t\t\t\tconfigureMedium(id, \"data\", \"in\", true)\n\t\t\t\t\t\t\ts[\"data\"] = true\n\t\t\t\t\t\telseif comsg[\"data\"] == false then\n\t\t\t\t\t\t\tconfigureMedium(id, \"data\", \"in\", false)\n\t\t\t\t\t\t\ts[\"data\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tlocal offer_audio = true\n\t\t\t\t\t\tif comsg[\"offer_audio\"] == false then\n\t\t\t\t\t\t\toffer_audio = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tlocal offer_video = true\n\t\t\t\t\t\tif comsg[\"offer_video\"] == false then\n\t\t\t\t\t\t\toffer_video = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tlocal offer_data = true\n\t\t\t\t\t\tif comsg[\"offer_data\"] == false then\n\t\t\t\t\t\t\toffer_data = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\t-- Prepare offer and send it back\n\t\t\t\t\t\tlocal baseOffer = sdp.parse(f[\"sdp\"])\n\t\t\t\t\t\t-- Check if we need to remove some m-lines\n\t\t\t\t\t\tif offer_audio == false then\n\t\t\t\t\t\t\t-- Remove audio m-line\n\t\t\t\t\t\t\tlogger.print(\"  -- Subscriber doesn't want audio\")\n\t\t\t\t\t\t\tsdp.removeMLine(baseOffer, \"audio\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"in\", false)\n\t\t\t\t\t\t\ts[\"audio\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif offer_video == false then\n\t\t\t\t\t\t\t-- Remove video m-line\n\t\t\t\t\t\t\tlogger.print(\"  -- Subscriber doesn't want video\")\n\t\t\t\t\t\t\tsdp.removeMLine(baseOffer, \"video\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"video\", \"in\", false)\n\t\t\t\t\t\t\ts[\"video\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif offer_data == false then\n\t\t\t\t\t\t\t-- Remove application m-line\n\t\t\t\t\t\t\tlogger.print(\"  -- Subscriber doesn't want data\")\n\t\t\t\t\t\t\tsdp.removeMLine(baseOffer, \"application\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"data\", \"in\", false)\n\t\t\t\t\t\t\ts[\"data\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\t-- Generate the offer\n\t\t\t\t\t\ts[\"sdp\"] = sdp.render(baseOffer)\n\t\t\t\t\t\tlocal event = { videoroom = \"attached\", room = roomId, id = feedId, display = f[\"display\"] }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tlogger.print(\"Prepared offer for subscriber: \" .. s[\"sdp\"])\n\t\t\t\t\t\tlocal offer = { type = \"offer\", sdp = s[\"sdp\"] }\n\t\t\t\t\t\tlocal offerjson = json.encode(offer)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, offerjson)\n\t\t\t\t\telse\n\t\t\t\t\t\tlogger.print(\"Invalid element\")\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, error = \"Invalid element (ptype)\" }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\tend\n\t\t\t\telseif request == \"configure\" or request == \"publish\" then\n\t\t\t\t\t-- Modify properties for a session, and/or start publishing\n\t\t\t\t\tlogger.print(\"Received a \" .. request .. \" by a \" .. s[\"pType\"] .. \": \" .. s[\"roomId\"])\n\t\t\t\t\tif s[\"pType\"] == \"publisher\" then\n\t\t\t\t\t\t-- Prepare a response\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", room = s[\"roomId\"], configured = \"ok\" }\n\t\t\t\t\t\t-- Check if there's an SDP offer\n\t\t\t\t\t\tlocal answerjson = nil\n\t\t\t\t\t\tif cojsep ~= nil then\n\t\t\t\t\t\t\t-- There's an SDP: is this a new offer, or a renegotiation?\n\t\t\t\t\t\t\tif cojsep[\"update\"] == true then\n\t\t\t\t\t\t\t\tlogger.print(\"Renegotiation occurring on the publisher\")\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\tlogger.print(\"Setting up new PeerConnection for publisher\")\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t-- Make sure the publisher is sendonly\n\t\t\t\t\t\t\tlocal room = rooms[s[\"roomId\"]]\n\t\t\t\t\t\t\tlocal sdpoffer = string.gsub(cojsep[\"sdp\"], \"sendrecv\", \"sendonly\")\n\t\t\t\t\t\t\tlocal offer = sdp.parse(sdpoffer)\n\t\t\t\t\t\t\tlogger.print(\"Got offer from publisher: \" .. sdp.render(offer))\n\t\t\t\t\t\t\t-- Check which codecs are allowed in the room\n\t\t\t\t\t\t\tlocal audioCodec = comsg[\"audiocodec\"]\n\t\t\t\t\t\t\tif audioCodec ~= nil then\n\t\t\t\t\t\t\t\t-- The publisher wants to use a specific codec, let's see if that's possible\n\t\t\t\t\t\t\t\tlogger.print(\"Publisher wants to use audio codec: \" .. audioCodec)\n\t\t\t\t\t\t\t\tfor index,codec in ipairs(room.audioCodec) do\n\t\t\t\t\t\t\t\t\tif codec == audioCodec then\n\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher audio codec found\");\n\t\t\t\t\t\t\t\t\t\ts.audioCodec = codec\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tif s.audioCodec == nil then\n\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher audio codec NOT found\");\n\t\t\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, error = \"Audio codec unavailable in this room\" }\n\t\t\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t-- Pick the best audio codec (SDP vs. room preferences)\n\t\t\t\t\t\t\t\ts[\"audioCodec\"] = nil\n\t\t\t\t\t\t\t\tfor index,codec in ipairs(room.audioCodec) do\n\t\t\t\t\t\t\t\t\tif s.audioCodec == nil then\n\t\t\t\t\t\t\t\t\t\tlogger.print(\"Looking for audio codec \" .. codec)\n\t\t\t\t\t\t\t\t\t\tif sdp.findPayloadType(offer, codec) == -1 then\n\t\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Not found, trying next audio codec...\")\n\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher audio codec found\")\n\t\t\t\t\t\t\t\t\t\t\ts.audioCodec = codec\n\t\t\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tlocal videoCodec = comsg[\"videocodec\"]\n\t\t\t\t\t\t\tif videoCodec ~= nil then\n\t\t\t\t\t\t\t\t-- The publisher wants to use a specific codec, let's see if that's possible\n\t\t\t\t\t\t\t\tlogger.print(\"Publisher wants to use video codec: \" .. videoCodec)\n\t\t\t\t\t\t\t\tfor index,codec in ipairs(room.videoCodec) do\n\t\t\t\t\t\t\t\t\tif codec == videoCodec then\n\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher video codec found\");\n\t\t\t\t\t\t\t\t\t\ts.videoCodec = codec\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tif s.videoCodec == nil then\n\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher video codec NOT found\");\n\t\t\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, error = \"Video codec unavailable in this room\" }\n\t\t\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t-- Pick the best video codec (SDP vs. room preferences)\n\t\t\t\t\t\t\t\ts[\"videoCodec\"] = nil\n\t\t\t\t\t\t\t\tfor index,codec in ipairs(room.videoCodec) do\n\t\t\t\t\t\t\t\t\tif s.videoCodec == nil then\n\t\t\t\t\t\t\t\t\t\tlogger.print(\"Looking for video codec \" .. codec)\n\t\t\t\t\t\t\t\t\t\tif codec == \"vp9\" and room.vp9Profile ~= nil and sdp.findPayloadType(offer, codec, room.vp9Profile) == 0 then\n\t\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher video codec found\")\n\t\t\t\t\t\t\t\t\t\t\ts.videoCodec = codec\n\t\t\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t\t\telseif codec == \"h264\" and room.h264Profile ~= nil and sdp.findPayloadType(offer, codec, room.h264Profile) == 0 then\n\t\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher video codec found\")\n\t\t\t\t\t\t\t\t\t\t\ts.videoCodec = codec\n\t\t\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\t\tif sdp.findPayloadType(offer, codec) == -1 then\n\t\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Not found, trying next video codec...\")\n\t\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t\tlogger.print(\"  -- Publisher video codec found\")\n\t\t\t\t\t\t\t\t\t\t\ts.videoCodec = codec\n\t\t\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\t\t-- Generate answer\n\t\t\t\t\t\t\tlocal answer = sdp.generateAnswer(offer, {\n\t\t\t\t\t\t\t\taudio = (s.audioCodec ~= nil), audioCodec = s.audioCodec,\n\t\t\t\t\t\t\t\tvideo = (s.videoCodec ~= nil), videoCodec = s.videoCodec,\n\t\t\t\t\t\t\t\t\tvp9Profile = room.vp9Profile, h264Profile = room.h264Profile,\n\t\t\t\t\t\t\t\tdata = true })\n\t\t\t\t\t\t\tlogger.print(\"Generated answer for publisher: \" .. sdp.render(answer))\n\t\t\t\t\t\t\tlocal jsepanswer = { type = \"answer\", sdp = sdp.render(answer) }\n\t\t\t\t\t\t\tanswerjson = json.encode(jsepanswer)\n\t\t\t\t\t\t\t-- Prepare a revised version of the offer to send to subscribers\n\t\t\t\t\t\t\ts[\"sdp\"] = string.gsub(jsepanswer.sdp, \"recvonly\", \"sendonly\")\n\t\t\t\t\t\t\t-- Prepare the event to send back\n\t\t\t\t\t\t\tevent[\"audio_codec\"] = s.audioCodec\n\t\t\t\t\t\t\tevent[\"video_codec\"] = s.videoCodec\n\t\t\t\t\t\tend\n\t\t\t\t\t\t-- Check what we need to configure\n\t\t\t\t\t\tif comsg[\"audio\"] == true then\n\t\t\t\t\t\t\tlogger.print(\"Enabling audio\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"out\", true)\n\t\t\t\t\t\t\ts[\"audio\"] = true\n\t\t\t\t\t\telseif comsg[\"audio\"] == false then\n\t\t\t\t\t\t\tlogger.print(\"Disabling audio\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"out\", false)\n\t\t\t\t\t\t\ts[\"audio\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"video\"] == true then\n\t\t\t\t\t\t\tlogger.print(\"Enabling video\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"video\", \"out\", true)\n\t\t\t\t\t\t\tsendPli(id)\n\t\t\t\t\t\t\ts[\"video\"] = true\n\t\t\t\t\t\telseif comsg[\"video\"] == false then\n\t\t\t\t\t\t\tlogger.print(\"Disabling video\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"video\", \"out\", false)\n\t\t\t\t\t\t\ts[\"video\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"data\"] == true then\n\t\t\t\t\t\t\tlogger.print(\"Enabling data\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"data\", \"out\", true)\n\t\t\t\t\t\t\ts[\"data\"] = true\n\t\t\t\t\t\telseif comsg[\"data\"] == false then\n\t\t\t\t\t\t\tlogger.print(\"Disabling data\")\n\t\t\t\t\t\t\tconfigureMedium(id, \"data\", \"out\", false)\n\t\t\t\t\t\t\ts[\"data\"] = false\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"bitrate\"] ~= nil then\n\t\t\t\t\t\t\tlogger.print(\"Setting bitrate: \" .. comsg[\"bitrate\"])\n\t\t\t\t\t\t\tsetBitrate(id, comsg[\"bitrate\"])\n\t\t\t\t\t\t\ts[\"bitrate\"] = comsg[\"bitrate\"]\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"fir_freq\"] ~= nil then\n\t\t\t\t\t\t\tlogger.print(\"Setting PLI frequency: \" .. comsg[\"fir_freq\"])\n\t\t\t\t\t\t\tsetPliFreq(id, comsg[\"fir_freq\"])\n\t\t\t\t\t\t\ts[\"pliFreq\"] = comsg[\"fir_freq\"]\n\t\t\t\t\t\tend\n\t\t\t\t\t\t-- Done\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, answerjson)\n\t\t\t\t\telseif s[\"pType\"] == \"subscriber\" then\n\t\t\t\t\t\t-- Configure the subscription properties\n\t\t\t\t\t\tif request == \"publish\" then\n\t\t\t\t\t\t\tlogger.print(\"Invalid request: \" .. request)\n\t\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, error = \"Invalid request\" }\n\t\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"audio\"] == true then\n\t\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"in\", true)\n\t\t\t\t\t\telseif comsg[\"audio\"] == false then\n\t\t\t\t\t\t\tconfigureMedium(id, \"audio\", \"in\", false)\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"video\"] == true then\n\t\t\t\t\t\t\tconfigureMedium(id, \"video\", \"in\", true)\n\t\t\t\t\t\t\tsendPli(id)\n\t\t\t\t\t\telseif comsg[\"video\"] == false then\n\t\t\t\t\t\t\tconfigureMedium(id, \"video\", \"in\", false)\n\t\t\t\t\t\tend\n\t\t\t\t\t\tif comsg[\"data\"] == true then\n\t\t\t\t\t\t\tconfigureMedium(id, \"data\", \"in\", true)\n\t\t\t\t\t\telseif comsg[\"data\"] == false then\n\t\t\t\t\t\t\tconfigureMedium(id, \"data\", \"in\", false)\n\t\t\t\t\t\tend\n\t\t\t\t\t\t-- Also check if we need to send an ICE restart\n\t\t\t\t\t\tlocal restartjson = nil\n\t\t\t\t\t\tif comsg[\"restart\"] == true then\n\t\t\t\t\t\t\t-- Prepare new offer and send it back\n\t\t\t\t\t\t\tlocal f = sessions[s[\"feedSessionId\"]]\n\t\t\t\t\t\t\tif f ~= nil and s[\"sdp\"] ~= nil then\n\t\t\t\t\t\t\t\tlogger.print(\"Preparing new offer (ICE restart) for subscriber: \" .. s[\"sdp\"])\n\t\t\t\t\t\t\t\tlocal offer = { type = \"offer\", sdp = s[\"sdp\"], restart = true }\n\t\t\t\t\t\t\t\trestartjson = json.encode(offer)\n\t\t\t\t\t\t\tend\n\t\t\t\t\t\tend\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", room = s[\"roomId\"], configured = \"ok\" }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, restartjson)\n\t\t\t\t\tend\n\t\t\t\telseif request == \"unpublish\" then\n\t\t\t\t\t-- Stop publishing in a room (publishers only)\n\t\t\t\t\tlogger.print(\"Unpublishing in room: \" .. s[\"roomId\"])\n\t\t\t\t\tif s[\"pType\"] ~= \"publisher\" then\n\t\t\t\t\t\tlogger.print(\"Invalid request: \" .. request)\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, error = \"Invalid request\" }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\t\t-- Close the PeerConnection\n\t\t\t\t\thangupMedia(id)\n\t\t\t\t\tclosePc(id)\n\t\t\t\t\tlocal event = { videoroom = \"event\", room = s[\"roomId\"], unpublished = \"ok\" }\n\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\telseif request == \"leave\" then\n\t\t\t\t\t-- Leave a room\n\t\t\t\t\tlogger.print(\"Leaving room: \" .. s[\"roomId\"])\n\t\t\t\t\t-- Clean up the PeerConnection\n\t\t\t\t\thangupMedia(id)\n\t\t\t\t\tclosePc(id)\n\t\t\t\t\tlocal event = { videoroom = \"event\", room = s[\"roomId\"], leaving = \"ok\" }\n\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t-- If private IDs are required to prevent lurking, get rid of the subscriptions as well\n\t\t\t\t\tlocal room = rooms[s[\"roomId\"]]\n\t\t\t\t\tif room ~= nil and room.requirePvtId == true then\n\t\t\t\t\t\tfor index,sub in ipairs(s.subscriptions) do\n\t\t\t\t\t\t\tlogger.print(\"  -- Getting rid of publisher's subscription: \" .. sub)\n\t\t\t\t\t\t\tendSession(sub)\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\t\ts.subscriptions = {}\n\t\t\t\t\tif room ~= nil and s.privateId ~= nil then\n\t\t\t\t\t\troom.privateIds[s.privateId] = nil\n\t\t\t\t\tend\n\t\t\t\t\troom.participants[s.userId] = nil\n\t\t\t\telseif request == \"start\" then\n\t\t\t\t\t-- Start subscribing to a publisher (subscribers only)\n\t\t\t\t\tlogger.print(\"Starting a subscription\")\n\t\t\t\t\tif s[\"pType\"] ~= \"subscriber\" then\n\t\t\t\t\t\tlogger.print(\"Invalid request: \" .. request)\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, error = \"Invalid request\" }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\t\tlocal event = { videoroom = \"event\", room = s[\"roomId\"], started = \"ok\" }\n\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\telseif request == \"switch\" then\n\t\t\t\t\t-- Switch to a new publisher (subscribers only)\n\t\t\t\t\tif s[\"pType\"] ~= \"subscriber\" then\n\t\t\t\t\t\tlogger.print(\"Invalid request: \" .. request)\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, error = \"Invalid request\" }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\t\t-- TODO\n\t\t\t\t\tlocal event = { videoroom = \"event\", room = s[\"roomId\"], switched = \"ok\" }\n\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\telseif request == \"keyframe\" then\n\t\t\t\t\t-- Programmatically ask the publisher for a keyframe\n\t\t\t\t\tif s[\"pType\"] ~= \"subscriber\" then\n\t\t\t\t\t\tlogger.print(\"Invalid request: \" .. request)\n\t\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, error = \"Invalid request\" }\n\t\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\t\t\treturn\n\t\t\t\t\tend\n\t\t\t\t\t-- Send a PLI to the publisher\n\t\t\t\t\tif s[\"feedSessionId\"] ~= nil then\n\t\t\t\t\t\tlocal f = sessions[s[\"feedSessionId\"]]\n\t\t\t\t\t\tif f ~= nil then\n\t\t\t\t\t\t\tlogger.print(\"Session \" .. id .. \" is going to be fed by \" .. f.id)\n\t\t\t\t\t\t\taddRecipient(f.id, id)\n\t\t\t\t\t\t\tsendPli(f.id)\n\t\t\t\t\t\tend\n\t\t\t\t\tend\n\t\t\t\t\t-- Done\n\t\t\t\t\tlocal event = { videoroom = \"event\", room = s[\"roomId\"], sent = \"ok\" }\n\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\telse\n\t\t\t\t\tlogger.print(\"Invalid request: \" .. request)\n\t\t\t\t\tlocal event = { videoroom = \"event\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, error = \"Invalid request\" }\n\t\t\t\t\tlocal eventjson = json.encode(event)\n\t\t\t\t\tpushEvent(id, tr, eventjson, nil)\n\t\t\t\tend\n\t\t\t\tlogger.print(\"Done handling request: \" .. request)\n\t\t\tend)\n\t\t\t-- Enqueue it: the scheduler will resume it later\n\t\t\ttasks[#tasks+1] = { co = async, id = id, tr = tr, msg = msgT, jsep = jsepT }\n\t\t\t-- Return explaining that this is will be handled asynchronously\n\t\t\tpokeScheduler()\n\t\t\treturn 1, nil\n\t\telse\n\t\t\tlocal response = { videoroom = \"error\", error_code = JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, error = \"Unknown request\" }\n\t\t\tlocal responsejson = json.encode(response)\n\t\t\treturn 0, responsejson\n\t\tend\n\tend\nend\n\nfunction setupMedia(id)\n\t-- WebRTC is now available\n\tlogger.print(\"WebRTC PeerConnection is up for session: \" .. id)\n\tlocal s = sessions[id]\n\tif s == nil then\n\t\treturn -1, \"Session not found\"\n\tend\n\ts[\"started\"] = true\n\t-- If this is a publisher, notify other users\n\tif(s[\"pType\"] == \"publisher\") then\n\t\tif(s[\"bitrate\"] ~= nil) then\n\t\t\tsetBitrate(id, s[\"bitrate\"])\n\t\tend\n\t\tlocal room = rooms[s.roomId]\n\t\tif room == nil then\n\t\t\treturn\n\t\tend\n\t\tlocal event = { videoroom = \"event\", room = room.roomId, description = room.description,\n\t\t\tid = s.userId, publishers = {} }\n\t\tevent.publishers[#event.publishers+1] = {\n\t\t\tid = s.userId,\n\t\t\tdisplay = s.display,\n\t\t\taudio_codec = s.audioCodec,\n\t\t\tvideo_codec = s.videoCodec\n\t\t}\n\t\tlocal eventjson = json.encode(event)\n\t\tif eventjson:find(\"\\\"publishers\\\":{}\") ~= nil then\n\t\t\t-- Ugly hack, as lua-json turns our empty array into an empty object\n\t\t\teventjson = string.gsub(eventjson, \"\\\"publishers\\\":{}\", \"\\\"publishers\\\":[]\")\n\t\tend\n\t\tfor index,partId in pairs(room.participants) do\n\t\t\tlocal p = sessions[partId]\n\t\t\tif p ~= nil and p.id ~= id then\n\t\t\t\tpushEvent(p.id, nil, eventjson, nil)\n\t\t\tend\n\t\tend\n\t-- If this is a subscriber, attach it as a recipient to the publisher\n\telseif(s[\"pType\"] == \"subscriber\") then\n\t\tlocal f = sessions[s[\"feedSessionId\"]]\n\t\tif f ~= nil then\n\t\t\tlogger.print(\"Session \" .. id .. \" is going to be fed by \" .. f.id)\n\t\t\taddRecipient(f.id, id)\n\t\t\tsendPli(f.id)\n\t\tend\n\tend\nend\n\nfunction hangupMedia(id)\n\t-- WebRTC not available anymore\n\tlogger.print(\"WebRTC PeerConnection is down for session: \" .. id)\n\tlocal s = sessions[id]\n\tif s == nil then\n\t\treturn -1, \"Session not found\"\n\tend\n\ts[\"started\"] = false\n\ts[\"sdp\"] = nil\n\t-- If this is a publisher, detach all subscribers, otherwise detach from publisher\n\tif(s[\"pType\"] == \"publisher\") then\n\t\t-- Detach all subscribers\n\t\tfor index,subId in pairs(s.subscribers) do\n\t\t\tlogger.print(\"Unlinking session \" .. subId .. \" from feed \" .. id)\n\t\t\tremoveRecipient(id, subId)\n\t\tend\n\t\ts.subscribers = {}\n\t\t-- Notify other participants this publisher is gone\n\t\tlocal room = rooms[s.roomId]\n\t\tif room == nil then\n\t\t\treturn\n\t\tend\n\t\tlocal event = { videoroom = \"event\", unpublished = s.userId, room = room.roomId }\n\t\tlocal eventjson = json.encode(event)\n\t\tfor index,partId in pairs(room.participants) do\n\t\t\tlocal p = sessions[partId]\n\t\t\tif p ~= nil and p.id ~= id then\n\t\t\t\tpushEvent(p.id, nil, eventjson, nil)\n\t\t\tend\n\t\tend\n\telseif(s[\"pType\"] == \"subscriber\") then\n\t\tlocal f = sessions[s[\"feedSessionId\"]]\n\t\tif f ~= nil then\n\t\t\tlogger.print(\"Unlinking session \" .. id .. \" from feed \" .. f.id)\n\t\t\tf.subscribers[id] = nil\n\t\t\tremoveRecipient(f.id, id)\n\t\tend\n\tend\nend\n\nfunction resumeScheduler()\n\t-- This is the function responsible for resuming coroutines associated\n\t-- with whatever is relevant to the Lua script, e.g., for this script,\n\t-- with asynchronous requests: if you're handling async stuff yourself,\n\t-- you're free not to use this and just return, but the C Lua plugin\n\t-- expects this method to exist so it MUST be present, even if empty\n\tlogger.print(\"Resuming coroutines\")\n\tfor index,task in ipairs(tasks) do\n\t\tlocal success, result = coroutine.resume(task.co, task.id, task.tr, task.msg, task.jsep)\n\t\tif not success then\n\t\t\tlogger.print(colors.red .. \" \" .. dumpTable(result) .. colors.reset)\n\t\tend\n\tend\n\tlogger.print(\"Coroutines resumed\")\n\ttasks = {}\nend\n\n-- Helper for logging tables\n-- https://stackoverflow.com/a/27028488\nfunction dumpTable(o)\n\tif type(o) == 'table' then\n\t\tlocal s = '{ '\n\t\tfor k,v in pairs(o) do\n\t\t\tif type(k) ~= 'number' then k = '\"'..k..'\"' end\n\t\t\ts = s .. '['..k..'] = ' .. dumpTable(v) .. ','\n\t\tend\n\t\treturn s .. '} '\n\telse\n\t\treturn tostring(o)\n\tend\nend\n\n-- Helper for splitting strings using a pattern (http://lua-users.org/wiki/SplitJoin)\nfunction split(str, pat)\n\tlocal t = {}  -- NOTE: use {n = 0} in Lua-5.0\n\tlocal fpat = \"(.-)\" .. pat\n\tlocal last_end = 1\n\tlocal s, e, cap = str:find(fpat, 1)\n\twhile s do\n\t\tif s ~= 1 or cap ~= \"\" then\n\t\t\ttable.insert(t,cap)\n\t\tend\n\t\tlast_end = e+1\n\t\ts, e, cap = str:find(fpat, last_end)\n\tend\n\tif last_end <= #str then\n\t\tcap = str:sub(last_end)\n\t\ttable.insert(t, cap)\n\tend\n\treturn t\nend\n\n-- Done\nlogger.print(\"Loaded\")\n"
  },
  {
    "path": "src/plugins/plugin.c",
    "content": "/*! \\file   plugin.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Plugin-Core communication (implementation)\n * \\details  Implementation of the janus_plugin_result stuff: all the\n * important things related to the actual plugin API is in plugin.h.\n *\n * \\ingroup pluginapi\n * \\ref pluginapi\n */\n\n#include \"plugin.h\"\n\n#include <jansson.h>\n\n#include \"../apierror.h\"\n#include \"../debug.h\"\n\n/* Plugin results */\njanus_plugin_result *janus_plugin_result_new(janus_plugin_result_type type, const char *text, json_t *content) {\n\tJANUS_LOG(LOG_HUGE, \"Creating plugin result...\\n\");\n\tjanus_plugin_result *result = g_malloc(sizeof(janus_plugin_result));\n\tresult->type = type;\n\tresult->text = text;\n\tresult->content = content;\n\treturn result;\n}\n\nvoid janus_plugin_result_destroy(janus_plugin_result *result) {\n\tJANUS_LOG(LOG_HUGE, \"Destroying plugin result...\\n\");\n\tresult->text = NULL;\n\tif(result->content)\n\t\tjson_decref(result->content);\n\tresult->content = NULL;\n\tg_free(result);\n}\n\n/* RTP, RTCP and data packets initialization */\nvoid janus_plugin_rtp_extensions_reset(janus_plugin_rtp_extensions *extensions) {\n\tif(extensions) {\n\t\t/* By extensions are not added to packets */\n\t\textensions->audio_level = -1;\n\t\textensions->audio_level_vad = FALSE;\n\t\textensions->video_rotation = -1;\n\t\textensions->video_back_camera = FALSE;\n\t\textensions->video_flipped = FALSE;\n\t\textensions->min_delay = -1;\n\t\textensions->max_delay = -1;\n\t\textensions->dd_len = 0;\n\t\tmemset(extensions->dd_content, 0, sizeof(extensions->dd_content));\n\t\textensions->spatial_layers = -1;\n\t\textensions->temporal_layers = -1;\n\t}\n}\nvoid janus_plugin_rtp_reset(janus_plugin_rtp *packet) {\n\tif(packet) {\n\t\tmemset(packet, 0, sizeof(janus_plugin_rtp));\n\t\tpacket->mindex = -1;\n\t\tjanus_plugin_rtp_extensions_reset(&packet->extensions);\n\t}\n}\njanus_plugin_rtp *janus_plugin_rtp_duplicate(janus_plugin_rtp *packet) {\n\tjanus_plugin_rtp *p = NULL;\n\tif(packet) {\n\t\tp = g_malloc(sizeof(janus_plugin_rtp));\n\t\tp->mindex = packet->mindex;\n\t\tp->video = packet->video;\n\t\tif(packet->buffer == NULL || packet->length == 0) {\n\t\t\tp->buffer = NULL;\n\t\t\tp->length = 0;\n\t\t} else {\n\t\t\tp->buffer = g_malloc(packet->length);\n\t\t\tmemcpy(p->buffer, packet->buffer, packet->length);\n\t\t\tp->length = packet->length;\n\t\t}\n\t\tp->extensions = packet->extensions;\n\t}\n\treturn p;\n}\nvoid janus_plugin_rtcp_reset(janus_plugin_rtcp *packet) {\n\tif(packet) {\n\t\tmemset(packet, 0, sizeof(janus_plugin_rtcp));\n\t\tpacket->mindex = -1;\n\t}\n}\nvoid janus_plugin_data_reset(janus_plugin_data *packet) {\n\tif(packet)\n\t\tmemset(packet, 0, sizeof(janus_plugin_data));\n}\n"
  },
  {
    "path": "src/plugins/plugin.h",
    "content": "/*! \\file   plugin.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Plugin-Core communication\n * \\details  This header contains the definition of the callbacks both\n * the Janus core and all the plugins need to implement to interact with\n * each other. The structures to make the communication possible are\n * defined here as well.\n *\n * In particular, the Janus core implements the \\c janus_callbacks interface.\n * This means that, as a plugin, you can use the methods it exposes to\n * contact the core, e.g., in order to have it relay a message, event\n * or RTP/RTCP packet to the peer you're handling. In particular, the\n * methods the core exposes to plugins are:\n *\n * - \\c push_event(): to send a JSON message/event to the peer (with or without\n * an attached JSEP formatted SDP to negotiate a WebRTC PeerConnection);\n * the syntax of the message/event is completely up to you, the only\n * important thing is that it MUST be a JSON object, as it will be included\n * as such within the Janus session/handle protocol;\n * - \\c relay_rtp(): to send/relay the peer an RTP packet;\n * - \\c relay_rtcp(): to send/relay the peer an RTCP message.\n * - \\c relay_data(): to send/relay the peer a SCTP DataChannel message.\n *\n * On the other hand, a plugin that wants to register at the Janus core\n * needs to implement the \\c janus_plugin interface. Besides, as a\n * plugin is a shared object, and as such external to the core itself,\n * in order to be dynamically loaded at startup it needs to implement\n * the \\c create_p() hook as well, that should return a pointer to the\n * plugin instance. This is an example of such a step:\n *\n\\verbatim\nstatic janus_plugin myplugin = {\n\t[..]\n};\n\njanus_plugin *create(void) {\n\tJANUS_LOG(LOG_VERB, , \"%s created!\\n\", MY_PLUGIN_NAME);\n\treturn &myplugin;\n}\n\\endverbatim\n *\n * This will make sure that your plugin is loaded at startup by the Janus core,\n * if it is deployed in the proper folder.\n *\n * As anticipated and described in the above example, a plugin must basically\n * be an instance of the \\c janus_plugin type. As such, it must implement\n * the following methods and callbacks for the core:\n *\n * - \\c init(): this is called by the Janus core as soon as your plugin is started;\n * this is where you should setup your plugin (e.g., static stuff and reading\n * the configuration file);\n * - \\c destroy(): on the other hand, this is called by the core when it\n * is shutting down, and your plugin should too;\n * - \\c get_api_compatibility(): this method MUST return JANUS_PLUGIN_API_VERSION;\n * - \\c get_version(): this method should return a numeric version identifier (e.g., 3);\n * - \\c get_version_string(): this method should return a verbose version identifier (e.g., \"v1.0.1\");\n * - \\c get_description(): this method should return a verbose description of your plugin (e.g., \"This is my awesome plugin that does this and that\");\n * - \\c get_name(): this method should return a short display name for your plugin (e.g., \"My Awesome Plugin\");\n * - \\c get_package(): this method should return a unique package identifier for your plugin (e.g., \"janus.plugin.myplugin\");\n * - \\c create_session(): this method is called by the core to create a session between you and a peer;\n * - \\c handle_message(): a callback to notify you the peer sent you a message/request;\n * - \\c handle_admin_message(): a callback to notify you a message/request came from the Admin API;\n * - \\c setup_media(): a callback to notify you the peer PeerConnection is now ready to be used;\n * - \\c incoming_rtp(): a callback to notify you a peer has sent you a RTP packet;\n * - \\c incoming_rtcp(): a callback to notify you a peer has sent you a RTCP message;\n * - \\c incoming_data(): a callback to notify you a peer has sent you a message on a SCTP DataChannel;\n * - \\c data_ready(): a callback to notify you data can be sent on the SCTP DataChannel;\n * - \\c slow_link(): a callback to notify you Janus or the peer have lost packets recently, and the media path may be slow;\n * - \\c hangup_media(): a callback to notify you the peer PeerConnection has been closed (e.g., after a DTLS alert);\n * - \\c query_session(): this method is called by the core to get plugin-specific info on a session between you and a peer;\n * - \\c destroy_session(): this method is called by the core to destroy a session between you and a peer.\n *\n * All the above methods and callbacks, except for \\c incoming_rtp ,\n * \\c incoming_rtcp , \\c incoming_data and \\c slow_link , are mandatory:\n * the Janus core will reject a plugin that doesn't implement any of the\n * mandatory callbacks. The previously mentioned ones, instead, are\n * optional, so you're free to implement only those you care about. If\n * your plugin will not handle any data channel, for instance, it makes\n * sense to not implement the \\c incoming_data callback at all. At the\n * same time, if your plugin is ONLY going to use data channels and\n * can't care less about RTP or RTCP, \\c incoming_rtp and \\c incoming_rtcp\n * can be left out. Finally, \\c slow_link is just there as a helper, some\n * additional information you may be interested about, but you're not\n * forced to receive it if you don't care.\n *\n * The Janus core \\c janus_callbacks interface is provided to a plugin, together\n * with the path to the configurations files folder, in the \\c init() method.\n * This path can be used to read and parse a configuration file for the\n * plugin: the plugins we made available out of the box use the package\n * name as a name for the file (e.g., \\c janus.plugin.echotest.cfg for\n * the Echo Test plugin), but you're free to use a different one, as long\n * as it doesn't collide with existing ones. Besides, the existing plugins\n * use the same INI format for configuration files the core uses (relying\n * on the \\c janus_config helpers for the purpose) but again, if you prefer\n * a different format (XML, JSON, etc.) that's up to you.\n *\n * Both the Janus core and a plugin can have several different sessions\n * with the same and/or different peers: to match a specific session,\n * a plugin can rely on a mapping called janus_plugin_session that\n * is what all the communication between the plugins and the core\n * (that is, both methods invoked by the core and callbacks invoked by\n * the plugins) will make use of. See the janus_videoroom.c plugin for\n * an example of multiple handles associated to the same peer.\n *\n * All messages/requests/events sent to and received from a plugin are\n * asynchronous, meaning there's no way to immediately reply to a message\n * sent by a browser, for instance. Messages/requests coming from browsers\n * in a \\c handle_message() callback, though, have a transaction\n * identifier, which you can use in a \\c push_event() reply to allow the\n * browser to match it to the original request, if needed.\n *\n * As anticipated, both \\c handle_message() and \\c push_event() can attach\n * a JSEP/SDP payload. This means that a browser, for instance, can attach\n * a JSEP/SDP offer to negotiate a WebRTC PeerConnection with a plugin: the plugin\n * would then need to provide, immediately or not, a JSEP/SDP answer to\n * do so. At the same time, a plugin may want to originate the call instead:\n * in that case, the plugin would attach a JSEP/SDP offer in a \\c push_event()\n * call, to which the browser would then need to reply with a JSEP/SDP answer,\n * as described in \\ref JS. Renegotiating a session can be done using the\n * same mechanism above: in case plugins want to force an ICE restart,\n * though, they must add a boolean property called \\c restart to the JSEP\n * object before passing it to the core. Notice that the core adds a property\n * called \\c update whenever the remote user is requesting a renegotiation,\n * whether it's for ICE restarts or just for some media related change.\n * \\note It's important to notice that, while the Janus core would indeed\n * take care of the WebRTC PeerConnection setup itself in terms of\n * ICE/DTLS/RT(C)P on your behalf, plugins are what will actually manipulate\n * the media flowing around, and as such it's them who are responsible for\n * what concerns the codec negotiation in a JSEP/SDP offer/answer. This\n * normally is not something you need to worry about, especially if you're\n * just moving SDP around (e.g., janus_echotest.c or janus_videocall.c).\n * If your plugin is going to generate media frames (e.g., as janus_audiobridge.c),\n * you only support some codecs (e.g., Opus in janus_audiobridge.c) or you\n * want to use the same SDP offer for several different sessions (e.g., a webinar),\n * you need to make sure that your offer/answer does not contain anything\n * you don't support. Besides, you also need to make sure that you use\n * SDP-provided information (e.g., payload types, increasing versions in\n * case of renegotiations) coherently.\n *\n * \\todo Right now plugins can only interact with peers through the Janus core.\n * Besides, a single PeerConnection can at the moment be used by only one\n * plugin, as that plugin is actually the \"owner\" of the PeerConnection itself.\n *\n * \\ingroup pluginapi\n * \\ref pluginapi\n */\n\n#ifndef JANUS_PLUGIN_H\n#define JANUS_PLUGIN_H\n\n#include <stdlib.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n#include <ctype.h>\n#include <unistd.h>\n#include <inttypes.h>\n\n#include <glib.h>\n\n#include \"../refcount.h\"\n\n\n/*! \\brief Version of the API, to match the one plugins were compiled against\n *\n * \\note This was added in version 0.0.7 of Janus, to address changes\n * to the API that might break existing plugin or the core itself. All\n * plugins MUST implement the get_api_compatibility() method to make\n * this work. Do NOT try to launch a pre 0.0.7 plugin on a >= 0.0.7\n * Janus instance or it will crash.\n *\n */\n#define JANUS_PLUGIN_API_VERSION\t106\n\n/*! \\brief Initialization of all plugin properties to NULL\n *\n * \\note This was added in version 0.0.8 of Janus, to address changes\n * to the API that might break existing plugin or the core itself. All\n * plugins MUST add this as the FIRST line when initializing their\n * plugin structure, e.g.:\n *\n\\verbatim\nstatic janus_plugin janus_echotest_plugin =\n\t{\n\t\tJANUS_PLUGIN_INIT,\n\n\t\t.init = janus_echotest_init,\n\t\t[..]\n\\endverbatim\n * */\n#define JANUS_PLUGIN_INIT(...) {\t\t\\\n\t\t.init = NULL,\t\t\t\t\t\\\n\t\t.destroy = NULL,\t\t\t\t\\\n\t\t.get_api_compatibility = NULL,\t\\\n\t\t.get_version = NULL,\t\t\t\\\n\t\t.get_version_string = NULL,\t\t\\\n\t\t.get_description = NULL,\t\t\\\n\t\t.get_name = NULL,\t\t\t\t\\\n\t\t.get_author = NULL,\t\t\t\t\\\n\t\t.get_package = NULL,\t\t\t\\\n\t\t.create_session = NULL,\t\t\t\\\n\t\t.handle_message = NULL,\t\t\t\\\n\t\t.handle_admin_message = NULL,\t\\\n\t\t.setup_media = NULL,\t\t\t\\\n\t\t.incoming_rtp = NULL,\t\t\t\\\n\t\t.incoming_rtcp = NULL,\t\t\t\\\n\t\t.incoming_data = NULL,\t\t\t\\\n\t\t.data_ready = NULL,\t\t\t\t\\\n\t\t.slow_link = NULL,\t\t\t\t\\\n\t\t.hangup_media = NULL,\t\t\t\\\n\t\t.destroy_session = NULL,\t\t\\\n\t\t.query_session = NULL, \t\t\t\\\n\t\t## __VA_ARGS__ }\n\n\n/*! \\brief Callbacks to contact the Janus core */\ntypedef struct janus_callbacks janus_callbacks;\n/*! \\brief The plugin session and callbacks interface */\ntypedef struct janus_plugin janus_plugin;\n/*! \\brief Plugin-Gateway session mapping */\ntypedef struct janus_plugin_session janus_plugin_session;\n/*! \\brief Result of individual requests passed to plugins */\ntypedef struct janus_plugin_result janus_plugin_result;\n\n/*! \\brief RTP packet exchanged with the core */\ntypedef struct janus_plugin_rtp janus_plugin_rtp;\n/*! \\brief RTP extensions parsed in an RTP packet */\ntypedef struct janus_plugin_rtp_extensions janus_plugin_rtp_extensions;\n/*! \\brief RTCP message exchanged with the core */\ntypedef struct janus_plugin_rtcp janus_plugin_rtcp;\n/*! \\brief Data message exchanged with the core */\ntypedef struct janus_plugin_data janus_plugin_data;\n\n/* Use forward declaration to avoid including jansson.h */\ntypedef struct json_t json_t;\n\n/*! \\brief Plugin-Gateway session mapping */\nstruct janus_plugin_session {\n\t/*! \\brief Opaque pointer to the Janus core-level handle */\n\tvoid *gateway_handle;\n\t/*! \\brief Opaque pointer to the plugin session */\n\tvoid *plugin_handle;\n\t/*! \\brief Whether this mapping has been stopped definitely or not: if so,\n\t * the plugin shouldn't make use of it anymore */\n\tvolatile gint stopped;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n};\n\n/*! \\brief The plugin session and callbacks interface */\nstruct janus_plugin {\n\t/*! \\brief Plugin initialization/constructor\n\t * @param[in] callback The callback instance the plugin can use to contact the Janus core\n\t * @param[in] config_path Path of the folder where the configuration for this plugin can be found\n\t * @returns 0 in case of success, a negative integer in case of error */\n\tint (* const init)(janus_callbacks *callback, const char *config_path);\n\t/*! \\brief Plugin deinitialization/destructor */\n\tvoid (* const destroy)(void);\n\n\t/*! \\brief Informative method to request the API version this plugin was compiled against\n\t *  \\note This was added in version 0.0.7 of Janus, to address changes\n\t * to the API that might break existing plugin or the core itself. All\n\t * plugins MUST implement this method and return JANUS_PLUGIN_API_VERSION\n\t * to make this work, or they will be rejected by the core. Do NOT try\n\t * to launch a <= 0.0.7 plugin on a >= 0.0.7 Janus or it will crash. */\n\tint (* const get_api_compatibility)(void);\n\t/*! \\brief Informative method to request the numeric version of the plugin */\n\tint (* const get_version)(void);\n\t/*! \\brief Informative method to request the string version of the plugin */\n\tconst char *(* const get_version_string)(void);\n\t/*! \\brief Informative method to request a description of the plugin */\n\tconst char *(* const get_description)(void);\n\t/*! \\brief Informative method to request the name of the plugin */\n\tconst char *(* const get_name)(void);\n\t/*! \\brief Informative method to request the author of the plugin */\n\tconst char *(* const get_author)(void);\n\t/*! \\brief Informative method to request the package name of the plugin (what will be used in web applications to refer to it) */\n\tconst char *(* const get_package)(void);\n\n\t/*! \\brief Method to create a new session/handle for a peer\n\t * @param[in] handle The plugin/gateway session that will be used for this peer\n\t * @param[out] error An integer that may contain information about any error */\n\tvoid (* const create_session)(janus_plugin_session *handle, int *error);\n\t/*! \\brief Method to handle an incoming message/request from a peer\n\t * @note The Janus core leaves ownership of both the \\c message and \\c jsep\n\t * json_t objects to plugins. This means that you'll have to decrease your own\n\t * reference yourself with a \\c json_decref when you're done with them.\n\t * You'll also have to free \\c transaction with \\c g_free\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[in] transaction The transaction identifier for this message/request\n\t * @param[in] message The json_t object containing the message/request JSON\n\t * @param[in] jsep The json_t object containing the JSEP type/SDP, if available\n\t * @returns A janus_plugin_result instance that may contain a response (for immediate/synchronous replies), an ack\n\t * (for asynchronously managed requests) or an error */\n\tstruct janus_plugin_result * (* const handle_message)(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);\n\t/*! \\brief Method to handle an incoming Admin API message/request\n\t * @param[in] message The json_t object containing the message/request JSON\n\t * @returns A json_t instance containing the response */\n\tstruct json_t * (* const handle_admin_message)(json_t *message);\n\t/*! \\brief Callback to be notified when the associated PeerConnection is up and ready to be used\n\t * @param[in] handle The plugin/gateway session used for this peer */\n\tvoid (* const setup_media)(janus_plugin_session *handle);\n\t/*! \\brief Method to handle an incoming RTP packet from a peer\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[in] packet The RTP packet and related data */\n\tvoid (* const incoming_rtp)(janus_plugin_session *handle, janus_plugin_rtp *packet);\n\t/*! \\brief Method to handle an incoming RTCP packet from a peer\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[in] packet The RTP packet and related data */\n\tvoid (* const incoming_rtcp)(janus_plugin_session *handle, janus_plugin_rtcp *packet);\n\t/*! \\brief Method to handle incoming SCTP/DataChannel data from a peer (text only, for the moment)\n\t * \\note We currently only support text data, binary data will follow... please also notice that\n\t * DataChannels send unterminated strings, so you'll have to terminate them with a \\0 yourself to\n\t * use them.\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[in] packet The message data and related info */\n\tvoid (* const incoming_data)(janus_plugin_session *handle, janus_plugin_data *packet);\n\t/*! \\brief Method to be notified about the fact that the datachannel is ready to be written\n\t * \\note This is not only called when the PeerConnection first becomes available, but also\n\t * when the SCTP socket becomes writable again, e.g., because the internal buffer is empty.\n\t * @param[in] handle The plugin/gateway session used for this peer */\n\tvoid (* const data_ready)(janus_plugin_session *handle);\n\t/*! \\brief Method to be notified by the core when too many NACKs have\n\t * been received or sent by Janus, and so a slow or potentially\n\t * unreliable network is to be expected for this peer\n\t * \\note Beware that this callback may be called more than once in a row,\n\t * (even though never more than once per second), until things go better for that\n\t * PeerConnection. You may or may not want to handle this callback and\n\t * act on it, considering you can get bandwidth information from REMB\n\t * feedback sent by the peer if the browser supports it. Besides, your\n\t * plugin may not have access to encoder related settings to slow down\n\t * or decreae the bitrate if required after the callback is called.\n\t * Nevertheless, it can be useful for debugging, or for informing your\n\t * users about potential issues that may be happening media-wise.\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[in] mindex Index of the stream the event refers to (relative to the SDP)\n\t * @param[in] video Whether this is related to an audio or a video stream\n\t * @param[in] uplink Whether this is related to the uplink (Janus to peer)\n\t * or downlink (peer to Janus) */\n\tvoid (* const slow_link)(janus_plugin_session *handle, int mindex, gboolean video, gboolean uplink);\n\t/*! \\brief Callback to be notified about DTLS alerts from a peer (i.e., the PeerConnection is not valid any more)\n\t * @param[in] handle The plugin/gateway session used for this peer */\n\tvoid (* const hangup_media)(janus_plugin_session *handle);\n\t/*! \\brief Method to destroy a session/handle for a peer\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[out] error An integer that may contain information about any error */\n\tvoid (* const destroy_session)(janus_plugin_session *handle, int *error);\n\t/*! \\brief Method to get plugin-specific info of a session/handle\n\t *  \\note This was added in version 0.0.7 of Janus. Janus assumes\n\t * the string is always allocated, so don't return constants here\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @returns A json_t object with the requested info */\n\tjson_t *(* const query_session)(janus_plugin_session *handle);\n\n};\n\n/*! \\brief Callbacks to contact the Janus core */\nstruct janus_callbacks {\n\t/*! \\brief Callback to push events/messages to a peer\n\t * @note The Janus core increases the references to both the \\c message and \\c jsep\n\t * json_t objects. This means that you'll have to decrease your own\n\t * reference yourself with a \\c json_decref after calling \\c push_event\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[in] plugin The plugin instance that is sending the message/event\n\t * @param[in] transaction The transaction identifier this message refers to\n\t * @param[in] message The json_t object containing the JSON message\n\t * @param[in] jsep The json_t object containing the JSEP type, the SDP attached to the message/event, if any (offer/answer), and whether this is an update */\n\tint (* const push_event)(janus_plugin_session *handle, janus_plugin *plugin, const char *transaction, json_t *message, json_t *jsep);\n\n\t/*! \\brief Callback to relay RTP packets to a peer\n\t * @param[in] handle The plugin/gateway session used for this peer\n\t * @param[in] packet The RTP packet and related data */\n\tvoid (* const relay_rtp)(janus_plugin_session *handle, janus_plugin_rtp *packet);\n\t/*! \\brief Callback to relay RTCP messages to a peer\n\t * @param[in] handle The plugin/gateway session that will be used for this peer\n\t * @param[in] packet The RTCP packet and related data */\n\tvoid (* const relay_rtcp)(janus_plugin_session *handle, janus_plugin_rtcp *packet);\n\t/*! \\brief Callback to relay SCTP/DataChannel messages to a peer\n\t * @note The protocol is only used for the first message sent on a new data\n\t * channel, as it will be used to create it; it will be ignored for following\n\t * messages on the same label, so you can set NULL after that\n\t * @param[in] handle The plugin/gateway session that will be used for this peer\n\t * @param[in] packet The message data and related info */\n\tvoid (* const relay_data)(janus_plugin_session *handle, janus_plugin_data *packet);\n\n\t/*! \\brief Helper to ask for a keyframe via a RTCP PLI to all video streams\n\t * @note This is a shortcut, as it is also possible to do the same by crafting\n\t * an RTCP PLI message manually, and passing it to the core via relay_rtcp\n\t * @param[in] handle The plugin/gateway session that will be used for this peer */\n\tvoid (* const send_pli)(janus_plugin_session *handle);\n\t/*! \\brief Helper to ask for a keyframe via a RTCP PLI to a specific video stream\n\t * @note This is a shortcut, as it is also possible to do the same by crafting\n\t * an RTCP PLI message manually, and passing it to the core via relay_rtcp\n\t * @param[in] handle The plugin/gateway session that will be used for this peer\n\t * @param[in] mindex Index of the stream to send the PLI to (relative to the SDP;\n\t * passing -1 will send it to the first video stream in the SDP) */\n\tvoid (* const send_pli_stream)(janus_plugin_session *handle, int mindex);\n\t/*! \\brief Helper to ask for a keyframe via a RTCP PLI\n\t * @note This is a shortcut, as it is also possible to do the same by crafting\n\t * an RTCP REMB message manually, and passing it to the core via relay_rtcp\n\t * @param[in] handle The plugin/gateway session that will be used for this peer\n\t * @param[in] bitrate The bitrate value to send in the REMB message */\n\tvoid (* const send_remb)(janus_plugin_session *handle, guint32 bitrate);\n\n\t/*! \\brief Callback to ask the core to close a WebRTC PeerConnection\n\t * \\note A call to this method will result in the core invoking the hangup_media\n\t * callback on this plugin when done, but only if a PeerConnection had been\n\t * created or was in the process of being negotiated (SDP exchanged)\n\t * @param[in] handle The plugin/gateway session that the PeerConnection is related to */\n\tvoid (* const close_pc)(janus_plugin_session *handle);\n\t/*! \\brief Callback to ask the core to get rid of a plugin/gateway session\n\t * \\note A call to this method will result in the core invoking the destroy_session\n\t * callback on this plugin when done\n\t * @param[in] handle The plugin/gateway session to get rid of */\n\tvoid (* const end_session)(janus_plugin_session *handle);\n\n\t/*! \\brief Callback to check whether the event handlers mechanism is enabled\n\t * @returns TRUE if it is, FALSE if it isn't (which means notify_event should NOT be called) */\n\tgboolean (* const events_is_enabled)(void);\n\t/*! \\brief Callback to notify an event to the registered and subscribed event handlers\n\t * \\note Don't unref the event object, the core will do that for you\n\t * @param[in] plugin The plugin originating the event\n\t * @param[in] handle The plugin/gateway session originating the event, if any\n\t * @param[in] event The event to notify as a Jansson json_t object */\n\tvoid (* const notify_event)(janus_plugin *plugin, janus_plugin_session *handle, json_t *event);\n\n\t/*! \\brief Method to check whether the core is using signed tokens\n\t * @returns TRUE if signed tokens are in use, FALSE otherwise */\n\tgboolean (* const auth_is_signed)(void);\n\t/*! \\brief Method to check whether a signed token is valid\n\t * \\note accepts only tokens with the plugin identifier as realm\n\t * @param[in] token The token to validate\n\t * @returns TRUE if the signature is valid and not expired, FALSE otherwise */\n\tgboolean (* const auth_is_signature_valid)(janus_plugin *plugin, const char *token);\n\t/*! \\brief Method to verify a signed token grants access to a descriptor\n\t * \\note accepts only tokens with the plugin identifier as realm\n\t * @param[in] token The token to validate\n\t * @param[in] desc The descriptor to search for\n\t * @returns TRUE if the token is valid, not expired and contains the descriptor, FALSE otherwise */\n\tgboolean (* const auth_signature_contains)(janus_plugin *plugin, const char *token, const char *descriptor);\n};\n\n/*! \\brief The hook that plugins need to implement to be created from the Janus core */\ntypedef janus_plugin* create_p(void);\n\n\n/** @name Janus plugin results\n * @brief When a client sends a message to a plugin (e.g., a request or a\n * command) this is notified to the plugin through a handle_message()\n * callback. The plugin can then either handle the request immediately\n * and provide a response (synchronous approach) or decide to queue it\n * and process it later (asynchronous approach). In both cases the plugin\n * must return a janus_plugin_result instance to the core, that will allow\n * the client to: 1. know whether a response is immediately available or\n * it will be later on through notifications, and 2. what the actual content\n * of the result might be. Of course, notifications related to the\n * transaction may occur later on even for synchronous requests, if the\n * plugin was implemented with use cases that envisage this approach.\n * @note An error may be returned as well, but this would cause a core-level\n * error to be returned to the client. If you want to provide indications\n * about a failed operation for application-level reason, the correct\n * approach is to return a success with a plugin-specific payload describing\n * the error.\n */\n///@{\n/*! \\brief Result types */\ntypedef enum janus_plugin_result_type {\n\t/*! \\brief A severe error happened (not an application level error) */\n\tJANUS_PLUGIN_ERROR = -1,\n\t/*! \\brief The request was correctly handled and a response is provided (synchronous) */\n\tJANUS_PLUGIN_OK,\n\t/*! \\brief The request was correctly handled and notifications will follow with more info (asynchronous) */\n\tJANUS_PLUGIN_OK_WAIT,\n} janus_plugin_result_type;\n\n/*! \\brief Janus plugin result */\nstruct janus_plugin_result {\n\t/*! \\brief Result type */\n\tjanus_plugin_result_type type;\n\t/*! \\brief Text associated with this plugin result.\n\t * @note This is ONLY used for JANUS_PLUGIN_OK_WAIT (to provide hints on\n\t * why a request is being handled asynchronously) and JANUS_PLUGIN_ERROR\n\t * (to provide a reason for the error). It is ignored for JANUS_PLUGIN_OK.\n\t * Besides, it is NOT freed when destroying the janus_plugin_result instance,\n\t * so if you allocated a string for that, you'll have to free it yourself. */\n\tconst char *text;\n\t/*! \\brief Result content\n\t * @note This is ONLY used for JANUS_PLUGIN_OK, and is ignored otherwise.\n\t * It MUST be a valid JSON payload (even when returning application\n\t * level errors). Its reference is decremented automatically when destroying\n\t * the janus_plugin_result instance, so if your plugin wants to re-use the\n\t * same object for multiple responses, you have to \\c json_incref the object before\n\t * passing it to the core, and \\c json_decref it when you're done with it. */\n\tjson_t *content;\n};\n\n/*! \\brief Helper to quickly create a janus_plugin_result instance\n * @param[in] type The type of result\n * @param[in] text String to add to the result (for JANUS_PLUGIN_OK_WAIT or JANUS_PLUGIN_ERROR), if any\n * @param[in] content The json_t object with the content of the result, if any\n * @returns A valid janus_plugin_result instance, if successful, or NULL otherwise */\njanus_plugin_result *janus_plugin_result_new(janus_plugin_result_type type, const char *text, json_t *content);\n\n/*! \\brief Helper to quickly destroy a janus_plugin_result instance\n * @param[in] result The janus_plugin_result instance to destroy */\nvoid janus_plugin_result_destroy(janus_plugin_result *result);\n///@}\n\n\n/** @name Janus plugin media packets\n * @brief The Janus core and plugins exchange different kind of media\n * packets, specifically RTP packets, RTCP messages and datachannel data.\n * While previously these were exchanged between core and plugins using\n * generic pointers and their length, Janus now uses a dedicated structure\n * for each of them: this allows metadata and other info to be carried\n * along the media data itself, making the exchange process extensible\n * as a result (the signature remains the same, the data contained in\n * the struct can change).\n *\n * The janus_plugin_rtp structure represents an RTP packet. When creating\n * a new packet, it should be initialized with janus_plugin_rtp_init. Besides\n * the data and its length, it also contains info on whether the packet is\n * audio or video, and a list of the parsed RTP extensions provided in\n * an instance of the janus_plugin_rtp_extensions structure. Notice that,\n * while this list of extensions is mostly a commodity when receiving a\n * packet, making it easier to access their values (the RTP extensions\n * will still be part of the incoming RTP packet, so plugins are still free\n * to parse them manually), they're very important when it comes to\n * outgoing packets instead: in fact, since the Janus core may needs to\n * terminate its own extensions with the peer, all RTP extensions that\n * are in an RTP packet sent by a plugin are stripped when relay_rtp is\n * called. This means that any attempt to inject an RTP extension in an\n * outgoing packet will fail if the RTP extension is added to the payload\n * manually, and will need to be set in the janus_plugin_rtp_extensions\n * structure instead. It's also important to initialize the extensions\n * structure before passing the packet to the core, as for each extension\n * there may be a different way of telling the core whether the extension\n * needs to be added or not (e.f., \\c -1 instead of \\c 0 or \\c NULL ) .\n * If the RTP extension management you need is not supported, it must be\n * added to the core to get it working.\n *\n * The janus_plugin_rtcp, instead, represents an RTCP packet, which may\n * contain one or more RTCP compound messages. The only info it contains\n * are whether it's related to the audio or video stream, and a pointer\n * to the data itself and its length. When creating a new packet, it should\n * be initialized with janus_plugin_rtcp_init. To make the generation of\n * some of the most common RTCP messages easier, a few helper core\n * callbacks are provided: this means that, while you can craft RTCP\n * messages yourself using the methods available in rtcp.h, it might be\n * easier to send, e.g., a keyframe request using the dedicated method,\n * which will leave the RTCP crafting process up tp the core.\n *\n * Finally, the janus_plugin_data represents a datachannel message. The\n * only info it contains are the label the message came from, a pointer\n * to the data itself and its length. When creating a new packet, it MUST\n * be initialized with janus_plugin_data_init.\n *\n */\n///@{\n/*! \\brief Janus plugin RTP extensions */\nstruct janus_plugin_rtp_extensions {\n\t/*! \\brief Audio level, in DB (0-127, 127=silence); -1 means no extension */\n\tint8_t audio_level;\n\t/*! \\brief Whether the encoder detected voice activity (part of audio-level extension)\n\t * @note Browsers apparently always set this to 1, so it's unreliable and should be ignored */\n\tgboolean audio_level_vad;\n\t/*! \\brief Video orientation rotation (0, 90, 180, 270); -1 means no extension */\n\tint16_t video_rotation;\n\t/*! \\brief Whether the video orientation extension says this is the back camera\n\t * @note Will be ignored if no rotation value is set */\n\tgboolean video_back_camera;\n\t/*! \\brief Whether the video orientation extension says it's flipped horizontally\n\t * @note Will be ignored if no rotation value is set */\n\tgboolean video_flipped;\n\t/*! \\brief Min and max playout delay, if available; -1 means no extension */\n\tint16_t min_delay, max_delay;\n\t/*! \\brief Length of Dependency Descriptor data, if available */\n\tuint8_t dd_len;\n\t/*! \\brief Dependency Descriptor content */\n\tuint8_t dd_content[256];\n\t/*! \\brief Absolute Capture Time timestamp */\n\tuint64_t abs_capture_ts;\n\t/*! \\brief Spatial and temporal layers, if available in a video-layers-allocation; -1 means no extension */\n\tint8_t spatial_layers, temporal_layers;\n};\n/*! \\brief Helper method to initialise/reset the RTP extensions field\n * @note This is important because each of the supported extensions may\n * use a different value to specify an \"extension missing\" state, which\n * may be different from a 0 or a NULL (e.g., a -1 instead)\n * @param[in] extensions Pointer to the janus_plugin_rtp_extensions instance to reset\n*/\nvoid janus_plugin_rtp_extensions_reset(janus_plugin_rtp_extensions *extensions);\n\n/*! \\brief Janus plugin RTP packet */\nstruct janus_plugin_rtp {\n\t/*! \\brief Index of the stream (relative to the SDP)\n\t * @note On outgoing packets you can set this to -1, to let the Janus\n\t * core find the first audio/video (depending on the \\c video property)\n\t * to send this on; notice that this tweak is only there for convenience,\n\t * and to make it easier for plugins not dealing with multistream, but\n\t * this shouldn't be relied upon too much as it may go away soon. */\n\tint mindex;\n\t/*! \\brief Whether this is an audio or video RTP packet */\n\tgboolean video;\n\t/*! \\brief The packet data */\n\tchar *buffer;\n\t/*! \\brief The packet length */\n\tuint16_t length;\n\t/*! \\brief RTP extensions */\n\tjanus_plugin_rtp_extensions extensions;\n};\n/*! \\brief Helper method to initialise/reset the RTP packet\n * @note The main motivation for this method comes from the presence of the\n * extensions as a janus_plugin_rtp_extensions instance.\n * @param[in] packet Pointer to the janus_plugin_rtp packet to reset\n*/\nvoid janus_plugin_rtp_reset(janus_plugin_rtp *packet);\n/*! \\brief Helper method to duplicate the RTP packet and its buffer\n * @note The core will always pass non-allocated packets to plugins, which\n * means they may have to duplicate them in case they need them for more time.\n * @param[in] packet Pointer to the janus_plugin_rtp packet to duplicate\n * @returns A pointer to the new janus_plugin_rtp, if successful, or NULL otherwise\n*/\njanus_plugin_rtp *janus_plugin_rtp_duplicate(janus_plugin_rtp *packet);\n\n/*! \\brief Janus plugin RTCP packet */\nstruct janus_plugin_rtcp {\n\t/*! \\brief Index of the stream (relative to the SDP)\n\t * @note On outgoing packets you can set this to -1, to let the Janus\n\t * core find the first audio/video (depending on the \\c video property)\n\t * to send this on; notice that this tweak is only there for convenience,\n\t * and to make it easier for plugins not dealing with multistream, but\n\t * this shouldn't be relied upon too much as it may go away soon. */\n\tint mindex;\n\t/*! \\brief Whether this is an audio or video RTCP packet */\n\tgboolean video;\n\t/*! \\brief The packet data */\n\tchar *buffer;\n\t/*! \\brief The packet length */\n\tuint16_t length;\n};\n/*! \\brief Helper method to initialise/reset the RTCP packet\n * @param[in] packet Pointer to the janus_plugin_rtcp packet to reset\n*/\nvoid janus_plugin_rtcp_reset(janus_plugin_rtcp *packet);\n\n/*! \\brief Janus plugin data message\n * @note At the moment, we only support text based datachannels. In the\n * future, once we add support for binary data, this structure may be\n * extended to include info on the nature of the data itself */\nstruct janus_plugin_data {\n\t/*! \\brief The label this message belongs to */\n\tchar *label;\n\t/*! \\brief The subprotocol this message refers to */\n\tchar *protocol;\n\t/*! \\brief Whether the message data is text (default=FALSE) or binary */\n\tgboolean binary;\n\t/*! \\brief The message data */\n\tchar *buffer;\n\t/*! \\brief The message length */\n\tuint16_t length;\n};\n/*! \\brief Helper method to initialise/reset the data message\n * @param[in] packet Pointer to the janus_plugin_data message to reset\n*/\nvoid janus_plugin_data_reset(janus_plugin_data *packet);\n///@}\n\n\n#endif\n"
  },
  {
    "path": "src/plugins/recordings/1234.nfo",
    "content": "[1234]\r\nname = Lorenzo says hello!\r\nprivate = false\r\ndate = 2014-10-22 14:44:36\r\naudio = rec-sample-audio.mjr\r\nvideo = rec-sample-video.mjr\r\n"
  },
  {
    "path": "src/plugins/streams/test_gstreamer.sh",
    "content": "#!/bin/sh\n\n# Note: if you're on Ubuntu, good chances are your Gstreamer distribution\n# will not have a plugin for Opus installed out of the box. You can try\n# using the one available on Debian, e.g., installing the package you\n# can find here:\n#    http://packages.debian.org/sid/i386/gstreamer0.10-plugins-bad/download\n# For instance:\n#    wget http://ftp.it.debian.org/debian/pool/main/g/gst-plugins-bad0.10/gstreamer0.10-plugins-bad_0.10.23-7.1_i386.deb\n#    ar xv gstreamer0.10-plugins-bad_0.10.23-7.1_i386.deb\n#    tar xfv data.tar.xv\n#    cp usr/lib/i386-linux-gnu/gstreamer-0.10/libgstopus.so /usr/lib/i386-linux-gnu/gstreamer-0.10/\n\ngst-launch \\\n\taudiotestsrc ! \\\n\t\taudioresample ! audio/x-raw-int,channels=1,rate=16000 ! \\\n\t\topusenc bitrate=20000 ! \\\n\t\t\trtpopuspay ! udpsink host=127.0.0.1 port=5002 \\\n\tvideotestsrc ! \\\n\t\tvideo/x-raw-rgb,width=320,height=240,framerate=15/1 ! \\\n\t\tvideoscale ! videorate ! ffmpegcolorspace ! timeoverlay ! \\\n\t\tvp8enc bitrate=256000 speed=2 max-latency=1 error-resilient=true ! \\\n\t\t\trtpvp8pay ! udpsink host=127.0.0.1 port=5004\n"
  },
  {
    "path": "src/plugins/streams/test_gstreamer1.sh",
    "content": "#!/bin/sh\ngst-launch-1.0 \\\n  audiotestsrc ! \\\n    audioresample ! audio/x-raw,channels=1,rate=16000 ! \\\n    opusenc bitrate=20000 ! \\\n      rtpopuspay ! udpsink host=127.0.0.1 port=5002 \\\n  videotestsrc ! \\\n    video/x-raw,width=320,height=240,framerate=15/1 ! \\\n    videoscale ! videorate ! videoconvert ! timeoverlay ! \\\n    vp8enc error-resilient=1 ! \\\n      rtpvp8pay mtu=1200 ! udpsink host=127.0.0.1 port=5004\n"
  },
  {
    "path": "src/plugins/streams/test_gstreamer1_multistream.sh",
    "content": "#!/bin/sh\ngst-launch-1.0 \\\n  audiotestsrc ! \\\n    audioresample ! audio/x-raw,channels=1,rate=16000 ! \\\n    opusenc bitrate=20000 ! \\\n      rtpopuspay ! udpsink host=127.0.0.1 port=5102 \\\n  videotestsrc pattern=ball ! \\\n    video/x-raw,width=320,height=240,framerate=15/1 ! \\\n    videoscale ! videorate ! videoconvert ! timeoverlay ! \\\n    vp8enc error-resilient=1 ! \\\n      rtpvp8pay mtu=1200 ! udpsink host=127.0.0.1 port=5104 \\\n  videotestsrc ! \\\n    video/x-raw,width=320,height=240,framerate=15/1 ! \\\n    videoscale ! videorate ! videoconvert ! timeoverlay ! \\\n    vp8enc error-resilient=1 ! \\\n      rtpvp8pay mtu=1200 ! udpsink host=127.0.0.1 port=5106\n"
  },
  {
    "path": "src/plugins/streams/test_gstreamer_multistream.sh",
    "content": "#!/bin/sh\n\n# Note: if you're on Ubuntu, good chances are your Gstreamer distribution\n# will not have a plugin for Opus installed out of the box. You can try\n# using the one available on Debian, e.g., installing the package you\n# can find here:\n#    http://packages.debian.org/sid/i386/gstreamer0.10-plugins-bad/download\n# For instance:\n#    wget http://ftp.it.debian.org/debian/pool/main/g/gst-plugins-bad0.10/gstreamer0.10-plugins-bad_0.10.23-7.1_i386.deb\n#    ar xv gstreamer0.10-plugins-bad_0.10.23-7.1_i386.deb\n#    tar xfv data.tar.xv\n#    cp usr/lib/i386-linux-gnu/gstreamer-0.10/libgstopus.so /usr/lib/i386-linux-gnu/gstreamer-0.10/\n\ngst-launch \\\n\taudiotestsrc ! \\\n\t\taudioresample ! audio/x-raw-int,channels=1,rate=16000 ! \\\n\t\topusenc bitrate=20000 ! \\\n\t\t\trtpopuspay ! udpsink host=127.0.0.1 port=5102 \\\n\tvideotestsrc pattern=ball ! \\\n\t\tvideo/x-raw-rgb,width=320,height=240,framerate=15/1 ! \\\n\t\tvideoscale ! videorate ! ffmpegcolorspace ! timeoverlay ! \\\n\t\tvp8enc bitrate=256000 speed=2 max-latency=1 error-resilient=true ! \\\n\t\t\trtpvp8pay ! udpsink host=127.0.0.1 port=5104 \\\n\tvideotestsrc ! \\\n\t\tvideo/x-raw-rgb,width=320,height=240,framerate=15/1 ! \\\n\t\tvideoscale ! videorate ! ffmpegcolorspace ! timeoverlay ! \\\n\t\tvp8enc bitrate=256000 speed=2 max-latency=1 error-resilient=true ! \\\n\t\t\trtpvp8pay ! udpsink host=127.0.0.1 port=5106\n"
  },
  {
    "path": "src/postprocessing/janus-pp-rec.1",
    "content": ".TH JANUS-PP-REC 1\n.SH NAME\njanus-pp-rec \\- Janus recordings post-processing utility.\n.SH SYNOPSIS\n.B janus-pp-rec [options]\n.IR source.mjr\n.IR [destination.[opus|ogg|mka|wav|webm|mkv|h264|srt]]\n.SH DESCRIPTION\n.B janus-pp-rec\nis a simple utility that allows you to post-process recordings generated by Janus plugins (e.g., VideoRoom or others). More specifically, since Janus recordings (.mjr files) are basically a structured dump of RTP packets, this utility reorders them all and extracts the frames in order to stick them together and save them to a playable media file. No transcoding is done.\n.TP\nThe target file depends on the codec used in the recording: for instance, VP8 and VP9 frames can be converted to a either a .webm or .mkv file, while H.264 frames can only be converted to a .mp4 or .mkv file instead. Right now, you can convert VP8/VP9 recordings to .webm/.mkv, H.264/H.265/AV1 recordings to .mp4/.mkv, G.711/G.722 recordings to .wav, Opus recordings to .opus/.ogg/.mka, and Data Channel text recordings to .srt. Binary Data Channel recordings can be dumped to files of any extension.\n.SH OPTIONS\n.TP\n.BR \\-h \", \" \\-\\-help\nPrint help and exit\n.TP\n.BR \\-V \", \" \\-\\-version\nPrint version and exit\n.TP\n.BR \\-F \", \" \\-\\-file-extensions\nOnly print the supported target file extensions per codec  (default=off)\n.TP\n.BR \\-j \", \" \\-\\-json\nOnly print JSON header  (default=off)\n.TP\n.BR \\-H \", \" \\-\\-header\nOnly parse .mjr header  (default=off)\n.TP\n.BR \\-p \", \" \\-\\-parse\nOnly parse and re-order packets  (default=off)\n.TP\n.BR \\-e \", \" \\-\\-extended-json\nOnly print extended JSON report (automatically enables --json)  (default=off)\n.TP\n.BR \\-m \", \" \\-\\-metadata=metadata\nSave this metadata string in the target file\n.TP\n.BR \\-i \", \" \\-\\-ignore-first=count\nNumber of first packets to ignore when processing, e.g., in case they're cause of issues (default=0)\n.TP\n.BR \\-P \", \" \\-\\-payload-type=pt\nIgnore all RTP packets that don't match the specified payload type (default=none)\n.TP\n.BR \\-a \", \" \\-\\-audiolevel-ext=id\nID of the audio-levels RTP extension (default=none)\n.TP\n.BR \\-v \", \" \\-\\-videoorient-ext=id\nID of the video-orientation RTP extension (default=none)\n.TP\n.BR \\-d \", \" \\-\\-debug-level=1-7\nDebug/logging level (0=disable debugging, 7=maximum debug level; default=4)\n.TP\n.BR \\-D \", \" \\-\\-debug-timestamps\nEnable debug/logging timestamps  (default=off)\n.TP\n.BR \\-o \", \" \\-\\-disable-colors\nDisable color in the logging  (default=off)\n.TP\n.BR \\-f \", \" \\-\\-format=STRING\nSpecifies the output format (overrides the format from the destination)  (possible values=\"opus\", \"ogg\", \"mka\", \"wav\", \"webm\", \"mkv\", \"mp4\", \"srt\")\n.TP\n.BR \\-t \", \" \\-\\-faststart\nFor mp4 files write the MOOV atom at the head of the file  (default=off)\n.TP\n.BR \\-S \", \" \\-\\-audioskew=milliseconds\nTime threshold to trigger an audio skew compensation, disabled if 0 (default=0)\n.TP\n.BR \\-C \", \" \\-\\-silence-distance=count\nRTP packets distance used to detect RTP silence suppression, disabled if 0 (default=0)\n.TP\n.BR \\-r \", \" \\-\\-restamp=count\nIf the latency of a packet is bigger than the `moving_average_latency * (<restamp>/1000)` the timestamps will be corrected, disabled if 0 (default=0)\n.TP\n.BR \\-c \", \" \\-\\-restamp\\-packets=count\nNumber of packets used for calculating moving average latency for timestamp correction. (default=10)\n.TP\n.BR \\-n \", \" \\-\\-restamp\\-min\\-th=milliseconds\nMinimum latency of moving average to reach before starting to correct timestamps. If the current latency is below this threshold the timestamps will not be changed. Below the threshold we ignore the moving average. (default=500)\n.TP\n.BR \\-t \", \" \\-\\-ignore\\-rtp\\-ts\nIgnore RTP timestamps, and use packet arrival timestamps for timing  (default=off)\n.SH EXAMPLES\n\\fBjanus-pp-rec \\-\\-header rec1234.mjr\\fR \\- Parse the recordings header (shows metadata info)\n.TP\n\\fBjanus-pp-rec \\-\\-parse rec1234.mjr\\fR \\- Parse the recordings packets without processing them\n.TP\n\\fBjanus-pp-rec rec1234.mjr rec1234.webm\\fR \\- Convert a VP8 .mjr recording to a .webm file\n.TP\n\\fBjanus-pp-rec \\-\\-restamp=1500 rec1234.mjr rec1234.opus\\fR \\- Convert audio .mjr recording to .opus while RTP correcting timestamps based on moving average latency\n.SH BUGS\n.TP\nIf you think you found a bug or want to contribute a feature, you can issue or a pull request on https://github.com/meetecho/janus-gateway/issues.\n.TP\nAnyway, before doing that make sure you read the documentation at https://janus.conf.meetecho.com/docs/ and that it has not been discussed already at https://janus.discourse.group/. We only use Github for code issues, and \\fBNOT\\fR for configuration or usage issues: use the group for that.\n.SH SEE ALSO\n.TP\nhttps://github.com/meetecho/janus-gateway \\- Official repository\n.TP\nhttps://janus.conf.meetecho.com \\- Demos and documentation\n.TP\nhttps://janus.discourse.group/ \\- Community\n.TP\nhttps://www.meetecho.com/blog/ \\- Tutorials and blog posts on Janus\n.SH AUTHORS\nLorenzo Miniero (lorenzo@meetecho.com)\n"
  },
  {
    "path": "src/postprocessing/janus-pp-rec.c",
    "content": "/*! \\file    janus-pp-rec.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Simple utility to post-process .mjr \\ref recordings saved by Janus\n * \\details  As explained in the \\ref recordings documentation,\n * our Janus WebRTC server provides a simple helper (janus_recorder)\n * to allow plugins to record audio, video and text frames sent by users. At the time\n * of writing, this helper has been integrated in several plugins in Janus.\n * To keep things simple on the Janus side, though, no processing\n * at all is done in the recording step: this means that the recorder\n * actually only dumps the RTP frames it receives to a file in a structured way,\n * so that they can be post-processed later on to extract playable media\n * files. This utility allows you to process those files, in order to\n * get a working media file you can playout with an external player.\n * The tool will generate a .webm/.mkv if the recording includes VP8 frames,\n * an .opus/.ogg/.mka if the recording includes Opus frames, an .mp4/.mkv if the\n * recording includes H.264/H.265/AV1 frames, and a .wav file if the recording includes\n * G.711 (mu-law or a-law) frames. In case the recording contains text\n * frames as received via data channels, instead, a .srt file will be\n * generated with the text content and the related timing information.\n *\n * Using the utility is quite simple. Just pass, as arguments to the tool,\n * the path to the .mjr source file you want to post-process, and the\n * path to the destination file, e.g.:\n *\n\\verbatim\n./janus-pp-rec /path/to/source.mjr /path/to/destination.[opus|ogg|mka|wav|webm|mkv|h264|srt]\n\\endverbatim\n *\n * An attempt to specify an output format that is not compliant with the\n * recording content (e.g., a .webm for H.264 frames) will result in an\n * error since, again, no transcoding is involved.\n *\n * You can also just print the internal header of the recording, or parse\n * it without processing it (e.g., for debugging), by invoking the tool\n * in a different way:\n *\n\\verbatim\n./janus-pp-rec --json /path/to/source.mjr\n./janus-pp-rec --header /path/to/source.mjr\n./janus-pp-rec --parse /path/to/source.mjr\n\\endverbatim\n *\n * For a more complete overview of the available command line settings,\n * launch the tool with no arguments or by passing \\c --help and it will\n * show something like this:\n *\n\\verbatim\nUsage: janus-pp-rec [OPTIONS] source.mjr\n[destination.[opus|ogg|mka|wav|webm|mkv|h264|srt]]\n\n  -h, --help                    Print help and exit\n  -V, --version                 Print version and exit\n  -F, --file-extensions         Only print the supported target file extensions\n                                  per codec  (default=off)\n  -j, --json                    Only print JSON header  (default=off)\n  -H, --header                  Only parse .mjr header  (default=off)\n  -p, --parse                   Only parse and re-order packets  (default=off)\n  -e, --extended-report         Only print extended report (automatically\n                                  enables --header)  (default=off)\n  -m, --metadata=metadata       Save this metadata string in the target file\n  -i, --ignore-first=count      Number of first packets to ignore when\n                                  processing, e.g., in case they're cause of\n                                  issues (default=0)\n  -P, --payload-type=pt         Ignore all RTP packets that don't match the\n                                  specified payload type (default=none)\n  -a, --audiolevel-ext=id       ID of the audio-levels RTP extension\n                                  (default=none)\n  -v, --videoorient-ext=id      ID of the video-orientation RTP extension\n                                  (default=none)\n  -d, --debug-level=1-7         Debug/logging level (0=disable debugging,\n                                  7=maximum debug level; default=4)\n  -D, --debug-timestamps        Enable debug/logging timestamps  (default=off)\n  -o, --disable-colors          Disable color in the logging  (default=off)\n  -f, --format=STRING           Specifies the output format (overrides the\n                                  format from the destination)  (possible\n                                  values=\"opus\", \"ogg\", \"mka\", \"wav\",\n                                  \"webm\", \"mkv\", \"mp4\", \"srt\")\n  -t, --faststart               For mp4 files write the MOOV atom at the head\n                                  of the file  (default=off)\n  -S, --audioskew=milliseconds  Time threshold to trigger an audio skew\n                                  compensation, disabled if 0 (default=0)\n  -C, --silence-distance=count  RTP packets distance used to detect RTP silence\n                                  suppression, disabled if 0 (default=0)\n  -r, --restamp=count           If the latency of a packet is bigger than the\n                                  `moving_average_latency * (<restamp>/1000)`\n                                  the timestamps will be corrected, disabled if\n                                  0 (default=0)\n  -c, --restamp-packets=count   Number of packets used for calculating moving\n                                  average latency for timestamp correction\n                                  (default=10)\n  -n, --restamp-min-th=milliseconds\n                                Minimum latency of moving average to reach\n                                  before starting to correct timestamps.\n                                  (default=500)\n\\endverbatim\n *\n * \\note This utility does not do any form of transcoding. It just\n * depacketizes the RTP frames in order to get the payload, and saves\n * the frames in a valid container. Any further post-processing (e.g.,\n * muxing audio and video belonging to the same media session in a single\n * .webm file) is up to third-party applications.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n#include <signal.h>\n\n#include <jansson.h>\n\n#include \"../debug.h\"\n#include \"../utils.h\"\n#include \"pp-options.h\"\n#include \"pp-rtp.h\"\n#include \"pp-webm.h\"\n#include \"pp-h264.h\"\n#include \"pp-av1.h\"\n#include \"pp-h265.h\"\n#include \"pp-opus.h\"\n#include \"pp-g711.h\"\n#include \"pp-g722.h\"\n#include \"pp-l16.h\"\n#include \"pp-srt.h\"\n#include \"pp-binary.h\"\n\nint janus_log_level = 4;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = TRUE;\nchar *janus_log_global_prefix = NULL;\nint lock_debug = 0;\n\nstatic janus_pprec_options options = { 0 };\n\nstatic janus_pp_frame_packet *list = NULL, *last = NULL;\nstatic int working = 0;\n\n#define SKEW_DETECTION_WAIT_TIME_SECS 10\n#define DEFAULT_AUDIO_SKEW_TH 0\n#define DEFAULT_SILENCE_DISTANCE 0\n#define DEFAULT_RESTAMP_MULTIPLIER 0\n#define DEFAULT_RESTAMP_MIN_TH 500\n#define DEFAULT_RESTAMP_PACKETS 10\n\n/* Signal handler */\nstatic void janus_pp_handle_signal(int signum) {\n\tworking = 0;\n}\n\n/* Helper method to return an audio level from the related RTP extension, if any */\nstatic int janus_pp_rtp_header_extension_parse_audio_level(char *buf, int len, int id, int *level);\n/* Helper method to return the video rotation from the related RTP extension, if any */\nstatic int janus_pp_rtp_header_extension_parse_video_orientation(char *buf, int len, int id, int *rotation);\n\ntypedef struct janus_pp_rtp_skew_context {\n\tguint32 ssrc, rate;\n\tguint64 reference_time, start_time, evaluating_start_time;\n\tguint64 start_ts, last_ts, prev_ts, target_ts;\n\tguint16 last_seq, prev_seq;\n\tgint64 prev_delay, active_delay;\n\tguint64 ts_offset;\n\tgint16 seq_offset;\n} janus_pp_rtp_skew_context;\nstatic gint janus_pp_skew_compensate_audio(janus_pp_frame_packet *pkt, janus_pp_rtp_skew_context *context);\n\n/* Helper methods for timestamp correction (restamp) */\nstatic double get_latency(const janus_pp_frame_packet *tmp, int rate);\nstatic double get_moving_average_of_latency(janus_pp_frame_packet *pkt, int rate, int num_of_packets);\n\n/* Helper method to check whether a processor accepts a specific extension */\nstatic gboolean janus_pp_extension_check(const char *extension, const char **allowed) {\n\tif(allowed == NULL || extension == NULL)\n\t\treturn FALSE;\n\tconst char **ext = allowed;\n\twhile(*ext != NULL) {\n\t\tif(!strcasecmp(extension, *ext))\n\t\t\treturn TRUE;\n\t\text++;\n\t}\n\t/* If we got here, we don't support this target format for this codec (yet) */\n\treturn FALSE;\n}\nstatic char *janus_pp_extensions_string(const char **allowed, char *supported, size_t suplen) {\n\tif(allowed == NULL || supported == NULL || suplen == 0)\n\t\treturn NULL;\n\tsupported[0] = '\\0';\n\tjanus_strlcat(supported, \"[\", suplen);\n\tconst char **ext = allowed;\n\twhile(*ext != NULL) {\n\t\tif(strnlen(supported, 1 + 1) > 1)\n\t\t\tjanus_strlcat(supported, \", \", suplen);\n\t\tjanus_strlcat(supported, *ext, suplen);\n\t\text++;\n\t}\n\tjanus_strlcat(supported, \"]\", suplen);\n\treturn supported;\n}\n\n/* Main Code */\nint main(int argc, char *argv[]) {\n\tjanus_log_init(FALSE, TRUE, NULL, NULL);\n\tatexit(janus_log_destroy);\n\n\t/* Initialize some command line options defaults */\n\toptions.debug_level = g_getenv(\"JANUS_PPREC_DEBUG\") ? atoi(g_getenv(\"JANUS_PPREC_DEBUG\")) : 4;\n\toptions.ignore_first_packets = g_getenv(\"JANUS_PPREC_IGNOREFIRST\") ? atoi(g_getenv(\"JANUS_PPREC_IGNOREFIRST\")) : 0;\n\toptions.match_pt = -1;\n\toptions.audio_level_extmap_id = g_getenv(\"JANUS_PPREC_AUDIOLEVELEXT\") ? atoi(g_getenv(\"JANUS_PPREC_AUDIOLEVELEXT\")) : -1;\n\toptions.video_orient_extmap_id = g_getenv(\"JANUS_PPREC_VIDEOORIENTEXT\") ? atoi(g_getenv(\"JANUS_PPREC_VIDEOORIENTEXT\")) : -1;\n\toptions.audioskew_th = g_getenv(\"JANUS_PPREC_AUDIOSKEW\") ? atoi(g_getenv(\"JANUS_PPREC_AUDIOSKEW\")) : DEFAULT_AUDIO_SKEW_TH;\n\toptions.silence_distance = g_getenv(\"JANUS_PPREC_SILENCE_DISTANCE\") ? atoi(g_getenv(\"JANUS_PPREC_SILENCE_DISTANCE\")) : DEFAULT_SILENCE_DISTANCE;\n\toptions.restamp_multiplier = g_getenv(\"JANUS_PPREC_RESTAMP\") ? atoi(g_getenv(\"JANUS_PPREC_RESTAMP\")) : DEFAULT_RESTAMP_MULTIPLIER;\n\toptions.restamp_min_th = g_getenv(\"JANUS_PPREC_RESTAMP_MIN_TH\") ? atoi(g_getenv(\"JANUS_PPREC_RESTAMP_MIN_TH\")) : DEFAULT_RESTAMP_MIN_TH;\n\toptions.restamp_packets = g_getenv(\"JANUS_PPREC_RESTAMP_PACKETS\") ? atoi(g_getenv(\"JANUS_PPREC_RESTAMP_PACKETS\")) : DEFAULT_RESTAMP_PACKETS;\n\t/* Let's call our cmdline parser */\n\tif(!janus_pprec_options_parse(&options, argc, argv))\n\t\texit(1);\n\n\t/* Check if we only need to print the supported extensions for all codecs */\n\tif(options.fileexts_only) {\n\t\tJANUS_LOG(LOG_INFO, \"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\t\tJANUS_LOG(LOG_INFO, \"Janus commit: %s\\n\", janus_build_git_sha);\n\t\tJANUS_LOG(LOG_INFO, \"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\t\tJANUS_LOG(LOG_INFO, \"Supported file extensions:\\n\");\n\t\tchar supported[100];\n\t\tJANUS_LOG(LOG_INFO, \"  -- Opus:   %s\\n\", janus_pp_extensions_string(janus_pp_opus_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- G.711:  %s\\n\", janus_pp_extensions_string(janus_pp_g711_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- G.722:  %s\\n\", janus_pp_extensions_string(janus_pp_g722_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- L16:    %s\\n\", janus_pp_extensions_string(janus_pp_l16_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- VP8:    %s\\n\", janus_pp_extensions_string(janus_pp_webm_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- VP9:    %s\\n\", janus_pp_extensions_string(janus_pp_webm_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- H.264:  %s\\n\", janus_pp_extensions_string(janus_pp_h264_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- AV1:    %s\\n\", janus_pp_extensions_string(janus_pp_av1_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- H.265:  %s\\n\", janus_pp_extensions_string(janus_pp_h265_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- Text:   %s\\n\", janus_pp_extensions_string(janus_pp_srt_get_extensions(), supported, sizeof(supported)));\n\t\tJANUS_LOG(LOG_INFO, \"  -- Binary: any\\n\");\n\t\tjanus_pprec_options_destroy();\n\t\texit(0);\n\t}\n\n\t/* If we're asked to print the JSON header as it is, we must not print anything else */\n\tgboolean jsonheader_only = FALSE, header_only = FALSE, parse_only = FALSE, extjson_only = FALSE;\n\tif(options.jsonheader_only)\n\t\tjsonheader_only = TRUE;\n\tif(options.header_only && !jsonheader_only)\n\t\theader_only = TRUE;\n\tif(options.parse_only && !jsonheader_only && !header_only)\n\t\tparse_only = TRUE;\n\tif(options.extjson_only) {\n\t\tjsonheader_only = FALSE;\n\t\theader_only = FALSE;\n\t\tparse_only = TRUE;\n\t\textjson_only = TRUE;\n\t}\n\n\t/* We support both command line arguments and, for backwards compatibility, env variables in some cases */\n\tjanus_log_level = (options.debug_level >= LOG_NONE && options.debug_level <= LOG_MAX) ? options.debug_level : 4;\n\tif(options.disable_colors)\n\t\tjanus_log_colors = FALSE;\n\tif(options.debug_timestamps)\n\t\tjanus_log_timestamps = TRUE;\n\tchar *metadata = NULL;\n\tif(options.metadata != NULL || (g_getenv(\"JANUS_PPREC_METADATA\") != NULL))\n\t\tmetadata = g_strdup(options.metadata ? options.metadata : g_getenv(\"JANUS_PPREC_METADATA\"));\n\tif(options.ignore_first_packets < 0)\n\t\toptions.ignore_first_packets = 0;\n\t/* If we're just printing the JSON header (extended or not), disable debugging */\n\tif(jsonheader_only || extjson_only)\n\t\tjanus_log_level = LOG_NONE;\n\n\tif(options.match_pt < 0 || options.match_pt > 127)\n\t\toptions.match_pt = -1;\n\tchar *extension = NULL;\n\tif(options.extension || (g_getenv(\"JANUS_PPREC_FORMAT\") != NULL))\n\t\textension = g_strdup(options.extension ? options.extension : g_getenv(\"JANUS_PPREC_FORMAT\"));\n\tif(options.audioskew_th < 0)\n\t\toptions.audioskew_th = DEFAULT_AUDIO_SKEW_TH;\n\tif(options.silence_distance < 0)\n\t\toptions.silence_distance = DEFAULT_SILENCE_DISTANCE;\n\tif(options.restamp_multiplier < 0)\n\t\toptions.restamp_multiplier = DEFAULT_RESTAMP_MULTIPLIER;\n\tif(options.restamp_packets < 0)\n\t\toptions.restamp_packets = DEFAULT_RESTAMP_PACKETS;\n\tif(options.restamp_min_th < 0)\n\t\toptions.restamp_packets = DEFAULT_RESTAMP_MIN_TH;\n\n\t/* Evaluate arguments to find source and target */\n\tchar *source = options.paths ? options.paths[0] : NULL;\n\tchar *destination = (options.paths && options.paths[0]) ? options.paths[1] : NULL;\n\tif(source == NULL || (destination == NULL && !jsonheader_only && !header_only && !parse_only)) {\n\t\tjanus_pprec_options_help();\n\t\tjanus_pprec_options_destroy();\n\t\texit(1);\n\t}\n\n\tif(!jsonheader_only) {\n\t\tJANUS_LOG(LOG_INFO, \"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\t\tJANUS_LOG(LOG_INFO, \"Janus commit: %s\\n\", janus_build_git_sha);\n\t\tJANUS_LOG(LOG_INFO, \"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\t\tJANUS_LOG(LOG_INFO, \"Logging level: %d\\n\", janus_log_level);\n\t\tif(metadata)\n\t\t\tJANUS_LOG(LOG_INFO, \"Metadata: %s\\n\", metadata);\n\t\tif(options.audioskew_th != DEFAULT_AUDIO_SKEW_TH)\n\t\t\tJANUS_LOG(LOG_INFO, \"Audio skew threshold: %d\\n\", options.audioskew_th);\n\t\tif(options.ignore_first_packets > 0)\n\t\t\tJANUS_LOG(LOG_INFO, \"Ignoring first packets: %d\\n\", options.ignore_first_packets);\n\t\tif(options.audio_level_extmap_id > 0)\n\t\t\tJANUS_LOG(LOG_INFO, \"Audio level extension ID: %d\\n\", options.audio_level_extmap_id);\n\t\tif(options.video_orient_extmap_id > 0)\n\t\t\tJANUS_LOG(LOG_INFO, \"Video orientation extension ID: %d\\n\", options.video_orient_extmap_id);\n\t\tif(options.silence_distance > 0)\n\t\t\tJANUS_LOG(LOG_INFO, \"RTP silence suppression distance: %d\\n\", options.silence_distance);\n\t\tif(options.ignore_rtp_ts)\n\t\t\tJANUS_LOG(LOG_INFO, \"Will ignore RTP timestamps, and use arrival times for timing\\n\");\n\t\tJANUS_LOG(LOG_INFO, \"\\n\");\n\t\tif(source != NULL)\n\t\t\tJANUS_LOG(LOG_INFO, \"Source file: %s\\n\", source);\n\t\tif(header_only)\n\t\t\tJANUS_LOG(LOG_INFO, \"  -- Showing header only\\n\");\n\t\tif(parse_only)\n\t\t\tJANUS_LOG(LOG_INFO, \"  -- Parsing header only\\n\");\n\t\tif(destination != NULL)\n\t\t\tJANUS_LOG(LOG_INFO, \"Target file: %s\\n\", destination);\n\t\tJANUS_LOG(LOG_INFO, \"\\n\");\n\t}\n\n\tif((destination != NULL) && (extension == NULL)) {\n\t\t/* Check the extension of the target file */\n\t\textension = strrchr(destination, '.');\n\t\tif(extension == NULL) {\n\t\t\t/* No extension? */\n\t\t\tJANUS_LOG(LOG_ERR, \"No extension? Unsupported target file\\n\");\n\t\t\tjanus_pprec_options_destroy();\n\t\t\texit(1);\n\t\t}\n\t\textension++;\n\t\textension = g_strdup(extension);\n\t}\n\n\tif(options.faststart && strcasecmp(extension, \"mp4\")) {\n\t\tJANUS_LOG(LOG_ERR, \"Faststart only supported for MP4\\n\");\n\t\tjanus_pprec_options_destroy();\n\t\texit(1);\n\t}\n\n\tFILE *file = fopen(source, \"rb\");\n\tif(file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not open file %s\\n\", source);\n\t\tjanus_pprec_options_destroy();\n\t\texit(1);\n\t}\n\tfseek(file, 0L, SEEK_END);\n\tlong fsize = ftell(file);\n\tfseek(file, 0L, SEEK_SET);\n\tif(!jsonheader_only)\n\t\tJANUS_LOG(LOG_INFO, \"File is %zu bytes\\n\", fsize);\n\n\t/* Handle SIGINT */\n\tworking = 1;\n\tsignal(SIGINT, janus_pp_handle_signal);\n\n\t/* Pre-parse */\n\tif(!jsonheader_only)\n\t\tJANUS_LOG(LOG_INFO, \"Pre-parsing file to generate ordered index...\\n\");\n\tjson_t *info = NULL;\n\tgboolean has_timestamps = FALSE;\n\tgboolean parsed_header = FALSE;\n\tgboolean video = FALSE, data = FALSE, textdata = FALSE;\n\tgboolean opus = FALSE, multiopus = FALSE, g711 = FALSE, g722 = FALSE, l16 = FALSE, l16_48k = FALSE,\n\t\tvp8 = FALSE, vp9 = FALSE, h264 = FALSE, av1 = FALSE, h265 = FALSE;\n\tint opusred_pt = 0;\n\tgboolean e2ee = FALSE;\n\tgint64 c_time = 0, w_time = 0;\n\tint bytes = 0, skip = 0;\n\tlong offset = 0;\n\tuint16_t len = 0;\n\tuint32_t count = 0;\n\tuint32_t ssrc = 0;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\tchar prebuffer2[1500];\n\tmemset(prebuffer2, 0, 1500);\n\t/* Let's look for timestamp resets first */\n\twhile(working && offset < fsize) {\n\t\tif(header_only && parsed_header) {\n\t\t\t/* We only needed to parse the header */\n\t\t\tjanus_pprec_options_destroy();\n\t\t\texit(0);\n\t\t}\n\t\t/* Read frame header */\n\t\tskip = 0;\n\t\tfseek(file, offset, SEEK_SET);\n\t\tbytes = fread(prebuffer, sizeof(char), 8, file);\n\t\tif(bytes != 8 || prebuffer[0] != 'M') {\n\t\t\tJANUS_LOG(LOG_WARN, \"Invalid header at offset %ld (%s), the processing will stop here...\\n\",\n\t\t\t\toffset, bytes != 8 ? \"not enough bytes\" : \"wrong prefix\");\n\t\t\tif(!parsed_header) {\n\t\t\t\t/* Not an MJR file? */\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tif(prebuffer[1] == 'E') {\n\t\t\t/* Either the old .mjr format header ('MEETECHO' header followed by 'audio' or 'video'), or a frame */\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len == 5 && !parsed_header) {\n\t\t\t\t/* This is the main header */\n\t\t\t\tparsed_header = TRUE;\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Old .mjr header format\\n\");\n\t\t\t\tif(jsonheader_only) {\t/* No JSON header to print */\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), 5, file);\n\t\t\t\tif(prebuffer[0] == 'v') {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"This is a video recording, assuming VP8\\n\");\n\t\t\t\t\tvideo = TRUE;\n\t\t\t\t\tdata = FALSE;\n\t\t\t\t\tvp8 = TRUE;\n\t\t\t\t\tif(extension && strcasecmp(extension, \"webm\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"VP8 RTP packets can only be converted to a .webm file\\n\");\n\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t} else if(prebuffer[0] == 'a') {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"This is an audio recording, assuming Opus\\n\");\n\t\t\t\t\tvideo = FALSE;\n\t\t\t\t\tdata = FALSE;\n\t\t\t\t\topus = TRUE;\n\t\t\t\t\tif(extension && strcasecmp(extension, \"opus\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Opus RTP packets can only be converted to an .opus file\\n\");\n\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t} else if(prebuffer[0] == 'd') {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"This is a text data recording, assuming SRT\\n\");\n\t\t\t\t\tvideo = FALSE;\n\t\t\t\t\tdata = TRUE;\n\t\t\t\t\tif(extension && strcasecmp(extension, \"srt\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Data channel packets can only be converted to a .srt file\\n\");\n\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported recording media type...\\n\");\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\toffset += len;\n\t\t\t\tcontinue;\n\t\t\t} else if(!data && len < 12) {\n\t\t\t\t/* Not RTP, skip */\n\t\t\t\tif(!jsonheader_only)\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping packet (not RTP?)\\n\");\n\t\t\t\toffset += len;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} else if(prebuffer[1] == 'J') {\n\t\t\t/* New .mjr format, the header may contain useful info */\n\t\t\tif(prebuffer[2] == 'R' && prebuffer[3] == '0' && prebuffer[4] == '0' &&\n\t\t\t\t\tprebuffer[5] == '0' && prebuffer[6] == '0' && prebuffer[7] == '2') {\n\t\t\t\t/* Main header is MJR00002: this means we have timestamps too */\n\t\t\t\thas_timestamps = TRUE;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"New .mjr format, will parse timestamps too\\n\");\n\t\t\t}\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len > 0 && !parsed_header) {\n\t\t\t\t/* This is the info header */\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\t\t\tparsed_header = TRUE;\n\t\t\t\tprebuffer[len] = '\\0';\n\t\t\t\tif(jsonheader_only && !extjson_only) {\n\t\t\t\t\t/* Print the header as it is and exit */\n\t\t\t\t\tJANUS_PRINT(\"%s\\n\", prebuffer);\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(0);\n\t\t\t\t}\n\t\t\t\tjson_error_t error;\n\t\t\t\tinfo = json_loads(prebuffer, 0, &error);\n\t\t\t\tif(!info) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error parsing info header...\\n\");\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\t/* First of all let's check if this is an end-to-end encrypted recording */\n\t\t\t\tjson_t *e = json_object_get(info, \"e\");\n\t\t\t\tif(e && json_is_true(e))\n\t\t\t\t\te2ee = TRUE;\n\t\t\t\t/* Is it audio or video? */\n\t\t\t\tjson_t *type = json_object_get(info, \"t\");\n\t\t\t\tif(!type || !json_is_string(type)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing/invalid recording type in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tconst char *t = json_string_value(type);\n\t\t\t\tif(!strcasecmp(t, \"v\")) {\n\t\t\t\t\tvideo = TRUE;\n\t\t\t\t\tdata = FALSE;\n\t\t\t\t} else if(!strcasecmp(t, \"a\")) {\n\t\t\t\t\tvideo = FALSE;\n\t\t\t\t\tdata = FALSE;\n\t\t\t\t} else if(!strcasecmp(t, \"d\")) {\n\t\t\t\t\tvideo = FALSE;\n\t\t\t\t\tdata = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported recording type '%s' in info header...\\n\", t);\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\t/* What codec was used? */\n\t\t\t\tjson_t *codec = json_object_get(info, \"c\");\n\t\t\t\tif(!codec || !json_is_string(codec)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing recording codec in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tconst char *c = json_string_value(codec);\n\t\t\t\tchar supported[100];\n\t\t\t\tif(video) {\n\t\t\t\t\tif(!strcasecmp(c, \"vp8\")) {\n\t\t\t\t\t\tvp8 = TRUE;\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_webm_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"VP8 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_webm_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(!strcasecmp(c, \"vp9\")) {\n\t\t\t\t\t\tvp9 = TRUE;\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_webm_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"VP9 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_webm_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(!strcasecmp(c, \"h264\")) {\n\t\t\t\t\t\th264 = TRUE;\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_h264_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"H.264 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_h264_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(!strcasecmp(c, \"av1\")) {\n\t\t\t\t\t\tav1 = TRUE;\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_av1_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"AV1 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_av1_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(!strcasecmp(c, \"h265\")) {\n\t\t\t\t\t\th265 = TRUE;\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_h265_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"H.265 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_h265_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The post-processor only supports VP8, VP9, H.264, AV1 and H.265 video for now (was '%s')...\\n\", c);\n\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t} else if(!video && !data) {\n\t\t\t\t\tif(!strcasecmp(c, \"opus\") || !strcasecmp(c, \"multiopus\")) {\n\t\t\t\t\t\topus = TRUE;\n\t\t\t\t\t\tmultiopus = !strcasecmp(c, \"multiopus\");\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_opus_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"%s RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tmultiopus ? \"Multiopus\" : \"Opus\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_opus_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(!strcasecmp(c, \"g711\") || !strcasecmp(c, \"pcmu\") || !strcasecmp(c, \"pcma\")) {\n\t\t\t\t\t\tg711 = TRUE;\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_g711_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"G.711 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_g711_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(!strcasecmp(c, \"g722\")) {\n\t\t\t\t\t\tg722 = TRUE;\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_g722_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"G.722 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_g722_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(!strcasecmp(c, \"l16\") || !strcasecmp(c, \"l16-48\")) {\n\t\t\t\t\t\tl16 = TRUE;\n\t\t\t\t\t\tl16_48k = !strcasecmp(c, \"l16-48\");\n\t\t\t\t\t\tif(extension && !janus_pp_extension_check(extension, janus_pp_l16_get_extensions())) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"L16 RTP packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_l16_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\t\texit(1);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The post-processor only supports Opus, G.711 and G.722 audio for now (was '%s')...\\n\", c);\n\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t} else if(data) {\n\t\t\t\t\tif(strcasecmp(c, \"text\") && strcasecmp(c, \"binary\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"The post-processor only supports text and binary data (was '%s')...\\n\", c);\n\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t\ttextdata = !strcasecmp(c, \"text\");\n\t\t\t\t\tif(textdata && extension && !janus_pp_extension_check(extension, janus_pp_srt_get_extensions())) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Text data channel packets cannot be converted to this target file, at the moment (supported formats: %s)\\n\",\n\t\t\t\t\t\t\tjanus_pp_extensions_string(janus_pp_srt_get_extensions(), supported, sizeof(supported)));\n\t\t\t\t\t\tjson_decref(info);\n\t\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\t\texit(1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Any codec-specific info? (just informational) */\n\t\t\t\tconst char *f = json_string_value(json_object_get(info, \"f\"));\n\t\t\t\t/* Is RED in use for audio? */\n\t\t\t\tif(!video && !data)\n\t\t\t\t\topusred_pt = json_integer_value(json_object_get(info, \"or\"));\n\t\t\t\t/* Check if there are RTP extensions */\n\t\t\t\tjson_t *exts = json_object_get(info, \"x\");\n\t\t\t\tif(exts != NULL) {\n\t\t\t\t\t/* There are: check if audio-level and/or video-orientation\n\t\t\t\t\t * are among them, as we might need them */\n\t\t\t\t\tint extid = 0;\n\t\t\t\t\tconst char *key = NULL, *extmap = NULL;\n\t\t\t\t\tjson_t *value = NULL;\n\t\t\t\t\tjson_object_foreach(exts, key, value) {\n\t\t\t\t\t\tif(key == NULL || value == NULL || !json_is_string(value))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\textid = atoi(key);\n\t\t\t\t\t\textmap = json_string_value(value);\n\t\t\t\t\t\tif(!strcasecmp(extmap, JANUS_PP_RTP_EXTMAP_AUDIO_LEVEL)) {\n\t\t\t\t\t\t\t/* Audio level */\n\t\t\t\t\t\t\tif(options.audio_level_extmap_id != -1) {\n\t\t\t\t\t\t\t\tif(options.audio_level_extmap_id != extid) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Audio level extension ID found in header (%d) is different from the one provided via argument (%d)\\n\",\n\t\t\t\t\t\t\t\t\t\toptions.audio_level_extmap_id, extid);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\toptions.audio_level_extmap_id = extid;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Audio level extension ID: %d\\n\", options.audio_level_extmap_id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if(!strcasecmp(extmap, JANUS_PP_RTP_EXTMAP_VIDEO_ORIENTATION)) {\n\t\t\t\t\t\t\t/* Video orientation */\n\t\t\t\t\t\t\tif(options.video_orient_extmap_id != -1) {\n\t\t\t\t\t\t\t\tif(options.video_orient_extmap_id != extid) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Video orientation extension ID found in header (%d) is different from the one provided via argument (%d)\\n\",\n\t\t\t\t\t\t\t\t\t\toptions.video_orient_extmap_id, extid);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\toptions.video_orient_extmap_id = extid;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Video orientation extension ID: %d\\n\", options.video_orient_extmap_id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* When was the file created? */\n\t\t\t\tjson_t *created = json_object_get(info, \"s\");\n\t\t\t\tif(!created || !json_is_integer(created)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing recording created time in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tc_time = json_integer_value(created);\n\t\t\t\t/* When was the first frame written? */\n\t\t\t\tjson_t *written = json_object_get(info, \"u\");\n\t\t\t\tif(!written || !json_is_integer(written)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing recording written time in info header...\\n\");\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tw_time = json_integer_value(written);\n\t\t\t\t/* Summary */\n\t\t\t\tJANUS_LOG(LOG_INFO, \"This is %s recording:\\n\", video ? \"a video\" : (data ? \"a text data\" : \"an audio\"));\n\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- Codec:   %s\\n\", c);\n\t\t\t\tif(f != NULL)\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- -- fmtp: %s\\n\", f);\n\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- Created: %\"SCNi64\"\\n\", c_time);\n\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- Written: %\"SCNi64\"\\n\", w_time);\n\t\t\t\tif(opusred_pt > 0)\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- Audio recording contains RED packets\\n\");\n\t\t\t\tif(e2ee)\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- Recording is end-to-end encrypted\\n\");\n\t\t\t\t/* Save the original string as a metadata to save in the media container, if possible */\n\t\t\t\tif(metadata == NULL)\n\t\t\t\t\tmetadata = g_strdup(prebuffer);\n\t\t\t\t/* Unless we need the extended report, get rid of the JSON object */\n\t\t\t\tif(!extjson_only) {\n\t\t\t\t\tjson_decref(info);\n\t\t\t\t\tinfo = NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid header...\\n\");\n\t\t\tjanus_pprec_options_destroy();\n\t\t\texit(1);\n\t\t}\n\t\t/* Skip data for now */\n\t\toffset += len;\n\t}\n\tif(!working || jsonheader_only) {\n\t\tg_free(metadata);\n\t\tg_free(extension);\n\t\tjanus_pprec_options_destroy();\n\t\texit(0);\n\t}\n\n\t/* Now that we know what we're working with, check the extension */\n\tif(extension && strcasecmp(extension, \"opus\") && strcasecmp(extension, \"wav\") &&\n\t\t\tstrcasecmp(extension, \"ogg\") && strcasecmp(extension, \"mka\") &&\n\t\t\tstrcasecmp(extension, \"webm\") && strcasecmp(extension, \"mp4\") &&\n\t\t\tstrcasecmp(extension, \"mkv\") &&\n\t\t\tstrcasecmp(extension, \"srt\") && (!data || (data && textdata))) {\n\t\t/* Unsupported extension? */\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported extension '%s'\\n\", extension);\n\t\tif(info)\n\t\t\tjson_decref(info);\n\t\tg_free(metadata);\n\t\tg_free(extension);\n\t\tjanus_pprec_options_destroy();\n\t\texit(1);\n\t}\n\n\t/* In case we need an extended report */\n\tjson_t *report = NULL, *rotations = NULL;\n\tif(extjson_only) {\n\t\treport = json_object();\n\t\tjson_object_set_new(info, \"extended\", report);\n\t}\n\n\t/* Now let's parse the frames and order them */\n\tuint32_t pkt_ts = 0, highest_rtp_ts = 0;\n\tuint16_t highest_seq = 0;\n\t/* Start from 1 to take into account late packets */\n\tint times_resetted = 1;\n\tuint64_t max32 = UINT32_MAX;\n\tint ignored = 0;\n\toffset = 0;\n\tgboolean started = FALSE;\n\t/* Silence suppression stuff */\n\tgboolean ssup_on = FALSE;\n\t/* Extensions, if any */\n\tint audiolevel = 0, rotation = 0, last_rotation = -1, rotated = -1;\n\tuint16_t rtp_header_len, rtp_read_n;\n\t/* Start loop */\n\twhile(working && offset < fsize) {\n\t\t/* Read frame header */\n\t\tskip = 0;\n\t\tfseek(file, offset, SEEK_SET);\n\t\tbytes = fread(prebuffer, sizeof(char), 8, file);\n\t\tif(bytes != 8 || prebuffer[0] != 'M') {\n\t\t\t/* Broken packet? Stop here */\n\t\t\tbreak;\n\t\t}\n\t\tif(has_timestamps) {\n\t\t\t/* Read the packet timestamp */\n\t\t\tmemcpy(&pkt_ts, prebuffer+4, sizeof(uint32_t));\n\t\t\tpkt_ts = ntohl(pkt_ts);\n\t\t}\n\t\tprebuffer[(has_timestamps && prebuffer[1] != 'J') ? 4 : 8] = '\\0';\n\t\tJANUS_LOG(LOG_VERB, \"Header: %s\\n\", prebuffer);\n\t\toffset += 8;\n\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\tlen = ntohs(len);\n\t\tJANUS_LOG(LOG_VERB, \"  -- Length: %\"SCNu16\"\\n\", len);\n\t\toffset += 2;\n\t\tif(prebuffer[1] == 'J' || (!data && len < 12)) {\n\t\t\t/* Not RTP, skip */\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Not RTP, skipping\\n\");\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\tif(has_timestamps) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Time: %\"SCNu32\"ms\\n\", pkt_ts);\n\t\t}\n\t\tif(!data && len > 1500) {\n\t\t\t/* Way too large, very likely not RTP, skip */\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Too large packet (%d bytes), skipping\\n\", len);\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\tif(options.ignore_first_packets && ignored < options.ignore_first_packets) {\n\t\t\t/* We've been told to ignore the first X packets */\n\t\t\tignored++;\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\tif(data) {\n\t\t\t/* Things are simpler for data, no reordering is needed: start by the data time */\n\t\t\tgint64 when = 0;\n\t\t\tbytes = fread(&when, 1, sizeof(gint64), file);\n\t\t\tif(bytes < (int)sizeof(gint64)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing data timestamp header\\n\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\twhen = ntohll((uint64_t)when);\n\t\t\toffset += sizeof(gint64);\n\t\t\tlen -= sizeof(gint64);\n\t\t\t/* Generate frame packet and insert in the ordered list */\n\t\t\tjanus_pp_frame_packet *p = g_malloc(sizeof(janus_pp_frame_packet));\n\t\t\tp->version = has_timestamps ? 2 : 1;\n\t\t\tp->p_ts = pkt_ts;\n\t\t\tp->seq = 0;\n\t\t\t/* We \"abuse\" the timestamp field for the timing info */\n\t\t\tp->ts = when-c_time;\n\t\t\tp->len = len;\n\t\t\tp->pt = 0;\n\t\t\tp->drop = 0;\n\t\t\tp->offset = offset;\n\t\t\tp->skip = 0;\n\t\t\tp->audiolevel = -1;\n\t\t\tp->rotation = -1;\n\t\t\tp->next = NULL;\n\t\t\tp->prev = NULL;\n\t\t\tif(list == NULL) {\n\t\t\t\tlist = p;\n\t\t\t} else {\n\t\t\t\tlast->next = p;\n\t\t\t}\n\t\t\tlast = p;\n\t\t\t/* Done */\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Only read RTP header */\n\t\trtp_header_len = 12;\n\t\tbytes = fread(prebuffer, sizeof(char), rtp_header_len, file);\n\t\tif(bytes < rtp_header_len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Missing RTP packet header data (%d instead %\"SCNu16\")\\n\", bytes, rtp_header_len);\n\t\t\tbreak;\n\t\t}\n\t\tjanus_pp_rtp_header *rtp = (janus_pp_rtp_header *)prebuffer;\n\t\tJANUS_LOG(LOG_VERB, \"  -- RTP packet (ssrc=%\"SCNu32\", pt=%\"SCNu16\", ext=%\"SCNu16\", seq=%\"SCNu16\", ts=%\"SCNu32\")\\n\",\n\t\t\t\tntohl(rtp->ssrc), rtp->type, rtp->extension, ntohs(rtp->seq_number), ntohl(rtp->timestamp));\n\t\t/* Check if we can get rid of the packet if we're expecting\n\t\t * static or specific payload types and they don't match */\n\t\tif((g711 && rtp->type != 0 && rtp->type != 8) || (g722 && rtp->type != 9)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping packet with unexpected payload type: %d != %s\\n\",\n\t\t\t\trtp->type, g711 ? \"0/8\" : \"9\");\n\t\t\t/* Skip data */\n\t\t\toffset += len;\n\t\t\tcount++;\n\t\t\tcontinue;\n\t\t}\n\t\tif(options.match_pt != -1 && rtp->type != options.match_pt) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping packet with non-matching payload type: %d != %d\\n\",\n\t\t\t\trtp->type, options.match_pt);\n\t\t\t/* Skip data */\n\t\t\toffset += len;\n\t\t\tcount++;\n\t\t\tcontinue;\n\t\t}\n\t\tif(rtp->csrccount) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- -- Skipping CSRC list\\n\");\n\t\t\tskip += rtp->csrccount*4;\n\t\t}\n\t\tif(rtp->csrccount || rtp->extension) {\n\t\t\trtp_read_n = (rtp->csrccount + rtp->extension)*4;\n\t\t\tbytes = fread(prebuffer+rtp_header_len, sizeof(char), rtp_read_n, file);\n\t\t\tif(bytes < rtp_read_n) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing RTP packet header data (%d instead %d)\\n\",\n\t\t\t\t\trtp_header_len+bytes, rtp_header_len+rtp_read_n);\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\trtp_header_len += rtp_read_n;\n\t\t\t}\n\t\t}\n\t\taudiolevel = -1;\n\t\trotation = -1;\n\t\tif(rtp->extension) {\n\t\t\tjanus_pp_rtp_header_extension *ext = (janus_pp_rtp_header_extension *)(prebuffer+12+skip);\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- -- RTP extension (type=0x%\"PRIX16\", length=%\"SCNu16\")\\n\",\n\t\t\t\tntohs(ext->type), ntohs(ext->length));\n\t\t\trtp_read_n = ntohs(ext->length)*4;\n\t\t\tskip += 4 + rtp_read_n;\n\t\t\tbytes = fread(prebuffer+rtp_header_len, sizeof(char), rtp_read_n, file);\n\t\t\tif(bytes < rtp_read_n) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing RTP packet header data (%d instead %d)\\n\",\n\t\t\t\t\trtp_header_len+bytes, rtp_header_len+rtp_read_n);\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\trtp_header_len += rtp_read_n;\n\t\t\t}\n\t\t\tif(options.audio_level_extmap_id > 0)\n\t\t\t\tjanus_pp_rtp_header_extension_parse_audio_level(prebuffer, len, options.audio_level_extmap_id, &audiolevel);\n\t\t\tif(options.video_orient_extmap_id > 0) {\n\t\t\t\tjanus_pp_rtp_header_extension_parse_video_orientation(prebuffer, len, options.video_orient_extmap_id, &rotation);\n\t\t\t\tif(rotation != -1 && rotation != last_rotation) {\n\t\t\t\t\tif(!extjson_only)\n\t\t\t\t\t\tlast_rotation = rotation;\n\t\t\t\t\trotated++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(ssrc == 0) {\n\t\t\tssrc = ntohl(rtp->ssrc);\n\t\t\tif(ssrc > 0)\n\t\t\t\tJANUS_LOG(LOG_INFO, \"SSRC detected: %\"SCNu32\"\\n\", ssrc);\n\t\t}\n\t\tif(ssrc != ntohl(rtp->ssrc)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping packet with unexpected SSRC: %\"SCNu32\" != %\"SCNu32\"\\n\",\n\t\t\t\tntohl(rtp->ssrc), ssrc);\n\t\t\t/* Skip data */\n\t\t\toffset += len;\n\t\t\tcount++;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Generate frame packet and insert in the ordered list */\n\t\tjanus_pp_frame_packet *p = g_malloc0(sizeof(janus_pp_frame_packet));\n\t\tp->header = rtp;\n\t\tp->version = has_timestamps ? 2 : 1;\n\t\tp->p_ts = pkt_ts;\n\t\tp->seq = ntohs(rtp->seq_number);\n\t\tp->pt = rtp->type;\n\t\tp->len = len;\n\t\tp->drop = 0;\n\t\tuint32_t rtp_ts = ntohl(rtp->timestamp);\n\t\t/* Due to resets, we need to mess a bit with the original timestamps */\n\t\tif(!started) {\n\t\t\t/* Simple enough... */\n\t\t\tstarted = TRUE;\n\t\t\thighest_rtp_ts = rtp_ts;\n\t\t\thighest_seq = p->seq;\n\t\t\tp->ts = (times_resetted*max32)+rtp_ts;\n\t\t} else {\n\t\t\tif(!video && !data) {\n\t\t\t\t/* Check if we need to handle the SIP silence suppression mode,\n\t\t\t\t * see https://github.com/meetecho/janus-gateway/pull/2328 */\n\t\t\t\tif(ssup_on) {\n\t\t\t\t\t/* Leaving silence suppression mode (RTP started flowing again) */\n\t\t\t\t\tssup_on = FALSE;\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Leaving RTP silence suppression (seq=%\"SCNu16\", rtp_ts=%\"SCNu32\")\\n\", ntohs(rtp->seq_number), rtp_ts);\n\t\t\t\t} else if(rtp->markerbit == 1) {\n\t\t\t\t\t/* Try to detect RTP silence suppression */\n\t\t\t\t\tint32_t seq_distance = abs((int16_t)(p->seq - highest_seq));\n\t\t\t\t\tif(seq_distance < options.silence_distance) {\n\t\t\t\t\t\t/* Consider 20 ms audio packets */\n\t\t\t\t\t\tint32_t inter_rtp_ts = opus ? 960 : 160;\n\t\t\t\t\t\tint32_t expected_rtp_distance = inter_rtp_ts * seq_distance;\n\t\t\t\t\t\tint32_t rtp_distance = abs((int32_t)(rtp_ts - highest_rtp_ts));\n\t\t\t\t\t\tif(rtp_distance > 10 * expected_rtp_distance) {\n\t\t\t\t\t\t\t/* Entering silence suppression mode (RTP will stop) */\n\t\t\t\t\t\t\tssup_on = TRUE;\n\t\t\t\t\t\t\t/* This is a close packet with not coherent RTP ts -> silence suppression */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Dropping audio RTP silence suppression (seq_distance=%d, rtp_distance=%d)\\n\", seq_distance, rtp_distance);\n\t\t\t\t\t\t\t/* Skip data */\n\t\t\t\t\t\t\toffset += len;\n\t\t\t\t\t\t\tcount++;\n\t\t\t\t\t\t\tg_free(p);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* Is the new timestamp smaller than the next one, and if so, is it a timestamp reset or simply out of order? */\n\t\t\tgboolean pre_reset_pkt = FALSE;\n\n\t\t\t/* In-order packet */\n\t\t\tif((int32_t)(rtp_ts-highest_rtp_ts) > 0) {\n\t\t\t\tif(rtp_ts < highest_rtp_ts) {\n\t\t\t\t\t/* Received TS is lower than highest --> reset */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Timestamp reset: %\"SCNu32\"\\n\", rtp_ts);\n\t\t\t\t\ttimes_resetted++;\n\t\t\t\t}\n\t\t\t\thighest_rtp_ts = rtp_ts;\n\t\t\t\thighest_seq = p->seq;\n\t\t\t}\n\n\t\t\t/* Out-of-order packet */\n\t\t\tif((int32_t)(rtp_ts-highest_rtp_ts) < 0) {\n\t\t\t\tif(rtp_ts > highest_rtp_ts) {\n\t\t\t\t\t/* Received TS is higher than highest --> late pre-reset packet */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Late pre-reset packet: %\"SCNu32\"\\n\", rtp_ts);\n\t\t\t\t\tpre_reset_pkt = TRUE;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* Take into account the number of resets when setting the internal, 64-bit, timestamp */\n\t\t\tif(!pre_reset_pkt)\n\t\t\t\tp->ts = (times_resetted*max32)+rtp_ts;\n\t\t\telse\n\t\t\t\tp->ts = ((times_resetted-1)*max32)+rtp_ts;\n\t\t}\n\t\tif(rtp->padding) {\n\t\t\t/* There's padding data, let's check the last byte to see how much data we should skip */\n\t\t\tfseek(file, offset + len - 1, SEEK_SET);\n\t\t\tbytes = fread(prebuffer2, sizeof(char), 1, file);\n\t\t\tuint8_t padlen = (uint8_t)prebuffer2[0];\n\t\t\tJANUS_LOG(LOG_VERB, \"Padding at sequence number %hu: %d/%d\\n\",\n\t\t\t\tntohs(rtp->seq_number), padlen, p->len);\n\t\t\tp->len -= padlen;\n\t\t\tif((p->len - skip - 12) <= 0) {\n\t\t\t\t/* Only padding, take note that we should drop the packet later */\n\t\t\t\tp->drop = 1;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"  -- All padding, marking packet as dropped\\n\");\n\t\t\t}\n\t\t}\n\t\tif(p->len <= 12) {\n\t\t\t/* Only header? take note that we should drop the packet later */\n\t\t\tp->drop = 1;\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Only RTP header, marking packet as dropped\\n\");\n\t\t}\n\t\t/* Fill in the rest of the details */\n\t\tp->offset = offset;\n\t\tp->skip = skip;\n\t\tp->audiolevel = audiolevel;\n\t\tp->rotation = rotation;\n\t\tp->next = NULL;\n\t\tp->prev = NULL;\n\t\tif(list == NULL) {\n\t\t\t/* First element becomes the list itself (and the last item), at least for now */\n\t\t\tlist = p;\n\t\t\tlast = p;\n\t\t} else if(!p->drop) {\n\t\t\t/* Check where we should insert this, starting from the end */\n\t\t\tint added = 0;\n\t\t\tjanus_pp_frame_packet *tmp = last;\n\t\t\twhile(tmp) {\n\t\t\t\tif(tmp->ts < p->ts) {\n\t\t\t\t\t/* The new timestamp is greater than the last one we have, append */\n\t\t\t\t\tadded = 1;\n\t\t\t\t\tif(tmp->next != NULL) {\n\t\t\t\t\t\t/* We're inserting */\n\t\t\t\t\t\ttmp->next->prev = p;\n\t\t\t\t\t\tp->next = tmp->next;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Update the last packet */\n\t\t\t\t\t\tlast = p;\n\t\t\t\t\t}\n\t\t\t\t\ttmp->next = p;\n\t\t\t\t\tp->prev = tmp;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if(tmp->ts == p->ts) {\n\t\t\t\t\t/* Same timestamp, check the sequence number */\n\t\t\t\t\tif(tmp->seq < p->seq && (abs(tmp->seq - p->seq) < 10000)) {\n\t\t\t\t\t\t/* The new sequence number is greater than the last one we have, append */\n\t\t\t\t\t\tadded = 1;\n\t\t\t\t\t\tif(tmp->next != NULL) {\n\t\t\t\t\t\t\t/* We're inserting */\n\t\t\t\t\t\t\ttmp->next->prev = p;\n\t\t\t\t\t\t\tp->next = tmp->next;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Update the last packet */\n\t\t\t\t\t\t\tlast = p;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttmp->next = p;\n\t\t\t\t\t\tp->prev = tmp;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(tmp->seq > p->seq && (abs(tmp->seq - p->seq) > 10000)) {\n\t\t\t\t\t\t/* The new sequence number (reset) is greater than the last one we have, append */\n\t\t\t\t\t\tadded = 1;\n\t\t\t\t\t\tif(tmp->next != NULL) {\n\t\t\t\t\t\t\t/* We're inserting */\n\t\t\t\t\t\t\ttmp->next->prev = p;\n\t\t\t\t\t\t\tp->next = tmp->next;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Update the last packet */\n\t\t\t\t\t\t\tlast = p;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttmp->next = p;\n\t\t\t\t\t\tp->prev = tmp;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(tmp->seq == p->seq) {\n\t\t\t\t\t\t/* Maybe a retransmission? Skip */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Skipping duplicate packet (seq=%\"SCNu16\")\\n\", p->seq);\n\t\t\t\t\t\tp->drop = 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* If either the timestamp or the sequence number we just got is smaller, keep going back */\n\t\t\t\ttmp = tmp->prev;\n\t\t\t}\n\t\t\tif(p->drop) {\n\t\t\t\t/* We don't need this */\n\t\t\t\tg_free(p);\n\t\t\t\tp = NULL;\n\t\t\t} else if(!added) {\n\t\t\t\t/* We reached the start */\n\t\t\t\tp->next = list;\n\t\t\t\tlist->prev = p;\n\t\t\t\tlist = p;\n\t\t\t}\n\t\t}\n\t\t/* Add to the extended header, if that's what we're doing */\n\t\tif(extjson_only && p && p->rotation != -1 && p->rotation != last_rotation) {\n\t\t\tlast_rotation = p->rotation;\n\t\t\tif(rotations == NULL)\n\t\t\t\trotations = json_array();\n\t\t\tdouble ts = (double)(p->ts - list->ts)/(double)90000;\n\t\t\tjson_t *r = json_object();\n\t\t\tjson_object_set_new(r, \"ts\", json_real(ts));\n\t\t\tjson_object_set_new(r, \"rotation\", json_integer(p->rotation));\n\t\t\tjson_array_append_new(rotations, r);\n\t\t}\n\t\t/* Skip data for now */\n\t\toffset += len;\n\t\tcount++;\n\t}\n\tif(!working) {\n\t\tif(info)\n\t\t\tjson_decref(info);\n\t\tg_free(metadata);\n\t\tg_free(extension);\n\t\tjanus_pprec_options_destroy();\n\t\texit(0);\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"Counted %\"SCNu32\" RTP packets\\n\", count);\n\tjanus_pp_frame_packet *tmp = list;\n\tcount = 0;\n\tint rate = video ? 90000 : 48000;\n\tif(g711 || g722)\n\t\trate = 8000;\n\telse if(l16 && !l16_48k)\n\t\trate = 16000;\n\tdouble ts = 0.0, pts = 0.0, orig_ts = 0.0;\n\tuint64_t orig_rtp_ts = 0, last_rtp_ts = 0, new_rtp_ts = list ? list->ts : 0;\n\twhile(tmp) {\n\t\tcount++;\n\t\tif(!data) {\n\t\t\tts = (double)(tmp->ts - list->ts)/(double)rate;\n\t\t\tpts = (double)tmp->p_ts/1000;\n\t\t\tif(!options.ignore_rtp_ts) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%10lu][%4d] seq=%\"SCNu16\", ts=%\"SCNu64\", time=%.2fs pts=%.2fs\\n\",\n\t\t\t\t\ttmp->offset, tmp->len, tmp->seq, tmp->ts, ts, pts);\n\t\t\t} else {\n\t\t\t\t/* We need to rewrite the timestamps so that they match\n\t\t\t\t * the actual interarrival of packets, all taking into\n\t\t\t\t * account the fact that, for video, we'll need some\n\t\t\t\t * packets to have all the same RTP pseudo-timestamps */\n\t\t\t\torig_rtp_ts = tmp->ts;\n\t\t\t\torig_ts = ts;\n\t\t\t\tif(orig_rtp_ts != last_rtp_ts) {\n\t\t\t\t\t/* Calculate a new RTP timestamp */\n\t\t\t\t\tnew_rtp_ts = list->ts + ((uint64_t)tmp->p_ts * (rate/1000));\n\t\t\t\t}\n\t\t\t\ttmp->ts = new_rtp_ts;\n\t\t\t\tts = (double)(tmp->ts - list->ts)/(double)rate;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%10lu][%4d] seq=%\"SCNu16\", ts=%\"SCNu64\" (orig=%\"SCNu64\"), time=%.2fs (orig=%.2fs) pts=%.2fs\\n\",\n\t\t\t\t\ttmp->offset, tmp->len, tmp->seq, tmp->ts, orig_rtp_ts, ts, orig_ts, pts);\n\t\t\t\tif(orig_rtp_ts != last_rtp_ts)\n\t\t\t\t\tlast_rtp_ts = orig_rtp_ts;\n\t\t\t}\n\t\t} else {\n\t\t\tts = (double)tmp->ts/G_USEC_PER_SEC;\n\t\t\tJANUS_LOG(LOG_VERB, \"[%10lu][%4d] time=%.2fs\\n\", tmp->offset, tmp->len, ts);\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tJANUS_LOG(LOG_INFO, \"Counted %\"SCNu32\" frame packets\\n\", count);\n\tif(!data && !video) {\n\t\tdouble diff = ts - pts;\n\t\tif(diff < -0.5 || diff > 0.5) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Detected audio clock mismatch, consider using skew compensation or restamping (rtp_time=%.2fs, real_time=%.2fs, diff=%.2fs)\\n\", ts, pts, diff);\n\t\t}\n\t}\n\tif(rotated != -1) {\n\t\tif(rotated == 0 && last_rotation != 0) {\n\t\t\tJANUS_LOG(LOG_INFO, \"The video is rotated\\n\");\n\t\t} else if(rotated > 0) {\n\t\t\tJANUS_LOG(LOG_INFO, \"The video changed orientation %d times\\n\", rotated);\n\t\t}\n\t}\n\n\tif(extjson_only) {\n\t\tjson_object_set_new(report, \"packets\", json_integer(count));\n\t\tjson_object_set_new(report, \"duration\", json_real(ts));\n\t\tif(rotations)\n\t\t\tjson_object_set_new(report, \"rotations\", rotations);\n\t}\n\n\tif(video) {\n\t\t/* Look for maximum width and height, if possible, and for the average framerate */\n\t\tjson_t *codec_info = NULL;\n\t\tif(extjson_only) {\n\t\t\tcodec_info = json_object();\n\t\t\tjson_object_set_new(report, \"codec\", codec_info);\n\t\t}\n\t\tif(vp8 || vp9) {\n\t\t\tif(janus_pp_webm_preprocess(file, list, vp8, codec_info) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error pre-processing %s RTP frames...\\n\", vp8 ? \"VP8\" : \"VP9\");\n\t\t\t\tif(info)\n\t\t\t\t\tjson_decref(info);\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(h264) {\n\t\t\tif(janus_pp_h264_preprocess(file, list, codec_info) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error pre-processing H.264 RTP frames...\\n\");\n\t\t\t\tif(info)\n\t\t\t\t\tjson_decref(info);\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(av1) {\n\t\t\tif(janus_pp_av1_preprocess(file, list, codec_info) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error pre-processing AV1 RTP frames...\\n\");\n\t\t\t\tif(info)\n\t\t\t\t\tjson_decref(info);\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(h265) {\n\t\t\tif(janus_pp_h265_preprocess(file, list, codec_info) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error pre-processing H.265 RTP frames...\\n\");\n\t\t\t\tif(info)\n\t\t\t\t\tjson_decref(info);\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t}\n\t}\n\n\tif(extjson_only) {\n\t\t/* Print the extended header and leave */\n\t\tchar *info_text = json_dumps(info, JSON_COMPACT | JSON_PRESERVE_ORDER);\n\t\tJANUS_PRINT(\"%s\\n\", info_text ? info_text : \"\");\n\t\tfree(info_text);\n\t\tjson_decref(info);\n\t\tg_free(metadata);\n\t\tg_free(extension);\n\t\tjanus_pprec_options_destroy();\n\t\texit(0);\n\t}\n\tif(parse_only) {\n\t\t/* We only needed to parse and re-order the packets, we're done here */\n\t\tJANUS_LOG(LOG_INFO, \"Parsing and reordering completed, bye!\\n\");\n\t\tg_free(metadata);\n\t\tg_free(extension);\n\t\tjanus_pprec_options_destroy();\n\t\texit(0);\n\t}\n\n\t/* Now we have to start working: stop here if it's an end-to-end encrypted\n\t * recording, though, as the processed file would NOT be playable... In\n\t * the future we may want to provide ways for users to pass custom decrypt\n\t * functions to the processor (e.g., for users with legitimate access to\n\t * the key to decrypt the content), but at the moment we just drop it */\n\tif(e2ee) {\n\t\tJANUS_LOG(LOG_ERR, \"End-to-end encrypted media recording, can't process...\\n\");\n\t\tg_free(metadata);\n\t\tg_free(extension);\n\t\tjanus_pprec_options_destroy();\n\t\texit(1);\n\t}\n\n\t/* Run restamping */\n\tgboolean restamping = FALSE;\n\tif(options.restamp_multiplier > 0) {\n\t\trestamping = TRUE;\n\t}\n\tif(!video && !data && restamping) {\n\t\ttmp = list;\n\t\tuint64_t restamping_offset = 0;\n\t\tdouble restamp_threshold = (double) options.restamp_min_th/1000;\n\t\tdouble restamp_jump_multiplier = (double) options.restamp_multiplier/1000;\n\n\t\twhile(tmp) {\n\t\t\tuint64_t original_ts = tmp->ts;\n\n\t\t\t/* Update ts with current offset */\n\t\t\tif(restamping_offset > 0) {\n\t\t\t\ttmp->ts += restamping_offset;\n\t\t\t}\n\n\t\t\t/* Calculate diff between time of reception and the rtp timestamp */\n\t\t\tdouble current_latency = get_latency(tmp, rate);\n\n\t\t\tif(current_latency > restamp_threshold) {\n\t\t\t\t/* Check for possible jump compared to previous latency values */\n\t\t\t\tdouble moving_avg_latency = get_moving_average_of_latency(tmp->prev, rate, options.restamp_packets);\n\t\t\t\tJANUS_LOG(LOG_VERB, \"latency=%.2f mavg=%.2f\\n\", current_latency, moving_avg_latency);\n\n\t\t\t\t/* Found a new jump in latency? */\n\t\t\t\tif(current_latency > (moving_avg_latency*restamp_jump_multiplier)) {\n\t\t\t\t\t/* Increase restamping offset with current offset */\n\t\t\t\t\trestamping_offset += (current_latency-moving_avg_latency)*rate;\n\n\t\t\t\t\t/* Calculate new_ts with new offset */\n\t\t\t\t\tuint64_t new_ts = original_ts+restamping_offset;\n\n\t\t\t\t\t/* Update current packet ts with new ts */\n\t\t\t\t\ttmp->ts = new_ts;\n\t\t\t\t\ttmp->restamped = 1;\n\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Timestamp gap detected. Restamping packets from here. Seq: %d\\n\", tmp->seq);\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"latency=%.2f mavg=%.2f original_ts=%.ld new_ts=%.ld offset=%.ld\\n\", current_latency, moving_avg_latency, original_ts, tmp->ts, restamping_offset);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttmp = tmp->next;\n\t\t}\n\t}\n\n\t/* Run audioskew */\n\tif(!video && !data && options.audioskew_th > 0) {\n\t\ttmp = list;\n\t\tjanus_pp_rtp_skew_context context = {};\n\t\tcontext.ssrc = ssrc;\n\t\tcontext.rate = rate;\n\t\tcontext.reference_time = tmp->p_ts;\n\t\tcontext.start_time = tmp->p_ts;\n\t\tcontext.start_ts = tmp->ts;\n\t\tjanus_pp_frame_packet *to_drop;\n\t\twhile(tmp) {\n\t\t\tto_drop = NULL;\n\t\t\tint ret = janus_pp_skew_compensate_audio(tmp, &context);\n\t\t\tif(ret < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"audio skew SSRC=%\"SCNu32\" dropping %d packets, source clock is too fast\\n\", ssrc, -ret);\n\t\t\t\tto_drop = tmp;\n\t\t\t\t/* Actually returns -1, so drop just one pkt */\n\t\t\t\tif (tmp->prev != NULL)\n\t\t\t\t\ttmp->prev->next = tmp->next;\n\t\t\t\tif (tmp->next != NULL)\n\t\t\t\t\ttmp->next->prev = tmp->prev;\n\t\t\t} else if(ret > 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"audio skew SSRC=%\"SCNu32\" jumping %d RTP sequence numbers, source clock is too slow\\n\", ssrc, ret);\n\t\t\t}\n\t\t\ttmp = tmp->next;\n\t\t\tg_free(to_drop);\n\t\t}\n\t}\n\n\tif(!video && !data) {\n\t\tif(opus) {\n\t\t\tif(janus_pp_opus_create(destination, metadata, multiopus, extension, opusred_pt) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .opus file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(g711) {\n\t\t\tif(janus_pp_g711_create(destination, metadata) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .wav file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(g722) {\n\t\t\tif(janus_pp_g722_create(destination, metadata) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .wav file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(l16) {\n\t\t\tif(janus_pp_l16_create(destination, l16_48k ? 48000 : 16000, metadata) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .wav file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t}\n\t} else if(data) {\n\t\tif(textdata) {\n\t\t\tif(janus_pp_srt_create(destination, metadata) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .srt file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else {\n\t\t\tif(janus_pp_binary_create(destination, metadata) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating binary file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif(vp8 || vp9) {\n\t\t\tif(janus_pp_webm_create(destination, metadata, vp8, extension) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .webm file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(h264) {\n\t\t\tif(janus_pp_h264_create(destination, metadata, options.faststart, extension) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .mp4 file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(av1) {\n\t\t\tif(janus_pp_av1_create(destination, metadata, options.faststart, extension) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .mp4 file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t} else if(h265) {\n\t\t\tif(janus_pp_h265_create(destination, metadata, options.faststart, extension) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating .mp4 file...\\n\");\n\t\t\t\tg_free(metadata);\n\t\t\t\tg_free(extension);\n\t\t\t\tjanus_pprec_options_destroy();\n\t\t\t\texit(1);\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Loop */\n\tif(!video && !data) {\n\t\tif(opus) {\n\t\t\tif(janus_pp_opus_process(file, list, restamping, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing Opus RTP frames...\\n\");\n\t\t\t}\n\t\t} else if(g711) {\n\t\t\tif(janus_pp_g711_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing G.711 RTP frames...\\n\");\n\t\t\t}\n\t\t} else if(g722) {\n\t\t\tif(janus_pp_g722_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing G.722 RTP frames...\\n\");\n\t\t\t}\n\t\t} else if(l16) {\n\t\t\tif(janus_pp_l16_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing L16 RTP frames...\\n\");\n\t\t\t}\n\t\t}\n\t} else if(data) {\n\t\tif(textdata) {\n\t\t\tif(janus_pp_srt_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing text data frames...\\n\");\n\t\t\t}\n\t\t} else {\n\t\t\tif(janus_pp_binary_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing text data frames...\\n\");\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif(vp8 || vp9) {\n\t\t\tif(janus_pp_webm_process(file, list, vp8, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing %s RTP frames...\\n\", vp8 ? \"VP8\" : \"VP9\");\n\t\t\t}\n\t\t} else if(h264) {\n\t\t\tif(janus_pp_h264_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing H.264 RTP frames...\\n\");\n\t\t\t}\n\t\t} else if(av1) {\n\t\t\tif(janus_pp_av1_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing AV1 RTP frames...\\n\");\n\t\t\t}\n\t\t} else if(h265) {\n\t\t\tif(janus_pp_h265_process(file, list, &working) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error processing H.265 RTP frames...\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Clean up */\n\tif(video) {\n\t\tif(vp8 || vp9) {\n\t\t\tjanus_pp_webm_close();\n\t\t} else if(h264) {\n\t\t\tjanus_pp_h264_close();\n\t\t} else if(av1) {\n\t\t\tjanus_pp_av1_close();\n\t\t} else if(h265) {\n\t\t\tjanus_pp_h265_close();\n\t\t}\n\t} else if(data) {\n\t\tif(textdata) {\n\t\t\tjanus_pp_srt_close();\n\t\t} else {\n\t\t\tjanus_pp_binary_close();\n\t\t}\n\t} else {\n\t\tif(opus) {\n\t\t\tjanus_pp_opus_close();\n\t\t} else if(g711) {\n\t\t\tjanus_pp_g711_close();\n\t\t} else if(g722) {\n\t\t\tjanus_pp_g722_close();\n\t\t} else if(l16) {\n\t\t\tjanus_pp_l16_close();\n\t\t}\n\t}\n\tfclose(file);\n\n\tfile = fopen(destination, \"rb\");\n\tif(file == NULL) {\n\t\tJANUS_LOG(LOG_INFO, \"No destination file %s??\\n\", destination);\n\t} else {\n\t\tfseek(file, 0L, SEEK_END);\n\t\tfsize = ftell(file);\n\t\tfseek(file, 0L, SEEK_SET);\n\t\tJANUS_LOG(LOG_INFO, \"%s is %zu bytes\\n\", destination, fsize);\n\t\tfclose(file);\n\t}\n\tjanus_pp_frame_packet *temp = list, *next = NULL;\n\twhile(temp) {\n\t\tnext = temp->next;\n\t\tg_free(temp);\n\t\ttemp = next;\n\t}\n\n\tg_free(metadata);\n\tg_free(extension);\n\tjanus_pprec_options_destroy();\n\n\tJANUS_LOG(LOG_INFO, \"Bye!\\n\");\n\treturn 0;\n}\n\n/* Static helper to quickly find the extension data */\nstatic int janus_pp_rtp_header_extension_find(char *buf, int len, int id,\n\t\tuint8_t *byte, uint32_t *word, char **ref) {\n\tif(!buf || len < 12)\n\t\treturn -1;\n\tjanus_pp_rtp_header *rtp = (janus_pp_rtp_header *)buf;\n\tint hlen = 12;\n\tif(rtp->csrccount)\t/* Skip CSRC if needed */\n\t\thlen += rtp->csrccount*4;\n\tif(rtp->extension) {\n\t\tjanus_pp_rtp_header_extension *ext = (janus_pp_rtp_header_extension *)(buf+hlen);\n\t\tint extlen = ntohs(ext->length)*4;\n\t\thlen += 4;\n\t\tif(len > (hlen + extlen)) {\n\t\t\t/* 1-Byte extension */\n\t\t\tif(ntohs(ext->type) == 0xBEDE) {\n\t\t\t\tconst uint8_t padding = 0x00, reserved = 0xF;\n\t\t\t\tuint8_t extid = 0, idlen;\n\t\t\t\tint i = 0;\n\t\t\t\twhile(i < extlen) {\n\t\t\t\t\textid = (uint8_t)buf[hlen+i] >> 4;\n\t\t\t\t\tif(extid == reserved) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(extid == padding) {\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tidlen = ((uint8_t)buf[hlen+i] & 0xF)+1;\n\t\t\t\t\tif(extid == id) {\n\t\t\t\t\t\t/* Found! */\n\t\t\t\t\t\tif(byte)\n\t\t\t\t\t\t\t*byte = (uint8_t)buf[hlen+i+1];\n\t\t\t\t\t\tif(word)\n\t\t\t\t\t\t\t*word = ntohl(*(uint32_t *)(buf+hlen+i));\n\t\t\t\t\t\tif(ref)\n\t\t\t\t\t\t\t*ref = &buf[hlen+i];\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\ti += 1 + idlen;\n\t\t\t\t}\n\t\t\t}\n\t\t\thlen += extlen;\n\t\t}\n\t}\n\treturn -1;\n}\n\nstatic int janus_pp_rtp_header_extension_parse_audio_level(char *buf, int len, int id, int *level) {\n\tuint8_t byte = 0;\n\tif(janus_pp_rtp_header_extension_find(buf, len, id, &byte, NULL, NULL) < 0)\n\t\treturn -1;\n\t/* a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level */\n\tint value = byte & 0x7F;\n\tif(level)\n\t\t*level = value;\n\treturn 0;\n}\n\nstatic int janus_pp_rtp_header_extension_parse_video_orientation(char *buf, int len, int id, int *rotation) {\n\tuint8_t byte = 0;\n\tif(janus_pp_rtp_header_extension_find(buf, len, id, &byte, NULL, NULL) < 0)\n\t\treturn -1;\n\t/* a=extmap:4 urn:3gpp:video-orientation */\n\tgboolean r1bit = (byte & 0x02) >> 1;\n\tgboolean r0bit = byte & 0x01;\n\tif(rotation) {\n\t\tif(!r0bit && !r1bit)\n\t\t\t*rotation = 0;\n\t\telse if(r0bit && !r1bit)\n\t\t\t*rotation = 90;\n\t\telse if(!r0bit && r1bit)\n\t\t\t*rotation = 180;\n\t\telse if(r0bit && r1bit)\n\t\t\t*rotation = 270;\n\t}\n\treturn 0;\n}\n\nstatic gint janus_pp_skew_compensate_audio(janus_pp_frame_packet *pkt, janus_pp_rtp_skew_context *context) {\n\t/* N \t: a N sequence number jump has been performed on the packet */\n\t/* 0  \t: no skew compensation needs to be done */\n\t/* -N  \t: a N packets drop must be performed by the caller */\n\tgint exit_status = 0;\n\n\tcontext->prev_seq = context->last_seq;\n\tcontext->last_seq = pkt->seq;\n\tcontext->prev_ts = context->last_ts;\n\tcontext->last_ts = pkt->ts;\n\n\tguint64 pts = pkt->p_ts;\n\tguint64 akhz = context->rate / 1000;\n\n\t/* Do not execute skew analysis in the first seconds */\n\tif (pts - context->reference_time < SKEW_DETECTION_WAIT_TIME_SECS / 2 * 1000) {\n\t\treturn 0;\n\t} else if (!context->start_time) {\n\t\tcontext->start_time = pts;\n\t\tif (!context->start_time)\n\t\t\tcontext->start_time = 1;\n\t\tcontext->evaluating_start_time = context->start_time;\n\t\tcontext->start_ts = context->last_ts;\n\t\tJANUS_LOG(LOG_INFO, \"audio skew SSRC=%\"SCNu32\" evaluation phase start, start_time=%\"SCNu64\" start_ts=%\"SCNu64\"\\n\", context->ssrc, context->start_time, context->start_ts);\n\t}\n\n\t/* Skew analysis */\n\t/* Are we waiting for a target timestamp? (a negative skew has been evaluated in a previous iteration) */\n\tif (context->target_ts > 0 && (gint64)(context->target_ts - context->last_ts) > 0) {\n\t\tcontext->seq_offset--;\n\t\texit_status = -1;\n\t} else {\n\t\tcontext->target_ts = 0;\n\t\t/* Do not execute analysis for out of order packets or multi-packets frame or if pts < start_time */\n\t\tif (context->last_seq == context->prev_seq + 1 && context->last_ts != context->prev_ts && pts >= context->start_time) {\n\t\t\t/* Evaluate the local RTP timestamp according to the local clock */\n\t\t\tguint64 expected_ts = ((pts - context->start_time) * akhz) + context->start_ts;\n\t\t\t/* Evaluate current delay */\n\t\t\tgint64 delay_now = context->last_ts - expected_ts;\n\t\t\t/* Exponentially weighted moving average estimation */\n\t\t\tgint64 delay_estimate = (63 * context->prev_delay + delay_now) / 64;\n\t\t\t/* Save previous delay for the next iteration*/\n\t\t\tcontext->prev_delay = delay_estimate;\n\t\t\t/* Evaluate the distance between active delay and current delay estimate */\n\t\t\tgint64 offset = context->active_delay - delay_estimate;\n\t\t\tJANUS_LOG(LOG_HUGE, \"audio skew SSRC=%\"SCNu32\" status RECVD_TS=%\"SCNu64\" EXPTD_TS=%\"SCNu64\" AVG_OFFSET=%\"SCNi64\" TS_OFFSET=%\"SCNi64\" SEQ_OFFSET=%\"SCNi16\"\\n\",\n\t\t\t\tcontext->ssrc, context->last_ts, expected_ts, offset, context->ts_offset, context->seq_offset);\n\t\t\tgint64 skew_th = options.audioskew_th * akhz;\n\n\t\t\t/* Evaluation phase */\n\t\t\tif (context->evaluating_start_time) {\n\t\t\t\t/* Check if the offset has surpassed half the threshold during the evaluating phase */\n\t\t\t\tif (pts - context->evaluating_start_time <= SKEW_DETECTION_WAIT_TIME_SECS / 2 * 1000) {\n\t\t\t\t\tif (llabs(offset) <= skew_th/2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"audio skew SSRC=%\"SCNu32\" evaluation phase continue\\n\", context->ssrc);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"audio skew SSRC=%\"SCNu32\" evaluation phase reset\\n\", context->ssrc);\n\t\t\t\t\t\tcontext->start_time = pts;\n\t\t\t\t\t\tif (!context->start_time)\n\t\t\t\t\t\t\tcontext->start_time = 1;\n\t\t\t\t\t\tcontext->evaluating_start_time = context->start_time;\n\t\t\t\t\t\tcontext->start_ts = context->last_ts;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"audio skew SSRC=%\"SCNu32\" evaluation phase stop, start_time=%\"SCNu64\" start_ts=%\"SCNu64\"\\n\", context->ssrc, context->start_time, context->start_ts);\n\t\t\t\t\tcontext->evaluating_start_time = 0;\n\t\t\t\t}\n\t\t\t\treturn 0;\n\t\t\t}\n\n\t\t\t/* Check if the offset has surpassed the threshold */\n\t\t\tif (offset >= skew_th) {\n\t\t\t\t/* The source is slowing down */\n\t\t\t\t/* Update active delay */\n\t\t\t\tcontext->active_delay = delay_estimate;\n\t\t\t\t/* Adjust ts offset */\n\t\t\t\tcontext->ts_offset += skew_th;\n\t\t\t\t/* Calculate last ts increase */\n\t\t\t\tguint64 ts_incr = context->last_ts - context->prev_ts;\n\t\t\t\t/* Evaluate sequence number jump */\n\t\t\t\tguint16 jump = (skew_th + ts_incr - 1) / ts_incr;\n\t\t\t\t/* Adjust seq num offset */\n\t\t\t\tcontext->seq_offset += jump;\n\t\t\t\texit_status = jump;\n\t\t\t} else if (offset <= -skew_th) {\n\t\t\t\t/* The source is speeding up*/\n\t\t\t\t/* Update active delay */\n\t\t\t\tcontext->active_delay = delay_estimate;\n\t\t\t\t/* Adjust ts offset */\n\t\t\t\tcontext->ts_offset -= skew_th;\n\t\t\t\t/* Set target ts */\n\t\t\t\tcontext->target_ts = context->last_ts + skew_th;\n\t\t\t\tif (context->target_ts == 0)\n\t\t\t\t\tcontext->target_ts = 1;\n\t\t\t\t/* Adjust seq num offset */\n\t\t\t\tcontext->seq_offset--;\n\t\t\t\texit_status = -1;\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Skew compensation */\n\t/* Fix header timestamp considering the active offset */\n\tguint64 fixed_rtp_ts = context->last_ts + context->ts_offset;\n\tpkt->ts = fixed_rtp_ts;\n\t/* Fix header sequence number considering the total offset */\n\tguint16 fixed_rtp_seq = context->last_seq + context->seq_offset;\n\tpkt->seq = fixed_rtp_seq;\n\n\treturn exit_status;\n}\n\nstatic double get_latency(const janus_pp_frame_packet *tmp, int rate) {\n\t/* Get latency of packet based on time of arrival (at server side) and the RTP timestamp */\n\tdouble corrected_ts = tmp->ts-list->ts;\n\tdouble rts = corrected_ts/(double) rate;\n\tdouble pts = (double) tmp->p_ts/1000;\n\treturn pts-rts;\n}\n\nstatic double get_moving_average_of_latency(janus_pp_frame_packet *pkt, int rate, int num_of_packets) {\n\t/* Get a moving average of packet latency using the get_latency function */\n\tif(!pkt || num_of_packets == 0) {\n\t\treturn 0;\n\t}\n\n\tint packets = 0;\n\tdouble sum = 0;\n\n\tjanus_pp_frame_packet *tmp = pkt;\n\twhile(tmp && packets < num_of_packets) {\n\t\tsum += get_latency(tmp, rate);\n\t\ttmp = tmp->prev;\n\t\tpackets++;\n\t}\n\n\treturn sum/packets;\n}\n"
  },
  {
    "path": "src/postprocessing/mjr2pcap.1",
    "content": ".TH MJR2PCAP 1\n.SH NAME\nmjr2pcap \\- Helper tool to convert a Janus recordings to pcap format.\n.SH SYNOPSIS\n.B mjr2pcap\n.IR source.mjr\n.IR destination.pcap\n.SH DESCRIPTION\n.B mjr2pcap\nis a simple utility that allows you to extract RTP packets from Janus recordings and save them to a .pcap file instead. Notice that network levels are simulated, and so are timestamps for when the packets have really been received, since that info is not stored in .mjr files. As such, its main purpose is helping analyze RTP packets, rather than investigate network issues.\n.TP\nThe tool requires two parameters: the path to the .mjr file to read, and a path to the target .pcap file. Notice that an attempt to process a non-RTP recording will result in an error.\n.SH EXAMPLES\n\\fBmjr2pcap rec1234.mjr rec1234.pcap\\fR \\- Convert the recording to pcap\n.SH BUGS\n.TP\nIf you think you found a bug or want to contribute a feature, you can issue or a pull request on https://github.com/meetecho/janus-gateway/issues.\n.TP\nAnyway, before doing that make sure you read the documentation at https://janus.conf.meetecho.com/docs/ and that it has not been discussed already at https://janus.discourse.group/. We only use Github for code issues, and \\fBNOT\\fR for configuration or usage issues: use the group for that.\n.SH SEE ALSO\n.TP\nhttps://github.com/meetecho/janus-gateway \\- Official repository\n.TP\nhttps://janus.conf.meetecho.com \\- Demos and documentation\n.TP\nhttps://janus.discourse.group/ \\- Community\n.TP\nhttps://www.meetecho.com/blog/ \\- Tutorials and blog posts on Janus\n.SH AUTHORS\nLorenzo Miniero (lorenzo@meetecho.com)\n"
  },
  {
    "path": "src/postprocessing/mjr2pcap.c",
    "content": "/*! \\file    mjr2pcap.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Helper tool to convert Janus .mjr recordings to .pcap files\n * \\details  Our Janus WebRTC gateway provides a simple helper (janus_recorder)\n * to allow plugins to record audio, video and text frames sent by users.\n * The \\c mjr2pcap tool is a simple utility that allows you remove extract\n * RTP packets from Janus recordings and save them to a .pcap file instead.\n * Notice that network levels are simulated, and so are timestamps for when\n * the packets have really been received, since that info is not stored in\n * .mjr files. As such, its main purpose is helping analyze RTP packets,\n * rather than investigate network issues.\n *\n * Using the utility is quite simple. Just pass, as arguments to the tool,\n * the path to the .mjr source file, and the path to the destination file, e.g.:\n *\n\\verbatim\n./mjr2pcap /path/to/source.mjr /path/to/destination.pcap\n\\endverbatim\n *\n * An attempt to process a non-RTP recording will result in an error.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n#include <signal.h>\n#include <sys/time.h>\n\n#include <glib.h>\n#include <jansson.h>\n\n#include \"../debug.h\"\n#include \"../version.h\"\n#include \"pp-rtp.h\"\n\n\n#define htonll(x) ((1==htonl(1)) ? (x) : ((gint64)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32))\n#define ntohll(x) ((1==ntohl(1)) ? (x) : ((gint64)ntohl((x) & 0xFFFFFFFF) << 32) | ntohl((x) >> 32))\n\nint janus_log_level = 4;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = TRUE;\nchar *janus_log_global_prefix = NULL;\nint lock_debug = 0;\n\nint working = 0;\n\n\n/* Helper struct to define a libpcap global header\n * https://wiki.wireshark.org/Development/LibpcapFileFormat */\ntypedef struct mjr2pcap_global_header {\n\tguint32 magic_number;\t/* Magic number */\n\tguint16 version_major;\t/* Major version number */\n\tguint16 version_minor;\t/* Minor version number */\n\tgint32  thiszone;\t\t/* GMT to local correction */\n\tguint32 sigfigs;\t\t/* Accuracy of timestamps */\n\tguint32 snaplen;\t\t/* Max length of captured packets, in octets */\n\tguint32 network;\t\t/* Data link type */\n} mjr2pcap_global_header;\n\n/* Helper struct to define a libpcap packet header\n * https://wiki.wireshark.org/Development/LibpcapFileFormat */\ntypedef struct mjr2pcap_packet_header {\n\tguint32 ts_sec;\t\t\t/* Timestamp seconds */\n\tguint32 ts_usec;\t\t/* Timestamp microseconds */\n\tguint32 incl_len;\t\t/* Number of octets of packet saved in file */\n\tguint32 orig_len;\t\t/* Actual length of packet */\n} mjr2pcap_packet_header;\n\n/* Ethernet header */\ntypedef struct mjr2pcap_ethernet_header {\n\tuint8_t dst[6];\n\tuint8_t src[6];\n\tuint16_t type;\n} mjr2pcap_ethernet_header;\nstatic void mjr2pcap_ethernet_header_init(mjr2pcap_ethernet_header *eth) {\n\tmemset(eth, 0, sizeof(*eth));\n\teth->type = htons(0x0800);\n}\n\n/* IP header */\ntypedef struct mjr2pcap_ip_header {\n#if __BYTE_ORDER == __BIG_ENDIAN\n\tuint8_t version:4;\n\tuint8_t hlen:4;\n#elif __BYTE_ORDER == __LITTLE_ENDIAN\n\tuint8_t hlen:4;\n\tuint8_t version:4;\n#endif\n\tuint8_t tos;\n\tuint16_t tlen;\n\tuint16_t id;\n\tuint16_t flags;\n\tuint8_t ttl;\n\tuint8_t protocol;\n\tuint16_t csum;\n\tuint8_t src[4];\n\tuint8_t dst[4];\n} mjr2pcap_ip_header;\nstatic void mjr2pcap_ip_header_init(mjr2pcap_ip_header *ip, int psize) {\n\tip->version = 4;\n\tip->hlen = 5;\n\tip->tos = 0;\n\tip->tlen = htons(28+psize);\n\tip->id = htons(0);\n\tip->flags = htons(0x4000);\n\tip->ttl = 64;\n\tip->protocol = 17;\n\tip->csum = 0;\n\tip->src[0] = 10;\n\tip->src[1] = 1;\n\tip->src[2] = 1;\n\tip->src[3] = 1;\n\tip->dst[0] = 10;\n\tip->dst[1] = 2;\n\tip->dst[2] = 2;\n\tip->dst[3] = 2;\n}\n\n/* UDP header */\ntypedef struct mjr2pcap_udp_header {\n\tuint16_t srcport;\n\tuint16_t dstport;\n\tuint16_t len;\n\tuint16_t csum;\n} mjr2pcap_udp_header;\nstatic void mjr2pcap_udp_header_init(mjr2pcap_udp_header *udp, int psize) {\n\tudp->srcport = htons(1000);\n\tudp->dstport = htons(2000);\n\tudp->len = htons(8+psize);\n\tudp->csum = 0;\n}\n\n\n/* Signal handler */\nstatic void janus_pp_handle_signal(int signum) {\n\tworking = 0;\n}\n\n\n/* Main Code */\nint main(int argc, char *argv[]) {\n\tjanus_log_init(FALSE, TRUE, NULL, NULL);\n\tatexit(janus_log_destroy);\n\n\tJANUS_LOG(LOG_INFO, \"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\tJANUS_LOG(LOG_INFO, \"Janus commit: %s\\n\", janus_build_git_sha);\n\tJANUS_LOG(LOG_INFO, \"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\n\t/* Evaluate arguments */\n\tif(argc != 3) {\n\t\tJANUS_LOG(LOG_INFO, \"Usage: %s source.mjr destination.pcap\\n\", argv[0]);\n\t\texit(1);\n\t}\n\tchar *source = NULL, *destination = NULL;\n\tsource = argv[1];\n\tdestination = argv[2];\n\tJANUS_LOG(LOG_INFO, \"%s --> %s\\n\", source, destination);\n\n\t/* Open the source file */\n\tFILE *file = fopen(source, \"rb\");\n\tif(file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not open file %s\\n\", source);\n\t\texit(1);\n\t}\n\tfseek(file, 0L, SEEK_END);\n\tlong fsize = ftell(file);\n\tfseek(file, 0L, SEEK_SET);\n\tJANUS_LOG(LOG_INFO, \"File is %zu bytes\\n\", fsize);\n\n\t/* Handle SIGINT */\n\tworking = 1;\n\tsignal(SIGINT, janus_pp_handle_signal);\n\n\t/* Pre-parse */\n\tJANUS_LOG(LOG_INFO, \"Pre-parsing file...\\n\");\n\tgboolean has_timestamps = FALSE;\n\tgboolean parsed_header = FALSE;\n\tjson_t *mjr_header = NULL;\n\tint bytes = 0;\n\tlong offset = 0;\n\tuint16_t len = 0;\n\tgint64 started = 0;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\t/* Let's look for timestamp resets first */\n\twhile(working && offset < fsize) {\n\t\t/* Read frame header */\n\t\tfseek(file, offset, SEEK_SET);\n\t\tbytes = fread(prebuffer, sizeof(char), 8, file);\n\t\tif(bytes != 8 || prebuffer[0] != 'M') {\n\t\t\tJANUS_LOG(LOG_WARN, \"Invalid header at offset %ld (%s), the processing will stop here...\\n\",\n\t\t\t\toffset, bytes != 8 ? \"not enough bytes\" : \"wrong prefix\");\n\t\t\tbreak;\n\t\t}\n\t\tif(prebuffer[1] == 'E') {\n\t\t\t/* Either the old .mjr format header ('MEETECHO' header followed by 'audio' or 'video'), or a frame */\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len == 5 && !parsed_header) {\n\t\t\t\t/* Old .mjr format, check if this is an RTP recording */\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), 5, file);\n\t\t\t\tif(prebuffer[0] != 'a' && prebuffer[0] != 'v') {\n\t\t\t\t\tfclose(file);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Not an RTP recording (data currently unsupported)...\\n\");\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t} else if(len < 12) {\n\t\t\t\t/* Not RTP, skip */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Skipping packet (not RTP?)\\n\");\n\t\t\t\toffset += len;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} else if(prebuffer[1] == 'J') {\n\t\t\t/* New .mjr format, check if this is an RTP recording */\n\t\t\tif(prebuffer[2] == 'R' && prebuffer[3] == '0' && prebuffer[4] == '0' &&\n\t\t\t\t\tprebuffer[5] == '0' && prebuffer[6] == '0' && prebuffer[7] == '2') {\n\t\t\t\t/* Main header is MJR00002: this means we have timestamps too */\n\t\t\t\thas_timestamps = TRUE;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"New .mjr format, will parse timestamps too\\n\");\n\t\t\t}\n\t\t\toffset += 8;\n\t\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\t\tlen = ntohs(len);\n\t\t\toffset += 2;\n\t\t\tif(len > 0 && !parsed_header) {\n\t\t\t\t/* This is the info header */\n\t\t\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\t\t\tprebuffer[len] = '\\0';\n\t\t\t\tjson_error_t error;\n\t\t\t\tmjr_header = json_loads(prebuffer, 0, &error);\n\t\t\t\tif(!mjr_header) {\n\t\t\t\t\tfclose(file);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error parsing header, JSON error: on line %d: %s\\n\", error.line, error.text);\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\t/* Make sure the content is RTP */\n\t\t\t\tjson_t *type = json_object_get(mjr_header, \"t\");\n\t\t\t\tif(!type || !json_is_string(type)) {\n\t\t\t\t\tjson_decref(mjr_header);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing/invalid recording type in info header...\\n\");\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tconst char *t = json_string_value(type);\n\t\t\t\tif(!strcasecmp(t, \"d\")) {\n\t\t\t\t\t/* Data recordings are not supported yet */\n\t\t\t\t\tjson_decref(mjr_header);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Not an RTP recording (data currently unsupported)...\\n\");\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tjson_t *updated = json_object_get(mjr_header, \"u\");\n\t\t\t\tif(!updated || !json_is_integer(updated)) {\n\t\t\t\t\tjson_decref(mjr_header);\n\t\t\t\t\tfclose(file);\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Missing/invalid updated time in info header...\\n\");\n\t\t\t\t\texit(1);\n\t\t\t\t}\n\t\t\t\tstarted = json_integer_value(updated);\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid header...\\n\");\n\t\t\tjson_decref(mjr_header);\n\t\t\tfclose(file);\n\t\t\texit(1);\n\t\t}\n\t\t/* Skip data for now */\n\t\toffset += len;\n\t}\n\n\t/* Create the target file */\n\tFILE *outfile = fopen(destination, \"wb\");\n\tif(outfile == NULL) {\n\t\tjson_decref(mjr_header);\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open output file\\n\");\n\t\texit(1);\n\t}\n\t/* Start with the PCAP header */\n\tmjr2pcap_global_header pcap_header = {\n\t\t0xa1b2c3d4, 2, 4, 0, 0, 65535, 1\n\t};\n\tfwrite(&pcap_header, sizeof(char), sizeof(pcap_header), outfile);\n\t/* Now iterate on all packets, and save them to the .pcap file */\n\toffset = 0;\n\tJANUS_LOG(LOG_INFO, \"Traversing RTP packets...\\n\");\n\tuint32_t pkt_ts = 0;\n\twhile(working && offset < fsize) {\n\t\t/* Read frame header */\n\t\tfseek(file, offset, SEEK_SET);\n\t\tbytes = fread(prebuffer, sizeof(char), 8, file);\n\t\tif(bytes != 8 || prebuffer[0] != 'M') {\n\t\t\t/* Broken packet? Stop here */\n\t\t\tbreak;\n\t\t}\n\t\tif(has_timestamps) {\n\t\t\t/* Read the packet timestamp */\n\t\t\tmemcpy(&pkt_ts, prebuffer+4, sizeof(uint32_t));\n\t\t\tpkt_ts = ntohl(pkt_ts);\n\t\t}\n\t\toffset += 8;\n\t\tbytes = fread(&len, sizeof(uint16_t), 1, file);\n\t\tlen = ntohs(len);\n\t\tJANUS_LOG(LOG_VERB, \"  -- Length: %\"SCNu16\"\\n\", len);\n\t\toffset += 2;\n\t\tif(prebuffer[1] == 'J' || len < 12) {\n\t\t\t/* Not RTP, skip */\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Not RTP, skipping\\n\");\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\tif(len > 1500) {\n\t\t\t/* Way too large, very likely not RTP, skip */\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Too large packet (%d bytes), skipping\\n\", len);\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Get the whole packet */\n\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"  -- Failed to read packet (%d != %d bytes), skipping\\n\", bytes, len);\n\t\t\toffset += len;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Save the packet to PCAP */\n\t\tint hsize = sizeof(mjr2pcap_ethernet_header) + sizeof(mjr2pcap_ip_header) +\n\t\t\tsizeof(mjr2pcap_udp_header) + len;\n\t\t/* We need a fake Ethernet/IP/UDP encapsulation for this packet */\n\t\tmjr2pcap_ethernet_header eth;\n\t\tmjr2pcap_ethernet_header_init(&eth);\n\t\tmjr2pcap_ip_header ip;\n\t\tmjr2pcap_ip_header_init(&ip, len);\n\t\tmjr2pcap_udp_header udp;\n\t\tmjr2pcap_udp_header_init(&udp, len);\n\t\t/* Now prepare the packet header */\n\t\tstruct timeval tv;\n\t\tif(has_timestamps) {\n\t\t\t/* Prepare a valid timestamp */\n\t\t\tgint64 timestamp = started + ((gint64)pkt_ts*1000);\n\t\t\ttv.tv_sec = timestamp / G_USEC_PER_SEC;\n\t\t\ttv.tv_usec = timestamp -  (tv.tv_sec*G_USEC_PER_SEC);\n\t\t} else {\n\t\t\t/* Craft a dummy timestamp */\n\t\t\tgettimeofday(&tv, NULL);\n\t\t}\n\t\tmjr2pcap_packet_header header = {\n\t\t\ttv.tv_sec, tv.tv_usec, hsize, hsize\n\t\t};\n\t\tfwrite(&header, sizeof(char), sizeof(header), outfile);\n\t\tfwrite(&eth, sizeof(char), sizeof(eth), outfile);\n\t\tfwrite(&ip, sizeof(char), sizeof(ip), outfile);\n\t\tfwrite(&udp, sizeof(char), sizeof(udp), outfile);\n\t\t/* The write the packet itself (or part of it) */\n\t\tint temp = 0, tot = len;\n\t\twhile(tot > 0) {\n\t\t\ttemp = fwrite(prebuffer+len-tot, sizeof(char), len, outfile);\n\t\t\tif(temp <= 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error dumping packet...\\n\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttot -= temp;\n\t\t}\n\t\toffset += len;\n\t}\n\t/* We're done */\n\tjson_decref(mjr_header);\n\tfclose(file);\n\tfclose(outfile);\n\toutfile = fopen(destination, \"rb\");\n\tif(outfile == NULL) {\n\t\tJANUS_LOG(LOG_INFO, \"No destination file %s??\\n\", destination);\n\t} else {\n\t\tfseek(outfile, 0L, SEEK_END);\n\t\tfsize = ftell(outfile);\n\t\tfseek(outfile, 0L, SEEK_SET);\n\t\tJANUS_LOG(LOG_INFO, \"%s is %zu bytes\\n\", destination, fsize);\n\t\tfclose(outfile);\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"Bye!\\n\");\n\treturn 0;\n}\n"
  },
  {
    "path": "src/postprocessing/pcap2mjr.1",
    "content": ".TH PCAP2MJR 1\n.SH NAME\npcap2mjr \\- Helper tool to convert a pcap dump to a Janus recording.\n.SH SYNOPSIS\n.B pcap2mjr [options]\n.IR source.pcap\n.IR destination.mjr\n.SH DESCRIPTION\n.B pcap2mjr\nis a simple utility that allows you take .pcap network captures, extract a specific RTP session via its SSRC, and convert it to an .mjr Janus recording instead. Its main purpose is helping convert .pcap captures to media files, or make it easier to replay them via Janus.\n.TP\nThe tool requires a path to the .pcap file to read, and a path to the target .pcap file; besides, it needs info on the codec and the SSRC to filter. Notice that if the tool can't detect any RTP packet with that SSRC, it will result in an error: if you want the tool to autodetect an RTP stream, pass 0.\n.SH OPTIONS\n.TP\n.BR \\-h \", \" \\-\\-help\nPrint help and exit\n.TP\n.BR \\-V \", \" \\-\\-version\nPrint version and exit\n.TP\n.BR \\-c \", \" \\-\\-codec=\\fIcodec\\fR\nCodec the recording will contain (e.g., opus, vp8, etc.)\n.TP\n.BR \\-s \", \" \\-\\-ssrc=\\fISSRC (numeric)\\fR\nSSRC of the packets in the pcap file to save (pass 0 to autodetected)\n.TP\n.BR \\-w \", \" \\-\\-warnings\nShow warnings for skipped packets (e.g., not RTP or wrong SSRC)\n.SH EXAMPLES\n\\fBpcap2mjr -c opus -s 12345678 rec1234.pcap rec1234.mjr\\fR \\- Read all RTP packets with SSRC 12345678 from the provided .pcap file, and save them to a new .mjr file as an Opus recording\n.SH BUGS\n.TP\nIf you think you found a bug or want to contribute a feature, you can issue or a pull request on https://github.com/meetecho/janus-gateway/issues.\n.TP\nAnyway, before doing that make sure you read the documentation at https://janus.conf.meetecho.com/docs/ and that it has not been discussed already at https://janus.discourse.group/. We only use Github for code issues, and \\fBNOT\\fR for configuration or usage issues: use the group for that.\n.SH SEE ALSO\n.TP\nhttps://github.com/meetecho/janus-gateway \\- Official repository\n.TP\nhttps://janus.conf.meetecho.com \\- Demos and documentation\n.TP\nhttps://janus.discourse.group/ \\- Community\n.TP\nhttps://www.meetecho.com/blog/ \\- Tutorials and blog posts on Janus\n.SH AUTHORS\nLorenzo Miniero (lorenzo@meetecho.com)\n"
  },
  {
    "path": "src/postprocessing/pcap2mjr.c",
    "content": "/*! \\file    pcap2mjr.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Helper tool to convert .pcap files to Janus .mjr recordings\n * \\details  Our Janus WebRTC gateway provides a simple helper (janus_recorder)\n * to allow plugins to record audio, video and text frames sent by users.\n * These recordings can then be processed and converted to playable files,\n * or replayed via WebRTC again. The \\c pcap2mjr tool is a simple utility\n * that allows you take .pcap network captures, extract a specific RTP\n * session via its SSRC, and convert it to an .mjr Janus recording instead.\n * Its main purpose is helping convert .pcap captures to media files, or\n * make it easier to replay them via Janus.\n *\n * Using the utility is quite simple. Just pass, as arguments to the tool,\n * the SSRC to extract, the codec used for the RTP packets originally, the\n * path to the .pcap source file, and the path to the destination file, e.g.:\n *\n\\verbatim\n./pcap2mjr -c vp8 -s 12345678 /path/to/source.pcap /path/to/destination.mjr\n\\endverbatim\n *\n * The SSRC is optional but recommended, since a pcap capture may contain\n * multiple streams, RTP or not, and so the tool might need help figuring\n * out which RTP stream specifically should be converted to .mjr. Omitting\n * the SSRC will instruct the tool to try and autodetect the first SSRC\n * it finds, and use that one as a filter.\n *\n * If the tool can't detect any RTP packet with the specified SSRC, itwill result in an error.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#ifdef __MACH__\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n#include <signal.h>\n#include <sys/time.h>\n#include <netinet/ip.h>\n#include <netinet/ip6.h>\n#include <netinet/udp.h>\n#include <errno.h>\n\n#include <glib.h>\n#include <jansson.h>\n\n#include <pcap.h>\n#include <pcap/sll.h>\n\n#include \"../debug.h\"\n#include \"../version.h\"\n#include \"pp-rtp.h\"\n\n\n#define htonll(x) ((1==htonl(1)) ? (x) : ((gint64)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32))\n#define ntohll(x) ((1==ntohl(1)) ? (x) : ((gint64)ntohl((x) & 0xFFFFFFFF) << 32) | ntohl((x) >> 32))\n\nint janus_log_level = 4;\ngboolean janus_log_timestamps = FALSE;\ngboolean janus_log_colors = TRUE;\nchar *janus_log_global_prefix = NULL;\nint lock_debug = 0;\n\nint working = 0;\n\n/* Info header in the structured recording */\nstatic const char *header = \"MJR00002\";\n/* Frame header in the structured recording */\nstatic const char *frame_header = \"MEET\";\n\n/* Ethernet header */\ntypedef struct pcap2mjr_ethernet_header {\n\tuint8_t dst[6];\n\tuint8_t src[6];\n\tuint16_t type;\n} pcap2mjr_ethernet_header;\n\n\n/* Signal handler */\nstatic void janus_p2m_handle_signal(int signum) {\n\tworking = 0;\n}\n\n/* Supported command-line arguments */\nstatic int64_t ssrc64 = 0;\nstatic uint32_t ssrc = 0;\nstatic const char *codec = NULL;\nstatic gboolean show_warnings = FALSE;\nstatic const char **paths = NULL;\nstatic GOptionEntry opt_entries[] = {\n\t{ \"codec\", 'c', 0, G_OPTION_ARG_STRING, &codec, \"Codec the recording will contain (e.g., opus, vp8, etc.)\", NULL },\n\t{ \"ssrc\", 's', 0, G_OPTION_ARG_INT64, &ssrc64, \"SSRC of the packets in the pcap file to save (pass 0 to autodetect)\", NULL },\n\t{ \"warnings\", 'w', 0, G_OPTION_ARG_NONE, &show_warnings, \"Show warnings for skipped packets (e.g., not RTP or wrong SSRC)\", NULL },\n\t{ G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &paths, NULL, NULL },\n\t{ NULL, 0, 0, 0, NULL, NULL, NULL },\n};\n\n/* Main Code */\nint main(int argc, char *argv[]) {\n\tjanus_log_init(FALSE, TRUE, NULL, NULL);\n\tatexit(janus_log_destroy);\n\n\tJANUS_LOG(LOG_INFO, \"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\tJANUS_LOG(LOG_INFO, \"Janus commit: %s\\n\", janus_build_git_sha);\n\tJANUS_LOG(LOG_INFO, \"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\n\t/* Parse the command-line arguments */\n\tGError *error = NULL;\n\tGOptionContext *opts = g_option_context_new(\"source.pcap destination.mjr\");\n\tg_option_context_set_help_enabled(opts, TRUE);\n\tg_option_context_add_main_entries(opts, opt_entries, NULL);\n\tif(!g_option_context_parse(opts, &argc, &argv, &error)) {\n\t\tg_print(\"%s\\n\", error->message);\n\t\tg_error_free(error);\n\t\texit(1);\n\t}\n\t/* If some arguments are missing, fail */\n\tif(codec == NULL || paths == NULL || paths[0] == NULL || paths[1] == NULL) {\n\t\tchar *help = g_option_context_get_help(opts, TRUE, NULL);\n\t\tg_print(\"%s\", help);\n\t\tg_free(help);\n\t\tg_option_context_free(opts);\n\t\texit(1);\n\t}\n\n\t/* Evaluate arguments to find source and target */\n\tgboolean video = FALSE;\n\tif(!strcasecmp(codec, \"vp8\") || !strcasecmp(codec, \"vp9\") || !strcasecmp(codec, \"h264\")\n\t\t\t|| !strcasecmp(codec, \"av1\") || !strcasecmp(codec, \"h265\")) {\n\t\tvideo = TRUE;\n\t} else if(!strcasecmp(codec, \"opus\") || !strcasecmp(codec, \"multiopus\")\n\t\t\t|| !strcasecmp(codec, \"g711\") || !strcasecmp(codec, \"pcmu\") || !strcasecmp(codec, \"pcma\")\n\t\t\t|| !strcasecmp(codec, \"g722\")) {\n\t\tvideo = FALSE;\n\t} else if(!strcasecmp(codec, \"text\") || !strcasecmp(codec, \"binary\")) {\n\t\t/* We only do processing for RTP */\n\t\tJANUS_LOG(LOG_ERR, \"Data channels not supported by this tool\\n\");\n\t\tg_option_context_free(opts);\n\t\texit(1);\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec '%s'\\n\", codec);\n\t\tg_option_context_free(opts);\n\t\texit(1);\n\t}\n\tconst char *source = paths[0], *destination = paths[1];\n\tif(source == NULL || destination == NULL) {\n\t\tchar *help = g_option_context_get_help(opts, TRUE, NULL);\n\t\tg_print(\"%s\", help);\n\t\tg_free(help);\n\t\tg_option_context_free(opts);\n\t\texit(1);\n\t}\n\tssrc = (uint32_t)ssrc64;\n\tJANUS_LOG(LOG_INFO, \"[%s/%\"SCNu32\"] %s --> %s\\n\", codec, ssrc, source, destination);\n\tif(ssrc == 0)\n\t\tJANUS_LOG(LOG_WARN, \"No SSRC provided, will try to autodetect an RTP stream\\n\");\n\n\t/* Open and parse the pcap file */\n\tchar errbuf[PCAP_ERRBUF_SIZE];\n\tpcap_t *pcap = pcap_open_offline(source, errbuf);\n\tif(pcap == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Could not open file %s: %s\\n\", source, errbuf);\n\t\tg_option_context_free(opts);\n\t\texit(1);\n\t}\n\tint link = pcap_datalink(pcap);\n\tif(link != DLT_LINUX_SLL && link != DLT_LINUX_SLL2 && link != DLT_EN10MB) {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported link type %d (%s) in capture\\n\",\n\t\t\tlink, pcap_datalink_val_to_name(link));\n\t\tg_option_context_free(opts);\n\t\texit(1);\n\t}\n\n\t/* Create the target file */\n\tFILE *outfile = fopen(destination, \"wb\");\n\tif(outfile == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open output file\\n\");\n\t\tg_option_context_free(opts);\n\t\tpcap_close(pcap);\n\t\texit(1);\n\t}\n\t/* Write the first part of the header */\n\tsize_t res = fwrite(header, sizeof(char), strlen(header), outfile);\n\tif(res != strlen(header)) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't write .mjr header (%zu != %zu, %s)\\n\",\n\t\t\tres, strlen(header), g_strerror(errno));\n\t\tg_option_context_free(opts);\n\t\tpcap_close(pcap);\n\t\texit(1);\n\t}\n\n\t/* Handle SIGINT */\n\tworking = 1;\n\tsignal(SIGINT, janus_p2m_handle_signal);\n\n\t/* TODO Loop */\n    struct pcap_pkthdr *header = NULL;\n    const u_char *buffer = NULL, *temp = NULL;\n\tuint32_t count = 0, written = 0, pssrc = 0;\n    int ret = 0;\n    size_t min_size = sizeof(pcap2mjr_ethernet_header) + sizeof(struct ip) +\n\t\tsizeof(struct udphdr) + 12, pkt_size = 0;\n\tgboolean header_written = FALSE;\n\tgint64 start_ts = 0, pkt_ts = 0;\n    pcap2mjr_ethernet_header eth;\n    struct sll_header lcc;\n    struct sll2_header lcc2;\n    struct ip v4;\n    struct ip6_hdr v6;\n    janus_pp_rtp_header rtp;\n    while(working && (ret = pcap_next_ex(pcap, &header, &buffer)) >= 0) {\n\t\tcount++;\n\t\tif(header->len != header->caplen) {\n\t\t\tif(show_warnings) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Packet and capture lengths differ (%d != %d), skipping packet #%\"SCNu32\"\\n\",\n\t\t\t\t\theader->len, header->caplen, count);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif(header->len < min_size) {\n\t\t\tif(show_warnings) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Packet too small (< %zu), skipping packet #%\"SCNu32\"\\n\", min_size, count);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\ttemp = buffer;\n\t\tpkt_size = header->len;\n\t\tpkt_ts = header->ts.tv_sec*G_USEC_PER_SEC + header->ts.tv_usec;\n\t\tif(start_ts == 0)\n\t\t\tstart_ts = pkt_ts;\n\t\t/* Traverse all the headers */\n\t\tint protocol = 0;\n\t\tif(link == DLT_EN10MB) {\n\t\t\t/* Ethernet */\n\t\t\tmemcpy(&eth, temp, sizeof(pcap2mjr_ethernet_header));\n\t\t\tprotocol = ntohs(eth.type);\n\t\t\ttemp += sizeof(pcap2mjr_ethernet_header);\n\t\t\tpkt_size -= sizeof(pcap2mjr_ethernet_header);\n\t\t} else if(link == DLT_LINUX_SLL) {\n\t\t\t/* Linux Cooked Capture */\n\t\t\tmemcpy(&lcc, temp, sizeof(struct sll_header));\n\t\t\tprotocol = ntohs(lcc.sll_protocol);\n\t\t\ttemp += sizeof(struct sll_header);\n\t\t\tpkt_size -= sizeof(struct sll_header);\n\t\t} else {\n\t\t\t/* Linux Cooked Capture v2 */\n\t\t\tmemcpy(&lcc2, temp, sizeof(struct sll2_header));\n\t\t\tprotocol = ntohs(lcc2.sll2_protocol);\n\t\t\ttemp += sizeof(struct sll2_header);\n\t\t\tpkt_size -= sizeof(struct sll2_header);\n\t\t}\n\t\tif(protocol == 0x0800) {\n\t\t\t/* IPv4 */\n\t\t\tmemcpy(&v4, temp, sizeof(struct ip));\n\t\t\tprotocol = v4.ip_p;\n\t\t\ttemp += sizeof(struct ip);\n\t\t\tpkt_size -= sizeof(struct ip);\n\t\t} else if(protocol == 0x86DD) {\n\t\t\t/* IPv6 */\n\t\t\tmemcpy(&v6, temp, sizeof(struct ip6_hdr));\n\t\t\tprotocol = v6.ip6_ctlun.ip6_un1.ip6_un1_nxt;\n\t\t\ttemp += sizeof(struct ip6_hdr);\n\t\t\tpkt_size -= sizeof(struct ip6_hdr);\n\t\t} else {\n\t\t\tif(show_warnings) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Not an IPv4 or IPv6 packet, skipping\\n\");\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif(protocol != 17) {\n\t\t\tif(show_warnings) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Not an UDP packet, skipping\\n\");\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\t/* UDP */\n\t\ttemp += sizeof(struct udphdr);\n\t\tpkt_size -= sizeof(struct udphdr);\n\t\t/* Make sure this is an RTP packet */\n\t\tmemcpy(&rtp, temp, sizeof(janus_pp_rtp_header));\n\t\tif(rtp.version != 2 || (rtp.type >= 64 && rtp.type < 96)) {\n\t\t\tif(show_warnings) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Not an RTP packet, skipping packet #%\"SCNu32\"\\n\", count);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tpssrc = htonl(rtp.ssrc);\n\t\tif(ssrc == 0) {\n\t\t\tssrc = pssrc;\n\t\t\tJANUS_LOG(LOG_INFO, \"Autodetected SSRC %\"SCNu32\"\\n\", ssrc);\n\t\t}\n\t\tif(pssrc != ssrc) {\n\t\t\tif(show_warnings) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Not the SSRC we need (%\"SCNu32\" != %\"SCNu32\"), skipping packet #%\"SCNu32\"\\n\",\n\t\t\t\t\tpssrc, ssrc, count);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\t/* Save the packet, but first check if we've written the .mjr header already */\n\t\tif(!header_written) {\n\t\t\t/* Write info header as a JSON formatted info */\n\t\t\theader_written = TRUE;\n\t\t\tjson_t *info = json_object();\n\t\t\t/* FIXME Codecs should be configurable in the future */\n\t\t\tconst char *type = NULL;\n\t\t\tif(video)\n\t\t\t\ttype = \"v\";\n\t\t\telse\n\t\t\t\ttype = \"a\";\n\t\t\tjson_object_set_new(info, \"t\", json_string(type));\n\t\t\tjson_object_set_new(info, \"c\", json_string(codec));\n\t\t\tjson_object_set_new(info, \"s\", json_integer(pkt_ts));\n\t\t\tjson_object_set_new(info, \"u\", json_integer(pkt_ts));\n\t\t\tgchar *info_text = json_dumps(info, JSON_PRESERVE_ORDER);\n\t\t\tjson_decref(info);\n\t\t\tuint16_t info_bytes = htons(strlen(info_text));\n\t\t\tsize_t res = fwrite(&info_bytes, sizeof(uint16_t), 1, outfile);\n\t\t\tif(res != 1) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write size of JSON header in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\t\tres, sizeof(uint16_t), g_strerror(errno));\n\t\t\t}\n\t\t\tres = fwrite(info_text, sizeof(char), strlen(info_text), outfile);\n\t\t\tif(res != strlen(info_text)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write JSON header in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\t\tres, strlen(info_text), g_strerror(errno));\n\t\t\t}\n\t\t\tfree(info_text);\n\t\t}\n\t\t/* Write frame header (fixed part[4], timestamp[4], length[2]) */\n\t\tsize_t res = fwrite(frame_header, sizeof(char), strlen(frame_header), outfile);\n\t\tif(res != strlen(frame_header)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write frame header in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\tres, strlen(frame_header), g_strerror(errno));\n\t\t}\n\t\tuint32_t timestamp = (uint32_t)(pkt_ts > start_ts ? ((pkt_ts - start_ts)/1000) : 0);\n\t\ttimestamp = htonl(timestamp);\n\t\tres = fwrite(&timestamp, sizeof(uint32_t), 1, outfile);\n\t\tif(res != 1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write frame timestamp in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\tres, sizeof(uint32_t), g_strerror(errno));\n\t\t}\n\t\tuint16_t header_bytes = htons(pkt_size);\n\t\tres = fwrite(&header_bytes, sizeof(uint16_t), 1, outfile);\n\t\tif(res != 1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write size of frame in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\tres, sizeof(uint16_t), g_strerror(errno));\n\t\t}\n\t\t/* Save packet on file */\n\t\twritten++;\n\t\tint tmp = 0, tot = pkt_size;\n\t\twhile(tot > 0) {\n\t\t\ttmp = fwrite(temp+pkt_size-tot, sizeof(char), tot, outfile);\n\t\t\tif(tmp <= 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error saving frame, stopping here...\\n\");\n\t\t\t\tgoto done;\n\t\t\t}\n\t\t\ttot -= tmp;\n\t\t}\n\t}\n\tJANUS_LOG(LOG_INFO, \"Saved %\"SCNu32\" out of %\"SCNu32\" packets\\n\", written, count);\n\ndone:\n\t/* We're done */\n\tpcap_close(pcap);\n\tfclose(outfile);\n\toutfile = fopen(destination, \"rb\");\n\tif(outfile == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"No destination file %s??\\n\", destination);\n\t} else {\n\t\tfseek(outfile, 0L, SEEK_END);\n\t\tsize_t fsize = ftell(outfile);\n\t\tfseek(outfile, 0L, SEEK_SET);\n\t\tJANUS_LOG(LOG_INFO, \"%s is %zu bytes\\n\", destination, fsize);\n\t\tfclose(outfile);\n\t}\n\n\tg_option_context_free(opts);\n\tJANUS_LOG(LOG_INFO, \"Bye!\\n\");\n\treturn 0;\n}\n"
  },
  {
    "path": "src/postprocessing/pp-av1.c",
    "content": "/*! \\file    pp-av1.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .mp4 files out of AV1 frames\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .mp4 files out of AV1 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-avformat.h\"\n#include \"pp-av1.h\"\n#include \"../debug.h\"\n\n/* MP4 output */\nstatic AVFormatContext *fctx;\n#if LIBAVCODEC_VER_AT_LEAST(58, 18)\nstatic AVStream *vStream;\n#endif\nstatic uint16_t max_width = 0, max_height = 0;\nstatic int fps = 0;\n\n/* Supported target formats */\nstatic const char *janus_pp_av1_formats[] = {\n\t\"mp4\", NULL\n};\nconst char **janus_pp_av1_get_extensions(void) {\n\treturn janus_pp_av1_formats;\n}\n\n/* Processing methods */\nint janus_pp_av1_create(char *destination, char *metadata, gboolean faststart, const char *extension) {\n\tif(destination == NULL)\n\t\treturn -1;\n#if !LIBAVCODEC_VER_AT_LEAST(58, 18)\n\tJANUS_LOG(LOG_ERR, \"This version of libavcodec doesn't support AV1...\\n\");\n\treturn -1;\n#else\n\n\t/* Video output */\n\tfctx = janus_pp_create_avformatcontext(extension, metadata, destination);\n\tif(fctx == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error allocating context\\n\");\n\t\treturn -1;\n\t}\n\n\tfctx->url = g_strdup(destination);\n\n\tvStream = janus_pp_new_video_avstream(fctx, AV_CODEC_ID_AV1, max_width, max_height);\n\tif(vStream == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error adding stream\\n\");\n\t\treturn -1;\n\t}\n\n\tAVDictionary *options = NULL;\n\tif(faststart)\n\t\tav_dict_set(&options, \"movflags\", \"+faststart\", 0);\n\n\tif(avformat_write_header(fctx, &options) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error writing header\\n\");\n\t\treturn -1;\n\t}\n\treturn 0;\n#endif\n}\n\n/* Helper to decode a leb128 integer  */\nstatic uint32_t janus_pp_av1_lev128_decode(uint8_t *base, uint16_t maxlen, size_t *read) {\n\tuint32_t val = 0;\n\tuint8_t *cur = base;\n\twhile((cur-base) < maxlen) {\n\t\t/* We only read the 7 least significant bits of each byte */\n\t\tval |= ((uint32_t)(*cur & 0x7f)) << ((cur-base)*7);\n\t\tif((*cur & 0x80) == 0) {\n\t\t\t/* Most significant bit is 0, we're done */\n\t\t\t*read = (cur-base)+1;\n\t\t\treturn val;\n\t\t}\n\t\tcur++;\n\t}\n\t/* If we got here, we read all bytes, but no one with 0 as MSB? */\n\treturn 0;\n}\n/* Helper to encode a leb128 integer  */\nstatic void janus_pp_av1_lev128_encode(uint32_t value, uint8_t *base, size_t *written) {\n\tuint8_t *cur = base;\n\twhile(value >= 0x80) {\n\t\t/* All these bytes need MSB=1 */\n\t\t*cur = (0x80 | (value & 0x7F));\n\t\tcur++;\n\t\tvalue >>= 7;\n\t}\n\t/* Last byte will have MSB=0 */\n\t*cur = value;\n\t*written = (cur-base)+1;\n}\n\n/* Helpers to read a bit, or group of bits, in a Sequence Header */\nstatic uint32_t janus_pp_av1_getbit(uint8_t *base, uint32_t offset) {\n\treturn ((*(base + (offset >> 0x3))) >> (0x7 - (offset & 0x7))) & 0x1;\n}\nstatic uint32_t janus_pp_av1_getbits(uint8_t *base, uint8_t num, uint32_t *offset) {\n\tuint32_t res = 0;\n\tint32_t i = 0;\n\tfor(i=num-1; i>=0; i--) {\n\t\tres |= janus_pp_av1_getbit(base, (*offset)++) << i;\n\t}\n\treturn res;\n}\n/* Helper to parse a Sequence Header (only to get the video resolution) */\nstatic void janus_pp_av1_parse_sh(char *buffer, uint16_t *width, uint16_t *height) {\n\t/* Evaluate/skip everything until we get to the resolution */\n\tuint32_t offset = 0, value = 0, i = 0;\n\tuint8_t *base = (uint8_t *)(buffer);\n\t/* Skip seq_profile (3 bits) */\n\tjanus_pp_av1_getbits(base, 3, &offset);\n\t/* Skip still_picture (1 bit) */\n\tjanus_pp_av1_getbit(base, offset++);\n\t/* Skip reduced_still_picture_header (1 bit) */\n\tvalue = janus_pp_av1_getbit(base, offset++);\n\tif(value) {\n\t\t/* Skip seq_level_idx (5 bits) */\n\t\tjanus_pp_av1_getbits(base, 5, &offset);\n\t} else {\n\t\tgboolean decoder_model_info = FALSE, initial_display_delay = FALSE;\n\t\tuint32_t bdlm1 = 0;\n\t\t/* Skip timing_info_present_flag (1 bit) */\n\t\tvalue = janus_pp_av1_getbit(base, offset++);\n\t\tif(value) {\n\t\t\t/* Skip num_units_in_display_tick (32 bits) */\n\t\t\tjanus_pp_av1_getbits(base, 32, &offset);\n\t\t\t/* Skip time_scale (32 bits) */\n\t\t\tjanus_pp_av1_getbits(base, 32, &offset);\n\t\t\t/* Skip equal_picture_interval (1 bit)*/\n\t\t\tvalue = janus_pp_av1_getbit(base, offset++);\n\t\t\tif(value) {\n\t\t\t\t/* TODO Skip num_ticks_per_picture_minus_1 (uvlc) */\n\t\t\t}\n\t\t\t/* Skip decoder_model_info_present_flag (1 bit) */\n\t\t\tvalue = janus_pp_av1_getbit(base, offset++);\n\t\t\tif(value) {\n\t\t\t\tdecoder_model_info = TRUE;\n\t\t\t\t/* Skip buffer_delay_length_minus_1 (5 bits) */\n\t\t\t\tbdlm1 = janus_pp_av1_getbits(base, 5, &offset);\n\t\t\t\t/* Skip num_units_in_decoding_tick (32 bits) */\n\t\t\t\tjanus_pp_av1_getbits(base, 32, &offset);\n\t\t\t\t/* Skip buffer_removal_time_length_minus_1 (5 bits) */\n\t\t\t\tjanus_pp_av1_getbits(base, 5, &offset);\n\t\t\t\t/* Skip frame_presentation_time_length_minus_1 (5 bits) */\n\t\t\t\tjanus_pp_av1_getbits(base, 5, &offset);\n\t\t\t}\n\t\t}\n\t\t/* Skip initial_display_delay_present_flag (1 bit) */\n\t\tvalue = janus_pp_av1_getbit(base, offset++);\n\t\tif(value)\n\t\t\tinitial_display_delay = TRUE;\n\t\t/* Skip operating_points_cnt_minus_1 (5 bits) */\n\t\tuint32_t opcm1 = janus_pp_av1_getbits(base, 5, &offset)+1;\n\t\tfor(i=0; i<opcm1; i++) {\n\t\t\t/* Skip operating_point_idc[i] (12 bits) */\n\t\t\tjanus_pp_av1_getbits(base, 12, &offset);\n\t\t\t/* Skip seq_level_idx[i] (5 bits) */\n\t\t\tvalue = janus_pp_av1_getbits(base, 5, &offset);\n\t\t\tif(value > 7) {\n\t\t\t\t/* Skip seq_tier[i] (1 bit) */\n\t\t\t\tjanus_pp_av1_getbit(base, offset++);\n\t\t\t}\n\t\t\tif(decoder_model_info) {\n\t\t\t\t/* Skip decoder_model_present_for_this_op[i] (1 bit) */\n\t\t\t\tvalue = janus_pp_av1_getbit(base, offset++);\n\t\t\t\tif(value) {\n\t\t\t\t\t/* Skip operating_parameters_info(i) */\n\t\t\t\t\tjanus_pp_av1_getbits(base, (2*bdlm1)+1, &offset);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(initial_display_delay) {\n\t\t\t\t/* Skip initial_display_delay_present_for_this_op[i] (1 bit) */\n\t\t\t\tvalue = janus_pp_av1_getbit(base, offset++);\n\t\t\t\tif(value) {\n\t\t\t\t\t/* Skip initial_display_delay_minus_1[i] (4 bits) */\n\t\t\t\t\tjanus_pp_av1_getbits(base, 4, &offset);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* Read frame_width_bits_minus_1 (4 bits) */\n\tuint32_t fwbm1 = janus_pp_av1_getbits(base, 4, &offset);\n\t/* Read frame_height_bits_minus_1 (4 bits) */\n\tuint32_t fhbm1 = janus_pp_av1_getbits(base, 4, &offset);\n\t/* Read max_frame_width_minus_1 (n bits) */\n\t*width = janus_pp_av1_getbits(base, fwbm1+1, &offset)+1;\n\t/* Read max_frame_height_minus_1 (n bits) */\n\t*height = janus_pp_av1_getbits(base, fhbm1+1, &offset)+1;\n}\n\nint janus_pp_av1_preprocess(FILE *file, janus_pp_frame_packet *list, json_t *info) {\n\tif(!file || !list)\n\t\treturn -1;\n\tjson_t *resolutions = NULL;\n\tint last_width = -1, last_height = -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tint bytes = 0, min_ts_diff = 0, max_ts_diff = 0;\n\tint rotation = -1;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\twhile(tmp) {\n\t\tif(tmp->prev != NULL && tmp->ts > tmp->prev->ts) {\n\t\t\tif(tmp->ts > tmp->prev->ts) {\n\t\t\t\tint diff = tmp->ts - tmp->prev->ts;\n\t\t\t\tif(min_ts_diff == 0 || min_ts_diff > diff)\n\t\t\t\t\tmin_ts_diff = diff;\n\t\t\t\tif(max_ts_diff == 0 || max_ts_diff < diff)\n\t\t\t\t\tmax_ts_diff = diff;\n\t\t\t}\n\t\t\tif(tmp->seq - tmp->prev->seq > 1) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/90000);\n\t\t\t}\n\t\t}\n\t\t/* Read the packet */\n\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\tint len = tmp->len-12-tmp->skip;\n\t\tif(len < 3) {\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\ttmp->drop = TRUE;\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Parse AV1 header now: first byte is the aggregation header */\n\t\tchar *payload = prebuffer;\n\t\tuint8_t aggrh = *payload;\n\t\tuint8_t zbit = (aggrh & 0x80) >> 7;\n\t\tuint8_t ybit = (aggrh & 0x40) >> 6;\n\t\tuint8_t w = (aggrh & 0x30) >> 4;\n\t\tuint8_t nbit = (aggrh & 0x08) >> 3;\n\t\tJANUS_LOG(LOG_HUGE, \"[%04d] z=%u, y=%u, w=%u, n=%u\\n\", len, zbit, ybit, w, nbit);\n\t\tpayload++;\n\t\tlen--;\n\t\tuint8_t obus = 0;\n\t\tuint32_t obusize = 0;\n\t\twhile(!zbit && len > 0) {\n\t\t\tobus++;\n\t\t\tif(w == 0 || w > obus) {\n\t\t\t\t/* Then the OBU size (leb128) */\n\t\t\t\tsize_t read = 0;\n\t\t\t\tobusize = janus_pp_av1_lev128_decode((uint8_t *)payload, len, &read);\n\t\t\t\tif(obusize == 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- OBU size is 0, something's broken\\n\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- OBU size: %\"SCNu32\"/%d (in %zu leb128 bytes)\\n\", obusize, len, read);\n\t\t\t\tpayload += read;\n\t\t\t\tlen -= read;\n\t\t\t} else {\n\t\t\t\tobusize = len;\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- OBU size: %d (last OBU)\\n\", len);\n\t\t\t}\n\t\t\t/* Then we have the OBU header */\n\t\t\tuint8_t obuh = *payload;\n\t\t\tuint8_t fbit = (obuh & 0x80) >> 7;\n\t\t\tuint8_t type = (obuh & 0x78) >> 3;\n\t\t\tuint8_t ebit = (obuh & 0x04) >> 2;\n\t\t\tuint8_t sbit = (obuh & 0x02) >> 1;\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- OBU header: f=%u, type=%u, e=%u, s=%u\\n\", fbit, type, ebit, sbit);\n\t\t\tif(ebit) {\n\t\t\t\t/* Skip the extension, if present */\n\t\t\t\tpayload++;\n\t\t\t\tlen--;\n\t\t\t\tobusize--;\n\t\t\t}\n\t\t\tif(type == 1) {\n\t\t\t\t/* Sequence header */\n\t\t\t\tuint16_t width = 0, height = 0;\n\t\t\t\t/* TODO Fix currently broken parsing of SH */\n\t\t\t\tjanus_pp_av1_parse_sh(payload+1, &width, &height);\n\t\t\t\tif(width*height > max_width*max_height) {\n\t\t\t\t\tmax_width = width;\n\t\t\t\t\tmax_height = height;\n\t\t\t\t}\n\t\t\t\tJANUS_LOG(LOG_INFO, \"  -- Detected new resolution: %\"SCNu16\"x%\"SCNu16\" (seq=%\"SCNu16\")\\n\", width, height, tmp->seq);\n\t\t\t\tif(info && (last_width != width || last_height != height)) {\n\t\t\t\t\tlast_width = width;\n\t\t\t\t\tlast_height = height;\n\t\t\t\t\tif(resolutions == NULL) {\n\t\t\t\t\t\tresolutions = json_array();\n\t\t\t\t\t\tjson_object_set_new(info, \"resolution\", resolutions);\n\t\t\t\t\t}\n\t\t\t\t\tjson_t *resolution = json_object();\n\t\t\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\t\t\tjson_object_set_new(resolution, \"ts\", json_real(ts));\n\t\t\t\t\tjson_object_set_new(resolution, \"width\", json_integer(width));\n\t\t\t\t\tjson_object_set_new(resolution, \"height\", json_integer(height));\n\t\t\t\t\tjson_array_append_new(resolutions, resolution);\n\t\t\t\t}\n\t\t\t}\n\t\t\tpayload += obusize;\n\t\t\tlen -= obusize;\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked video packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/90000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->rotation != -1 && tmp->rotation != rotation) {\n\t\t\trotation = tmp->rotation;\n\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\tJANUS_LOG(LOG_INFO, \"[%8.3fs] Video rotation: %d degrees\\n\", ts, rotation);\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tint mean_ts = min_ts_diff;\t/* FIXME: was an actual mean, (max_ts_diff+min_ts_diff)/2; */\n\tfps = (90000/(mean_ts > 0 ? mean_ts : 30));\n\tJANUS_LOG(LOG_INFO, \"  -- %\"SCNu16\"x%\"SCNu16\" (fps [%d,%d] ~ %d)\\n\", max_width, max_height, min_ts_diff, max_ts_diff, fps);\n\tif(max_width == 0 && max_height == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No resolution info?? assuming 640x480...\\n\");\n\t\tmax_width = 640;\n\t\tmax_height = 480;\n\t}\n\tif(max_width < 160) {\n\t\tJANUS_LOG(LOG_WARN, \"Width seems weirdly low (%d), setting 640 instead...\\n\", max_width);\n\t\tmax_width = 640;\n\t}\n\tif(max_height < 120) {\n\t\tJANUS_LOG(LOG_WARN, \"Height seems weirdly low (%d), setting 480 instead...\\n\", max_height);\n\t\tmax_height = 480;\n\t}\n\tif(fps == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No fps?? assuming 1...\\n\");\n\t\tfps = 1;\t/* Prevent divide by zero error */\n\t}\n\treturn 0;\n}\n\nint janus_pp_av1_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\n\tint bytes = 0, numBytes = max_width*max_height*3;\t/* FIXME */\n\tuint8_t *received_frame = g_malloc0(numBytes), *obu_data = g_malloc0(numBytes);\n\tuint8_t *buffer = g_malloc0(1500), *start = buffer;\n\tint len = 0, frameLen = 0, total = 0, dataLen = 0;\n\tint keyFrame = 0;\n\tgboolean keyframe_found = FALSE;\n#ifdef FF_API_INIT_PACKET\n\tAVPacket *packet = av_packet_alloc();\n#else\n\tAVPacket pkt = { 0 }, *packet = &pkt;\n#endif\n\tAVRational timebase = {1, 90000};\n\n\twhile(*working && tmp != NULL) {\n\t\tkeyFrame = 0;\n\t\tframeLen = 0;\n\t\tdataLen = 0;\n\t\tlen = 0;\n\t\twhile(tmp != NULL) {\n\t\t\tif(tmp->drop) {\n\t\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* RTP payload */\n\t\t\tbuffer = start;\n\t\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\t\tlen = tmp->len-12-tmp->skip;\n\t\t\tif(len < 1) {\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\t\tif(bytes != len) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* Parse AV1 header now: first byte is the aggregation header */\n\t\t\tuint8_t aggrh = *buffer;\n\t\t\tuint8_t zbit = (aggrh & 0x80) >> 7;\n\t\t\tuint8_t ybit = (aggrh & 0x40) >> 6;\n\t\t\tuint8_t w = (aggrh & 0x30) >> 4;\n\t\t\tuint8_t nbit = (aggrh & 0x08) >> 3;\n\t\t\t/* FIXME Ugly hack: we consider a packet with Z=0 and N=1 a keyframe */\n\t\t\tkeyFrame = (!zbit && nbit);\n\t\t\tif(keyFrame && !keyframe_found) {\n\t\t\t\tkeyframe_found = TRUE;\n\t\t\t\tJANUS_LOG(LOG_INFO, \"First keyframe: %\"SCNu64\"\\n\", tmp->ts-list->ts);\n\t\t\t}\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t\tuint8_t obus = 0;\n\t\t\tuint32_t obusize = 0;\n\t\t\twhile(!zbit && len > 0) {\n\t\t\t\tobus++;\n\t\t\t\tif(w == 0 || w > obus) {\n\t\t\t\t\t/* Read the OBU size (leb128) */\n\t\t\t\t\tsize_t read = 0;\n\t\t\t\t\tobusize = janus_pp_av1_lev128_decode((uint8_t *)buffer, len, &read);\n\t\t\t\t\tif(obusize == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  -- OBU size is 0, something's broken\\n\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tbuffer += read;\n\t\t\t\t\tlen -= read;\n\t\t\t\t} else {\n\t\t\t\t\tobusize = len;\n\t\t\t\t}\n\t\t\t\t/* Update the OBU header to set the S bit */\n\t\t\t\tuint8_t obuh = *buffer;\n\t\t\t\tobuh |= (1 << 1);\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] OBU header: 1\\n\", tmp->ts);\n\t\t\t\tmemcpy(received_frame + frameLen, &obuh, sizeof(uint8_t));\n\t\t\t\tframeLen++;\n\t\t\t\tbuffer++;\n\t\t\t\tlen--;\n\t\t\t\tobusize--;\n\t\t\t\tif(w == 0 || w > obus || !ybit) {\n\t\t\t\t\t/* We have the whole OBU, write the OBU size */\n\t\t\t\t\tsize_t written = 0;\n\t\t\t\t\tuint8_t leb[8];\n\t\t\t\t\tjanus_pp_av1_lev128_encode(obusize, leb, &written);\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] OBU size (%\"SCNu32\"): %zu\\n\", tmp->ts, obusize, written);\n\t\t\t\t\tmemcpy(received_frame + frameLen, leb, written);\n\t\t\t\t\tframeLen += written;\n\t\t\t\t\t/* Copy the actual data */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] OBU data: %\"SCNu32\"\\n\", tmp->ts, obusize);\n\t\t\t\t\tmemcpy(received_frame + frameLen, buffer, obusize);\n\t\t\t\t\tframeLen += obusize;\n\t\t\t\t} else {\n\t\t\t\t\t/* OBU will continue in another packet, buffer the data */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] OBU data (part.): %d\\n\", tmp->ts, obusize);\n\t\t\t\t\tmemcpy(obu_data + dataLen, buffer, obusize);\n\t\t\t\t\tdataLen += obusize;\n\t\t\t\t}\n\t\t\t\t/* Move to the next OBU, if any */\n\t\t\t\tbuffer += obusize;\n\t\t\t\tlen -= obusize;\n\t\t\t}\n\t\t\t/* Frame manipulation */\n\t\t\tif(len > 0) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] OBU data (cont.): %d\\n\", tmp->ts, len);\n\t\t\t\tmemcpy(obu_data + dataLen, buffer, len);\n\t\t\t\tdataLen += len;\n\t\t\t}\n\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\tbreak;\n\t\t\ttmp = tmp->next;\n\t\t}\n\t\tif(dataLen > 0) {\n\t\t\t/* We have a buffered OBU, write the OBU size */\n\t\t\tsize_t written = 0;\n\t\t\tuint8_t leb[8];\n\t\t\tjanus_pp_av1_lev128_encode(dataLen, leb, &written);\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] OBU size (%d): %zu\\n\", tmp->ts, dataLen, written);\n\t\t\tmemcpy(received_frame + frameLen, leb, written);\n\t\t\tframeLen += written;\n\t\t\t/* Copy the actual data */\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] OBU data: %\"SCNu32\"\\n\", tmp->ts, dataLen);\n\t\t\tmemcpy(received_frame + frameLen, obu_data, dataLen);\n\t\t\tframeLen += dataLen;\n\t\t}\n\t\tif(frameLen > 0) {\n\t\t\t/* Save the frame */\n\t\t\tmemset(received_frame + frameLen, 0, FF_INPUT_BUFFER_PADDING_SIZE);\n\t\t\ttotal += frameLen;\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Saving frame: %d (tot=%d)\\n\", tmp->ts, frameLen, total);\n\n#ifdef FF_API_INIT_PACKET\n\t\t\tav_packet_unref(packet);\n#else\n\t\t\tav_init_packet(packet);\n#endif\n\t\t\tpacket->stream_index = 0;\n\t\t\tpacket->data = received_frame;\n\t\t\tpacket->size = frameLen;\n\t\t\tif(keyFrame)\n\t\t\t\tpacket->flags |= AV_PKT_FLAG_KEY;\n\n\t\t\t/* First we save to the file... */\n\t\t\tpacket->pts = packet->dts = av_rescale_q(tmp->ts-list->ts, timebase, fctx->streams[0]->time_base);\n\t\t\tJANUS_LOG(LOG_HUGE, \"%\"SCNu64\" - %\"SCNu64\" --> %\"SCNu64\"\\n\",\n\t\t\t\ttmp->ts, list->ts, packet->pts);\n\t\t\tif(fctx) {\n\t\t\t\tint res = av_write_frame(fctx, packet);\n\t\t\t\tif(res < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error writing video frame to file... (error %d, %s)\\n\",\n\t\t\t\t\t\tres, av_err2str(res));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n#ifdef FF_API_INIT_PACKET\n\tav_packet_free(&packet);\n#endif\n\tg_free(received_frame);\n\tg_free(obu_data);\n\tg_free(start);\n\treturn 0;\n}\n\n/* Close MP4 file */\nvoid janus_pp_av1_close(void) {\n\tif(fctx != NULL) {\n\t\tav_write_trailer(fctx);\n\t\tavio_close(fctx->pb);\n\t\tg_free(fctx->url);\n\t\tfctx->url = NULL;\n\t\tavformat_free_context(fctx);\n\t}\n}\n"
  },
  {
    "path": "src/postprocessing/pp-av1.h",
    "content": "/*! \\file    pp-av1.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .mp4 files out of AV1 frames (headers)\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .mp4 files out of AV1 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_AV1\n#define JANUS_PP_AV1\n\n#include <stdio.h>\n#include <jansson.h>\n\n#include \"pp-rtp.h\"\n\n/* AV1 stuff */\nconst char **janus_pp_av1_get_extensions(void);\nint janus_pp_av1_create(char *destination, char *metadata, gboolean faststart, const char *extension);\nint janus_pp_av1_preprocess(FILE *file, janus_pp_frame_packet *list, json_t *info);\nint janus_pp_av1_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_av1_close(void);\n\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-avformat.c",
    "content": "/*! \\file    pp-avformat.c\n * \\copyright GNU General Public License v3\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include \"pp-avformat.h\"\n\nvoid janus_pp_setup_avformat(void) {\n\t/* Setup FFmpeg */\n#if ( LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100) )\n\tav_register_all();\n#endif\n\t/* Adjust logging to match the postprocessor's */\n\tav_log_set_level(janus_log_level <= LOG_NONE ? AV_LOG_QUIET :\n\t\t(janus_log_level == LOG_FATAL ? AV_LOG_FATAL :\n\t\t\t(janus_log_level == LOG_ERR ? AV_LOG_ERROR :\n\t\t\t\t(janus_log_level == LOG_WARN ? AV_LOG_WARNING :\n\t\t\t\t\t(janus_log_level == LOG_INFO ? AV_LOG_INFO :\n\t\t\t\t\t\t(janus_log_level == LOG_VERB ? AV_LOG_VERBOSE : AV_LOG_DEBUG))))));\n\n}\n\nAVFormatContext *janus_pp_create_avformatcontext(const char *format, const char *metadata, const char *destination) {\n\tjanus_pp_setup_avformat();\n\n\tAVFormatContext *ctx = avformat_alloc_context();\n\tif(!ctx)\n\t\treturn NULL;\n\n\t/* We save the metadata part as a comment (see #1189) */\n\tif(metadata)\n\t\tav_dict_set(&ctx->metadata, \"comment\", metadata, 0);\n\n\tctx->oformat = av_guess_format(format, NULL, NULL);\n        if(ctx->oformat == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error guessing format\\n\");\n\t\tavformat_free_context(ctx);\n\t\treturn NULL;\n\t}\n\n\tint res = avio_open(&ctx->pb, destination, AVIO_FLAG_WRITE);\n\tif(res < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error opening file for output (%d, %s)\\n\",\n\t\t\tres, av_err2str(res));\n\t\tavformat_free_context(ctx);\n\t\treturn NULL;\n\t}\n\n\treturn ctx;\n}\n\nAVStream *janus_pp_new_audio_avstream(AVFormatContext *fctx, int codec_id, int samplerate, int channels, const uint8_t *extradata, int size) {\n\tAVStream *st = avformat_new_stream(fctx, NULL);\n\tif(!st)\n\t\treturn NULL;\n\n#ifdef USE_CODECPAR\n\tAVCodecParameters *c = st->codecpar;\n#else\n\tAVCodecContext *c = st->codec;\n#endif\n\tc->codec_id = codec_id;\n\tc->codec_type = AVMEDIA_TYPE_AUDIO;\n\tc->sample_rate = samplerate;\n#ifdef NEW_CHANNEL_LAYOUT\n\tc->ch_layout.nb_channels = channels;\n#else\n\tc->channels = channels;\n#endif\n\tif(extradata) {\n\t\tc->extradata_size = size;\n\t\tc->extradata = av_memdup(extradata, size);\n\t}\n\n\treturn st;\n}\n\nAVStream *janus_pp_new_video_avstream(AVFormatContext *fctx, int codec_id, int width, int height) {\n\tAVStream *st = avformat_new_stream(fctx, NULL);\n\tif(!st)\n\t\treturn NULL;\n\n#ifdef USE_CODECPAR\n\tAVCodecParameters *c = st->codecpar;\n#else\n\tAVCodecContext *c = st->codec;\n#endif\n\tc->codec_id = codec_id;\n\tc->codec_type = AVMEDIA_TYPE_VIDEO;\n\tc->width = width;\n\tc->height = height;\n\n\treturn st;\n}\n\n"
  },
  {
    "path": "src/postprocessing/pp-avformat.h",
    "content": "/*! \\file    pp-avformat.h\n * \\copyright GNU General Public License v3\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_AVFORMAT\n#define JANUS_PP_AVFORMAT\n\n#include \"../debug.h\"\n\n#include <libavcodec/avcodec.h>\n#include <libavformat/avformat.h>\n\n#define LIBAVCODEC_VER_AT_LEAST(major, minor) \\\n\t(LIBAVCODEC_VERSION_MAJOR > major || \\\n\t (LIBAVCODEC_VERSION_MAJOR == major && \\\n\t  LIBAVCODEC_VERSION_MINOR >= minor))\n\n#define LIBAVFORMAT_VER_AT_LEAST(major, minor) \\\n\t(LIBAVFORMAT_VERSION_MAJOR > major || \\\n\t (LIBAVFORMAT_VERSION_MAJOR == major && \\\n\t  LIBAVFORMAT_VERSION_MINOR >= minor))\n\n#if LIBAVCODEC_VER_AT_LEAST(51, 42)\n#define PIX_FMT_YUV420P AV_PIX_FMT_YUV420P\n#endif\n\n#if LIBAVCODEC_VER_AT_LEAST(56, 56)\n#ifndef CODEC_FLAG_GLOBAL_HEADER\n#define CODEC_FLAG_GLOBAL_HEADER AV_CODEC_FLAG_GLOBAL_HEADER\n#endif\n#ifndef FF_INPUT_BUFFER_PADDING_SIZE\n#define FF_INPUT_BUFFER_PADDING_SIZE AV_INPUT_BUFFER_PADDING_SIZE\n#endif\n#endif\n\n#if LIBAVCODEC_VER_AT_LEAST(57, 14)\n#define USE_CODECPAR\n#endif\n\n/* https://github.com/FFmpeg/FFmpeg/commit/cdba98bb80e2ab73d34659c610771b020afc6a77 */\n#if LIBAVCODEC_VER_AT_LEAST(59, 24)\n#define NEW_CHANNEL_LAYOUT\n#endif\n\nvoid janus_pp_setup_avformat(void);\n\nAVFormatContext *janus_pp_create_avformatcontext(const char *format, const char *metadata, const char *destination);\n\nAVStream *janus_pp_new_video_avstream(AVFormatContext *fctx, int codec_id, int width, int height);\nAVStream *janus_pp_new_audio_avstream(AVFormatContext *fctx, int codec_id, int samplerate, int channels, const uint8_t *extradata, int size);\n\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-binary.c",
    "content": "/*! \\file    pp-binary.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate binary files out of binary data recordings\n * \\details  Implementation of the post-processing code needed to\n * generate binary files out of binary data recordings: more precisely,\n * the code simply extracts the data from the packets and appends it to\n * the provided file exactly as it is, with no header/footer.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-binary.h\"\n#include \"../debug.h\"\n\n\nFILE *binary_file = NULL;\n\n/* Processing methods */\nint janus_pp_binary_create(char *destination, char *metadata) {\n\t/* Create the file */\n\tbinary_file = fopen(destination, \"wb\");\n\tif(binary_file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open output file\\n\");\n\t\treturn -1;\n\t}\n\t/* Note: we're creating a binary file whose only content will be the\n\t * binary data messages, so there's no way we can add a text prefix,\n\t * header or intro, and nothing we can do with the metadata either */\n\n\treturn 0;\n}\n\nint janus_pp_binary_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working || !binary_file)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tuint bytes = 0;\n\tuint16_t bufsize = 1500;\n\tuint8_t *buffer = g_malloc0(bufsize);\n\n\twhile(*working && tmp != NULL) {\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked text packet (time ~%\"SCNu64\"s)\\n\", tmp->ts);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Let's read the content and write it to the file */\n\t\tfseek(file, tmp->offset, SEEK_SET);\n\t\tJANUS_LOG(LOG_VERB, \"Reading %d bytes...\\n\", tmp->len);\n\t\tuint16_t total = tmp->len;\n\t\twhile(total > 0) {\n\t\t\tbytes = fread(buffer, sizeof(char), total > bufsize ? bufsize : total, file);\n\t\t\tif(bytes == 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error reading from file...\\n\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Read %d bytes...\\n\", bytes);\n\t\t\tif(fwrite(buffer, sizeof(char), bytes, binary_file) != bytes) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write all the buffer...\\n\");\n\t\t\t}\n\t\t\ttotal -= bytes;\n\t\t}\n\t\tfflush(binary_file);\n\t\t/* Next? */\n\t\ttmp = tmp->next;\n\t}\n\tg_free(buffer);\n\n\treturn 0;\n}\n\nvoid janus_pp_binary_close(void) {\n\t/* Flush and close file */\n\tif(binary_file != NULL) {\n\t\tfflush(binary_file);\n\t\tfclose(binary_file);\n\t}\n\tbinary_file = NULL;\n}\n"
  },
  {
    "path": "src/postprocessing/pp-binary.h",
    "content": "/*! \\file    pp-binary.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate binary files out of binary data recordings (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate .srt files out of text data recordings.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_BINARY\n#define JANUS_PP_BINARY\n\n#include <stdio.h>\n\n#include \"pp-rtp.h\"\n\nint janus_pp_binary_create(char *destination, char *metadata);\nint janus_pp_binary_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_binary_close(void);\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-g711.c",
    "content": "/*! \\file    pp-g711.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .wav files (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate raw .wav files out of G.711 (mu-law or a-law) RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-g711.h\"\n#include \"../debug.h\"\n\n\n/* WAV header */\ntypedef struct janus_pp_g711_wav {\n\tchar riff[4];\n\tuint32_t len;\n\tchar wave[4];\n\tchar fmt[4];\n\tuint32_t formatsize;\n\tuint16_t format;\n\tuint16_t channels;\n\tuint32_t samplerate;\n\tuint32_t avgbyterate;\n\tuint16_t samplebytes;\n\tuint16_t channelbits;\n\tchar data[4];\n\tuint32_t blocksize;\n} janus_pp_g711_wav;\nstatic FILE *wav_file = NULL;\n\n\n/* mu-law decoding table */\nint16_t janus_pp_g711_mulaw_table[256] =\n{\n     -32124,-31100,-30076,-29052,-28028,-27004,-25980,-24956,\n     -23932,-22908,-21884,-20860,-19836,-18812,-17788,-16764,\n     -15996,-15484,-14972,-14460,-13948,-13436,-12924,-12412,\n     -11900,-11388,-10876,-10364, -9852, -9340, -8828, -8316,\n      -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,\n      -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,\n      -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,\n      -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,\n      -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,\n      -1372, -1308, -1244, -1180, -1116, -1052,  -988,  -924,\n       -876,  -844,  -812,  -780,  -748,  -716,  -684,  -652,\n       -620,  -588,  -556,  -524,  -492,  -460,  -428,  -396,\n       -372,  -356,  -340,  -324,  -308,  -292,  -276,  -260,\n       -244,  -228,  -212,  -196,  -180,  -164,  -148,  -132,\n       -120,  -112,  -104,   -96,   -88,   -80,   -72,   -64,\n        -56,   -48,   -40,   -32,   -24,   -16,    -8,     0,\n      32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,\n      23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,\n      15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,\n      11900, 11388, 10876, 10364,  9852,  9340,  8828,  8316,\n       7932,  7676,  7420,  7164,  6908,  6652,  6396,  6140,\n       5884,  5628,  5372,  5116,  4860,  4604,  4348,  4092,\n       3900,  3772,  3644,  3516,  3388,  3260,  3132,  3004,\n       2876,  2748,  2620,  2492,  2364,  2236,  2108,  1980,\n       1884,  1820,  1756,  1692,  1628,  1564,  1500,  1436,\n       1372,  1308,  1244,  1180,  1116,  1052,   988,   924,\n        876,   844,   812,   780,   748,   716,   684,   652,\n        620,   588,   556,   524,   492,   460,   428,   396,\n        372,   356,   340,   324,   308,   292,   276,   260,\n        244,   228,   212,   196,   180,   164,   148,   132,\n        120,   112,   104,    96,    88,    80,    72,    64,\n         56,    48,    40,    32,    24,    16,     8,     0\n};\n\n/* a-law decoding table */\nint16_t janus_pp_g711_alaw_table[256] =\n{\n     -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,\n     -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,\n     -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,\n     -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,\n     -22016,-20992,-24064,-23040,-17920,-16896,-19968,-18944,\n     -30208,-29184,-32256,-31232,-26112,-25088,-28160,-27136,\n     -11008,-10496,-12032,-11520,-8960, -8448, -9984, -9472,\n     -15104,-14592,-16128,-15616,-13056,-12544,-14080,-13568,\n     -344,  -328,  -376,  -360,  -280,  -264,  -312,  -296,\n     -472,  -456,  -504,  -488,  -408,  -392,  -440,  -424,\n     -88,   -72,   -120,  -104,  -24,   -8,    -56,   -40,\n     -216,  -200,  -248,  -232,  -152,  -136,  -184,  -168,\n     -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,\n     -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,\n     -688,  -656,  -752,  -720,  -560,  -528,  -624,  -592,\n     -944,  -912,  -1008, -976,  -816,  -784,  -880,  -848,\n      5504,  5248,  6016,  5760,  4480,  4224,  4992,  4736,\n      7552,  7296,  8064,  7808,  6528,  6272,  7040,  6784,\n      2752,  2624,  3008,  2880,  2240,  2112,  2496,  2368,\n      3776,  3648,  4032,  3904,  3264,  3136,  3520,  3392,\n      22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,\n      30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,\n      11008, 10496, 12032, 11520, 8960,  8448,  9984,  9472,\n      15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,\n      344,   328,   376,   360,   280,   264,   312,   296,\n      472,   456,   504,   488,   408,   392,   440,   424,\n      88,    72,   120,   104,    24,     8,    56,    40,\n      216,   200,   248,   232,   152,   136,   184,   168,\n      1376,  1312,  1504,  1440,  1120,  1056,  1248,  1184,\n      1888,  1824,  2016,  1952,  1632,  1568,  1760,  1696,\n      688,   656,   752,   720,   560,   528,   624,   592,\n      944,   912,  1008,   976,   816,   784,   880,   848\n};\n\n/* Supported target formats */\nstatic const char *janus_pp_g711_formats[] = {\n\t\"wav\", NULL\n};\nconst char **janus_pp_g711_get_extensions(void) {\n\treturn janus_pp_g711_formats;\n}\n\n/* Processing methods */\nint janus_pp_g711_create(char *destination, char *metadata) {\n\t/* Create wav file */\n\twav_file = fopen(destination, \"wb\");\n\tif(wav_file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open output file\\n\");\n\t\treturn -1;\n\t}\n\t/* Add header */\n\tJANUS_LOG(LOG_INFO, \"Writing .wav file header\\n\");\n\tjanus_pp_g711_wav header = {\n\t\t{'R', 'I', 'F', 'F'},\n\t\t0,\n\t\t{'W', 'A', 'V', 'E'},\n\t\t{'f', 'm', 't', ' '},\n\t\t16,\n\t\t1,\n\t\t1,\n\t\t8000,\n\t\t16000,\n\t\t2,\n\t\t16,\n\t\t{'d', 'a', 't', 'a'},\n\t\t0\n\t};\n\t/* Note: .wav files don't seem to support arbitrary comments\n\t * so there's nothing we can do with the provided metadata*/\n\tif(fwrite(&header, 1, sizeof(header), wav_file) != sizeof(header)) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't write WAV header, expect problems...\\n\");\n\t}\n\tfflush(wav_file);\n\treturn 0;\n}\n\nint janus_pp_g711_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tlong int offset = 0;\n\tint bytes = 0, len = 0, last_seq = 0;\n\tuint8_t *buffer = g_malloc0(1500);\n\tint16_t samples[1500];\n\tmemset(samples, 0, sizeof(samples));\n\tsize_t num_samples = 160;\n\twhile(*working && tmp != NULL) {\n\t\tif(tmp->prev != NULL && ((tmp->ts - tmp->prev->ts)/8/20 > 1)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/8000);\n\t\t\tint silence_count = (tmp->ts - tmp->prev->ts)/8/20 - 1;\n\t\t\tint i=0;\n\t\t\tfor(i=0; i<silence_count; i++) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[FILL] Writing silence (seq=%d, index=%d)\\n\",\n\t\t\t\t\ttmp->prev->seq+i+1, i+1);\n\t\t\t\t/* Add silence */\n\t\t\t\tmemset(samples, 0, num_samples*2);\n\t\t\t\tif(wav_file != NULL) {\n\t\t\t\t\tif(fwrite(samples, sizeof(uint16_t), num_samples, wav_file) != num_samples) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write sample...\\n\");\n\t\t\t\t\t}\n\t\t\t\t\tfflush(wav_file);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked audio packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/8000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->audiolevel != -1) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Audio level: %d dB\\n\", tmp->audiolevel);\n\t\t}\n\t\tguint16 diff = tmp->prev == NULL ? 1 : (tmp->seq - tmp->prev->seq);\n\t\tlen = 0;\n\t\t/* RTP payload */\n\t\toffset = tmp->offset+12+tmp->skip;\n\t\tfseek(file, offset, SEEK_SET);\n\t\tlen = tmp->len-12-tmp->skip;\n\t\tif(len < 1) {\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(last_seq == 0)\n\t\t\tlast_seq = tmp->seq;\n\t\tif(tmp->seq < last_seq) {\n\t\t\tlast_seq = tmp->seq;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Writing %d bytes out of %d (seq=%\"SCNu16\", step=%\"SCNu16\", ts=%\"SCNu64\", time=%\"SCNu64\"s)\\n\",\n\t\t\tbytes, tmp->len, tmp->seq, diff, tmp->ts, (tmp->ts-list->ts)/8000);\n\t\t/* Decode and save to wav */\n\t\tuint8_t *data = (uint8_t *)buffer;\n\t\tint i=0;\n\t\tif(tmp->pt == 0) {\n\t\t\t/* mu-law */\n\t\t\tfor(i=0; i<bytes; i++)\n\t\t\t\tsamples[i] = janus_pp_g711_mulaw_table[*data++];\n\t\t} else if(tmp->pt == 8) {\n\t\t\t/* a-law */\n\t\t\tfor(i=0; i<bytes; i++)\n\t\t\t\tsamples[i] = janus_pp_g711_alaw_table[*data++];\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Skipping unsupported payload type %d\\n\", tmp->pt);\n\t\t}\n\t\tif(wav_file != NULL) {\n\t\t\tnum_samples = bytes;\n\t\t\tif(fwrite(samples, sizeof(int16_t), bytes, wav_file) != num_samples) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write sample...\\n\");\n\t\t\t}\n\t\t\tfflush(wav_file);\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tg_free(buffer);\n\treturn 0;\n}\n\nvoid janus_pp_g711_close(void) {\n\t/* Flush and close file */\n\tif(wav_file != NULL) {\n\t\t/* Update the header */\n\t\tfseek(wav_file, 0, SEEK_END);\n\t\tuint32_t size = ftell(wav_file) - 8;\n\t\tfseek(wav_file, 4, SEEK_SET);\n\t\tfwrite(&size, sizeof(uint32_t), 1, wav_file);\n\t\tsize += 8;\n\t\tfseek(wav_file, 40, SEEK_SET);\n\t\tfwrite(&size, sizeof(uint32_t), 1, wav_file);\n\t\tfflush(wav_file);\n\t\tfclose(wav_file);\n\t}\n\twav_file = NULL;\n}\n"
  },
  {
    "path": "src/postprocessing/pp-g711.h",
    "content": "/*! \\file    pp-g711.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .wav files (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate raw .wav files out of G.711 (mu-law or a-law) RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_G711\n#define JANUS_PP_G711\n\n#include <stdio.h>\n\n#include \"pp-rtp.h\"\n\n/* G.711 stuff */\nconst char **janus_pp_g711_get_extensions(void);\nint janus_pp_g711_create(char *destination, char *metadata);\nint janus_pp_g711_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_g711_close(void);\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-g722.c",
    "content": "/*! \\file    pp-g722.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .wav files out of G.722 (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate raw .wav files out of G.722 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined (__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n\n#include \"pp-avformat.h\"\n#include \"pp-g722.h\"\n#include \"../debug.h\"\n\n/* G.722 decoder */\nstatic const AVCodec *dec_codec;\t/* FFmpeg decoding codec */\nstatic AVCodecContext *dec_ctx;\t\t/* FFmpeg decoding context */\n\n/* WAV header */\ntypedef struct janus_pp_g722_wav {\n\tchar riff[4];\n\tuint32_t len;\n\tchar wave[4];\n\tchar fmt[4];\n\tuint32_t formatsize;\n\tuint16_t format;\n\tuint16_t channels;\n\tuint32_t samplerate;\n\tuint32_t avgbyterate;\n\tuint16_t samplebytes;\n\tuint16_t channelbits;\n\tchar data[4];\n\tuint32_t blocksize;\n} janus_pp_g722_wav;\nstatic FILE *wav_file = NULL;\n\n/* Supported target formats */\nstatic const char *janus_pp_g722_formats[] = {\n\t\"wav\", NULL\n};\nconst char **janus_pp_g722_get_extensions(void) {\n\treturn janus_pp_g722_formats;\n}\n\n/* Processing methods */\nint janus_pp_g722_create(char *destination, char *metadata) {\n\tif(destination == NULL)\n\t\treturn -1;\n\tjanus_pp_setup_avformat();\n\t/* Create decoding context */\n#if LIBAVCODEC_VER_AT_LEAST(53, 21)\n\tint codec = AV_CODEC_ID_ADPCM_G722;\n#else\n\tint codec = CODEC_ID_ADPCM_G722;\n#endif\n\tdec_codec = avcodec_find_decoder(codec);\n\tif(!dec_codec) {\n\t\t/* Error finding G.722 codec... */\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported decoder (G.722)...\\n\");\n\t\treturn -1;\n\t}\n\tdec_ctx = avcodec_alloc_context3(dec_codec);\n\tif(!dec_ctx) {\n\t\t/* Error creating FFmpeg context... */\n\t\tJANUS_LOG(LOG_ERR, \"Error creating FFmpeg context...\\n\");\n\t\treturn -1;\n\t}\n\tif(avcodec_open2(dec_ctx, dec_codec, NULL) < 0) {\n\t\t/* Error finding video codec... */\n\t\tJANUS_LOG(LOG_ERR, \"Error opening G.722 decoder...\\n\");\n\t\treturn -1;\n\t}\n\t/* Create wav file */\n\twav_file = fopen(destination, \"wb\");\n\tif(wav_file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open output file\\n\");\n\t\treturn -1;\n\t}\n\t/* Add header */\n\tJANUS_LOG(LOG_INFO, \"Writing .wav file header\\n\");\n\tjanus_pp_g722_wav header = {\n\t\t{'R', 'I', 'F', 'F'},\n\t\t0,\n\t\t{'W', 'A', 'V', 'E'},\n\t\t{'f', 'm', 't', ' '},\n\t\t16,\n\t\t1,\n\t\t1,\n\t\t16000,\n\t\t16000,\n\t\t2,\n\t\t16,\n\t\t{'d', 'a', 't', 'a'},\n\t\t0\n\t};\n\t/* Note: .wav files don't seem to support arbitrary comments\n\t * so there's nothing we can do with the provided metadata*/\n\tif(fwrite(&header, 1, sizeof(header), wav_file) != sizeof(header)) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't write WAV header, expect problems...\\n\");\n\t}\n\tfflush(wav_file);\n\treturn 0;\n}\n\nint janus_pp_g722_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tlong int offset = 0;\n\tint bytes = 0, len = 0, last_seq = 0;\n\tuint8_t *buffer = g_malloc0(1500);\n\tint16_t samples[1500];\n\tmemset(samples, 0, sizeof(samples));\n\tuint num_samples = 320;\n\twhile(*working && tmp != NULL) {\n\t\tif(tmp->prev != NULL && ((tmp->ts - tmp->prev->ts)/8/20 > 1)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/8000);\n\t\t\tint silence_count = (tmp->ts - tmp->prev->ts)/8/20 - 1;\n\t\t\tint i=0;\n\t\t\tfor(i=0; i<silence_count; i++) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[FILL] Writing silence (seq=%d, index=%d)\\n\",\n\t\t\t\t\ttmp->prev->seq+i+1, i+1);\n\t\t\t\t/* Add silence */\n\t\t\t\tmemset(samples, 0, num_samples*2);\n\t\t\t\tif(wav_file != NULL) {\n\t\t\t\t\tif(fwrite(samples, sizeof(uint16_t), num_samples, wav_file) != num_samples) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write sample...\\n\");\n\t\t\t\t\t}\n\t\t\t\t\tfflush(wav_file);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked audio packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/8000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->audiolevel != -1) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Audio level: %d dB\\n\", tmp->audiolevel);\n\t\t}\n\t\tguint16 diff = tmp->prev == NULL ? 1 : (tmp->seq - tmp->prev->seq);\n\t\tlen = 0;\n\t\t/* RTP payload */\n\t\toffset = tmp->offset+12+tmp->skip;\n\t\tfseek(file, offset, SEEK_SET);\n\t\tlen = tmp->len-12-tmp->skip;\n\t\tif(len < 1) {\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(last_seq == 0)\n\t\t\tlast_seq = tmp->seq;\n\t\tif(tmp->seq < last_seq) {\n\t\t\tlast_seq = tmp->seq;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Writing %d bytes out of %d (seq=%\"SCNu16\", step=%\"SCNu16\", ts=%\"SCNu64\", time=%\"SCNu64\"s)\\n\",\n\t\t\tbytes, tmp->len, tmp->seq, diff, tmp->ts, (tmp->ts-list->ts)/8000);\n\t\t/* Decode and save to wav */\n#ifdef FF_API_INIT_PACKET\n\t\tAVPacket *avpacket = av_packet_alloc();\n#else\n\t\tAVPacket avpkt = { 0 }, *avpacket = &avpkt;\n\t\tav_init_packet(avpacket);\n#endif\n\t\tavpacket->data = (uint8_t *)buffer;\n\t\tavpacket->size = bytes;\n\t\tint err = 0;\n#if LIBAVCODEC_VER_AT_LEAST(55,28)\n\t\tAVFrame *frame = av_frame_alloc();\n#else\n\t\tAVFrame *frame = avcodec_alloc_frame();\n#endif\n#ifdef USE_CODECPAR\n\t\terr = avcodec_send_packet(dec_ctx, avpacket);\n\t\tif(err < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error decoding audio frame... (%d, %s)\\n\",\n\t\t\t\terr, av_err2str(err));\n\t\t} else {\n\t\t\terr = avcodec_receive_frame(dec_ctx, frame);\n\t\t}\n\t\tif(err > -1) {\n#else\n\t\tint got_frame = 0;\n\t\terr = avcodec_decode_audio4(dec_ctx, frame, &got_frame, avpacket);\n\t\tif(err < 0 || !got_frame) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error decoding audio frame... (%d, %s)\\n\",\n\t\t\t\terr, av_err2str(err));\n\t\t} else {\n#endif\n\t\t\tif(wav_file != NULL) {\n\t\t\t\tint data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);\n\t\t\t\tint i=0, ch=0;\n\t\t\t\tfor(i=0; i<frame->nb_samples; i++) {\n#ifdef NEW_CHANNEL_LAYOUT\n\t\t\t\t\tint channels = dec_ctx->ch_layout.nb_channels;\n#else\n\t\t\t\t\tint channels = dec_ctx->channels;\n#endif\n\t\t\t\t\tfor(ch=0; ch<channels; ch++) {\n\t\t\t\t\t\tfwrite(frame->data[ch] + data_size*i, 1, data_size, wav_file);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfflush(wav_file);\n\t\t\t}\n\t\t}\n#if LIBAVCODEC_VER_AT_LEAST(55,28)\n\t\tav_frame_free(&frame);\n#else\n\t\tavcodec_free_frame(&frame);\n#endif\n#ifdef FF_API_INIT_PACKET\n\t\tav_packet_free(&avpacket);\n#endif\n\t\ttmp = tmp->next;\n\t}\n\tg_free(buffer);\n\treturn 0;\n}\n\nvoid janus_pp_g722_close(void) {\n\t/* Close decoder */\n\tavcodec_free_context(&dec_ctx);\n\t/* Flush and close file */\n\tif(wav_file != NULL) {\n\t\t/* Update the header */\n\t\tfseek(wav_file, 0, SEEK_END);\n\t\tuint32_t size = ftell(wav_file) - 8;\n\t\tfseek(wav_file, 4, SEEK_SET);\n\t\tfwrite(&size, sizeof(uint32_t), 1, wav_file);\n\t\tsize += 8;\n\t\tfseek(wav_file, 40, SEEK_SET);\n\t\tfwrite(&size, sizeof(uint32_t), 1, wav_file);\n\t\tfflush(wav_file);\n\t\tfclose(wav_file);\n\t}\n\twav_file = NULL;\n}\n"
  },
  {
    "path": "src/postprocessing/pp-g722.h",
    "content": "/*! \\file    pp-g722.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .wav files from G.722 (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate raw .wav files out of G.722 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_G722\n#define JANUS_PP_G722\n\n#include <stdio.h>\n\n#include \"pp-rtp.h\"\n\n/* G.722 stuff */\nconst char **janus_pp_g722_get_extensions(void);\nint janus_pp_g722_create(char *destination, char *metadata);\nint janus_pp_g722_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_g722_close(void);\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-h264.c",
    "content": "/*! \\file    pp-h264.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .mp4 files out of H.264 frames\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .mp4 files out of H.264 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-avformat.h\"\n#include \"pp-h264.h\"\n#include \"../debug.h\"\n\n/* MP4 output */\nstatic AVFormatContext *fctx;\nstatic AVStream *vStream;\n#ifdef USE_CODECPAR\nstatic AVCodecContext *vEncoder;\n#endif\nstatic int max_width = 0, max_height = 0, fps = 0;\n\n/* Supported target formats */\nstatic const char *janus_pp_h264_formats[] = {\n\t\"mp4\", \"mkv\", NULL\n};\nconst char **janus_pp_h264_get_extensions(void) {\n\treturn janus_pp_h264_formats;\n}\n\n/* Processing methods */\nint janus_pp_h264_create(char *destination, char *metadata, gboolean faststart, const char *extension) {\n\tif(destination == NULL)\n\t\treturn -1;\n\n\tjanus_pp_setup_avformat();\n\n\t/* .mkv is Matroska video */\n\tif(!strcasecmp(extension, \"mkv\"))\n\t\textension = \"matroska\";\n\n\t/* Video output */\n\tfctx = avformat_alloc_context();\n\tif(fctx == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error allocating context\\n\");\n\t\treturn -1;\n\t}\n\t/* We save the metadata part as a comment (see #1189) */\n\tif(metadata)\n\t\tav_dict_set(&fctx->metadata, \"comment\", metadata, 0);\n\tfctx->oformat = av_guess_format(extension, NULL, NULL);\n\tif(fctx->oformat == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error guessing format\\n\");\n\t\treturn -1;\n\t}\n    char filename[1024];\n\tsnprintf(filename, sizeof(filename), \"%s\", destination);\n#ifdef USE_CODECPAR\n#if LIBAVCODEC_VER_AT_LEAST(59, 18)\n\tconst AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);\n#else\n\tAVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);\n#endif\n\tif(!codec) {\n\t\t/* Error opening video codec */\n\t\tJANUS_LOG(LOG_ERR, \"Encoder not available\\n\");\n\t\treturn -1;\n\t}\n\tfctx->video_codec = codec;\n\tvStream = avformat_new_stream(fctx, codec);\n\tvStream->id = fctx->nb_streams-1;\n\tvEncoder = avcodec_alloc_context3(codec);\n\tvEncoder->width = max_width;\n\tvEncoder->height = max_height;\n\tvEncoder->time_base = (AVRational){ 1, fps };\n\tvEncoder->pix_fmt = AV_PIX_FMT_YUV420P;\n\tvEncoder->flags |= CODEC_FLAG_GLOBAL_HEADER;\n\tif(avcodec_open2(vEncoder, codec, NULL) < 0) {\n\t\t/* Error opening video codec */\n\t\tJANUS_LOG(LOG_ERR, \"Encoder error\\n\");\n\t\treturn -1;\n\t}\n\tavcodec_parameters_from_context(vStream->codecpar, vEncoder);\n#else\n\tvStream = avformat_new_stream(fctx, 0);\n\tif(vStream == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error adding stream\\n\");\n\t\treturn -1;\n\t}\n#if LIBAVCODEC_VER_AT_LEAST(53, 21)\n\tavcodec_get_context_defaults3(vStream->codec, AVMEDIA_TYPE_VIDEO);\n#else\n\tavcodec_get_context_defaults2(vStream->codec, AVMEDIA_TYPE_VIDEO);\n#endif\n#if LIBAVCODEC_VER_AT_LEAST(54, 25)\n\tvStream->codec->codec_id = AV_CODEC_ID_H264;\n#else\n\tvStream->codec->codec_id = CODEC_ID_H264;\n#endif\n\tvStream->codec->codec_type = AVMEDIA_TYPE_VIDEO;\n\tvStream->codec->time_base = (AVRational){1, fps};\n\tvStream->time_base = (AVRational){1, 90000};\n\tvStream->codec->width = max_width;\n\tvStream->codec->height = max_height;\n\tvStream->codec->pix_fmt = PIX_FMT_YUV420P;\n\t//~ if (fctx->flags & AVFMT_GLOBALHEADER)\n\t\tvStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;\n#endif\n\tAVDictionary *options = NULL;\n\tif(faststart)\n\t\tav_dict_set(&options, \"movflags\", \"+faststart\", 0);\n\n\tint res = avio_open2(&fctx->pb, filename, AVIO_FLAG_WRITE, NULL, &options);\n\tif(res < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error opening file for output (%d, %s)\\n\", res, av_err2str(res));\n\t\treturn -1;\n\t}\n#if LIBAVFORMAT_VER_AT_LEAST(58, 7)\n\tfctx->url = g_strdup(filename);\n#endif\n\tif(avformat_write_header(fctx, &options) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error writing header\\n\");\n\t\treturn -1;\n\t}\n\treturn 0;\n}\n\n/* Helpers to decode Exp-Golomb */\nstatic uint32_t janus_pp_h264_eg_getbit(uint8_t *base, uint32_t offset) {\n\treturn ((*(base + (offset >> 0x3))) >> (0x7 - (offset & 0x7))) & 0x1;\n}\n\nstatic uint32_t janus_pp_h264_eg_decode(uint8_t *base, uint32_t *offset) {\n\tuint32_t zeros = 0;\n\twhile(janus_pp_h264_eg_getbit(base, (*offset)++) == 0)\n\t\tzeros++;\n\tuint32_t res = 1 << zeros;\n\tif(zeros > 0) {\n\t\tint32_t i = 0;\n\t\tfor(i=zeros-1; i>=0; i--) {\n\t\t\tres |= janus_pp_h264_eg_getbit(base, (*offset)++) << i;\n\t\t}\n\t}\n\treturn res-1;\n}\n\n/* Helper to parse a SPS (only to get the video resolution) */\nstatic void janus_pp_h264_parse_sps(char *buffer, int *width, int *height) {\n\t/* Let's check if it's the right profile, first */\n\tint index = 1;\n\tint profile_idc = *(buffer+index);\n\tif(profile_idc != 66) {\n\t\tJANUS_LOG(LOG_WARN, \"Profile is not baseline (%d != 66)\\n\", profile_idc);\n\t}\n\t/* Then let's skip 2 bytes and evaluate/skip the rest */\n\tindex += 3;\n\tuint32_t offset = 0;\n\tuint8_t *base = (uint8_t *)(buffer+index);\n\t/* Skip seq_parameter_set_id */\n\tjanus_pp_h264_eg_decode(base, &offset);\n\tif(profile_idc >= 100) {\n\t\t/* Skip chroma_format_idc */\n\t\tjanus_pp_h264_eg_decode(base, &offset);\n\t\t/* Skip bit_depth_luma_minus8 */\n\t\tjanus_pp_h264_eg_decode(base, &offset);\n\t\t/* Skip bit_depth_chroma_minus8 */\n\t\tjanus_pp_h264_eg_decode(base, &offset);\n\t\t/* Skip qpprime_y_zero_transform_bypass_flag */\n\t\tjanus_pp_h264_eg_getbit(base, offset++);\n\t\t/* Skip seq_scaling_matrix_present_flag */\n\t\tjanus_pp_h264_eg_getbit(base, offset++);\n\t}\n\t/* Skip log2_max_frame_num_minus4 */\n\tjanus_pp_h264_eg_decode(base, &offset);\n\t/* Evaluate pic_order_cnt_type */\n\tint pic_order_cnt_type = janus_pp_h264_eg_decode(base, &offset);\n\tif(pic_order_cnt_type == 0) {\n\t\t/* Skip log2_max_pic_order_cnt_lsb_minus4 */\n\t\tjanus_pp_h264_eg_decode(base, &offset);\n\t} else if(pic_order_cnt_type == 1) {\n\t\t/* Skip delta_pic_order_always_zero_flag, offset_for_non_ref_pic,\n\t\t * offset_for_top_to_bottom_field and num_ref_frames_in_pic_order_cnt_cycle */\n\t\tjanus_pp_h264_eg_getbit(base, offset++);\n\t\tjanus_pp_h264_eg_decode(base, &offset);\n\t\tjanus_pp_h264_eg_decode(base, &offset);\n\t\tint num_ref_frames_in_pic_order_cnt_cycle = janus_pp_h264_eg_decode(base, &offset);\n\t\tint i = 0;\n\t\tfor(i=0; i<num_ref_frames_in_pic_order_cnt_cycle; i++) {\n\t\t\tjanus_pp_h264_eg_decode(base, &offset);\n\t\t}\n\t}\n\t/* Skip max_num_ref_frames and gaps_in_frame_num_value_allowed_flag */\n\tjanus_pp_h264_eg_decode(base, &offset);\n\tjanus_pp_h264_eg_getbit(base, offset++);\n\t/* We need the following three values */\n\tint pic_width_in_mbs_minus1 = janus_pp_h264_eg_decode(base, &offset);\n\tint pic_height_in_map_units_minus1 = janus_pp_h264_eg_decode(base, &offset);\n\tint frame_mbs_only_flag = janus_pp_h264_eg_getbit(base, offset++);\n\tif(!frame_mbs_only_flag) {\n\t\t/* Skip mb_adaptive_frame_field_flag */\n\t\tjanus_pp_h264_eg_getbit(base, offset++);\n\t}\n\t/* Skip direct_8x8_inference_flag */\n\tjanus_pp_h264_eg_getbit(base, offset++);\n\t/* We need the following value to evaluate offsets, if any */\n\tint frame_cropping_flag = janus_pp_h264_eg_getbit(base, offset++);\n\tint frame_crop_left_offset = 0, frame_crop_right_offset = 0,\n\t\tframe_crop_top_offset = 0, frame_crop_bottom_offset = 0;\n\tif(frame_cropping_flag) {\n\t\tframe_crop_left_offset = janus_pp_h264_eg_decode(base, &offset);\n\t\tframe_crop_right_offset = janus_pp_h264_eg_decode(base, &offset);\n\t\tframe_crop_top_offset = janus_pp_h264_eg_decode(base, &offset);\n\t\tframe_crop_bottom_offset = janus_pp_h264_eg_decode(base, &offset);\n\t}\n\t/* Skip vui_parameters_present_flag */\n\tjanus_pp_h264_eg_getbit(base, offset++);\n\n\t/* We skipped what we didn't care about and got what we wanted, compute width/height */\n\tif(width)\n\t\t*width = ((pic_width_in_mbs_minus1 +1)*16) - frame_crop_left_offset*2 - frame_crop_right_offset*2;\n\tif(height)\n\t\t*height = ((2 - frame_mbs_only_flag)* (pic_height_in_map_units_minus1 +1) * 16) - (frame_crop_top_offset * 2) - (frame_crop_bottom_offset * 2);\n}\n\n\nint janus_pp_h264_preprocess(FILE *file, janus_pp_frame_packet *list, json_t *info) {\n\tif(!file || !list)\n\t\treturn -1;\n\tjson_t *resolutions = NULL;\n\tint last_width = -1, last_height = -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tint bytes = 0, min_ts_diff = 0, max_ts_diff = 0;\n\tint rotation = -1;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\twhile(tmp) {\n\t\tif(tmp->prev != NULL && tmp->ts > tmp->prev->ts) {\n\t\t\tif(tmp->ts > tmp->prev->ts) {\n\t\t\t\tint diff = tmp->ts - tmp->prev->ts;\n\t\t\t\tif(min_ts_diff == 0 || min_ts_diff > diff)\n\t\t\t\t\tmin_ts_diff = diff;\n\t\t\t\tif(max_ts_diff == 0 || max_ts_diff < diff)\n\t\t\t\t\tmax_ts_diff = diff;\n\t\t\t}\n\t\t\tif(tmp->seq - tmp->prev->seq > 1) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/90000);\n\t\t\t}\n\t\t}\n\t\t/* Parse H264 header now */\n\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\tint len = tmp->len-12-tmp->skip;\n\t\tif(len < 1) {\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif((prebuffer[0] & 0x1F) == 7) {\n\t\t\t/* SPS, see if we can extract the width/height as well */\n\t\t\tJANUS_LOG(LOG_VERB, \"Parsing width/height\\n\");\n\t\t\tint width = 0, height = 0;\n\t\t\tjanus_pp_h264_parse_sps(prebuffer, &width, &height);\n\t\t\tif(width*height > max_width*max_height) {\n\t\t\t\tmax_width = width;\n\t\t\t\tmax_height = height;\n\t\t\t}\n\t\t\tif(info && (last_width != width || last_height != height)) {\n\t\t\t\tlast_width = width;\n\t\t\t\tlast_height = height;\n\t\t\t\tif(resolutions == NULL) {\n\t\t\t\t\tresolutions = json_array();\n\t\t\t\t\tjson_object_set_new(info, \"resolution\", resolutions);\n\t\t\t\t}\n\t\t\t\tjson_t *resolution = json_object();\n\t\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\t\tjson_object_set_new(resolution, \"ts\", json_real(ts));\n\t\t\t\tjson_object_set_new(resolution, \"width\", json_integer(width));\n\t\t\t\tjson_object_set_new(resolution, \"height\", json_integer(height));\n\t\t\t\tjson_array_append_new(resolutions, resolution);\n\t\t\t}\n\t\t} else if((prebuffer[0] & 0x1F) == 24) {\n\t\t\t/* May we find an SPS in this STAP-A? */\n\t\t\tJANUS_LOG(LOG_HUGE, \"Parsing STAP-A...\\n\");\n\t\t\tchar *buffer = prebuffer;\n\t\t\tbuffer++;\n\t\t\tint tot = len-1;\n\t\t\tuint16_t psize = 0;\n\t\t\twhile(tot > 0) {\n\t\t\t\tmemcpy(&psize, buffer, 2);\n\t\t\t\tpsize = ntohs(psize);\n\t\t\t\tbuffer += 2;\n\t\t\t\ttot -= 2;\n\t\t\t\tint nal = *buffer & 0x1F;\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- NALU of size %u: %d\\n\", psize, nal);\n\t\t\t\tif(nal == 7) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Parsing width/height\\n\");\n\t\t\t\t\tint width = 0, height = 0;\n\t\t\t\t\tjanus_pp_h264_parse_sps(buffer, &width, &height);\n\t\t\t\t\tif(width*height > max_width*max_height) {\n\t\t\t\t\t\tmax_width = width;\n\t\t\t\t\t\tmax_height = height;\n\t\t\t\t\t}\n\t\t\t\t\tif(info && (last_width != width || last_height != height)) {\n\t\t\t\t\t\tlast_width = width;\n\t\t\t\t\t\tlast_height = height;\n\t\t\t\t\t\tif(resolutions == NULL) {\n\t\t\t\t\t\t\tresolutions = json_array();\n\t\t\t\t\t\t\tjson_object_set_new(info, \"resolution\", resolutions);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjson_t *resolution = json_object();\n\t\t\t\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\t\t\t\tjson_object_set_new(resolution, \"ts\", json_real(ts));\n\t\t\t\t\t\tjson_object_set_new(resolution, \"width\", json_integer(width));\n\t\t\t\t\t\tjson_object_set_new(resolution, \"height\", json_integer(height));\n\t\t\t\t\t\tjson_array_append_new(resolutions, resolution);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbuffer += psize;\n\t\t\t\ttot -= psize;\n\t\t\t}\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked video packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/90000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->rotation != -1 && tmp->rotation != rotation) {\n\t\t\trotation = tmp->rotation;\n\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\tJANUS_LOG(LOG_INFO, \"[%8.3fs] Video rotation: %d degrees\\n\", ts, rotation);\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tint mean_ts = min_ts_diff;\t/* FIXME: was an actual mean, (max_ts_diff+min_ts_diff)/2; */\n\tfps = (90000/(mean_ts > 0 ? mean_ts : 30));\n\tJANUS_LOG(LOG_INFO, \"  -- %dx%d (fps [%d,%d] ~ %d)\\n\", max_width, max_height, min_ts_diff, max_ts_diff, fps);\n\tif(max_width == 0 && max_height == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No resolution info?? assuming 640x480...\\n\");\n\t\tmax_width = 640;\n\t\tmax_height = 480;\n\t}\n\tif(max_width < 160) {\n\t\tJANUS_LOG(LOG_WARN, \"Width seems weirdly low (%d), setting 640 instead...\\n\", max_width);\n\t\tmax_width = 640;\n\t}\n\tif(max_height < 120) {\n\t\tJANUS_LOG(LOG_WARN, \"Height seems weirdly low (%d), setting 480 instead...\\n\", max_height);\n\t\tmax_height = 480;\n\t}\n\tif(fps == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No fps?? assuming 1...\\n\");\n\t\tfps = 1;\t/* Prevent divide by zero error */\n\t}\n\treturn 0;\n}\n\nint janus_pp_h264_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\n\tint bytes = 0, numBytes = max_width*max_height*3;\t/* FIXME */\n\tuint8_t *received_frame = g_malloc0(numBytes);\n\tuint8_t *buffer = g_malloc0(numBytes), *start = buffer;\n\tint len = 0, frameLen = 0;\n\tint keyFrame = 0;\n\tgboolean keyframe_found = FALSE;\n#ifdef FF_API_INIT_PACKET\n\tAVPacket *packet = av_packet_alloc();\n#else\n\tAVPacket pkt = { 0 }, *packet = &pkt;\n#endif\n\tAVRational timebase = {1, 90000};\n\n\twhile(*working && tmp != NULL) {\n\t\tkeyFrame = 0;\n\t\tframeLen = 0;\n\t\tlen = 0;\n\t\twhile(tmp != NULL) {\n\t\t\tif(tmp->drop) {\n\t\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* RTP payload */\n\t\t\tbuffer = start;\n\t\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\t\tlen = tmp->len-12-tmp->skip;\n\t\t\tif(len < 1) {\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\t\tif(bytes != len) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* H.264 depay */\n\t\t\tint jump = 0;\n\t\t\tuint8_t fragment = *buffer & 0x1F;\n\t\t\tuint8_t nal = *(buffer+1) & 0x1F;\n\t\t\tuint8_t start_bit = *(buffer+1) & 0x80;\n\t\t\tif(fragment == 28 || fragment == 29)\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Fragment=%d, NAL=%d, Start=%d (len=%d, frameLen=%d)\\n\", fragment, nal, start_bit, len, frameLen);\n\t\t\telse\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Fragment=%d (len=%d, frameLen=%d)\\n\", fragment, len, frameLen);\n\t\t\tif(fragment == 5 ||\n\t\t\t\t\t((fragment == 28 || fragment == 29) && nal == 5 && start_bit == 128)) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"(seq=%\"SCNu16\", ts=%\"SCNu64\") Key frame\\n\", tmp->seq, tmp->ts);\n\t\t\t\tkeyFrame = 1;\n\t\t\t\t/* Is this the first keyframe we find? */\n\t\t\t\tif(!keyframe_found) {\n\t\t\t\t\tkeyframe_found = TRUE;\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"First keyframe: %\"SCNu64\"\\n\", tmp->ts-list->ts);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Frame manipulation */\n\t\t\tif((fragment > 0) && (fragment < 24)) {\t/* Add a start code */\n\t\t\t\tuint8_t *temp = received_frame + frameLen;\n\t\t\t\tmemset(temp, 0x00, 1);\n\t\t\t\tmemset(temp + 1, 0x00, 1);\n\t\t\t\tmemset(temp + 2, 0x01, 1);\n\t\t\t\tframeLen += 3;\n\t\t\t} else if(fragment == 24) {\t/* STAP-A */\n\t\t\t\t/* De-aggregate the NALs and write each of them separately */\n\t\t\t\tbuffer++;\n\t\t\t\tint tot = len-1;\n\t\t\t\tuint16_t psize = 0;\n\t\t\t\twhile(tot > 0) {\n\t\t\t\t\tmemcpy(&psize, buffer, 2);\n\t\t\t\t\tpsize = ntohs(psize);\n\t\t\t\t\tif((frameLen + psize) >= numBytes) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid size %u + %\"SCNu16\" (exceeds buffer size)\\n\", frameLen, psize);\n\t\t\t\t\t\t/* Done, we'll wait for the next video data to write the frame */\n\t\t\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\ttmp = tmp->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tbuffer += 2;\n\t\t\t\t\ttot -= 2;\n\t\t\t\t\t/* Now we have a single NAL */\n\t\t\t\t\tuint8_t *temp = received_frame + frameLen;\n\t\t\t\t\tmemset(temp, 0x00, 1);\n\t\t\t\t\tmemset(temp + 1, 0x00, 1);\n\t\t\t\t\tmemset(temp + 2, 0x01, 1);\n\t\t\t\t\tframeLen += 3;\n\t\t\t\t\tmemcpy(received_frame + frameLen, buffer, psize);\n\t\t\t\t\tframeLen += psize;\n\t\t\t\t\t/* Go on */\n\t\t\t\t\tbuffer += psize;\n\t\t\t\t\ttot -= psize;\n\t\t\t\t}\n\t\t\t\t/* Done, we'll wait for the next video data to write the frame */\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t} else if((fragment == 28) || (fragment == 29)) {\t/* FIXME true fr FU-A, not FU-B */\n\t\t\t\tuint8_t indicator = *buffer;\n\t\t\t\tuint8_t header = *(buffer+1);\n\t\t\t\tjump = 2;\n\t\t\t\tlen -= 2;\n\t\t\t\tif(header & 0x80) {\n\t\t\t\t\t/* First part of fragmented packet (S bit set) */\n\t\t\t\t\tuint8_t *temp = received_frame + frameLen;\n\t\t\t\t\tmemset(temp, 0x00, 1);\n\t\t\t\t\tmemset(temp + 1, 0x00, 1);\n\t\t\t\t\tmemset(temp + 2, 0x01, 1);\n\t\t\t\t\tmemset(temp + 3, (indicator & 0xE0) | (header & 0x1F), 1);\n\t\t\t\t\tframeLen += 4;\n\t\t\t\t} else if (header & 0x40) {\n\t\t\t\t\t/* Last part of fragmented packet (E bit set) */\n\t\t\t\t}\n\t\t\t}\n\t\t\tmemcpy(received_frame + frameLen, buffer+jump, len);\n\t\t\tframeLen += len;\n\t\t\tif(len == 0)\n\t\t\t\tbreak;\n\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\tbreak;\n\t\t\ttmp = tmp->next;\n\t\t}\n\t\tif(frameLen > 0) {\n\t\t\t/* Save the frame */\n\t\t\tmemset(received_frame + frameLen, 0, FF_INPUT_BUFFER_PADDING_SIZE);\n\n#ifdef FF_API_INIT_PACKET\n\t\t\tav_packet_unref(packet);\n#else\n\t\t\tav_init_packet(packet);\n#endif\n\t\t\tpacket->stream_index = 0;\n\t\t\tpacket->data = received_frame;\n\t\t\tpacket->size = frameLen;\n\t\t\tif(keyFrame)\n\t\t\t\tpacket->flags |= AV_PKT_FLAG_KEY;\n\n\t\t\t/* First we save to the file... */\n\t\t\tpacket->pts = packet->dts = av_rescale_q(tmp->ts-list->ts, timebase, fctx->streams[0]->time_base);\n\t\t\tJANUS_LOG(LOG_HUGE, \"%\"SCNu64\" - %\"SCNu64\" --> %\"SCNu64\"\\n\",\n\t\t\t\ttmp->ts, list->ts, packet->pts);\n\t\t\tif(fctx) {\n\t\t\t\tint res = av_write_frame(fctx, packet);\n\t\t\t\tif(res < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error writing video frame to file... (error %d, %s)\\n\",\n\t\t\t\t\t\tres, av_err2str(res));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n#ifdef FF_API_INIT_PACKET\n\tav_packet_free(&packet);\n#endif\n\tg_free(received_frame);\n\tg_free(start);\n\treturn 0;\n}\n\n/* Close MP4 file */\nvoid janus_pp_h264_close(void) {\n\tif(fctx != NULL) {\n\t\tav_write_trailer(fctx);\n#ifdef USE_CODECPAR\n\tif(vEncoder != NULL)\n\t\tavcodec_free_context(&vEncoder);\n#else\n\tif(vStream != NULL && vStream->codec != NULL)\n\t\tavcodec_free_context(&(vStream->codec));\n#endif\n\t\tavio_close(fctx->pb);\n\t\tavformat_free_context(fctx);\n\t}\n}\n"
  },
  {
    "path": "src/postprocessing/pp-h264.h",
    "content": "/*! \\file    pp-h264.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .mp4 files out of H.264 frames (headers)\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .mp4 files out of H.264 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_H264\n#define JANUS_PP_H264\n\n#include <stdio.h>\n#include <jansson.h>\n\n#include \"pp-rtp.h\"\n\n/* H.264 stuff */\nconst char **janus_pp_h264_get_extensions(void);\nint janus_pp_h264_create(char *destination, char *metadata, gboolean faststart, const char *extension);\nint janus_pp_h264_preprocess(FILE *file, janus_pp_frame_packet *list, json_t *info);\nint janus_pp_h264_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_h264_close(void);\n\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-h265.c",
    "content": "/*! \\file    pp-h265.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .mp4 files out of H.265 frames\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .mp4 files out of H.265 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-avformat.h\"\n#include \"pp-h265.h\"\n#include \"../debug.h\"\n\n/* MP4 output */\nstatic AVFormatContext *fctx;\nstatic AVStream *vStream;\n#ifdef USE_CODECPAR\nstatic AVCodecContext *vEncoder;\n#endif\nstatic int max_width = 0, max_height = 0, fps = 0;\n\n/* Supported target formats */\nstatic const char *janus_pp_h265_formats[] = {\n\t\"mp4\", \"mkv\", NULL\n};\nconst char **janus_pp_h265_get_extensions(void) {\n\treturn janus_pp_h265_formats;\n}\n\n/* Processing methods */\nint janus_pp_h265_create(char *destination, char *metadata, gboolean faststart, const char *extension) {\n\tif(destination == NULL)\n\t\treturn -1;\n\n\tjanus_pp_setup_avformat();\n\n\t/* .mkv is Matroska video */\n\tif(!strcasecmp(extension, \"mkv\"))\n\t\textension = \"matroska\";\n\n\t/* Video output */\n\tfctx = avformat_alloc_context();\n\tif(fctx == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error allocating context\\n\");\n\t\treturn -1;\n\t}\n\t/* We save the metadata part as a comment (see #1189) */\n\tif(metadata)\n\t\tav_dict_set(&fctx->metadata, \"comment\", metadata, 0);\n\tfctx->oformat = av_guess_format(extension, NULL, NULL);\n\tif(fctx->oformat == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error guessing format\\n\");\n\t\treturn -1;\n\t}\n    char filename[1024];\n\tsnprintf(filename, sizeof(filename), \"%s\", destination);\n#ifdef USE_CODECPAR\n#if LIBAVCODEC_VER_AT_LEAST(59, 18)\n\tconst AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H265);\n#else\n\tAVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H265);\n#endif\n\tif(!codec) {\n\t\t/* Error opening video codec */\n\t\tJANUS_LOG(LOG_ERR, \"Encoder not available\\n\");\n\t\treturn -1;\n\t}\n\tfctx->video_codec = codec;\n\tvStream = avformat_new_stream(fctx, codec);\n\tvStream->id = fctx->nb_streams-1;\n\tvEncoder = avcodec_alloc_context3(codec);\n\tvEncoder->width = max_width;\n\tvEncoder->height = max_height;\n\tvEncoder->time_base = (AVRational){ 1, fps };\n\tvEncoder->pix_fmt = AV_PIX_FMT_YUV420P;\n\tvEncoder->flags |= CODEC_FLAG_GLOBAL_HEADER;\n\tif(avcodec_open2(vEncoder, codec, NULL) < 0) {\n\t\t/* Error opening video codec */\n\t\tJANUS_LOG(LOG_ERR, \"Encoder error\\n\");\n\t\treturn -1;\n\t}\n\tavcodec_parameters_from_context(vStream->codecpar, vEncoder);\n#else\n\tvStream = avformat_new_stream(fctx, 0);\n\tif(vStream == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error adding stream\\n\");\n\t\treturn -1;\n\t}\n#if LIBAVCODEC_VER_AT_LEAST(53, 21)\n\tavcodec_get_context_defaults3(vStream->codec, AVMEDIA_TYPE_VIDEO);\n#else\n\tavcodec_get_context_defaults2(vStream->codec, AVMEDIA_TYPE_VIDEO);\n#endif\n#if LIBAVCODEC_VER_AT_LEAST(54, 25)\n\tvStream->codec->codec_id = AV_CODEC_ID_H265;\n#else\n\tvStream->codec->codec_id = CODEC_ID_H265;\n#endif\n\tvStream->codec->codec_type = AVMEDIA_TYPE_VIDEO;\n\tvStream->codec->time_base = (AVRational){1, fps};\n\tvStream->time_base = (AVRational){1, 90000};\n\tvStream->codec->width = max_width;\n\tvStream->codec->height = max_height;\n\tvStream->codec->pix_fmt = PIX_FMT_YUV420P;\n\tvStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;\n#endif\n\tAVDictionary *options = NULL;\n\tif(faststart)\n\t\tav_dict_set(&options, \"movflags\", \"+faststart\", 0);\n\n\tint res = avio_open2(&fctx->pb, filename, AVIO_FLAG_WRITE, NULL, &options);\n\tif(res < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error opening file for output (%d, %s)\\n\", res, av_err2str(res));\n\t\treturn -1;\n\t}\n#if LIBAVFORMAT_VER_AT_LEAST(58, 7)\n\tfctx->url = g_strdup(filename);\n#endif\n\tif(avformat_write_header(fctx, &options) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error writing header\\n\");\n\t\treturn -1;\n\t}\n\treturn 0;\n}\n\n/* Helpers to decode Exp-Golomb */\nstatic uint32_t janus_pp_h265_eg_getbit(uint8_t *base, uint32_t offset) {\n\treturn ((*(base + (offset >> 0x3))) >> (0x7 - (offset & 0x7))) & 0x1;\n}\n\nstatic uint32_t janus_pp_h265_eg_getbits(uint8_t *base, uint8_t num, uint32_t *offset) {\n\tuint32_t res = 0;\n\tint32_t i = 0;\n\tfor(i=num-1; i>=0; i--) {\n\t\tres |= janus_pp_h265_eg_getbit(base, (*offset)++) << i;\n\t}\n\treturn res;\n}\n\nstatic uint32_t janus_pp_h265_eg_decode(uint8_t *base, uint32_t *offset) {\n\tuint32_t zeros = 0;\n\twhile(janus_pp_h265_eg_getbit(base, (*offset)++) == 0)\n\t\tzeros++;\n\tuint32_t res = 1 << zeros;\n\tif(zeros > 0) {\n\t\tint32_t i = 0;\n\t\tfor(i=zeros-1; i>=0; i--) {\n\t\t\tres |= janus_pp_h265_eg_getbit(base, (*offset)++) << i;\n\t\t}\n\t}\n\treturn res-1;\n}\n\n/* Helper to parse a SPS (only to get the video resolution) */\nstatic void janus_pp_h265_parse_sps(char *buffer, int *width, int *height) {\n\t/* Get the layer ID first */\n\tuint16_t unit = 0;\n\tmemcpy(&unit, buffer, sizeof(uint16_t));\n\tunit = ntohs(unit);\n\tuint8_t lid = (unit & 0x01F8) >> 3;\n\tgboolean multilayer = (lid > 0);\n\tuint8_t sps_maxorext_m1 = 0;\n\t/* Evaluate/skip everything until we get to the resolution */\n\tuint32_t offset = 0;\n\tuint8_t *base = (uint8_t *)(buffer+2);\n\tint i = 0;\n\t/* Skip sps_video_parameter_set_id (4 bits) */\n\tjanus_pp_h265_eg_getbits(base, 4, &offset);\n\tif(lid == 0) {\n\t\t/* Skip sps_max_sub_layers_minus1 (3 bits) */\n\t\tsps_maxorext_m1 = janus_pp_h265_eg_getbits(base, 3, &offset);\n\t} else {\n\t\t/* Skip sps_ext_or_max_sub_layers_minus1 (3 bits) */\n\t\tsps_maxorext_m1 = janus_pp_h265_eg_getbits(base, 3, &offset);\n\t\tif(sps_maxorext_m1 == 7)\n\t\t\tmultilayer = TRUE;\n\t}\n\tif(!multilayer) {\n\t\t/* Skip sps_temporal_id_nesting_flag (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t/* profile_tier_level is variable, start skipping general_profile_space (2 bits) */\n\t\tjanus_pp_h265_eg_getbits(base, 2, &offset);\n\t\t/* Skip general_tier_flag (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t/* Skip general_profile_idc (5 bits) */\n\t\tjanus_pp_h265_eg_getbits(base, 5, &offset);\n\t\t/* Skip general_profile_compatibility_flag (32 bits) */\n\t\tjanus_pp_h265_eg_getbits(base, 32, &offset);\n\t\t/* Skip general_progressive_source_flag (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t/* Skip general_interlaced_source_flag (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t/* Skip general_non_packed_constraint_flag (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t/* Skip general_frame_only_constraint_flag (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t/* Skip general_reserved_zero_43bits (43 bits) */\n\t\tjanus_pp_h265_eg_getbits(base, 43, &offset);\n\t\t/* Skip general_reserved_zero_bit (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t/* Skip general_level_idc (8 bits) */\n\t\tjanus_pp_h265_eg_getbits(base, 8, &offset);\n\t\t/* Skip sub layer bits (2 per layer-1) */\n\t\tif(sps_maxorext_m1) {\n\t\t\tfor(i=0; i<8; i++) {\n\t\t\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t\t\t}\n\t\t}\n\t\t/* FIXME There are other things to skip for multiple layers... */\n\t}\n\t/* Skip sps_seq_parameter_set_id */\n\tjanus_pp_h265_eg_decode(base, &offset);\n\t/* Skip chroma_format_idc */\n\tuint32_t cfidc = janus_pp_h265_eg_decode(base, &offset);\n\tif(cfidc == 3) {\n\t\t/* Skip separate_colour_plane_flag (1 bit) */\n\t\tjanus_pp_h265_eg_getbit(base, offset++);\n\t}\n\t/* We need pic_width_in_luma_samples and pic_heigth_in_luma_samples */\n\t*width = janus_pp_h265_eg_decode(base, &offset);\n\t*height = janus_pp_h265_eg_decode(base, &offset);\n}\n\nint janus_pp_h265_preprocess(FILE *file, janus_pp_frame_packet *list, json_t *info) {\n\tif(!file || !list)\n\t\treturn -1;\n\tjson_t *resolutions = NULL;\n\tint last_width = -1, last_height = -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tint bytes = 0, min_ts_diff = 0, max_ts_diff = 0;\n\tint rotation = -1;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\twhile(tmp) {\n\t\tif(tmp->prev != NULL && tmp->ts > tmp->prev->ts) {\n\t\t\tif(tmp->ts > tmp->prev->ts) {\n\t\t\t\tint diff = tmp->ts - tmp->prev->ts;\n\t\t\t\tif(min_ts_diff == 0 || min_ts_diff > diff)\n\t\t\t\t\tmin_ts_diff = diff;\n\t\t\t\tif(max_ts_diff == 0 || max_ts_diff < diff)\n\t\t\t\t\tmax_ts_diff = diff;\n\t\t\t}\n\t\t\tif(tmp->seq - tmp->prev->seq > 1) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/90000);\n\t\t\t}\n\t\t}\n\t\t/* Read the packet */\n\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\tint len = tmp->len-12-tmp->skip;\n\t\tif(len < 1) {\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tbytes = fread(prebuffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\ttmp->drop = TRUE;\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Parse H.265 header now */\n\t\tif(len < 2) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Packet too small...\\n\");\n\t\t\ttmp->drop = TRUE;\n\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\tbreak;\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tuint16_t unit = 0;\n\t\tmemcpy(&unit, prebuffer, sizeof(uint16_t));\n\t\tunit = ntohs(unit);\n\t\tuint8_t fbit = (unit & 0x8000) >> 15;\n\t\tuint8_t type = (unit & 0x7E00) >> 9;\n\t\tuint8_t lid = (unit & 0x01F8) >> 3;\n\t\tuint8_t tid = (unit & 0x0007);\n\t\tif(type == 32) {\n\t\t\t/* VPS */\n\t\t\tJANUS_LOG(LOG_HUGE, \"[VPS] %u/%u/%u/%u\\n\", fbit, type, lid, tid);\n\t\t} else if(type == 33) {\n\t\t\t/* SPS */\n\t\t\tJANUS_LOG(LOG_HUGE, \"[SPS] %u/%u/%u/%u\\n\", fbit, type, lid, tid);\n\t\t\t/* Get rid of the Emulation Prevention code, if present */\n\t\t\tint i = 0, j = 0, zeros = 0;\n\t\t\tfor(i=0; i<len; i++) {\n\t\t\t\tif(zeros == 2 && prebuffer[i] == 0x03) {\n\t\t\t\t\t/* Found, get rid of it */\n\t\t\t\t\ti++;\n\t\t\t\t\tzeros = 0;\n\t\t\t\t}\n\t\t\t\t/* Update the content of the buffer as we go along */\n\t\t\t\tprebuffer[j] = prebuffer[i];\n\t\t\t\tif(prebuffer[i] == 0x00) {\n\t\t\t\t\t/* Found a zero, may be part of a start code */\n\t\t\t\t\tzeros++;\n\t\t\t\t} else {\n\t\t\t\t\t/* Not a zero, so not the beginning of a start code */\n\t\t\t\t\tzeros = 0;\n\t\t\t\t}\n\t\t\t\tj++;\n\t\t\t}\n\t\t\t/* Update the length of the buffer */\n\t\t\tlen = j;\n\t\t\t/* Parse to get width/height */\n\t\t\tint width = 0, height = 0;\n\t\t\tjanus_pp_h265_parse_sps(prebuffer, &width, &height);\n\t\t\tif(width*height > max_width*max_height) {\n\t\t\t\tmax_width = width;\n\t\t\t\tmax_height = height;\n\t\t\t}\n\t\t\tif(info && (last_width != width || last_height != height)) {\n\t\t\t\tlast_width = width;\n\t\t\t\tlast_height = height;\n\t\t\t\tif(resolutions == NULL) {\n\t\t\t\t\tresolutions = json_array();\n\t\t\t\t\tjson_object_set_new(info, \"resolution\", resolutions);\n\t\t\t\t}\n\t\t\t\tjson_t *resolution = json_object();\n\t\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\t\tjson_object_set_new(resolution, \"ts\", json_real(ts));\n\t\t\t\tjson_object_set_new(resolution, \"width\", json_integer(width));\n\t\t\t\tjson_object_set_new(resolution, \"height\", json_integer(height));\n\t\t\t\tjson_array_append_new(resolutions, resolution);\n\t\t\t}\n\t\t} else if(type == 34) {\n\t\t\t/* PPS */\n\t\t\tJANUS_LOG(LOG_HUGE, \"[PPS] %u/%u/%u/%u\\n\", fbit, type, lid, tid);\n\t\t} else if(type == 48) {\n\t\t\t/* AP */\n\t\t\tJANUS_LOG(LOG_HUGE, \"[AP] %u/%u/%u/%u\\n\", fbit, type, lid, tid);\n\t\t\tuint8_t *end = (uint8_t*)prebuffer + len, *p = NULL;\n\t\t\tuint16_t payload_len;\n\t\t\tfor(p = (uint8_t*)prebuffer + 2; p < end - 2; p += payload_len) {\n\t\t\t\tpayload_len = (p[0] << 8 | p[1]);\n\t\t\t\tp += 2;\n\n\t\t\t\tuint16_t unit = 0;\n\t\t\t\tmemcpy(&unit, p, sizeof(uint16_t));\n\t\t\t\tunit = ntohs(unit);\n\t\t\t\tuint8_t fbit = (unit & 0x8000) >> 15;\n\t\t\t\tuint8_t type = (unit & 0x7E00) >> 9;\n\t\t\t\tuint8_t lid = (unit & 0x01F8) >> 3;\n\t\t\t\tuint8_t tid = (unit & 0x0007);\n\n\t\t\t\tswitch(type) {\n\t\t\t\t\tcase 32:\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[VPS] %u/%u/%u/%u\\n\", fbit, type, lid, tid);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 33:\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[SPS] %u/%u/%u/%u\\n\", fbit, type, lid, tid);\n\t\t\t\t\t\t/* Get rid of the Emulation Prevention code, if present */\n\t\t\t\t\t\tint i = 0, j = 0, zeros = 0;\n\t\t\t\t\t\tfor(i=0; i<payload_len; i++) {\n\t\t\t\t\t\t\tif(zeros == 2 && p[i] == 0x03) {\n\t\t\t\t\t\t\t\t/* Found, get rid of it */\n\t\t\t\t\t\t\t\ti++;\n\t\t\t\t\t\t\t\tzeros = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/* Update the content of the buffer as we go along */\n\t\t\t\t\t\t\tp[j] = p[i];\n\t\t\t\t\t\t\tif(p[i] == 0x00) {\n\t\t\t\t\t\t\t\t/* Found a zero, may be part of a start code */\n\t\t\t\t\t\t\t\tzeros++;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t/* Not a zero, so not the beginning of a start code */\n\t\t\t\t\t\t\t\tzeros = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tj++;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Parse to get width/height */\n\t\t\t\t\t\tint width = 0, height = 0;\n\t\t\t\t\t\tjanus_pp_h265_parse_sps((char*)p, &width, &height);\n\t\t\t\t\t\tif(width*height > max_width*max_height) {\n\t\t\t\t\t\t\tmax_width = width;\n\t\t\t\t\t\t\tmax_height = height;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(info && (last_width != width || last_height != height)) {\n\t\t\t\t\t\t\tlast_width = width;\n\t\t\t\t\t\t\tlast_height = height;\n\t\t\t\t\t\t\tif(resolutions == NULL) {\n\t\t\t\t\t\t\t\tresolutions = json_array();\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"resolution\", resolutions);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_t *resolution = json_object();\n\t\t\t\t\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"ts\", json_real(ts));\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"width\", json_integer(width));\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"height\", json_integer(height));\n\t\t\t\t\t\t\tjson_array_append_new(resolutions, resolution);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 34:\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[PPS] %u/%u/%u/%u\\n\", fbit, type, lid, tid);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(type == 49) {\n\t\t\t/* FU */\n\t\t\tuint8_t fuh = prebuffer[2];\n\t\t\tuint8_t startbit = (fuh & 0x80) >> 7;\n\t\t\tuint8_t endbit = (fuh & 0x40) >> 6;\n\t\t\tuint16_t nut = (fuh & 0x1F);\n\t\t\tJANUS_LOG(LOG_HUGE, \"[FU] %u/%u/%u/%u, %u/%u/%u\\n\", fbit, type, lid, tid, startbit, endbit, nut);\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked video packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/90000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->rotation != -1 && tmp->rotation != rotation) {\n\t\t\trotation = tmp->rotation;\n\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\tJANUS_LOG(LOG_INFO, \"[%8.3fs] Video rotation: %d degrees\\n\", ts, rotation);\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tint mean_ts = min_ts_diff;\t/* FIXME: was an actual mean, (max_ts_diff+min_ts_diff)/2; */\n\tfps = (90000/(mean_ts > 0 ? mean_ts : 30));\n\tJANUS_LOG(LOG_INFO, \"  -- %dx%d (fps [%d,%d] ~ %d)\\n\", max_width, max_height, min_ts_diff, max_ts_diff, fps);\n\tif(max_width == 0 && max_height == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No resolution info?? assuming 640x480...\\n\");\n\t\tmax_width = 640;\n\t\tmax_height = 480;\n\t}\n\tif(max_width < 160) {\n\t\tJANUS_LOG(LOG_WARN, \"Width seems weirdly low (%d), setting 640 instead...\\n\", max_width);\n\t\tmax_width = 640;\n\t}\n\tif(max_height < 120) {\n\t\tJANUS_LOG(LOG_WARN, \"Height seems weirdly low (%d), setting 480 instead...\\n\", max_height);\n\t\tmax_height = 480;\n\t}\n\tif(fps == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No fps?? assuming 1...\\n\");\n\t\tfps = 1;\t/* Prevent divide by zero error */\n\t}\n\treturn 0;\n}\n\nint janus_pp_h265_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\n\tint bytes = 0, numBytes = max_width*max_height*3;\t/* FIXME */\n\tuint8_t *received_frame = g_malloc0(numBytes);\n\tuint8_t *buffer = g_malloc0(numBytes), *start = buffer;\n\tint len = 0, frameLen = 0;\n\tint keyFrame = 0;\n\tgboolean keyframe_found = FALSE;\n#ifdef FF_API_INIT_PACKET\n\tAVPacket *packet = av_packet_alloc();\n#else\n\tAVPacket pkt = { 0 }, *packet = &pkt;\n#endif\n\tAVRational timebase = {1, 90000};\n\n\twhile(*working && tmp != NULL) {\n\t\tkeyFrame = 0;\n\t\tframeLen = 0;\n\t\tlen = 0;\n\t\twhile(tmp != NULL) {\n\t\t\tif(tmp->drop) {\n\t\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* RTP payload */\n\t\t\tbuffer = start;\n\t\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\t\tlen = tmp->len-12-tmp->skip;\n\t\t\tif(len < 1) {\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\t\tif(bytes != len) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* H.265 depay */\n\t\t\tif(len < 2) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Packet too small...\\n\");\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* Read the header and skip it */\n\t\t\tuint16_t unit = 0;\n\t\t\tmemcpy(&unit, buffer, sizeof(uint16_t));\n\t\t\tunit = ntohs(unit);\n\t\t\tuint8_t type = (unit & 0x7E00) >> 9;\n\t\t\tif(type == 32 || type == 33 || type == 34 || type == 1) {\n\t\t\t\tif(type == 32 || type == 33) {\n\t\t\t\t\tkeyFrame = 1;\n\t\t\t\t\tif(!keyframe_found) {\n\t\t\t\t\t\tkeyframe_found = TRUE;\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"First keyframe: %\"SCNu64\"\\n\", tmp->ts-list->ts);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Add the NAL delimiter */\n\t\t\t\tuint8_t *temp = received_frame + frameLen;\n\t\t\t\tmemset(temp, 0x00, 1);\n\t\t\t\tmemset(temp + 1, 0x00, 1);\n\t\t\t\tmemset(temp + 2, 0x01, 1);\n\t\t\t\tframeLen += 3;\n\t\t\t} else if(type == 48) {\n\t\t\t\t/* AP */\n\t\t\t\tuint8_t *end = buffer + len, *p = NULL;\n\t\t\t\tuint16_t payload_len;\n\t\t\t\tfor(p = buffer + 2; p < end - 2; p += payload_len) {\n\t\t\t\t\tpayload_len = (p[0] << 8 | p[1]);\n\t\t\t\t\tp += 2;\n\t\t\t\t\tuint8_t *temp = received_frame + frameLen;\n\t\t\t\t\ttemp[0] = 0;\n\t\t\t\t\ttemp[1] = 0;\n\t\t\t\t\ttemp[2] = 1;\n\t\t\t\t\tmemcpy(temp + 3, p, payload_len);\n\t\t\t\t\tswitch(p[0] >> 1) {\n\t\t\t\t\t\tcase 32:\n\t\t\t\t\t\tcase 33:\n\t\t\t\t\t\tcase 34:\n\t\t\t\t\t\t\tkeyFrame = 1;\n\t\t\t\t\t\t\tif(!keyframe_found) {\n\t\t\t\t\t\t\t\tkeyframe_found = TRUE;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"First keyframe: %\"SCNu64\"\\n\", tmp->ts-list->ts);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tframeLen += 3 + payload_len;\n\t\t\t\t}\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t} else if(type == 49) {\n\t\t\t\t/* Check if this is the beginning of the FU */\n\t\t\t\tuint8_t fuh = *(buffer+2);\n\t\t\t\tuint8_t startbit = (fuh & 0x80) >> 7;\n\t\t\t\tif(startbit) {\n\t\t\t\t\t/* Add the NAL delimiter */\n\t\t\t\t\tuint8_t *temp = received_frame + frameLen;\n\t\t\t\t\tmemset(temp, 0x00, 1);\n\t\t\t\t\tmemset(temp + 1, 0x00, 1);\n\t\t\t\t\tmemset(temp + 2, 0x01, 1);\n\t\t\t\t\tframeLen += 3;\n\t\t\t\t\t/* Update the NAL unit */\n\t\t\t\t\tuint16_t fbit = (unit & 0x8000);\n\t\t\t\t\tuint16_t nut = (fuh & 0x1F);\n\t\t\t\t\tuint16_t lid = (unit & 0x01F8);\n\t\t\t\t\tuint16_t tid = (unit & 0x0007);\n\t\t\t\t\tunit = fbit + (nut << 9) + lid + tid;\n\t\t\t\t\tunit = htons(unit);\n\t\t\t\t\tmemcpy(received_frame + frameLen, &unit, sizeof(uint16_t));\n\t\t\t\t\tframeLen += 2;\n\t\t\t\t}\n\t\t\t\t/* Skip the FU header */\n\t\t\t\tbuffer += 3;\n\t\t\t\tlen -= 3;\n\t\t\t}\n\t\t\t/* Frame manipulation */\n\t\t\tmemcpy(received_frame + frameLen, buffer, len);\n\t\t\tframeLen += len;\n\t\t\tif(len == 0)\n\t\t\t\tbreak;\n\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\tbreak;\n\t\t\ttmp = tmp->next;\n\t\t}\n\t\tif(frameLen > 0) {\n\t\t\t/* Save the frame */\n\t\t\tmemset(received_frame + frameLen, 0, FF_INPUT_BUFFER_PADDING_SIZE);\n\n#ifdef FF_API_INIT_PACKET\n\t\t\tav_packet_unref(packet);\n#else\n\t\t\tav_init_packet(packet);\n#endif\n\t\t\tpacket->stream_index = 0;\n\t\t\tpacket->data = received_frame;\n\t\t\tpacket->size = frameLen;\n\t\t\tif(keyFrame)\n\t\t\t\tpacket->flags |= AV_PKT_FLAG_KEY;\n\n\t\t\t/* First we save to the file... */\n\t\t\tpacket->pts = packet->dts = av_rescale_q(tmp->ts-list->ts, timebase, fctx->streams[0]->time_base);\n\t\t\tJANUS_LOG(LOG_HUGE, \"%\"SCNu64\" - %\"SCNu64\" --> %\"SCNu64\"\\n\",\n\t\t\t\ttmp->ts, list->ts, packet->pts);\n\t\t\tif(fctx) {\n\t\t\t\tint res = av_write_frame(fctx, packet);\n\t\t\t\tif(res < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error writing video frame to file... (error %d, %s)\\n\",\n\t\t\t\t\t\tres, av_err2str(res));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n#ifdef FF_API_INIT_PACKET\n\tav_packet_free(&packet);\n#endif\n\tg_free(received_frame);\n\tg_free(start);\n\treturn 0;\n}\n\n/* Close MP4 file */\nvoid janus_pp_h265_close(void) {\n\tif(fctx != NULL) {\n\t\tav_write_trailer(fctx);\n#ifdef USE_CODECPAR\n\tif(vEncoder != NULL)\n\t\tavcodec_free_context(&vEncoder);\n#else\n\tif(vStream != NULL && vStream->codec != NULL)\n\t\tavcodec_free_context(&(vStream->codec));\n#endif\n\t\tavio_close(fctx->pb);\n\t\tavformat_free_context(fctx);\n\t}\n}\n"
  },
  {
    "path": "src/postprocessing/pp-h265.h",
    "content": "/*! \\file    pp-h265.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .mp4 files out of H.265 frames (headers)\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .mp4 files out of H.265 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_H265\n#define JANUS_PP_H265\n\n#include <stdio.h>\n#include <jansson.h>\n\n#include \"pp-rtp.h\"\n\n/* H.265 stuff */\nconst char **janus_pp_h265_get_extensions(void);\nint janus_pp_h265_create(char *destination, char *metadata, gboolean faststart, const char *extension);\nint janus_pp_h265_preprocess(FILE *file, janus_pp_frame_packet *list, json_t *info);\nint janus_pp_h265_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_h265_close(void);\n\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-l16.c",
    "content": "/*! \\file    pp-l16.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .wav files out of L16 frames (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate raw .wav files out of L16 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n#include <errno.h>\n\n#include \"pp-l16.h\"\n#include \"../debug.h\"\n\n\n/* WAV header */\ntypedef struct janus_pp_l16_wav {\n\tchar riff[4];\n\tuint32_t len;\n\tchar wave[4];\n\tchar fmt[4];\n\tuint32_t formatsize;\n\tuint16_t format;\n\tuint16_t channels;\n\tuint32_t samplerate;\n\tuint32_t avgbyterate;\n\tuint16_t samplebytes;\n\tuint16_t channelbits;\n\tchar data[4];\n\tuint32_t blocksize;\n} janus_pp_l16_wav;\nstatic FILE *wav_file = NULL;\n\n/* Supported target formats */\nstatic const char *janus_pp_l16_formats[] = {\n\t\"wav\", NULL\n};\nconst char **janus_pp_l16_get_extensions(void) {\n\treturn janus_pp_l16_formats;\n}\n\n/* Processing methods */\nstatic int samplerate = 0;\nint janus_pp_l16_create(char *destination, int rate, char *metadata) {\n\tsamplerate = rate;\n\tif(samplerate != 16000 && samplerate != 48000) {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported sample rate %d (should be 16000 or 48000)\\n\", rate);\n\t\treturn -1;\n\t}\n\t/* Create wav file */\n\twav_file = fopen(destination, \"wb\");\n\tif(wav_file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open output file\\n\");\n\t\treturn -1;\n\t}\n\t/* Add header */\n\tJANUS_LOG(LOG_INFO, \"Writing .wav file header\\n\");\n\tjanus_pp_l16_wav header = {\n\t\t{'R', 'I', 'F', 'F'},\n\t\t0,\n\t\t{'W', 'A', 'V', 'E'},\n\t\t{'f', 'm', 't', ' '},\n\t\t16,\n\t\t1,\n\t\t1,\n\t\tsamplerate,\n\t\tsamplerate * 2,\n\t\t2,\n\t\t16,\n\t\t{'d', 'a', 't', 'a'},\n\t\t0\n\t};\n\t/* Note: .wav files don't seem to support arbitrary comments\n\t * so there's nothing we can do with the provided metadata*/\n\tif(fwrite(&header, 1, sizeof(header), wav_file) != sizeof(header)) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't write WAV header, expect problems...\\n\");\n\t}\n\tfflush(wav_file);\n\treturn 0;\n}\n\nint janus_pp_l16_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tlong int offset = 0;\n\tint bytes = 0, len = 0, last_seq = 0;\n\tuint8_t *buffer = g_malloc0(1500);\n\tint16_t samples[1500];\n\tmemset(samples, 0, sizeof(samples));\n\tsize_t num_samples = samplerate/100/2;\n\tint sr = samplerate/1000;\n\twhile(*working && tmp != NULL) {\n\t\tif(tmp->prev != NULL && ((tmp->ts - tmp->prev->ts)/sr/10 > 1)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/samplerate);\n\t\t\tint silence_count = (tmp->ts - tmp->prev->ts)/sr/10 - 1;\n\t\t\tint i=0;\n\t\t\tfor(i=0; i<silence_count; i++) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[FILL] Writing silence (seq=%d, index=%d)\\n\",\n\t\t\t\t\ttmp->prev->seq+i+1, i+1);\n\t\t\t\t/* Add silence */\n\t\t\t\tmemset(samples, 0, num_samples*2);\n\t\t\t\tif(wav_file != NULL) {\n\t\t\t\t\tif(fwrite(samples, sizeof(char), num_samples*2, wav_file) != num_samples) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write sample...\\n\");\n\t\t\t\t\t}\n\t\t\t\t\tfflush(wav_file);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked audio packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/8000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->audiolevel != -1) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Audio level: %d dB\\n\", tmp->audiolevel);\n\t\t}\n\t\tguint16 diff = tmp->prev == NULL ? 1 : (tmp->seq - tmp->prev->seq);\n\t\tlen = 0;\n\t\t/* RTP payload */\n\t\toffset = tmp->offset+12+tmp->skip;\n\t\tfseek(file, offset, SEEK_SET);\n\t\tlen = tmp->len-12-tmp->skip;\n\t\tif(len < 1) {\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(last_seq == 0)\n\t\t\tlast_seq = tmp->seq;\n\t\tif(tmp->seq < last_seq) {\n\t\t\tlast_seq = tmp->seq;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Writing %d bytes out of %d (seq=%\"SCNu16\", step=%\"SCNu16\", ts=%\"SCNu64\", time=%\"SCNu64\"s)\\n\",\n\t\t\tbytes, tmp->len, tmp->seq, diff, tmp->ts, (tmp->ts-list->ts)/samplerate);\n\t\tnum_samples = bytes/2;\n\t\tint i=0;\n\t\tfor(i=0; i<(int)num_samples; i++) {\n\t\t\tmemcpy(&samples[i], buffer + i*2, sizeof(int16_t));\n\t\t\tsamples[i] = ntohs(samples[i]);\n\t\t}\n\t\tif(wav_file != NULL) {\n\t\t\tif(fwrite(samples, sizeof(int16_t), num_samples, wav_file) != num_samples) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write sample...\\n\");\n\t\t\t}\n\t\t\tfflush(wav_file);\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tg_free(buffer);\n\treturn 0;\n}\n\nvoid janus_pp_l16_close(void) {\n\t/* Flush and close file */\n\tif(wav_file != NULL) {\n\t\t/* Update the header */\n\t\tfseek(wav_file, 0, SEEK_END);\n\t\tlong int fs = ftell(wav_file);\n\t\tif(fs < 8) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error getting file position, wav file will be broken... %s\\n\", g_strerror(errno));\n\t\t} else {\n\t\t\tuint32_t size = fs - 8;\n\t\t\tfseek(wav_file, 4, SEEK_SET);\n\t\t\tfwrite(&size, sizeof(uint32_t), 1, wav_file);\n\t\t\tsize += 8;\n\t\t\tfseek(wav_file, 40, SEEK_SET);\n\t\t\tfwrite(&size, sizeof(uint32_t), 1, wav_file);\n\t\t}\n\t\tfflush(wav_file);\n\t\tfclose(wav_file);\n\t}\n\twav_file = NULL;\n}\n"
  },
  {
    "path": "src/postprocessing/pp-l16.h",
    "content": "/*! \\file    pp-l16.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .wav files out of L16 frames (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate raw .wav files out of L16 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_L16\n#define JANUS_PP_L16\n\n#include <stdio.h>\n\n#include \"pp-rtp.h\"\n\n/* L16 stuff */\nconst char **janus_pp_l16_get_extensions(void);\nint janus_pp_l16_create(char *destination, int samplerate, char *metadata);\nint janus_pp_l16_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_l16_close(void);\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-options.c",
    "content": "/*! \\file    pp-options.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Command line options parser for janus-pp-rec\n * \\details  Helper code to parse the janus-pp-rec command line options\n * using GOptionEntry.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include \"../debug.h\"\n#include \"pp-options.h\"\n\nstatic GOptionContext *opts = NULL;\n\ngboolean janus_pprec_options_parse(janus_pprec_options *options, int argc, char *argv[]) {\n\t/* Supported command-line arguments */\n\tGOptionEntry opt_entries[] = {\n\t\t{ \"file-extensions\", 'F', 0, G_OPTION_ARG_NONE, &options->fileexts_only, \"Only print the supported target file extensions per codec\", NULL },\n\t\t{ \"json\", 'j', 0, G_OPTION_ARG_NONE, &options->jsonheader_only, \"Only print JSON header\", NULL },\n\t\t{ \"header\", 'H', 0, G_OPTION_ARG_NONE, &options->header_only, \"Only parse .mjr header\", NULL },\n\t\t{ \"parse\", 'p', 0, G_OPTION_ARG_NONE, &options->parse_only, \"Only parse and re-order packets\", NULL },\n\t\t{ \"extended-json\", 'e', 0, G_OPTION_ARG_NONE, &options->extjson_only, \"Only print extended JSON report (automatically enables --json)\", NULL },\n\t\t{ \"metadata\", 'm', 0, G_OPTION_ARG_STRING, &options->metadata, \"Save this metadata string in the target file\", NULL },\n\t\t{ \"ignore-first\", 'i', 0, G_OPTION_ARG_INT, &options->ignore_first_packets, \"Number of first packets to ignore when processing, e.g., in case they're cause of issues (default=0)\", NULL },\n\t\t{ \"payload-type\", 'P', 0, G_OPTION_ARG_INT, &options->match_pt, \"Ignore all RTP packets that don't match the specified payload type (default=none)\", NULL },\n\t\t{ \"audiolevel-ext\", 'a', 0, G_OPTION_ARG_INT, &options->audio_level_extmap_id, \"ID of the audio-levels RTP extension (default=none)\", NULL },\n\t\t{ \"videoorient-ext\", 'v', 0, G_OPTION_ARG_INT, &options->video_orient_extmap_id, \"ID of the video-orientation RTP extension (default=none)\", NULL },\n\t\t{ \"debug-level\", 'd', 0, G_OPTION_ARG_INT, &options->debug_level, \"Debug/logging level (0=disable debugging, 7=maximum debug level; default=4)\", NULL },\n\t\t{ \"debug-timestamps\", 'D', 0, G_OPTION_ARG_NONE, &options->debug_timestamps, \"Enable debug/logging timestamps\", NULL },\n\t\t{ \"disable-colors\", 'o', 0, G_OPTION_ARG_NONE, &options->disable_colors, \"Disable color in the logging\", NULL },\n\t\t{ \"format\", 'f', 0, G_OPTION_ARG_STRING, &options->extension, \"Specifies the output format (overrides the format from the destination)\", NULL },\n\t\t{ \"faststart\", 't', 0, G_OPTION_ARG_NONE, &options->faststart, \"For mp4 files write the MOOV atom at the head of the file\", NULL },\n\t\t{ \"audioskew\", 'S', 0, G_OPTION_ARG_INT, &options->audioskew_th, \"Time threshold to trigger an audio skew compensation, disabled if 0 (default=0)\", NULL },\n\t\t{ \"silence-distance\", 'C', 0, G_OPTION_ARG_INT, &options->silence_distance, \"RTP packets distance used to detect RTP silence suppression, disabled if 0 (default=0)\", NULL },\n\t\t{ \"restamp\", 'r', 0, G_OPTION_ARG_INT, &options->restamp_multiplier, \"If the latency of a packet is bigger than the `moving_average_latency * (<restamp>/1000)` the timestamps will be corrected, disabled if 0 (default=0)\", NULL },\n\t\t{ \"restamp-packets\", 'c', 0, G_OPTION_ARG_INT, &options->restamp_packets, \"Number of packets used for calculating moving average latency for timestamp correction (default=10)\", NULL },\n\t\t{ \"restamp-min-th\", 'n', 0, G_OPTION_ARG_INT, &options->restamp_min_th, \"Minimum latency of moving average to reach before starting to correct timestamps (default=500)\", NULL },\n\t\t{ \"ignore-rtp-ts\", 'I', 0, G_OPTION_ARG_NONE, &options->ignore_rtp_ts, \"Ignore RTP timestamps, and use packet arrival timestamps for timing (default=no)\", NULL },\n\t\t{ G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &options->paths, NULL, NULL },\n\t\t{ NULL, 0, 0, 0, NULL, NULL, NULL },\n\t};\n\n\t/* Parse the command-line arguments */\n\tGError *error = NULL;\n\topts = g_option_context_new(\"source.mjr [destination.[opus|ogg|mka|wav|webm|mkv|h264|srt]]\");\n\tg_option_context_set_help_enabled(opts, TRUE);\n\tg_option_context_add_main_entries(opts, opt_entries, NULL);\n\tif(!g_option_context_parse(opts, &argc, &argv, &error)) {\n\t\tJANUS_LOG(LOG_INFO, \"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\t\tJANUS_LOG(LOG_INFO, \"Janus commit: %s\\n\", janus_build_git_sha);\n\t\tJANUS_LOG(LOG_INFO, \"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\t\tg_print(\"%s\\n\", error->message);\n\t\tg_error_free(error);\n\t\tjanus_pprec_options_destroy();\n\t\treturn FALSE;\n\t}\n\n\t/* Done */\n\treturn TRUE;\n}\n\nvoid janus_pprec_options_help(void) {\n\tJANUS_LOG(LOG_INFO, \"Janus version: %d (%s)\\n\", janus_version, janus_version_string);\n\tJANUS_LOG(LOG_INFO, \"Janus commit: %s\\n\", janus_build_git_sha);\n\tJANUS_LOG(LOG_INFO, \"Compiled on:  %s\\n\\n\", janus_build_git_time);\n\tchar *help = g_option_context_get_help(opts, TRUE, NULL);\n\tg_print(\"%s\", help);\n\tg_free(help);\n}\n\nvoid janus_pprec_options_destroy(void) {\n\tg_option_context_free(opts);\n\topts = NULL;\n}\n"
  },
  {
    "path": "src/postprocessing/pp-options.h",
    "content": "/*! \\file    pp-options.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Command line options parser for janus-pp-rec (headers)\n * \\details  Helper code to parse the janus-pp-rec command line options\n * using GOptionEntry.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_OPTIONS\n#define JANUS_PP_OPTIONS\n\n#include <glib.h>\n#include \"../version.h\"\n\n/*! \\brief Struct containing the parsed command line options for janus-pp-rec */\ntypedef struct janus_pprec_options {\n\tgboolean fileexts_only;\n\tgboolean jsonheader_only;\n\tgboolean header_only;\n\tgboolean parse_only;\n\tgboolean extjson_only;\n\tconst char *metadata;\n\tgboolean ignore_first_packets;\n\tint match_pt;\n\tint audio_level_extmap_id;\n\tint video_orient_extmap_id;\n\tint debug_level;\n\tint debug_timestamps;\n\tgboolean disable_colors;\n\tconst char *extension;\n\tgboolean faststart;\n\tint audioskew_th;\n\tint silence_distance;\n\tint restamp_multiplier;\n\tint restamp_min_th;\n\tint restamp_packets;\n\tgboolean ignore_rtp_ts;\n\tchar **paths;\n} janus_pprec_options;\n\n/*! \\brief Helper method to parse the command line options\n * @param opts A pointer to the janus_pprec_options instance to save the options to\n * @param argc The number of arguments\n * @param argv The command line arguments\n * @returns TRUE if successful, FALSE otherwise */\ngboolean janus_pprec_options_parse(janus_pprec_options *opts, int argc, char *argv[]);\n\n/*! \\brief Helper method to print the command line options help summary */\nvoid janus_pprec_options_help(void);\n\n/*! \\brief Helper method to get rid of the options parser resources */\nvoid janus_pprec_options_destroy(void);\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-opus-silence.h",
    "content": "/*! \\file    pp-opus-silence.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    A sample Opus packet that only contains silence\n * \\details  To account for times when an audio recording has gaps, in\n * the form of missing packets, we insert silence when post processing.\n * \n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\nstatic uint8_t opus_silence[] = {\n\t0xf8, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00\n};\n"
  },
  {
    "path": "src/postprocessing/pp-opus.c",
    "content": "/*! \\file    pp-opus.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .opus files\n * \\details  Implementation of the post-processing code (based on libogg)\n * needed to generate .opus files out of Opus RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-avformat.h\"\n#include \"pp-opus.h\"\n#include \"pp-opus-silence.h\"\n#include \"../debug.h\"\n#include \"../version.h\"\n\nstatic gboolean multichannel_opus = FALSE;\nstatic AVFormatContext *fctx;\nstatic AVStream *vStream;\n\nstatic const uint8_t opus_extradata[19] = {\n\t'O', 'p', 'u', 's', 'H', 'e', 'a', 'd',\n\t1, 2, 0, 0, 128, 187,\n\t0, 0, 0, 0, 0,\n};\nstatic const uint8_t multiopus_extradata[27] = {\n\t'O', 'p', 'u', 's', 'H', 'e', 'a', 'd',\n\t1, 6, 0, 0, 128, 187,\n\t0, 0, 0, 0, 1,\n\t/* FIXME The following is the mapping of the streams: we should\n\t * check what was negotiated in the SDP, but for now we hardcode it */\n\t4, 2, 0, 4, 1, 2, 3, 5\n};\n\n/* In case we need to decapsulate RED */\nstatic int red_pt = 0;\n\n/* Supported target formats */\nstatic const char *janus_pp_opus_formats[] = {\n\t\"opus\", \"ogg\", \"mka\", NULL\n};\nconst char **janus_pp_opus_get_extensions(void) {\n\treturn janus_pp_opus_formats;\n}\n\n/* Processing methods */\nint janus_pp_opus_create(char *destination, char *metadata, gboolean multiopus, const char *extension, int opusred_pt) {\n\tif(destination == NULL)\n\t\treturn -1;\n\n\t/* .opus and .ogg are the same thing */\n\tif(!strcasecmp(extension, \"opus\"))\n\t\textension = \"ogg\";\n\t/* .mka is Matroska audio */\n\tif(!strcasecmp(extension, \"mka\"))\n\t\textension = \"matroska\";\n\n\t/* Audio output */\n\tfctx = janus_pp_create_avformatcontext(extension, metadata, destination);\n\tif(fctx == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error allocating context\\n\");\n\t\treturn -1;\n\t}\n\n\tmultichannel_opus = multiopus;\n\tif(!multichannel_opus) {\n\t\t/* Regular Opus stream */\n\t\tvStream = janus_pp_new_audio_avstream(fctx, AV_CODEC_ID_OPUS, 48000, 2, opus_extradata, sizeof(opus_extradata));\n\t} else {\n\t\t/* Multichannel Opus Stream*/\n\t\tvStream = janus_pp_new_audio_avstream(fctx, AV_CODEC_ID_OPUS, 48000, 6, multiopus_extradata, sizeof(multiopus_extradata));\n\t}\n\tif(vStream == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error adding stream\\n\");\n\t\treturn -1;\n\t}\n\n\tif(avformat_write_header(fctx, NULL) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error writing header\\n\");\n\t\treturn -1;\n\t}\n\n\tif(opusred_pt > 0) {\n\t\tred_pt = opusred_pt;\n\t\tJANUS_LOG(LOG_INFO, \"  -- Enabling RED decapsulation (pt=%d)\\n\", red_pt);\n\t}\n\treturn 0;\n}\n\n// It assumes ALL the packets are of the 20ms kind\n#define OPUS_PACKET_DURATION 48 * 20;\n\nint janus_pp_opus_process(FILE *file, janus_pp_frame_packet *list, gboolean restamping, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tlong int offset = 0;\n\tint bytes = 0, len = 0, last_seq = 0;\n\tuint64_t pos = 0;\n\tdouble ts = 0.0;\n\tuint8_t *buffer = g_malloc0(1500);\n\n\t/* Before we start, check if we're dealing with RED: if so, we need to pre-traverse the\n\t * list to decapsulate all RED packets to Opus packets, filling the blanks if needed */\n\tif(red_pt > 0) {\n\t\twhile(*working && tmp != NULL) {\n\t\t\t/* Check if we need to decapsulate RED */\n\t\t\tif(tmp->pt == red_pt) {\n\t\t\t\t/* RTP payload */\n\t\t\t\toffset = tmp->offset+12+tmp->skip;\n\t\t\t\tfseek(file, offset, SEEK_SET);\n\t\t\t\tlen = tmp->len-12-tmp->skip;\n\t\t\t\tif(len < 1) {\n\t\t\t\t\ttmp = tmp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\t\t\tif(bytes != len) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\t\t\ttmp = tmp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tuint8_t *payload = buffer;\n\t\t\t\tint plen = bytes;\n\t\t\t\t/* Find out how many generations are in the RED packet */\n\t\t\t\tint gens = 0;\n\t\t\t\tuint32_t red_block;\n\t\t\t\tuint8_t follow = 0, block_pt = 0;\n\t\t\t\tuint16_t ts_offset = 0, block_len = 0;\n\t\t\t\tGList *lengths = NULL;\n\t\t\t\t/* Parse the header */\n\t\t\t\twhile(payload != NULL && plen > 0) {\n\t\t\t\t\t/* Go through the header for the different generations */\n\t\t\t\t\tgens++;\n\t\t\t\t\tfollow = ((*payload) & 0x80) >> 7;\n\t\t\t\t\tblock_pt = (*payload) & 0x7F;\n\t\t\t\t\tif(follow && plen > 3) {\n\t\t\t\t\t\t/* Read the rest of the header */\n\t\t\t\t\t\tmemcpy(&red_block, payload, sizeof(red_block));\n\t\t\t\t\t\tred_block = ntohl(red_block);\n\t\t\t\t\t\tts_offset = (red_block & 0x00FFFC00) >> 10;\n\t\t\t\t\t\tblock_len = (red_block & 0x000003FF);\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  [%d] f=%u, pt=%u, tsoff=%\"SCNu16\", blen=%\"SCNu16\"\\n\",\n\t\t\t\t\t\t\tgens, follow, block_pt, ts_offset, block_len);\n\t\t\t\t\t\tlengths = g_list_append(lengths, GUINT_TO_POINTER(block_len));\n\t\t\t\t\t\tpayload += 4;\n\t\t\t\t\t\tplen -= 4;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Header parsed */\n\t\t\t\t\t\tpayload++;\n\t\t\t\t\t\tplen--;\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  [%d] f=%u, pt=%u, tsoff=0, blen=TBD.\\n\",\n\t\t\t\t\t\t\tgens, follow, block_pt);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Go through the blocks, iterating on the lengths */\n\t\t\t\tif(lengths != NULL) {\n\t\t\t\t\tint tot_gens = gens;\n\t\t\t\t\tgens = 0;\n\t\t\t\t\tuint16_t length = 0;\n\t\t\t\t\tGList *temp = lengths;\n\t\t\t\t\twhile(temp != NULL) {\n\t\t\t\t\t\tgens++;\n\t\t\t\t\t\ttot_gens--;\n\t\t\t\t\t\tlength = GPOINTER_TO_UINT(temp->data);\n\t\t\t\t\t\tif(length > plen) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"  >> [%d] Broken red payload:\\n\", gens);\n\t\t\t\t\t\t\tpayload = NULL;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(length > 0) {\n\t\t\t\t\t\t\t/* Redundant data, check if we have this packet already */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  >> [%d] plen=%\"SCNu16\"\\n\", gens, length);\n\t\t\t\t\t\t\tif(tmp->prev != NULL) {\n\t\t\t\t\t\t\t\t/* Go back until we either find it, or find a hole where it's supposed to be */\n\t\t\t\t\t\t\t\tjanus_pp_frame_packet *prev = tmp->prev;\n\t\t\t\t\t\t\t\tts_offset = tot_gens*960;\n\t\t\t\t\t\t\t\twhile(prev) {\n\t\t\t\t\t\t\t\t\tif(prev->ts < ts_offset) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Redundant packet precedes start of recording, ignoring\\n\");\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t} else if(prev->ts == (tmp->ts - ts_offset)) {\n\t\t\t\t\t\t\t\t\t\t/* We have this packet already */\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t} else if(prev->ts < (tmp->ts - ts_offset)) {\n\t\t\t\t\t\t\t\t\t\t/* We're missing this packet, insert it here */\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Missing packet (ts=%\"SCNi64\"), restoring using redundant information\\n\",\n\t\t\t\t\t\t\t\t\t\t\t(tmp->ts - ts_offset));\n\t\t\t\t\t\t\t\t\t\t/* TODO */\n\t\t\t\t\t\t\t\t\t\tjanus_pp_frame_packet *p = g_malloc0(sizeof(janus_pp_frame_packet));\n\t\t\t\t\t\t\t\t\t\tp->header = tmp->header;\n\t\t\t\t\t\t\t\t\t\tp->version = tmp->version;\n\t\t\t\t\t\t\t\t\t\tp->ts = tmp->ts - ts_offset;\n\t\t\t\t\t\t\t\t\t\tp->p_ts = tmp->p_ts - ts_offset;\n\t\t\t\t\t\t\t\t\t\tp->seq = tmp->seq - tot_gens;\n\t\t\t\t\t\t\t\t\t\tp->pt = block_pt;\n\t\t\t\t\t\t\t\t\t\tp->drop = tmp->drop;\n\t\t\t\t\t\t\t\t\t\tp->skip = tmp->skip;\n\t\t\t\t\t\t\t\t\t\tp->offset = (payload-buffer);\n\t\t\t\t\t\t\t\t\t\tp->len = (length + 12 + tmp->skip);\n\t\t\t\t\t\t\t\t\t\tp->audiolevel = tmp->audiolevel;\n\t\t\t\t\t\t\t\t\t\tp->next = prev->next;\n\t\t\t\t\t\t\t\t\t\tp->next->prev = p;\n\t\t\t\t\t\t\t\t\t\tp->prev = prev;\n\t\t\t\t\t\t\t\t\t\tprev->next = p;\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tprev = prev->prev;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tpayload += length;\n\t\t\t\t\t\t\tplen -= length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t\t/* The last block is the primary data, so just update the current\n\t\t\t\t\t * stored packet with the Opus payload type and the right offset/len */\n\t\t\t\t\tgens++;\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  >> [%d] plen=%d\\n\", gens, plen);\n\t\t\t\t\ttmp->pt = block_pt;\n\t\t\t\t\ttmp->offset += (payload-buffer);\n\t\t\t\t\ttmp->len = (plen + 12 + tmp->skip);\n\t\t\t\t\tg_list_free(lengths);\n\t\t\t\t}\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttmp = tmp->next;\n\t\t}\n\t}\n\n\t/* Get to work */\n\ttmp = list;\n#ifdef FF_API_INIT_PACKET\n\tAVPacket *pkt = av_packet_alloc();\n#else\n\tAVPacket apkt = { 0 }, *pkt = &apkt;\n#endif\n\tAVRational timebase = {1, 48000};\n\n\twhile(*working && tmp != NULL) {\n\t\tif(tmp->prev != NULL && ((tmp->ts - tmp->prev->ts)/48 > 20)) {\n\t\t\tint silence_count = 0;\n\t\t\tif(tmp->seq != tmp->prev->seq+1) {\n\t\t\t\t/* Packet Lost */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/48000);\n\t\t\t\t/* insert 20ms silence packets before the current packet */\n\t\t\t\tsilence_count = (tmp->ts - tmp->prev->ts)/48/20 - 1;\n\t\t\t} else if(restamping && tmp->restamped == 1) {\n\t\t\t\t/* Packet restamped due to RTP clock issues */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Restamped packet detected (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/48000);\n\t\t\t\t/* insert 20ms silence packets before the current packet */\n\t\t\t\tsilence_count = (tmp->ts - tmp->prev->ts)/48/20 - 1;\n\t\t\t} else {\n\t\t\t\t/* plen > 20 ms, DTX ?*/\n\t\t\t\tJANUS_LOG(LOG_WARN, \"DTX packet detected (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/48000);\n\t\t\t\t/* insert 20ms silence packets for the whole DTX duration */\n\t\t\t\tsilence_count = (tmp->ts - tmp->prev->ts)/48/20;\n\t\t\t\t/* drop this packet since it's DTX silence */\n\t\t\t\ttmp->drop = 1;\n\t\t\t}\n\t\t\t/* use ts differ to insert silence packet */\n\t\t\tpos = (tmp->prev->ts - list->ts) / 48 / 20 + 1;\n\t\t\tJANUS_LOG(LOG_WARN, \"[FILL] pos: %06\"SCNu64\", writing silences (count=%d)\\n\", pos, silence_count);\n\t\t\tint i=0;\n\t\t\tpos = tmp->prev->ts - list->ts;\n\t\t\tfor(i=0; i<silence_count; i++) {\n\t\t\t\tpos += OPUS_PACKET_DURATION;\n\t\t\t\tif(tmp->next != NULL && pos >= (tmp->next->ts - list->ts)) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[SKIP] pos: %06\" SCNu64 \", skipping remaining silence\\n\", pos / 48 / 20 + 1);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n#ifdef FF_API_INIT_PACKET\n\t\t\t\tav_packet_unref(pkt);\n#endif\n\t\t\t\tpkt->stream_index = 0;\n\t\t\t\tpkt->data = opus_silence;\n\t\t\t\tpkt->size = sizeof(opus_silence);\n\t\t\t\tpkt->pts = pkt->dts = av_rescale_q(pos, timebase, fctx->streams[0]->time_base);\n\t\t\t\tpkt->duration = OPUS_PACKET_DURATION;\n\n\t\t\t\tint res = av_write_frame(fctx, pkt);\n\t\t\t\tif(res < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error writing audio frame to file... (error %d, %s)\\n\",\n\t\t\t\t\t\tres, av_err2str(res));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked audio packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/48000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(red_pt > 0 && tmp->pt == red_pt) {\n\t\t\t/* There's still a RED packet in the list? Shouldn't happen, drop it */\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->audiolevel != -1) {\n\t\t\tts = (double)(tmp->ts - list->ts)/(double)48000;\n\t\t\tJANUS_LOG(LOG_VERB, \"[audiolevel][%.2f] Audio level: %d dB\\n\", ts, tmp->audiolevel);\n\t\t}\n\t\tguint16 diff = tmp->prev == NULL ? 1 : (tmp->seq - tmp->prev->seq);\n\t\tlen = 0;\n\t\t/* RTP payload */\n\t\toffset = tmp->offset+12+tmp->skip;\n\t\tfseek(file, offset, SEEK_SET);\n\t\tlen = tmp->len-12-tmp->skip;\n\t\tif(len < 1) {\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\tif(bytes != len) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(last_seq == 0)\n\t\t\tlast_seq = tmp->seq;\n\t\tif(tmp->seq < last_seq) {\n\t\t\tlast_seq = tmp->seq;\n\t\t}\n\t\tpos = tmp->prev != NULL ? ((tmp->prev->ts - list->ts) / 48 / 20 + 1) : 0;\n\t\tJANUS_LOG(LOG_VERB, \"pos: %06\"SCNu64\", writing %d bytes out of %d (seq=%\"SCNu16\", step=%\"SCNu16\", ts=%\"SCNu64\", time=%\"SCNu64\"s)\\n\",\n\t\t\tpos, bytes, tmp->len, tmp->seq, diff, tmp->ts, (tmp->ts-list->ts)/48000);\n#ifdef FF_API_INIT_PACKET\n\t\tav_packet_unref(pkt);\n#else\n\t\tav_init_packet(pkt);\n#endif\n\t\tpkt->stream_index = 0;\n\t\tpkt->data = buffer;\n\t\tpkt->size = bytes;\n\t\tpkt->pts = pkt->dts = av_rescale_q(tmp->ts - list->ts, timebase, fctx->streams[0]->time_base);\n\t\tpkt->duration = OPUS_PACKET_DURATION;\n\n\t\tif(av_write_frame(fctx, pkt) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error writing audio frame to file...\\n\");\n\t\t}\n\n\t\ttmp = tmp->next;\n\t}\n\tg_free(buffer);\n#ifdef FF_API_INIT_PACKET\n\tav_packet_free(&pkt);\n#endif\n\treturn 0;\n}\n\nvoid janus_pp_opus_close(void) {\n\tif(fctx != NULL) {\n                av_write_trailer(fctx);\n                avio_close(fctx->pb);\n                avformat_free_context(fctx);\n\t}\n}\n"
  },
  {
    "path": "src/postprocessing/pp-opus.h",
    "content": "/*! \\file    pp-opus.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .opus files (headers)\n * \\details  Implementation of the post-processing code (based on libogg)\n * needed to generate .opus files out of Opus RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_OPUS\n#define JANUS_PP_OPUS\n\n#include <stdio.h>\n\n#include \"pp-rtp.h\"\n\n/* Opus stuff */\nconst char **janus_pp_opus_get_extensions(void);\nint janus_pp_opus_create(char *destination, char *metadata, gboolean multiopus, const char *extension, int opusred_pt);\nint janus_pp_opus_process(FILE *file, janus_pp_frame_packet *list, gboolean restamping, int *working);\nvoid janus_pp_opus_close(void);\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-rtp.h",
    "content": "/*! \\file    pp-rtp.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Helper structures to handle RTP post-processing (headers)\n * \\details  A few structures to ease the post-processing of RTP frames:\n * the RTP header, its extensions (that we just skip), and a linked list\n * we use to re-order them for post-processing audio/video later on.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_RTP\n#define JANUS_PP_RTP\n\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#define __BYTE_ORDER BYTE_ORDER\n#define __BIG_ENDIAN BIG_ENDIAN\n#define __LITTLE_ENDIAN LITTLE_ENDIAN\n#else\n#include <endian.h>\n#endif\n\n#include <glib.h>\n\n#define JANUS_PP_RTP_EXTMAP_AUDIO_LEVEL\t\t\t\"urn:ietf:params:rtp-hdrext:ssrc-audio-level\"\n#define JANUS_PP_RTP_EXTMAP_VIDEO_ORIENTATION\t\"urn:3gpp:video-orientation\"\n\ntypedef struct janus_pp_rtp_header\n{\n#if __BYTE_ORDER == __BIG_ENDIAN\n\tuint16_t version:2;\n\tuint16_t padding:1;\n\tuint16_t extension:1;\n\tuint16_t csrccount:4;\n\tuint16_t markerbit:1;\n\tuint16_t type:7;\n#elif __BYTE_ORDER == __LITTLE_ENDIAN\n\tuint16_t csrccount:4;\n\tuint16_t extension:1;\n\tuint16_t padding:1;\n\tuint16_t version:2;\n\tuint16_t type:7;\n\tuint16_t markerbit:1;\n#endif\n\tuint16_t seq_number;\n\tuint32_t timestamp;\n\tuint32_t ssrc;\n\tuint32_t csrc[16];\n} janus_pp_rtp_header;\n\ntypedef struct janus_pp_rtp_header_extension {\n\tuint16_t type;\n\tuint16_t length;\n} janus_pp_rtp_header_extension;\n\ntypedef struct janus_pp_frame_packet {\n\tjanus_pp_rtp_header *header; /* Pointer to RTP header */\n\tint version;\t\t/* Version of the .mjr file (2=has timestamps) */\n\tuint32_t p_ts;\t\t/* Packet timestamp as saved by Janus (if available) */\n\tuint16_t seq;\t\t/* RTP Sequence number */\n\tuint64_t ts;\t\t/* RTP Timestamp */\n\tuint16_t len;\t\t/* Length of the data */\n\tint pt;\t\t\t\t/* Payload type of the data */\n\tlong offset;\t\t/* Offset of the data in the file */\n\tint skip;\t\t\t/* Bytes to skip, besides the RTP header */\n\tuint8_t drop;\t  \t/* Whether this packet can be dropped (e.g., padding)*/\n\tuint8_t restamped;\t/* Whether this packet has been restamped */\n\tint audiolevel;\t\t/* Value of audio level in RTP extension, if parsed */\n\tint rotation;\t\t/* Value of rotation in RTP extension, if parsed */\n\tstruct janus_pp_frame_packet *next;\n\tstruct janus_pp_frame_packet *prev;\n} janus_pp_frame_packet;\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-srt.c",
    "content": "/*! \\file    pp-srt.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .srt files\n * \\details  Implementation of the post-processing code needed to\n * generate .srt files out of text data recordings.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-srt.h\"\n#include \"../debug.h\"\n\n\nFILE *srt_file = NULL;\n\n/* Helper method to print times */\nstatic void janus_pp_srt_format_time(char *buffer, int len, guint64 when) {\n\tgint64 seconds = when/G_USEC_PER_SEC;\n\tgint64 ms = (when/1000)-seconds*1000;\n\tgint64 minutes = seconds/60;\n\tseconds -= minutes*60;\n\tgint64 hours = minutes/60;\n\tminutes -= hours*60;\n\tg_snprintf(buffer, len, \"%02\"SCNi64\":%02\"SCNi64\":%02\"SCNi64\".%03\"SCNi64, hours, minutes, seconds, ms);\n}\n\n/* Supported target formats */\nstatic const char *janus_pp_srt_formats[] = {\n\t\"srt\", NULL\n};\nconst char **janus_pp_srt_get_extensions(void) {\n\treturn janus_pp_srt_formats;\n}\n\n/* Processing methods */\nint janus_pp_srt_create(char *destination, char *metadata) {\n\t/* Create srt file */\n\tsrt_file = fopen(destination, \"wb\");\n\tif(srt_file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't open output file\\n\");\n\t\treturn -1;\n\t}\n\t/* Note: apparently .srt files don't have any \"comment\" syntax or\n\t * anything like that, so there's no way we can add a text prefix,\n\t * header or intro, and nothing we can do with the metadata either */\n\n\treturn 0;\n}\n\nint janus_pp_srt_process(FILE *file, janus_pp_frame_packet *list, int *working) {\n\tif(!file || !list || !working || !srt_file)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tuint seq = 0;\n\tuint bytes = 0;\n\tuint16_t bufsize = 1500;\n\tuint8_t *buffer = g_malloc0(bufsize);\n\tchar srt_buffer[2048], from[20], to[20];\n\tsize_t buflen = 0;\n\n\twhile(*working && tmp != NULL) {\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked text packet (time ~%\"SCNu64\"s)\\n\", tmp->ts);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Increase sequence number */\n\t\tseq++;\n\t\t/* Compute from/to times */\n\t\tjanus_pp_srt_format_time(from, sizeof(from), tmp->ts);\n\t\tif(tmp->next)\n\t\t\tjanus_pp_srt_format_time(to, sizeof(from), tmp->next->ts-1000);\n\t\telse\n\t\t\tjanus_pp_srt_format_time(to, sizeof(from), tmp->ts + 5*G_USEC_PER_SEC);\n\t\t/* Write the header lines */\n\t\tg_snprintf(srt_buffer, 2048, \"%d\\n%s --> %s\\n\", seq, from, to);\n\t\tbuflen = strlen(srt_buffer);\n\t\tif(fwrite(srt_buffer, sizeof(char), buflen, srt_file) != buflen) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write header text...\\n\");\n\t\t}\n\t\t/* Now let's read the content and write it to the file */\n\t\tfseek(file, tmp->offset, SEEK_SET);\n\t\tJANUS_LOG(LOG_VERB, \"Reading %d bytes...\\n\", tmp->len);\n\t\tuint16_t total = tmp->len;\n\t\twhile(total > 0) {\n\t\t\tbytes = fread(buffer, sizeof(char), total > bufsize ? bufsize : total, file);\n\t\t\tif(bytes == 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error reading from file...\\n\");\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"Read %d bytes...\\n\", bytes);\n\t\t\tif(fwrite(buffer, sizeof(char), bytes, srt_file) != bytes) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write all the buffer...\\n\");\n\t\t\t}\n\t\t\ttotal -= bytes;\n\t\t}\n\t\t/* Write the trailer line returns */\n\t\tg_snprintf(srt_buffer, 2048, \"\\n\\n\");\n\t\tbuflen = strlen(srt_buffer);\n\t\tif(fwrite(srt_buffer, sizeof(char), buflen, srt_file) != buflen) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't write trailer text...\\n\");\n\t\t}\n\t\tfflush(srt_file);\n\t\t/* Next? */\n\t\ttmp = tmp->next;\n\t}\n\tg_free(buffer);\n\n\treturn 0;\n}\n\nvoid janus_pp_srt_close(void) {\n\t/* Flush and close file */\n\tif(srt_file != NULL) {\n\t\tfflush(srt_file);\n\t\tfclose(srt_file);\n\t}\n\tsrt_file = NULL;\n}\n"
  },
  {
    "path": "src/postprocessing/pp-srt.h",
    "content": "/*! \\file    pp-srt.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .srt files (headers)\n * \\details  Implementation of the post-processing code needed to\n * generate .srt files out of text data recordings.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_SRT\n#define JANUS_PP_SRT\n\n#include <stdio.h>\n\n#include \"pp-rtp.h\"\n\n/* SRT stuff */\nconst char **janus_pp_srt_get_extensions(void);\nint janus_pp_srt_create(char *destination, char *metadata);\nint janus_pp_srt_process(FILE *file, janus_pp_frame_packet *list, int *working);\nvoid janus_pp_srt_close(void);\n\n#endif\n"
  },
  {
    "path": "src/postprocessing/pp-webm.c",
    "content": "/*! \\file    pp-webm.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .webm files\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .webm files out of VP8/VP9 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#include <arpa/inet.h>\n#if defined(__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <stdlib.h>\n\n#include \"pp-avformat.h\"\n#include \"pp-webm.h\"\n#include \"../debug.h\"\n\n/* WebRTC stuff (VP8/VP9) */\n#if defined(__ppc__) || defined(__ppc64__)\n\t# define swap2(d)  \\\n\t((d&0x000000ff)<<8) |  \\\n\t((d&0x0000ff00)>>8)\n#else\n\t# define swap2(d) d\n#endif\n\n\n/* WebM output */\nstatic AVFormatContext *fctx;\nstatic AVStream *vStream;\nstatic int max_width = 0, max_height = 0, fps = 0;\n\n/* Supported target formats */\nstatic const char *janus_pp_webm_formats[] = {\n\t\"webm\", \"mkv\", NULL\n};\nconst char **janus_pp_webm_get_extensions(void) {\n\treturn janus_pp_webm_formats;\n}\n\n/* Processing methods */\nint janus_pp_webm_create(char *destination, char *metadata, gboolean vp8, const char *extension) {\n\tif(destination == NULL)\n\t\treturn -1;\n#if LIBAVCODEC_VERSION_MAJOR < 55\n\tif(!vp8) {\n\t\tJANUS_LOG(LOG_FATAL, \"Your FFmpeg version does not support VP9\\n\");\n\t\treturn -1;\n\t}\n#endif\n\t/* .mkv is Matroska video */\n\tif(!strcasecmp(extension, \"mkv\"))\n\t\textension = \"matroska\";\n\n\t/* Video output */\n\tfctx = janus_pp_create_avformatcontext(extension, metadata, destination);\n\tif(fctx == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error allocating context\\n\");\n\t\treturn -1;\n\t}\n\n\tint codec_id;\n#if LIBAVCODEC_VER_AT_LEAST(54, 25)\n\t#if LIBAVCODEC_VERSION_MAJOR >= 55\n\tcodec_id = vp8 ? AV_CODEC_ID_VP8 : AV_CODEC_ID_VP9;\n\t#else\n\tcodec_id = AV_CODEC_ID_VP8;\n\t#endif\n#else\n\tcodec_id = CODEC_ID_VP8;\n#endif\n\n\tvStream = janus_pp_new_video_avstream(fctx, codec_id, max_width, max_height);\n\tif(vStream == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Error adding stream\\n\");\n\t\treturn -1;\n\t}\n\n\tif(avformat_write_header(fctx, NULL) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error writing header\\n\");\n\t\treturn -1;\n\t}\n\treturn 0;\n}\n\nint janus_pp_webm_preprocess(FILE *file, janus_pp_frame_packet *list, gboolean vp8, json_t *info) {\n\tif(!file || !list)\n\t\treturn -1;\n\tjson_t *resolutions = NULL;\n\tint last_width = -1, last_height = -1;\n\tjanus_pp_frame_packet *tmp = list;\n\tint bytes = 0, min_ts_diff = 0, max_ts_diff = 0;\n\tint rotation = -1;\n\tchar prebuffer[1500];\n\tmemset(prebuffer, 0, 1500);\n\twhile(tmp) {\n\t\tif(tmp == list || tmp->ts > tmp->prev->ts) {\n\t\t\tif(tmp->prev != NULL && tmp->ts > tmp->prev->ts) {\n\t\t\t\tint diff = tmp->ts - tmp->prev->ts;\n\t\t\t\tif(min_ts_diff == 0 || min_ts_diff > diff)\n\t\t\t\t\tmin_ts_diff = diff;\n\t\t\t\tif(max_ts_diff == 0 || max_ts_diff < diff)\n\t\t\t\t\tmax_ts_diff = diff;\n\t\t\t}\n\t\t\tif(tmp->prev != NULL && (tmp->seq - tmp->prev->seq > 1)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Lost a packet here? (got seq %\"SCNu16\" after %\"SCNu16\", time ~%\"SCNu64\"s)\\n\",\n\t\t\t\t\ttmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/90000);\n\t\t\t}\n\t\t}\n\t\tif(tmp->drop) {\n\t\t\t/* We marked this packet as one to drop, before */\n\t\t\tJANUS_LOG(LOG_WARN, \"Dropping previously marked video packet (time ~%\"SCNu64\"s)\\n\", (tmp->ts-list->ts)/90000);\n\t\t\ttmp = tmp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(tmp->rotation != -1 && tmp->rotation != rotation) {\n\t\t\trotation = tmp->rotation;\n\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\tJANUS_LOG(LOG_INFO, \"[%8.3fs] Video rotation: %d degrees\\n\", ts, rotation);\n\t\t}\n\t\tif(vp8) {\n\t\t\t/* https://tools.ietf.org/html/draft-ietf-payload-vp8 */\n\t\t\t/* Read the first bytes of the payload, and get the first octet (VP8 Payload Descriptor) */\n\t\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\t\tbytes = fread(prebuffer, sizeof(char), 16, file);\n\t\t\tif(bytes != 16) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < 16)...\\n\", bytes);\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tchar *buffer = (char *)&prebuffer;\n\t\t\tuint8_t vp8pd = *buffer;\n\t\t\tuint8_t xbit = (vp8pd & 0x80);\n\t\t\tuint8_t sbit = (vp8pd & 0x10);\n\t\t\t/* Read the Extended control bits octet */\n\t\t\tif(xbit) {\n\t\t\t\tbuffer++;\n\t\t\t\tvp8pd = *buffer;\n\t\t\t\tuint8_t ibit = (vp8pd & 0x80);\n\t\t\t\tuint8_t lbit = (vp8pd & 0x40);\n\t\t\t\tuint8_t tbit = (vp8pd & 0x20);\n\t\t\t\tuint8_t kbit = (vp8pd & 0x10);\n\t\t\t\tif(ibit) {\n\t\t\t\t\t/* Read the PictureID octet */\n\t\t\t\t\tbuffer++;\n\t\t\t\t\tvp8pd = *buffer;\n\t\t\t\t\tuint16_t picid = vp8pd, wholepicid = picid;\n\t\t\t\t\tuint8_t mbit = (vp8pd & 0x80);\n\t\t\t\t\tif(mbit) {\n\t\t\t\t\t\tmemcpy(&picid, buffer, sizeof(uint16_t));\n\t\t\t\t\t\twholepicid = ntohs(picid);\n\t\t\t\t\t\tpicid = (wholepicid & 0x7FFF);\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(lbit) {\n\t\t\t\t\t/* Read the TL0PICIDX octet */\n\t\t\t\t\tbuffer++;\n\t\t\t\t\tvp8pd = *buffer;\n\t\t\t\t}\n\t\t\t\tif(tbit || kbit) {\n\t\t\t\t\t/* Read the TID/KEYIDX octet */\n\t\t\t\t\tbuffer++;\n\t\t\t\t\tvp8pd = *buffer;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbuffer++;\t/* Now we're in the payload */\n\t\t\tif(sbit) {\n\t\t\t\tunsigned long int vp8ph = 0;\n\t\t\t\tmemcpy(&vp8ph, buffer, 4);\n\t\t\t\tvp8ph = ntohl(vp8ph);\n\t\t\t\tuint8_t pbit = ((vp8ph & 0x01000000) >> 24);\n\t\t\t\tif(!pbit) {\n\t\t\t\t\t/* Get resolution */\n\t\t\t\t\tunsigned char *c = (unsigned char *)buffer+3;\n\t\t\t\t\t/* vet via sync code */\n\t\t\t\t\tif(c[0]!=0x9d||c[1]!=0x01||c[2]!=0x2a) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"First 3-bytes after header not what they're supposed to be?\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tunsigned short val3, val5;\n\t\t\t\t\t\tmemcpy(&val3,c+3,sizeof(short));\n\t\t\t\t\t\tint vp8w = swap2(val3)&0x3fff;\n\t\t\t\t\t\tint vp8ws = swap2(val3)>>14;\n\t\t\t\t\t\tmemcpy(&val5,c+5,sizeof(short));\n\t\t\t\t\t\tint vp8h = swap2(val5)&0x3fff;\n\t\t\t\t\t\tint vp8hs = swap2(val5)>>14;\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"(seq=%\"SCNu16\", ts=%\"SCNu64\") Key frame: %dx%d (scale=%dx%d)\\n\", tmp->seq, tmp->ts, vp8w, vp8h, vp8ws, vp8hs);\n\t\t\t\t\t\tif(vp8w*vp8h > max_width*max_height) {\n\t\t\t\t\t\t\tmax_width = vp8w;\n\t\t\t\t\t\t\tmax_height = vp8h;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(info && (last_width != vp8w || last_height != vp8h)) {\n\t\t\t\t\t\t\tlast_width = vp8w;\n\t\t\t\t\t\t\tlast_height = vp8h;\n\t\t\t\t\t\t\tif(resolutions == NULL) {\n\t\t\t\t\t\t\t\tresolutions = json_array();\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"resolution\", resolutions);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_t *resolution = json_object();\n\t\t\t\t\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"ts\", json_real(ts));\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"width\", json_integer(vp8w));\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"height\", json_integer(vp8h));\n\t\t\t\t\t\t\tjson_array_append_new(resolutions, resolution);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t/* https://tools.ietf.org/html/draft-ietf-payload-vp9 */\n\t\t\t/* Read the first bytes of the payload, and get the first octet (VP9 Payload Descriptor) */\n\t\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\t\tbytes = fread(prebuffer, sizeof(char), 16, file);\n\t\t\tif(bytes != 16) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < 16)...\\n\", bytes);\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tchar *buffer = (char *)&prebuffer;\n\t\t\tuint8_t vp9pd = *buffer;\n\t\t\tuint8_t ibit = (vp9pd & 0x80);\n\t\t\tuint8_t pbit = (vp9pd & 0x40);\n\t\t\tuint8_t lbit = (vp9pd & 0x20);\n\t\t\tuint8_t fbit = (vp9pd & 0x10);\n\t\t\tuint8_t vbit = (vp9pd & 0x02);\n\t\t\tbuffer++;\n\t\t\tif(ibit) {\n\t\t\t\t/* Read the PictureID octet */\n\t\t\t\tvp9pd = *buffer;\n\t\t\t\tuint16_t picid = vp9pd, wholepicid = picid;\n\t\t\t\tuint8_t mbit = (vp9pd & 0x80);\n\t\t\t\tif(!mbit) {\n\t\t\t\t\tbuffer++;\n\t\t\t\t} else {\n\t\t\t\t\tmemcpy(&picid, buffer, sizeof(uint16_t));\n\t\t\t\t\twholepicid = ntohs(picid);\n\t\t\t\t\tpicid = (wholepicid & 0x7FFF);\n\t\t\t\t\tbuffer += 2;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(lbit) {\n\t\t\t\tbuffer++;\n\t\t\t\tif(!fbit) {\n\t\t\t\t\t/* Non-flexible mode, skip TL0PICIDX */\n\t\t\t\t\tbuffer++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(fbit && pbit) {\n\t\t\t\t/* Skip reference indices */\n\t\t\t\tuint8_t nbit = 1;\n\t\t\t\twhile(nbit) {\n\t\t\t\t\tvp9pd = *buffer;\n\t\t\t\t\tnbit = (vp9pd & 0x01);\n\t\t\t\t\tbuffer++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(vbit) {\n\t\t\t\t/* Parse and skip SS */\n\t\t\t\tvp9pd = *buffer;\n\t\t\t\tuint n_s = (vp9pd & 0xE0) >> 5;\n\t\t\t\tn_s++;\n\t\t\t\tuint8_t ybit = (vp9pd & 0x10);\n\t\t\t\tif(ybit) {\n\t\t\t\t\t/* Iterate on all spatial layers and get resolution */\n\t\t\t\t\tbuffer++;\n\t\t\t\t\tuint i=0;\n\t\t\t\t\tfor(i=0; i<n_s; i++) {\n\t\t\t\t\t\t/* Width */\n\t\t\t\t\t\tuint16_t w;\n\t\t\t\t\t\tmemcpy(&w, buffer, sizeof(uint16_t));\n\t\t\t\t\t\tint width = ntohs(w);\n\t\t\t\t\t\tbuffer += 2;\n\t\t\t\t\t\t/* Height */\n\t\t\t\t\t\tuint16_t h;\n\t\t\t\t\t\tmemcpy(&h, buffer, sizeof(uint16_t));\n\t\t\t\t\t\tint height = ntohs(h);\n\t\t\t\t\t\tbuffer += 2;\n\t\t\t\t\t\tif(width*height > max_width*max_height) {\n\t\t\t\t\t\t\tmax_width = width;\n\t\t\t\t\t\t\tmax_height = height;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(info && (last_width != width || last_height != height)) {\n\t\t\t\t\t\t\tlast_width = width;\n\t\t\t\t\t\t\tlast_height = height;\n\t\t\t\t\t\t\t\t\tif(resolutions == NULL) {\n\t\t\t\t\t\t\t\tresolutions = json_array();\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"resolution\", resolutions);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tjson_t *resolution = json_object();\n\t\t\t\t\t\t\tdouble ts = (double)(tmp->ts-list->ts)/(double)90000;\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"ts\", json_real(ts));\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"width\", json_integer(width));\n\t\t\t\t\t\t\tjson_object_set_new(resolution, \"height\", json_integer(height));\n\t\t\t\t\t\t\tjson_array_append_new(resolutions, resolution);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n\tint mean_ts = min_ts_diff;\t/* FIXME: was an actual mean, (max_ts_diff+min_ts_diff)/2; */\n\tfps = (90000/(mean_ts > 0 ? mean_ts : 30));\n\tJANUS_LOG(LOG_INFO, \"  -- %dx%d (fps [%d,%d] ~ %d)\\n\", max_width, max_height, min_ts_diff, max_ts_diff, fps);\n\tif(max_width == 0 && max_height == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No key frame?? assuming 640x480...\\n\");\n\t\tmax_width = 640;\n\t\tmax_height = 480;\n\t}\n\tif(fps == 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No fps?? assuming 1...\\n\");\n\t\tfps = 1;\t/* Prevent divide by zero error */\n\t}\n\treturn 0;\n}\n\nint janus_pp_webm_process(FILE *file, janus_pp_frame_packet *list, gboolean vp8, int *working) {\n\tif(!file || !list || !working)\n\t\treturn -1;\n\tjanus_pp_frame_packet *tmp = list;\n\n\tint bytes = 0, numBytes = max_width*max_height*3;\t/* FIXME */\n\tuint8_t *received_frame = g_malloc0(numBytes);\n\tuint8_t *buffer = g_malloc0(numBytes), *start = buffer;\n\tint len = 0, frameLen = 0;\n\tint keyFrame = 0;\n\tgboolean keyframe_found = FALSE;\n#ifdef FF_API_INIT_PACKET\n\tAVPacket *packet = av_packet_alloc();\n#else\n\tAVPacket pkt = { 0 }, *packet = &pkt;\n#endif\n\tAVRational timebase = {1, 90000};\n\n\twhile(*working && tmp != NULL) {\n\t\tkeyFrame = 0;\n\t\tframeLen = 0;\n\t\tlen = 0;\n\t\twhile(tmp != NULL) {\n\t\t\tif(tmp->drop) {\n\t\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* RTP payload */\n\t\t\tbuffer = start;\n\t\t\tfseek(file, tmp->offset+12+tmp->skip, SEEK_SET);\n\t\t\tlen = tmp->len-12-tmp->skip;\n\t\t\tif(len < 1) {\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbytes = fread(buffer, sizeof(char), len, file);\n\t\t\tif(bytes != len) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Didn't manage to read all the bytes we needed (%d < %d)...\\n\", bytes, len);\n\t\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\t\tbreak;\n\t\t\t\ttmp = tmp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(vp8) {\n\t\t\t\t/* VP8 depay */\n\t\t\t\t\t/* https://tools.ietf.org/html/draft-ietf-payload-vp8 */\n\t\t\t\t/* Read the first octet (VP8 Payload Descriptor) */\n\t\t\t\tlen--;\n\t\t\t\tuint8_t vp8pd = *buffer;\n\t\t\t\tuint8_t xbit = (vp8pd & 0x80);\n\t\t\t\tuint8_t sbit = (vp8pd & 0x10);\n\n\t\t\t\tif (xbit) {\n\t\t\t\t\tbuffer++;\n\t\t\t\t\tlen--;\n\n\t\t\t\t\tvp8pd = *buffer;\n\t\t\t\t\tuint8_t ibit = (vp8pd & 0x80);\n\t\t\t\t\tuint8_t lbit = (vp8pd & 0x40);\n\t\t\t\t\tuint8_t tbit = (vp8pd & 0x20);\n\t\t\t\t\tuint8_t kbit = (vp8pd & 0x10);\n\t\t\t\t\tif(ibit) {\n\t\t\t\t\t\t/* Read the PictureID octet */\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t\tvp8pd = *buffer;\n\t\t\t\t\t\tuint16_t picid = vp8pd, wholepicid = picid;\n\t\t\t\t\t\tuint8_t mbit = (vp8pd & 0x80);\n\t\t\t\t\t\tif(mbit) {\n\t\t\t\t\t\t\tmemcpy(&picid, buffer, sizeof(uint16_t));\n\t\t\t\t\t\t\twholepicid = ntohs(picid);\n\t\t\t\t\t\t\tpicid = (wholepicid & 0x7FFF);\n\t\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\t\tlen--;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(lbit) {\n\t\t\t\t\t\t/* Read the TL0PICIDX octet */\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t\tvp8pd = *buffer;\n\t\t\t\t\t}\n\t\t\t\t\tif(tbit || kbit) {\n\t\t\t\t\t\t/* Read the TID/KEYIDX octet */\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t\tvp8pd = *buffer;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbuffer++;\t/* Now we're in the payload */\n\t\t\t\tif(sbit) {\n\t\t\t\t\tunsigned long int vp8ph = 0;\n\t\t\t\t\tmemcpy(&vp8ph, buffer, 4);\n\t\t\t\t\tvp8ph = ntohl(vp8ph);\n\t\t\t\t\tuint8_t pbit = ((vp8ph & 0x01000000) >> 24);\n\t\t\t\t\tif(!pbit) {\n\t\t\t\t\t\tkeyFrame = 1;\n\t\t\t\t\t\t/* Get resolution */\n\t\t\t\t\t\tunsigned char *c = buffer+3;\n\t\t\t\t\t\t/* vet via sync code */\n\t\t\t\t\t\tif(c[0]!=0x9d||c[1]!=0x01||c[2]!=0x2a) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"First 3-bytes after header not what they're supposed to be?\\n\");\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tunsigned short val3, val5;\n\t\t\t\t\t\t\tmemcpy(&val3,c+3,sizeof(short));\n\t\t\t\t\t\t\tint vp8w = swap2(val3)&0x3fff;\n\t\t\t\t\t\t\tint vp8ws = swap2(val3)>>14;\n\t\t\t\t\t\t\tmemcpy(&val5,c+5,sizeof(short));\n\t\t\t\t\t\t\tint vp8h = swap2(val5)&0x3fff;\n\t\t\t\t\t\t\tint vp8hs = swap2(val5)>>14;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"(seq=%\"SCNu16\", ts=%\"SCNu64\") Key frame: %dx%d (scale=%dx%d)\\n\", tmp->seq, tmp->ts, vp8w, vp8h, vp8ws, vp8hs);\n\t\t\t\t\t\t\t/* Is this the first keyframe we find? */\n\t\t\t\t\t\t\tif(!keyframe_found) {\n\t\t\t\t\t\t\t\tkeyframe_found = TRUE;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"First keyframe: %\"SCNu64\"\\n\", tmp->ts-list->ts);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\t/* VP9 depay */\n\t\t\t\t\t/* https://tools.ietf.org/html/draft-ietf-payload-vp9-02 */\n\t\t\t\t/* Read the first octet (VP9 Payload Descriptor) */\n\t\t\t\tuint8_t vp9pd = *buffer;\n\t\t\t\tuint8_t ibit = (vp9pd & 0x80);\n\t\t\t\tuint8_t pbit = (vp9pd & 0x40);\n\t\t\t\tuint8_t lbit = (vp9pd & 0x20);\n\t\t\t\tuint8_t fbit = (vp9pd & 0x10);\n\t\t\t\tuint8_t vbit = (vp9pd & 0x02);\n\t\t\t\t/* Move to the next octet and see what's there */\n\t\t\t\tbuffer++;\n\t\t\t\tlen--;\n\t\t\t\tif(ibit) {\n\t\t\t\t\t/* Read the PictureID octet */\n\t\t\t\t\tvp9pd = *buffer;\n\t\t\t\t\tuint16_t picid = vp9pd, wholepicid = picid;\n\t\t\t\t\tuint8_t mbit = (vp9pd & 0x80);\n\t\t\t\t\tif(!mbit) {\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmemcpy(&picid, buffer, sizeof(uint16_t));\n\t\t\t\t\t\twholepicid = ntohs(picid);\n\t\t\t\t\t\tpicid = (wholepicid & 0x7FFF);\n\t\t\t\t\t\tbuffer += 2;\n\t\t\t\t\t\tlen -= 2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(lbit) {\n\t\t\t\t\tbuffer++;\n\t\t\t\t\tlen--;\n\t\t\t\t\tif(!fbit) {\n\t\t\t\t\t\t/* Non-flexible mode, skip TL0PICIDX */\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(fbit && pbit) {\n\t\t\t\t\t/* Skip reference indices */\n\t\t\t\t\tuint8_t nbit = 1;\n\t\t\t\t\twhile(nbit) {\n\t\t\t\t\t\tvp9pd = *buffer;\n\t\t\t\t\t\tnbit = (vp9pd & 0x01);\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(vbit) {\n\t\t\t\t\t/* Parse and skip SS */\n\t\t\t\t\tvp9pd = *buffer;\n\t\t\t\t\tint n_s = (vp9pd & 0xE0) >> 5;\n\t\t\t\t\tn_s++;\n\t\t\t\t\tuint8_t ybit = (vp9pd & 0x10);\n\t\t\t\t\tuint8_t gbit = (vp9pd & 0x08);\n\t\t\t\t\tif(ybit) {\n\t\t\t\t\t\tkeyFrame = 1;\n\t\t\t\t\t\t/* Iterate on all spatial layers and get resolution */\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t\tint i=0;\n\t\t\t\t\t\tfor(i=0; i<n_s; i++) {\n\t\t\t\t\t\t\t/* Been there, done that: skip skip skip */\n\t\t\t\t\t\t\tbuffer += 4;\n\t\t\t\t\t\t\tlen -= 4;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Is this the first keyframe we find?\n\t\t\t\t\t\t * (FIXME assuming this really means \"keyframe...) */\n\t\t\t\t\t\tif(!keyframe_found) {\n\t\t\t\t\t\t\tkeyframe_found = TRUE;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"First keyframe: %\"SCNu64\"\\n\", tmp->ts-list->ts);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(gbit) {\n\t\t\t\t\t\tif(!ybit) {\n\t\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\t\tlen--;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tuint8_t n_g = *buffer;\n\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\tlen--;\n\t\t\t\t\t\tif(n_g > 0) {\n\t\t\t\t\t\t\tuint i=0;\n\t\t\t\t\t\t\tfor(i=0; i<n_g; i++) {\n\t\t\t\t\t\t\t\t/* Read the R bits */\n\t\t\t\t\t\t\t\tvp9pd = *buffer;\n\t\t\t\t\t\t\t\tint r = (vp9pd & 0x0C) >> 2;\n\t\t\t\t\t\t\t\tif(r > 0) {\n\t\t\t\t\t\t\t\t\t/* Skip reference indices */\n\t\t\t\t\t\t\t\t\tbuffer += r;\n\t\t\t\t\t\t\t\t\tlen -= r;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbuffer++;\n\t\t\t\t\t\t\t\tlen--;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Frame manipulation */\n\t\t\tif(len > 0) {\n\t\t\t\tif(frameLen + len + AV_INPUT_BUFFER_PADDING_SIZE > numBytes) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Frame exceeds buffer size...\\n\");\n\t\t\t\t} else {\n\t\t\t\t\tmemcpy(received_frame + frameLen, buffer, len);\n\t\t\t\t\tframeLen += len;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(len == 0)\n\t\t\t\tbreak;\n\t\t\t/* Check if timestamp changes: marker bit is not mandatory, and may be lost as well */\n\t\t\tif(tmp->next == NULL || tmp->next->ts > tmp->ts)\n\t\t\t\tbreak;\n\t\t\ttmp = tmp->next;\n\t\t}\n\t\tif(frameLen > 0) {\n\t\t\tmemset(received_frame + frameLen, 0, FF_INPUT_BUFFER_PADDING_SIZE);\n\n#ifdef FF_API_INIT_PACKET\n\t\t\tav_packet_unref(packet);\n#else\n\t\t\tav_init_packet(packet);\n#endif\n\t\t\tpacket->stream_index = 0;\n\t\t\tpacket->data = received_frame;\n\t\t\tpacket->size = frameLen;\n\t\t\tif(keyFrame)\n\t\t\t\t//~ packet.flags |= PKT_FLAG_KEY;\n\t\t\t\tpacket->flags |= AV_PKT_FLAG_KEY;\n\n\t\t\t/* First we save to the file... */\n\t\t\tpacket->pts = packet->dts = av_rescale_q(tmp->ts-list->ts, timebase, fctx->streams[0]->time_base);\n\t\t\tif(fctx) {\n\t\t\t\tint res = av_write_frame(fctx, packet);\n\t\t\t\tif(res < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error writing video frame to file... (error %d, %s)\\n\",\n\t\t\t\t\t\tres, av_err2str(res));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttmp = tmp->next;\n\t}\n#ifdef FF_API_INIT_PACKET\n\tav_packet_free(&packet);\n#endif\n\tg_free(received_frame);\n\tg_free(start);\n\treturn 0;\n}\n\n/* Close WebM file */\nvoid janus_pp_webm_close(void) {\n\tif(fctx != NULL) {\n\t\tav_write_trailer(fctx);\n\t\tavio_close(fctx->pb);\n\t\tavformat_free_context(fctx);\n\t}\n}\n"
  },
  {
    "path": "src/postprocessing/pp-webm.h",
    "content": "/*! \\file    pp-webm.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Post-processing to generate .webm files (headers)\n * \\details  Implementation of the post-processing code (based on FFmpeg)\n * needed to generate .webm files out of VP8/VP9 RTP frames.\n *\n * \\ingroup postprocessing\n * \\ref postprocessing\n */\n\n#ifndef JANUS_PP_WEBM\n#define JANUS_PP_WEBM\n\n#include <stdio.h>\n#include <jansson.h>\n\n#include \"pp-rtp.h\"\n\n/* WebM stuff */\nconst char **janus_pp_webm_get_extensions(void);\nint janus_pp_webm_create(char *destination, char *metadata, gboolean vp8, const char *extension);\nint janus_pp_webm_preprocess(FILE *file, janus_pp_frame_packet *list, gboolean vp8, json_t *info);\nint janus_pp_webm_process(FILE *file, janus_pp_frame_packet *list, gboolean vp8, int *working);\nvoid janus_pp_webm_close(void);\n\n\n#endif\n"
  },
  {
    "path": "src/record.c",
    "content": "/*! \\file    record.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Audio/Video recorder\n * \\details  Implementation of a simple recorder utility that plugins\n * can make use of to record audio/video frames to a Janus file. This\n * file just saves RTP frames in a structured way, so that they can be\n * post-processed later on to get a valid container file (e.g., a .opus\n * file for Opus audio or a .webm file for VP8 video) and keep things\n * simpler on the plugin and core side. Check the \\ref recordings\n * documentation for more details.\n * \\note If you want to record both audio and video, you'll have to use\n * two different recorders. Any muxing in the same container will have\n * to be done in the post-processing phase.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <arpa/inet.h>\n#include <sys/stat.h>\n#include <errno.h>\n#include <libgen.h>\n\n#include <glib.h>\n#include <jansson.h>\n\n#include \"record.h\"\n#include \"debug.h\"\n#include \"utils.h\"\n\n\n/* Info header in the structured recording */\nstatic const char *header = \"MJR00002\";\n/* Frame header in the structured recording */\nstatic const char *frame_header = \"MEET\";\n\n/* Whether the filenames should have a temporary extension, while saving, or not (default=false) */\nstatic gboolean rec_tempname = FALSE;\n/* Extension to add in case tempnames is true (default=\"tmp\" --> \".tmp\") */\nstatic char *rec_tempext = NULL;\n\nvoid janus_recorder_init(gboolean tempnames, const char *extension) {\n\tJANUS_LOG(LOG_INFO, \"Initializing recorder code\\n\");\n\tif(tempnames) {\n\t\trec_tempname = TRUE;\n\t\tif(extension == NULL) {\n\t\t\trec_tempext = g_strdup(\"tmp\");\n\t\t\tJANUS_LOG(LOG_INFO, \"  -- No extension provided, using default one (tmp)\\n\");\n\t\t} else {\n\t\t\trec_tempext = g_strdup(extension);\n\t\t\tJANUS_LOG(LOG_INFO, \"  -- Using temporary extension .%s\\n\", rec_tempext);\n\t\t}\n\t}\n}\n\nvoid janus_recorder_deinit(void) {\n\trec_tempname = FALSE;\n\tg_free(rec_tempext);\n}\n\nstatic void janus_recorder_free(const janus_refcount *recorder_ref) {\n\tjanus_recorder *recorder = janus_refcount_containerof(recorder_ref, janus_recorder, ref);\n\t/* This recorder can be destroyed, free all the resources */\n\tjanus_recorder_close(recorder);\n\tg_free(recorder->dir);\n\trecorder->dir = NULL;\n\tg_free(recorder->filename);\n\trecorder->filename = NULL;\n\tif(recorder->file != NULL)\n\t\tfclose(recorder->file);\n\trecorder->file = NULL;\n\tg_free(recorder->codec);\n\trecorder->codec = NULL;\n\tg_free(recorder->fmtp);\n\trecorder->fmtp = NULL;\n\tg_free(recorder->description);\n\trecorder->description = NULL;\n\tif(recorder->extensions != NULL)\n\t\tg_hash_table_destroy(recorder->extensions);\n\tjanus_mutex_destroy(&recorder->mutex);\n\tg_free(recorder);\n}\n\njanus_recorder *janus_recorder_create(const char *dir, const char *codec, const char *filename) {\n\t/* Same as janus_recorder_create_full, but with no fmtp */\n\treturn janus_recorder_create_full(dir, codec, NULL, filename);\n}\njanus_recorder *janus_recorder_create_full(const char *dir, const char *codec, const char *fmtp, const char *filename) {\n\tjanus_recorder_medium type = JANUS_RECORDER_AUDIO;\n\tif(codec == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Missing codec information\\n\");\n\t\treturn NULL;\n\t}\n\tif(!strcasecmp(codec, \"vp8\") || !strcasecmp(codec, \"vp9\") || !strcasecmp(codec, \"h264\")\n\t\t\t || !strcasecmp(codec, \"av1\") || !strcasecmp(codec, \"h265\")) {\n\t\ttype = JANUS_RECORDER_VIDEO;\n\t} else if(!strcasecmp(codec, \"opus\") || !strcasecmp(codec, \"multiopus\")\n\t\t\t|| !strcasecmp(codec, \"g711\") || !strcasecmp(codec, \"pcmu\") || !strcasecmp(codec, \"pcma\")\n\t\t\t|| !strcasecmp(codec, \"g722\") || !strcasecmp(codec, \"l16-48\") || !strcasecmp(codec, \"l16\")) {\n\t\ttype = JANUS_RECORDER_AUDIO;\n\t} else if(!strcasecmp(codec, \"text\") || !strcasecmp(codec, \"binary\")) {\n\t\t/* Data channels may be text or binary, so that's what we can save too */\n\t\ttype = JANUS_RECORDER_DATA;\n\t} else {\n\t\t/* We don't recognize the codec: while we might go on anyway, we'd rather fail instead */\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec '%s'\\n\", codec);\n\t\treturn NULL;\n\t}\n\t/* Create the recorder */\n\tjanus_recorder *rc = g_malloc0(sizeof(janus_recorder));\n\tjanus_refcount_init(&rc->ref, janus_recorder_free);\n\tjanus_rtp_switching_context_reset(&rc->context);\n\trc->dir = NULL;\n\trc->filename = NULL;\n\trc->file = NULL;\n\trc->codec = g_strdup(codec);\n\trc->fmtp = fmtp ? g_strdup(fmtp) : NULL;\n\trc->description = NULL;\n\trc->created = janus_get_real_time();\n\tjanus_mutex_init(&rc->mutex);\n\tconst char *rec_dir = NULL;\n\tconst char *rec_file = NULL;\n\tchar *copy_for_parent = NULL;\n\tchar *copy_for_base = NULL;\n\t/* Check dir and filename values */\n\tif(filename != NULL) {\n\t\t/* Helper copies to avoid overwriting */\n\t\tcopy_for_parent = g_strdup(filename);\n\t\tcopy_for_base = g_strdup(filename);\n\t\t/* Get filename parent folder */\n\t\tconst char *filename_parent = dirname(copy_for_parent);\n\t\t/* Get filename base file */\n\t\tconst char *filename_base = basename(copy_for_base);\n\t\tif(!dir) {\n\t\t\t/* If dir is NULL we have to create filename_parent and filename_base */\n\t\t\trec_dir = filename_parent;\n\t\t\trec_file = filename_base;\n\t\t} else {\n\t\t\t/* If dir is valid we have to create dir and filename*/\n\t\t\trec_dir = dir;\n\t\t\trec_file = filename;\n\t\t\tif(strcasecmp(filename_parent, \".\") || strcasecmp(filename_base, filename)) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported combination of dir and filename %s %s\\n\", dir, filename);\n\t\t\t}\n\t\t}\n\t}\n\tif(rec_dir != NULL) {\n\t\t/* Check if this directory exists, and create it if needed */\n\t\tstruct stat s;\n\t\tint err = stat(rec_dir, &s);\n\t\tif(err == -1) {\n\t\t\tif(ENOENT == errno) {\n\t\t\t\t/* Directory does not exist, try creating it */\n\t\t\t\tif(janus_mkdir(rec_dir, 0755) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"mkdir (%s) error: %d (%s)\\n\", rec_dir, errno, g_strerror(errno));\n\t\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\t\tg_free(copy_for_parent);\n\t\t\t\t\tg_free(copy_for_base);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"stat (%s) error: %d (%s)\\n\", rec_dir, errno, g_strerror(errno));\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tg_free(copy_for_parent);\n\t\t\t\tg_free(copy_for_base);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t} else {\n\t\t\tif(S_ISDIR(s.st_mode)) {\n\t\t\t\t/* Directory exists */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Directory exists: %s\\n\", rec_dir);\n\t\t\t} else {\n\t\t\t\t/* File exists but it's not a directory? */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Not a directory? %s\\n\", rec_dir);\n\t\t\t\tjanus_recorder_destroy(rc);\n\t\t\t\tg_free(copy_for_parent);\n\t\t\t\tg_free(copy_for_base);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t}\n\t}\n\tchar newname[1024];\n\tmemset(newname, 0, 1024);\n\tif(rec_file == NULL) {\n\t\t/* Choose a random username */\n\t\tif(!rec_tempname) {\n\t\t\t/* Use .mjr as an extension right away */\n\t\t\tg_snprintf(newname, 1024, \"janus-recording-%\"SCNu32\".mjr\", janus_random_uint32());\n\t\t} else {\n\t\t\t/* Append the temporary extension to .mjr, we'll rename when closing */\n\t\t\tg_snprintf(newname, 1024, \"janus-recording-%\"SCNu32\".mjr.%s\", janus_random_uint32(), rec_tempext);\n\t\t}\n\t} else {\n\t\t/* Just append the extension */\n\t\tif(!rec_tempname) {\n\t\t\t/* Use .mjr as an extension right away */\n\t\t\tg_snprintf(newname, 1024, \"%s.mjr\", rec_file);\n\t\t} else {\n\t\t\t/* Append the temporary extension to .mjr, we'll rename when closing */\n\t\t\tg_snprintf(newname, 1024, \"%s.mjr.%s\", rec_file, rec_tempext);\n\t\t}\n\t}\n\t/* Try opening the file now */\n\tif(rec_dir == NULL) {\n\t\t/* Make sure folder to save to is not protected */\n\t\tif(janus_is_folder_protected(newname)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Target recording path '%s' is in protected folder...\\n\", newname);\n\t\t\tjanus_recorder_destroy(rc);\n\t\t\tg_free(copy_for_parent);\n\t\t\tg_free(copy_for_base);\n\t\t\treturn NULL;\n\t\t}\n\t\trc->file = fopen(newname, \"wb\");\n\t} else {\n\t\tchar path[1024];\n\t\tmemset(path, 0, 1024);\n\t\tg_snprintf(path, 1024, \"%s/%s\", rec_dir, newname);\n\t\t/* Make sure folder to save to is not protected */\n\t\tif(janus_is_folder_protected(path)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Target recording path '%s' is in protected folder...\\n\", path);\n\t\t\tjanus_recorder_destroy(rc);\n\t\t\tg_free(copy_for_parent);\n\t\t\tg_free(copy_for_base);\n\t\t\treturn NULL;\n\t\t}\n\t\trc->file = fopen(path, \"wb\");\n\t}\n\tif(rc->file == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"fopen error: %d\\n\", errno);\n\t\tjanus_recorder_destroy(rc);\n\t\tg_free(copy_for_parent);\n\t\tg_free(copy_for_base);\n\t\treturn NULL;\n\t}\n\tif(rec_dir)\n\t\trc->dir = g_strdup(rec_dir);\n\trc->filename = g_strdup(newname);\n\trc->type = type;\n\t/* Write the first part of the header */\n\tsize_t res = fwrite(header, sizeof(char), strlen(header), rc->file);\n\tif(res != strlen(header)) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't write .mjr header (%zu != %zu, %s)\\n\",\n\t\t\tres, strlen(header), g_strerror(errno));\n\t\tjanus_recorder_destroy(rc);\n\t\tg_free(copy_for_parent);\n\t\tg_free(copy_for_base);\n\t\treturn NULL;\n\t}\n\tg_atomic_int_set(&rc->writable, 1);\n\t/* We still need to also write the info header first */\n\tg_atomic_int_set(&rc->header, 0);\n\t/* Done */\n\tg_atomic_int_set(&rc->destroyed, 0);\n\tg_free(copy_for_parent);\n\tg_free(copy_for_base);\n\treturn rc;\n}\n\nint janus_recorder_pause(janus_recorder *recorder) {\n\tif(!recorder)\n\t\treturn -1;\n\tif(g_atomic_int_compare_and_exchange(&recorder->paused, 0, 1))\n\t\treturn 0;\n\treturn -2;\n}\n\nint janus_recorder_resume(janus_recorder *recorder) {\n\tif(!recorder)\n\t\treturn -1;\n\tjanus_mutex_lock_nodebug(&recorder->mutex);\n\tif(g_atomic_int_compare_and_exchange(&recorder->paused, 1, 0)) {\n\t\tif(recorder->type == JANUS_RECORDER_AUDIO || recorder->type == JANUS_RECORDER_VIDEO) {\n\t\t\trecorder->context.ts_reset = TRUE;\n\t\t\trecorder->context.seq_reset = TRUE;\n\t\t\trecorder->context.last_time = janus_get_monotonic_time();\n\t\t}\n\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\treturn 0;\n\t}\n\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\treturn -2;\n}\n\nint janus_recorder_add_extmap(janus_recorder *recorder, int id, const char *extmap) {\n\tif(!recorder || g_atomic_int_get(&recorder->header) || id < 1 || id > 15 || extmap == NULL)\n\t\treturn -1;\n\tjanus_mutex_lock_nodebug(&recorder->mutex);\n\tif(recorder->extensions == NULL)\n\t\trecorder->extensions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)g_free);\n\tg_hash_table_insert(recorder->extensions, GINT_TO_POINTER(id), g_strdup(extmap));\n\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\treturn 0;\n}\n\nint janus_recorder_description(janus_recorder *recorder, const char *description) {\n\tif(!recorder || !description)\n\t\treturn -1;\n\tjanus_mutex_lock_nodebug(&recorder->mutex);\n\tif(g_atomic_int_get(&recorder->header)) {\n\t\t/* No use setting description once it's already written in the MJR file */\n\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\treturn 0;\n\t}\n\tg_free(recorder->description);\n\trecorder->description = g_strdup(description);\n\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\treturn 0;\n}\n\nint janus_recorder_opusred(janus_recorder *recorder, int red_pt) {\n\tif(!recorder)\n\t\treturn -1;\n\tif(!g_atomic_int_get(&recorder->header)) {\n\t\trecorder->opusred_pt = red_pt;\n\t\treturn 0;\n\t}\n\treturn -1;\n}\n\nint janus_recorder_encrypted(janus_recorder *recorder) {\n\tif(!recorder)\n\t\treturn -1;\n\tif(!g_atomic_int_get(&recorder->header)) {\n\t\trecorder->encrypted = TRUE;\n\t\treturn 0;\n\t}\n\treturn -1;\n}\n\nint janus_recorder_save_frame(janus_recorder *recorder, char *buffer, uint length) {\n\tif(!recorder)\n\t\treturn -1;\n\tjanus_mutex_lock_nodebug(&recorder->mutex);\n\tif(!buffer || length < 1) {\n\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\treturn -2;\n\t}\n\tif(!recorder->file) {\n\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\treturn -3;\n\t}\n\tif(!g_atomic_int_get(&recorder->writable)) {\n\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\treturn -4;\n\t}\n\tif(g_atomic_int_get(&recorder->paused)) {\n\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\treturn -5;\n\t}\n\tgint64 now = janus_get_monotonic_time();\n\tif(!g_atomic_int_get(&recorder->header)) {\n\t\t/* Write info header as a JSON formatted info */\n\t\tjson_t *info = json_object();\n\t\t/* FIXME Codecs should be configurable in the future */\n\t\tconst char *type = NULL;\n\t\tif(recorder->type == JANUS_RECORDER_AUDIO)\n\t\t\ttype = \"a\";\n\t\telse if(recorder->type == JANUS_RECORDER_VIDEO)\n\t\t\ttype = \"v\";\n\t\telse if(recorder->type == JANUS_RECORDER_DATA)\n\t\t\ttype = \"d\";\n\t\tjson_object_set_new(info, \"t\", json_string(type));\t\t\t\t\t\t\t\t/* Audio/Video/Data */\n\t\tjson_object_set_new(info, \"c\", json_string(recorder->codec));\t\t\t\t\t/* Media codec */\n\t\tif(recorder->fmtp)\n\t\t\tjson_object_set_new(info, \"f\", json_string(recorder->fmtp));\t\t\t\t/* Codec-specific info */\n\t\tif(recorder->description)\n\t\t\tjson_object_set_new(info, \"d\", json_string(recorder->description));\t\t/* Stream description */\n\t\tif(recorder->extensions) {\n\t\t\t/* Add the extmaps to the JSON object */\n\t\t\tjson_t *extmaps = NULL;\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer key, value;\n\t\t\tg_hash_table_iter_init(&iter, recorder->extensions);\n\t\t\twhile(g_hash_table_iter_next(&iter, &key, &value)) {\n\t\t\t\tint id = GPOINTER_TO_INT(key);\n\t\t\t\tchar *extmap = (char *)value;\n\t\t\t\tif(id > 0 && id < 16 && extmap != NULL) {\n\t\t\t\t\tif(extmaps == NULL)\n\t\t\t\t\t\textmaps = json_object();\n\t\t\t\t\tchar id_str[3];\n\t\t\t\t\tg_snprintf(id_str, sizeof(id_str), \"%d\", id);\n\t\t\t\t\tjson_object_set_new(extmaps, id_str, json_string(extmap));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(extmaps != NULL)\n\t\t\t\tjson_object_set_new(info, \"x\", extmaps);\n\t\t}\n\t\tjson_object_set_new(info, \"s\", json_integer(recorder->created));\t\t\t\t/* Created time */\n\t\tjson_object_set_new(info, \"u\", json_integer(janus_get_real_time()));\t\t\t/* First frame written time */\n\t\t/* If this is audio and using RED, take note of the payload type */\n\t\tif(recorder->type == JANUS_RECORDER_AUDIO && recorder->opusred_pt > 0)\n\t\t\tjson_object_set_new(info, \"or\", json_integer(recorder->opusred_pt));\n\t\t/* If media will be end-to-end encrypted, mark it in the recording header */\n\t\tif(recorder->encrypted)\n\t\t\tjson_object_set_new(info, \"e\", json_true());\n\t\tgchar *info_text = json_dumps(info, JSON_PRESERVE_ORDER);\n\t\tjson_decref(info);\n\t\tif(info_text == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error converting header to text...\\n\");\n\t\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\t\treturn -5;\n\t\t}\n\t\tuint16_t info_bytes = htons(strlen(info_text));\n\t\tsize_t res = fwrite(&info_bytes, sizeof(uint16_t), 1, recorder->file);\n\t\tif(res != 1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write size of JSON header in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\tres, sizeof(uint16_t), g_strerror(errno));\n\t\t}\n\t\tres = fwrite(info_text, sizeof(char), strlen(info_text), recorder->file);\n\t\tif(res != strlen(info_text)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write JSON header in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\tres, strlen(info_text), g_strerror(errno));\n\t\t}\n\t\tfree(info_text);\n\t\t/* Done */\n\t\trecorder->started = now;\n\t\tg_atomic_int_set(&recorder->header, 1);\n\t}\n\t/* Write frame header (fixed part[4], timestamp[4], length[2]) */\n\tsize_t res = fwrite(frame_header, sizeof(char), strlen(frame_header), recorder->file);\n\tif(res != strlen(frame_header)) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't write frame header in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\tres, strlen(frame_header), g_strerror(errno));\n\t}\n\tuint32_t timestamp = (uint32_t)(now > recorder->started ? ((now - recorder->started)/1000) : 0);\n\ttimestamp = htonl(timestamp);\n\tres = fwrite(&timestamp, sizeof(uint32_t), 1, recorder->file);\n\tif(res != 1) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't write frame timestamp in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\tres, sizeof(uint32_t), g_strerror(errno));\n\t}\n\tuint16_t header_bytes = htons(recorder->type == JANUS_RECORDER_DATA ? (length+sizeof(gint64)) : length);\n\tres = fwrite(&header_bytes, sizeof(uint16_t), 1, recorder->file);\n\tif(res != 1) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't write size of frame in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\tres, sizeof(uint16_t), g_strerror(errno));\n\t}\n\tif(recorder->type == JANUS_RECORDER_DATA) {\n\t\t/* If it's data, then we need to prepend timing related info, as it's not there by itself */\n\t\tgint64 now = htonll((uint64_t)janus_get_real_time());\n\t\tres = fwrite(&now, sizeof(gint64), 1, recorder->file);\n\t\tif(res != 1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't write data timestamp in .mjr file (%zu != %zu, %s), expect issues post-processing\\n\",\n\t\t\t\tres, sizeof(gint64), g_strerror(errno));\n\t\t}\n\t}\n\t/* Edit packet header if needed */\n\tjanus_rtp_header *header = (janus_rtp_header *)buffer;\n\tuint32_t ssrc = 0;\n\tuint16_t seq = 0;\n\tif(recorder->type != JANUS_RECORDER_DATA) {\n\t\tssrc = ntohl(header->ssrc);\n\t\tseq = ntohs(header->seq_number);\n\t\ttimestamp = ntohl(header->timestamp);\n\t\tjanus_rtp_header_update(header, &recorder->context, recorder->type == JANUS_RECORDER_VIDEO, 0);\n\t}\n\t/* Save packet on file */\n\tint temp = 0, tot = length;\n\twhile(tot > 0) {\n\t\ttemp = fwrite(buffer+length-tot, sizeof(char), tot, recorder->file);\n\t\tif(temp <= 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error saving frame...\\n\");\n\t\t\tif(recorder->type != JANUS_RECORDER_DATA) {\n\t\t\t\t/* Restore packet header data */\n\t\t\t\theader->ssrc = htonl(ssrc);\n\t\t\t\theader->seq_number = htons(seq);\n\t\t\t\theader->timestamp = htonl(timestamp);\n\t\t\t}\n\t\t\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\t\t\treturn -6;\n\t\t}\n\t\ttot -= temp;\n\t}\n\tif(recorder->type != JANUS_RECORDER_DATA) {\n\t\t/* Restore packet header data */\n\t\theader->ssrc = htonl(ssrc);\n\t\theader->seq_number = htons(seq);\n\t\theader->timestamp = htonl(timestamp);\n\t}\n\t/* Done */\n\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\treturn 0;\n}\n\nint janus_recorder_close(janus_recorder *recorder) {\n\tif(!recorder || !g_atomic_int_compare_and_exchange(&recorder->writable, 1, 0))\n\t\treturn -1;\n\tjanus_mutex_lock_nodebug(&recorder->mutex);\n\tif(recorder->file) {\n\t\tfseek(recorder->file, 0L, SEEK_END);\n\t\tsize_t fsize = ftell(recorder->file);\n\t\tfseek(recorder->file, 0L, SEEK_SET);\n\t\tJANUS_LOG(LOG_INFO, \"File is %zu bytes: %s\\n\", fsize, recorder->filename);\n\t}\n\tif(rec_tempname) {\n\t\t/* We need to rename the file, to remove the temporary extension */\n\t\tchar newname[1024];\n\t\tmemset(newname, 0, 1024);\n\t\tg_snprintf(newname, strlen(recorder->filename)-strlen(rec_tempext), \"%s\", recorder->filename);\n\t\tchar oldpath[1024];\n\t\tmemset(oldpath, 0, 1024);\n\t\tchar newpath[1024];\n\t\tmemset(newpath, 0, 1024);\n\t\tif(recorder->dir) {\n\t\t\tg_snprintf(newpath, 1024, \"%s/%s\", recorder->dir, newname);\n\t\t\tg_snprintf(oldpath, 1024, \"%s/%s\", recorder->dir, recorder->filename);\n\t\t} else {\n\t\t\tg_snprintf(newpath, 1024, \"%s\", newname);\n\t\t\tg_snprintf(oldpath, 1024, \"%s\", recorder->filename);\n\t\t}\n\t\tif(rename(oldpath, newpath) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error renaming %s to %s...\\n\", recorder->filename, newname);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_INFO, \"Recording renamed: %s\\n\", newname);\n\t\t\tg_free(recorder->filename);\n\t\t\trecorder->filename = g_strdup(newname);\n\t\t}\n\t}\n\tjanus_mutex_unlock_nodebug(&recorder->mutex);\n\treturn 0;\n}\n\nvoid janus_recorder_destroy(janus_recorder *recorder) {\n\tif(!recorder || !g_atomic_int_compare_and_exchange(&recorder->destroyed, 0, 1))\n\t\treturn;\n\tjanus_refcount_decrease(&recorder->ref);\n}\n"
  },
  {
    "path": "src/record.h",
    "content": "/*! \\file    record.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Audio/Video recorder (headers)\n * \\details  Implementation of a simple recorder utility that plugins\n * can make use of to record audio/video frames to a Janus file. This\n * file just saves RTP frames in a structured way, so that they can be\n * post-processed later on to get a valid container file (e.g., a .opus\n * file for Opus audio or a .webm file for VP8 video) and keep things\n * simpler on the plugin and core side. Check the \\ref recordings\n * documentation for more details.\n * \\note If you want to record both audio and video, you'll have to use\n * two different recorders. Any muxing in the same container will have\n * to be done in the post-processing phase.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_RECORD_H\n#define JANUS_RECORD_H\n\n#include <inttypes.h>\n#include <string.h>\n#include <stdio.h>\n#include <stdlib.h>\n\n#include \"mutex.h\"\n#include \"refcount.h\"\n#include \"rtp.h\"\n\n\n/*! \\brief Media types we can record */\ntypedef enum janus_recorder_medium {\n\tJANUS_RECORDER_AUDIO,\n\tJANUS_RECORDER_VIDEO,\n\tJANUS_RECORDER_DATA\n} janus_recorder_medium;\n\n/*! \\brief Structure that represents a recorder */\ntypedef struct janus_recorder {\n\t/*! \\brief Absolute path to the directory where the recorder file is stored */\n\tchar *dir;\n\t/*! \\brief Filename of this recorder file */\n\tchar *filename;\n\t/*! \\brief Recording file */\n\tFILE *file;\n\t/*! \\brief Codec the packets to record are encoded in (\"vp8\", \"vp9\", \"h264\", \"opus\", \"pcma\", \"pcmu\", \"g722\") */\n\tchar *codec;\n\t/*! \\brief Codec-specific info (e.g., H.264 or VP9 profile) */\n\tchar *fmtp;\n\t/*! \\brief Stream description */\n\tchar *description;\n\t/*! \\brief List of RTP extensions (as a hashtable, indexed by ID) in this recording */\n\tGHashTable *extensions;\n\t/*! \\brief When the recording file has been created and started */\n\tgint64 created, started;\n\t/*! \\brief Media this instance is recording */\n\tjanus_recorder_medium type;\n\t/*! \\brief In case RED is used for Opus, its payload types */\n\tint opusred_pt;\n\t/*! \\brief Whether the recording contains end-to-end encrypted media or not */\n\tgboolean encrypted;\n\t/*! \\brief Whether the info header for this recorder instance has already been written or not */\n\tvolatile int header;\n\t/*! \\brief Whether this recorder instance can be used for writing or not */\n\tvolatile int writable;\n\t/*! \\brief Whether writing s/RTP packets/data is paused */\n\tvolatile int paused;\n\t/*! \\brief RTP switching context for rewriting RTP headers */\n\tjanus_rtp_switching_context context;\n\t/*! \\brief Mutex to lock/unlock this recorder instance */\n\tjanus_mutex mutex;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_recorder;\n\n/*! \\brief Initialize the recorder code\n * @param[in] tempnames Whether the filenames should have a temporary extension, while saving, or not\n * @param[in] extension Extension to add in case tempnames is true */\nvoid janus_recorder_init(gboolean tempnames, const char *extension);\n/*! \\brief De-initialize the recorder code */\nvoid janus_recorder_deinit(void);\n\n/*! \\brief Create a new recorder\n * \\note If no target directory is provided, the current directory will be used. If no filename\n * is passed, a random filename will be used.\n * @param[in] dir Path of the directory to save the recording into (will try to create it if it doesn't exist)\n * @param[in] codec Codec the packets to record are encoded in (\"vp8\", \"opus\", \"h264\", \"g711\", \"vp9\")\n * @param[in] filename Filename to use for the recording\n * @returns A valid janus_recorder instance in case of success, NULL otherwise */\njanus_recorder *janus_recorder_create(const char *dir, const char *codec, const char *filename);\n/*! \\brief Create a new recorder with additional info\n * \\note This is to allow adding more arguments to janus_recorder_create, but\n * still keep janus_recorder_create in place for backwards compatibility.\n * @param[in] dir Path of the directory to save the recording into (will try to create it if it doesn't exist)\n * @param[in] codec Codec the packets to record are encoded in (\"vp8\", \"opus\", \"h264\", \"g711\", \"vp9\")\n * @param[in] fmtp Codec-specific details (e.g., the H.264 or VP9 profile)\n * @param[in] filename Filename to use for the recording\n * @returns A valid janus_recorder instance in case of success, NULL otherwise */\njanus_recorder *janus_recorder_create_full(const char *dir, const char *codec, const char *fmtp, const char *filename);\n/*! \\brief Pause recording packets\n * \\note This is to allow pause and resume recorder functionality.\n * @param[in] recorder The janus_recorder to pause\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_pause(janus_recorder *recorder);\n/*! \\brief Resume recording packets\n * \\note This is to allow pause and resume recorder functionality.\n * @param[in] recorder The janus_recorder to resume\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_resume(janus_recorder *recorder);\n/*! \\brief Add an RTP extension to this recording\n * \\note This will only be possible BEFORE the first frame is written, as it needs to\n * be reflected in the .mjr header: doing this after that will return an error.\n * @param[in] recorder The janus_recorder instance to add the extension to\n * @param[in] id Numeric ID of the RTP extension\n * @param[in] extmap Namespace of the RTP extension\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_add_extmap(janus_recorder *recorder, int id, const char *extmap);\n/*! \\brief Set the description for this recording\n * @param[in] recorder The janus_recorder instance to add the description to\n * @param[in] description The description\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_description(janus_recorder *recorder, const char *description);\n/*! \\brief Mark this recording as using RED for audio\n * \\note This will only be possible BEFORE the first frame is written, as it needs to\n * be reflected in the .mjr header: doing this after that will return an error.\n * @param[in] recorder The janus_recorder instance to configure\n * @param[in] red_pt Payload type of RED\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_opusred(janus_recorder *recorder, int red_pt);\n/*! \\brief Mark this recorder as end-to-end encrypted (e.g., via Insertable Streams)\n * \\note This will only be possible BEFORE the first frame is written, as it needs to\n * be reflected in the .mjr header: doing this after that will return an error. Also\n * notice that an encrypted recording will NOT be processable with \\c janus-pp-rec\n * out of the box, since the post-processor will not have access to unencrypted media\n * @param[in] recorder The janus_recorder instance to mark as encrypted\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_encrypted(janus_recorder *recorder);\n/*! \\brief Save an RTP frame in the recorder\n * @param[in] recorder The janus_recorder instance to save the frame to\n * @param[in] buffer The frame data to save\n * @param[in] length The frame data length\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_save_frame(janus_recorder *recorder, char *buffer, uint length);\n/*! \\brief Close the recorder\n * @param[in] recorder The janus_recorder instance to close\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_recorder_close(janus_recorder *recorder);\n/*! \\brief Destroy the recorder instance\n * @param[in] recorder The janus_recorder instance to destroy */\nvoid janus_recorder_destroy(janus_recorder *recorder);\n\n#endif\n"
  },
  {
    "path": "src/refcount.h",
    "content": "/*! \\file    refcount.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Reference counter mechanism\n * \\details  Implementation of a simple reference counter that can be\n * used to keep track of memory management in Janus, in order to avoid\n * the need for timed garbage collectord and the like which have proven\n * ineffective in the past (e.g., crashes whenever race conditions\n * occurred). This implementation is heavily based on an excellent\n * <a href=\"http://nullprogram.com/blog/2015/02/17/\">blog post</a>\n * written by Chris Wellons.\n *\n * Objects interested in leveraging this reference counter mechanism\n * must add a janus_refcount instance as one of the members of the object\n * itself, and then call janus_refcall_init() to set it up. Initializing\n * the reference counter just needs a pointer to the function to invoke\n * when the object needs to be destroyed (counter reaches 0), while it\n * will automatically set the counter to 1. To increase and decrease the\n * counter just call janus_refcount_increase() and janus_refcount_decrease().\n * When the counter reaches 0, the function passed when initializing it will\n * be invoked: this means it's up to you to then free all the resources\n * the object may have allocated. Notice that if this involves other\n * objects that are reference counted, freeing the resource will just\n * mean decreasing the related counter, and not destroying it right away.\n *\n * The free function must be defined like this:\n *\n\\verbatim\nvoid my_free_function(janus_refcount *counter);\n\\endverbatim\n *\n * Since the reference counter cannot know the size of the object to be\n * freed, or where in the list of members the counter has been placed,\n * retrieving the pointer to the object to free is up to you, using the\n * janus_refcount_containerof macro. This is an example of how the\n * free function we have defined above may be implemented:\n *\n\\verbatim\ntypedef my_struct {\n\tint number;\n\tchar *string;\n\tjanus_refcount myref;\n}\n\nvoid my_free_function(janus_refcount *counter) {\n\tstruct my_struct *my_object = janus_refcount_containerof(counter, struct my_struct, myref);\n\tif(my_object->string)\n\t\tfree(my_object->string);\n\tfree(my_object);\n}\n\\endverbatim\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_REFCOUNT_H\n#define JANUS_REFCOUNT_H\n\n#include <glib.h>\n#include \"mutex.h\"\n\n//~ #define REFCOUNT_DEBUG\n\nextern int refcount_debug;\n\n/*! \\brief Macro to programmatically address the object itself from its counter\n * \\details \\c refptr is the pointer to the janus_refcount instance, \\c type\n * is the type of the object itself (e.g., <code>struct mystruct</code>),\n * while \\c member is how the janus_refcount instance is called in the\n * object that contains it. */\n#define janus_refcount_containerof(refptr, type, member) \\\n\t((type *)((char *)(refptr) - offsetof(type, member)))\n\n\n/*! \\brief Janus reference counter structure */\ntypedef struct janus_refcount janus_refcount;\nstruct janus_refcount {\n\t/*! \\brief The reference counter itself */\n\tgint count;\n\t/*! \\brief Pointer to the function that will be used to free the object */\n\tvoid (*free)(const janus_refcount *);\n};\n\n\n#ifdef REFCOUNT_DEBUG\n/* Reference counters debugging */\nextern GHashTable *counters;\nextern janus_mutex counters_mutex;\n#define janus_refcount_track(refp) { \\\n\tjanus_mutex_lock(&counters_mutex); \\\n\tif(counters == NULL) \\\n\t\tcounters = g_hash_table_new(NULL, NULL); \\\n\tg_hash_table_insert(counters, refp, refp); \\\n\tjanus_mutex_unlock(&counters_mutex); \\\n}\n#define janus_refcount_untrack(refp) { \\\n\tjanus_mutex_lock(&counters_mutex); \\\n\tg_hash_table_remove(counters, refp); \\\n\tjanus_mutex_unlock(&counters_mutex); \\\n}\n#endif\n\n\n/*! \\brief Janus reference counter initialization (debug according to settings)\n * \\note Also sets the counter to 1 automatically, so no need to increase\n * it again manually via janus_refcount_increase() after the initialization\n * @param refp Pointer to the Janus reference counter instance\n * @param free_fn Pointer to the function to invoke when the object the counter\n * refers to needs to be destroyed */\n#define janus_refcount_init(refp, free_fn) { \\\n\tif(!refcount_debug) { \\\n\t\tjanus_refcount_init_nodebug(refp, free_fn); \\\n\t} else { \\\n\t\tjanus_refcount_init_debug(refp, free_fn); \\\n\t} \\\n}\n/*! \\brief Janus reference counter initialization (no debug)\n * \\note Also sets the counter to 1 automatically, so no need to increase\n * it again manually via janus_refcount_increase() after the initialization\n * @param refp Pointer to the Janus reference counter instance\n * @param free_fn Pointer to the function to invoke when the object the counter\n * refers to needs to be destroyed */\n#ifdef REFCOUNT_DEBUG\n#define janus_refcount_init_nodebug(refp, free_fn) { \\\n\t(refp)->count = 1; \\\n\t(refp)->free = free_fn; \\\n\tjanus_refcount_track((refp)); \\\n}\n#else\n#define janus_refcount_init_nodebug(refp, free_fn) { \\\n\t(refp)->count = 1; \\\n\t(refp)->free = free_fn; \\\n}\n#endif\n/*! \\brief Janus reference counter initialization (debug)\n * \\note Also sets the counter to 1 automatically, so no need to increase\n * it again manually via janus_refcount_increase() after the initialization\n * @param refp Pointer to the Janus reference counter instance\n * @param free_fn Pointer to the function to invoke when the object the counter\n * refers to needs to be destroyed */\n#ifdef REFCOUNT_DEBUG\n#define janus_refcount_init_debug(refp, free_fn) { \\\n\t(refp)->count = 1; \\\n\tJANUS_PRINT(\"[%s:%s:%d:init] %p (%d)\\n\", __FILE__, __FUNCTION__, __LINE__, refp, (refp)->count); \\\n\t(refp)->free = free_fn; \\\n\tjanus_refcount_track((refp)); \\\n}\n#else\n#define janus_refcount_init_debug(refp, free_fn) { \\\n\t(refp)->count = 1; \\\n\tJANUS_PRINT(\"[%s:%s:%d:init] %p (%d)\\n\", __FILE__, __FUNCTION__, __LINE__, refp, (refp)->count); \\\n\t(refp)->free = free_fn; \\\n}\n#endif\n\n/*! \\brief Increase the Janus reference counter (debug according to settings)\n * @param refp Pointer to the Janus reference counter instance */\n#define janus_refcount_increase(refp) { \\\n\tif(!refcount_debug) { \\\n\t\tjanus_refcount_increase_nodebug(refp); \\\n\t} else { \\\n\t\tjanus_refcount_increase_debug(refp); \\\n\t} \\\n}\n/*! \\brief Increase the Janus reference counter (no debug)\n * @param refp Pointer to the Janus reference counter instance */\n#define janus_refcount_increase_nodebug(refp)  { \\\n\tg_atomic_int_inc((gint *)&(refp)->count); \\\n}\n/*! \\brief Increase the Janus reference counter (debug)\n * @param refp Pointer to the Janus reference counter instance */\n#define janus_refcount_increase_debug(refp)  { \\\n\tJANUS_PRINT(\"[%s:%s:%d:increase] %p (%d)\\n\", __FILE__, __FUNCTION__, __LINE__, refp, (refp)->count+1); \\\n\tg_atomic_int_inc((gint *)&(refp)->count); \\\n}\n\n/*! \\brief Decrease the Janus reference counter (debug according to settings)\n * \\note Will invoke the \\c free function if the counter reaches 0\n * @param refp Pointer to the Janus reference counter instance */\n#define janus_refcount_decrease(refp) { \\\n\tif(!refcount_debug) { \\\n\t\tjanus_refcount_decrease_nodebug(refp); \\\n\t} else { \\\n\t\tjanus_refcount_decrease_debug(refp); \\\n\t} \\\n}\n/*! \\brief Decrease the Janus reference counter (debug)\n * \\note Will invoke the \\c free function if the counter reaches 0\n * @param refp Pointer to the Janus reference counter instance */\n#ifdef REFCOUNT_DEBUG\n#define janus_refcount_decrease_debug(refp)  { \\\n\tJANUS_PRINT(\"[%s:%s:%d:decrease] %p (%d)\\n\", __FILE__, __FUNCTION__, __LINE__, refp, (refp)->count-1); \\\n\tif(g_atomic_int_dec_and_test((gint *)&(refp)->count)) { \\\n\t\t(refp)->free(refp); \\\n\t\tjanus_refcount_untrack((refp)); \\\n\t} \\\n}\n#else\n#define janus_refcount_decrease_debug(refp)  { \\\n\tJANUS_PRINT(\"[%s:%s:%d:decrease] %p (%d)\\n\", __FILE__, __FUNCTION__, __LINE__, refp, (refp)->count-1); \\\n\tif(g_atomic_int_dec_and_test((gint *)&(refp)->count)) { \\\n\t\t(refp)->free(refp); \\\n\t} \\\n}\n#endif\n/*! \\brief Decrease the Janus reference counter (no debug)\n * \\note Will invoke the \\c free function if the counter reaches 0\n * @param refp Pointer to the Janus reference counter instance */\n#ifdef REFCOUNT_DEBUG\n#define janus_refcount_decrease_nodebug(refp)  { \\\n\tif(g_atomic_int_dec_and_test((gint *)&(refp)->count)) { \\\n\t\t(refp)->free(refp); \\\n\t\tjanus_refcount_untrack((refp)); \\\n\t} \\\n}\n#else\n#define janus_refcount_decrease_nodebug(refp)  { \\\n\tif(g_atomic_int_dec_and_test((gint *)&(refp)->count)) { \\\n\t\t(refp)->free(refp); \\\n\t} \\\n}\n#endif\n\n#endif\n"
  },
  {
    "path": "src/rtcp.c",
    "content": "/*! \\file    rtcp.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    RTCP processing\n * \\details  Implementation (based on the oRTP structures) of the RTCP\n * messages. RTCP messages coming through the server are parsed and,\n * if needed (according to http://tools.ietf.org/html/draft-ietf-straw-b2bua-rtcp-00),\n * fixed before they are sent to the peers (e.g., to fix SSRCs that may\n * have been changed by the server). Methods to generate FIR messages\n * and generate/cap REMB messages are provided as well.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#include <math.h>\n#include <stdlib.h>\n#include <sys/time.h>\n\n#include \"debug.h\"\n#include \"rtp.h\"\n#include \"rtcp.h\"\n#include \"utils.h\"\n\n\n/* Transport CC statuses */\ntypedef enum janus_rtp_packet_status {\n\tjanus_rtp_packet_status_notreceived = 0,\n\tjanus_rtp_packet_status_smalldelta = 1,\n\tjanus_rtp_packet_status_largeornegativedelta = 2,\n\tjanus_rtp_packet_status_reserved = 3\n} janus_rtp_packet_status;\nstatic const char *janus_rtp_packet_status_description(janus_rtp_packet_status status) {\n\tswitch(status) {\n\t\tcase janus_rtp_packet_status_notreceived: return \"notreceived\";\n\t\tcase janus_rtp_packet_status_smalldelta: return \"smalldelta\";\n\t\tcase janus_rtp_packet_status_largeornegativedelta: return \"largeornegativedelta\";\n\t\tcase janus_rtp_packet_status_reserved: return \"reserved\";\n\t\tdefault: break;\n\t}\n\treturn NULL;\n}\n\n\ngboolean janus_is_rtcp(char *buf, guint len) {\n\tif (len < 8)\n\t\treturn FALSE;\n\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\treturn ((header->type >= 64) && (header->type < 96));\n}\n\nint janus_rtcp_parse(janus_rtcp_context *ctx, char *packet, int len) {\n\treturn janus_rtcp_fix_ssrc(ctx, packet, len, 0, 0, 0);\n}\n\nguint32 janus_rtcp_get_sender_ssrc(char *packet, int len) {\n\tif(packet == NULL || len == 0)\n\t\treturn 0;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\tbreak;\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_SR: {\n\t\t\t\t/* SR, sender report */\n\t\t\t\tjanus_rtcp_sr *sr = (janus_rtcp_sr *)rtcp;\n\t\t\t\treturn ntohl(sr->ssrc);\n\t\t\t}\n\t\t\tcase RTCP_RR: {\n\t\t\t\t/* RR, receiver report */\n\t\t\t\tjanus_rtcp_rr *rr = (janus_rtcp_rr *)rtcp;\n\t\t\t\treturn ntohl(rr->ssrc);\n\t\t\t}\n\t\t\tcase RTCP_RTPFB: {\n\t\t\t\t/* RTPFB, Transport layer FB message (rfc4585) */\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\treturn ntohl(rtcpfb->ssrc);\n\t\t\t}\n\t\t\tcase RTCP_PSFB: {\n\t\t\t\t/* PSFB, Payload-specific FB message (rfc4585) */\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\treturn ntohl(rtcpfb->ssrc);\n\t\t\t}\n\t\t\tcase RTCP_XR: {\n\t\t\t\t/* XR, extended reports (rfc3611) */\n\t\t\t\tjanus_rtcp_xr *xr = (janus_rtcp_xr *)rtcp;\n\t\t\t\treturn ntohl(xr->ssrc);\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0) {\n\t\t\tbreak;\n\t\t}\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn 0;\n}\n\nguint32 janus_rtcp_get_receiver_ssrc(char *packet, int len) {\n\tif(packet == NULL || len == 0)\n\t\treturn 0;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\tbreak;\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_SR: {\n\t\t\t\t/* SR, sender report */\n\t\t\t\tif (!janus_rtcp_check_sr(rtcp, total))\n\t\t\t\t\tbreak;\n\t\t\t\tjanus_rtcp_sr *sr = (janus_rtcp_sr *)rtcp;\n\t\t\t\tif(sr->header.rc > 0) {\n\t\t\t\t\treturn ntohl(sr->rb[0].ssrc);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_RR: {\n\t\t\t\t/* RR, receiver report */\n\t\t\t\tif (!janus_rtcp_check_rr(rtcp, total))\n\t\t\t\t\tbreak;\n\t\t\t\tjanus_rtcp_rr *rr = (janus_rtcp_rr *)rtcp;\n\t\t\t\tif(rr->header.rc > 0) {\n\t\t\t\t\treturn ntohl(rr->rb[0].ssrc);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_RTPFB: {\n\t\t\t\t/* RTPFB, Transport layer FB message (rfc4585) */\n\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 4))\n\t\t\t\t\tbreak;\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\treturn ntohl(rtcpfb->media);\n\t\t\t}\n\t\t\tcase RTCP_PSFB: {\n\t\t\t\t/* PSFB, Payload-specific FB message (rfc4585) */\n\t\t\t\tif(rtcp->rc == 1) {\n\t\t\t\t\t/* PLI has no FCI data */\n\t\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 0))\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\t\treturn ntohl(rtcpfb->media);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0) {\n\t\t\tbreak;\n\t\t}\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn 0;\n}\n\nvoid janus_rtcp_swap_report_blocks(char *packet, int len, uint32_t rtx_ssrc) {\n\tif(packet == NULL || len == 0)\n\t\treturn;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\tbreak;\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_SR: {\n\t\t\t\t/* SR, sender report */\n\t\t\t\tif (!janus_rtcp_check_sr(rtcp, total))\n\t\t\t\t\tbreak;\n\t\t\t\tjanus_rtcp_sr *sr = (janus_rtcp_sr *)rtcp;\n\t\t\t\tif(sr->header.rc >= 2 && ntohl(sr->rb[0].ssrc) == rtx_ssrc) {\n\t\t\t\t\tjanus_report_block rb0_copy = sr->rb[0];\n\t\t\t\t\tsr->rb[0] = sr->rb[1];\n\t\t\t\t\tsr->rb[1] = rb0_copy;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_RR: {\n\t\t\t\t/* RR, receiver report */\n\t\t\t\tif (!janus_rtcp_check_rr(rtcp, total))\n\t\t\t\t\tbreak;\n\t\t\t\tjanus_rtcp_rr *rr = (janus_rtcp_rr *)rtcp;\n\t\t\t\tif(rr->header.rc >= 2 && ntohl(rr->rb[0].ssrc) == rtx_ssrc) {\n\t\t\t\t\tjanus_report_block rb0_copy = rr->rb[0];\n\t\t\t\t\trr->rb[0] = rr->rb[1];\n\t\t\t\t\trr->rb[1] = rb0_copy;\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Switched incoming RTCP Report Blocks %\"SCNu32\"(rtx) <--> %\"SCNu32\"\\n\",\n\t\t\t\t\t\t\trtx_ssrc, ntohl(rr->rb[0].ssrc));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0) {\n\t\t\tbreak;\n\t\t}\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n}\n\n/* Helper to handle an incoming SR: triggered by a call to janus_rtcp_fix_ssrc with a valid context pointer */\nstatic void janus_rtcp_incoming_sr(janus_rtcp_context *ctx, janus_rtcp_sr *sr) {\n\tif(ctx == NULL)\n\t\treturn;\n\t/* Update the context with info on the monotonic time of last SR received */\n\tctx->lsr_ts = janus_get_monotonic_time();\n\t/* Compute the last SR received as well */\n\tuint64_t ntp = ntohl(sr->si.ntp_ts_msw);\n\tntp = (ntp << 32) | ntohl(sr->si.ntp_ts_lsw);\n\tctx->lsr = (ntp >> 16);\n}\n\n/* Helper to handle an incoming transport-cc feedback: triggered by a call to janus_rtcp_fix_ssrc a valid context pointer */\nstatic void janus_rtcp_incoming_transport_cc(janus_rtcp_context *ctx, janus_rtcp_fb *twcc, int total) {\n\tif(ctx == NULL || twcc == NULL || total < 20)\n\t\treturn;\n\tif(!janus_rtcp_check_fci((janus_rtcp_header *)twcc, total, 4))\n\t\treturn;\n\t/* Parse the header first */\n\tuint8_t *data = (uint8_t *)twcc->fci;\n\tuint16_t base_seq = 0, status_count = 0;\n\tuint32_t reference = 0;\n\tuint8_t fb_pkt = 0;\n\tmemcpy(&base_seq, data, sizeof(uint16_t));\n\tbase_seq = ntohs(base_seq);\n\tmemcpy(&status_count, data+2, sizeof(uint16_t));\n\tstatus_count = ntohs(status_count);\n\tmemcpy(&reference, data+4, sizeof(uint32_t));\n\treference = ntohl(reference) >> 8;\n\tfb_pkt = *(data+7);\n\tJANUS_LOG(LOG_HUGE, \"[TWCC] seq=%\"SCNu16\", psc=%\"SCNu16\", ref=%\"SCNu32\", fbpc=%\"SCNu8\"\\n\",\n\t\tbase_seq, status_count, reference, fb_pkt);\n\t/* Now traverse the feedback: packet chunks first, and then recv deltas */\n\ttotal -= 20;\n\tdata += 8;\n\tuint16_t psc = status_count;\n\tuint16_t chunk = 0;\n\tuint8_t t = 0, ss = 0, s = 0, length = 0;\n\t/* Iterate on all packet chunks */\n\tJANUS_LOG(LOG_HUGE, \"[TWCC] Chunks:\\n\");\n\tuint16_t num = 0;\n\tGList *list = NULL;\n\twhile(psc > 0 && total > 1) {\n\t\tnum++;\n\t\tmemcpy(&chunk, data, sizeof(uint16_t));\n\t\tchunk = ntohs(chunk);\n\t\tt = (chunk & 0x8000) >> 15;\n\t\tif(t == 0) {\n\t\t\t/* Run length */\n\t\t\ts = (chunk & 0x6000) >> 13;\n\t\t\tlength = (chunk & 0x1FFF);\n\t\t\tJANUS_LOG(LOG_HUGE, \"  [%\"SCNu16\"] t=run-length, s=%s, l=%\"SCNu8\"\\n\", num,\n\t\t\t\tjanus_rtp_packet_status_description(s), length);\n\t\t\twhile(length > 0 && psc > 0) {\n\t\t\t\tlist = g_list_prepend(list, GUINT_TO_POINTER(s));\n\t\t\t\tlength--;\n\t\t\t\tpsc--;\n\t\t\t}\n\t\t} else {\n\t\t\t/* Status vector */\n\t\t\tss = (chunk & 0x4000) >> 14;\n\t\t\tlength = (ss ? 7 : 14);\n\t\t\tJANUS_LOG(LOG_HUGE, \"  [%\"SCNu16\"] t=status-vector, ss=%ss, l=%\"SCNu8\"\\n\", num,\n\t\t\t\tss ? \"2-bit\" : \"bit\", length);\n\t\t\twhile(length > 0 && psc > 0) {\n\t\t\t\tif(!ss)\n\t\t\t\t\ts = (chunk & (1 << (length-1))) ? janus_rtp_packet_status_smalldelta : janus_rtp_packet_status_notreceived;\n\t\t\t\telse\n\t\t\t\t\ts = (chunk & (3 << (2*length-2))) >> (2*length-2);\n\t\t\t\tlist = g_list_prepend(list, GUINT_TO_POINTER(s));\n\t\t\t\tlength--;\n\t\t\t\tpsc--;\n\t\t\t}\n\t\t}\n\t\ttotal -= 2;\n\t\tdata += 2;\n\t}\n\tif(psc > 0) {\n\t\t/* Incomplete feedback? Drop... */\n\t\tg_list_free(list);\n\t\treturn;\n\t}\n\tlist = g_list_reverse(list);\n\t/* Iterate on all recv deltas */\n\tJANUS_LOG(LOG_HUGE, \"[TWCC] Recv Deltas (%d/%\"SCNu16\"):\\n\", g_list_length(list), status_count);\n\tnum = 0;\n\tuint16_t delta = 0;\n\tuint32_t delta_us = 0;\n\tGList *iter = list;\n\twhile(iter != NULL && total > 0) {\n\t\tnum++;\n\t\tdelta = 0;\n\t\ts = GPOINTER_TO_UINT(iter->data);\n\t\tif(s == janus_rtp_packet_status_smalldelta) {\n\t\t\t/* Small delta = 1 byte */\n\t\t\tdelta = *data;\n\t\t\ttotal--;\n\t\t\tdata++;\n\t\t} else if(s == janus_rtp_packet_status_largeornegativedelta) {\n\t\t\t/* Large or negative delta = 2 bytes */\n\t\t\tif(total < 2)\n\t\t\t\tbreak;\n\t\t\tmemcpy(&delta, data, sizeof(uint16_t));\n\t\t\tdelta = ntohs(delta);\n\t\t\ttotal -= 2;\n\t\t\tdata += 2;\n\t\t}\n\t\tdelta_us = delta*250;\n\t\t/* Print summary */\n\t\tJANUS_LOG(LOG_HUGE, \"  [%02\"SCNu16\"][%\"SCNu16\"] %s (%\"SCNu32\"us)\\n\", num, (uint16_t)(base_seq+num-1),\n\t\t\tjanus_rtp_packet_status_description(s), delta_us);\n\t\titer = iter->next;\n\t}\n\t/* TODO Update the context with the feedback we got */\n\tg_list_free(list);\n}\n\n/* Link quality estimate filter coefficient */\n#define LINK_QUALITY_FILTER_K 3.0\n\nstatic double janus_rtcp_link_quality_filter(double last, double in) {\n\t/* Note: the last!=last is there to check for NaN */\n\tif(last == 0 || last == in || last != last) {\n\t\treturn in;\n\t} else {\n\t\treturn (1.0 - 1.0/LINK_QUALITY_FILTER_K) * last + (1.0/LINK_QUALITY_FILTER_K) * in;\n\t}\n}\n\n/* Update link quality stats based on RR */\nstatic void janus_rtcp_rr_update_stats(rtcp_context *ctx, janus_report_block rb) {\n\tint64_t ts = janus_get_monotonic_time();\n\tint64_t delta_t = ts - ctx->rr_last_ts;\n\tif(delta_t < 2*G_USEC_PER_SEC) {\n\t\treturn;\n\t}\n\tint32_t total_lost = ntohl(rb.flcnpl) & 0x00FFFFFF;\n\t/* Sign extend from 24 to 32 bits */\n\ttotal_lost = (int32_t)(((uint32_t)total_lost << 8)) >> 8;\n\tif(ctx->rr_last_ehsnr != 0) {\n\t\tint32_t sent = g_atomic_int_get(&ctx->sent_packets_since_last_rr);\n\t\tuint32_t expect = ntohl(rb.ehsnr) - ctx->rr_last_ehsnr;\n\t\tint32_t nacks = g_atomic_int_get(&ctx->nack_count) - ctx->rr_last_nack_count;\n\t\tdouble link_q;\n\t\t/* Handle special cases separately */\n\t\tif(!nacks)\n\t\t\tlink_q = 100.0;\n\t\telse if(!sent || nacks >= sent)\n\t\t\t/* In case the number of NACKs is higher than the number of sent packets\n\t\t\t * set link quality to 0, otherwise the value will overflow, see:\n\t\t\t * https://github.com/meetecho/janus-gateway/issues/2579 */\n\t\t\tlink_q = 0.0;\n\t\telse\n\t\t\tlink_q = 100.0 * (1.0 - ((double)nacks / (double)sent));\n\t\tctx->out_link_quality = janus_rtcp_link_quality_filter(ctx->out_link_quality, link_q);\n\t\tint32_t lost = total_lost - ctx->rr_last_lost;\n\t\tdouble media_link_q;\n\t\t/* Handle special cases separately */\n\t\tif(lost <= 0)\n\t\t\tmedia_link_q = 100.0;\n\t\telse if(!expect || (uint32_t)lost >= expect)\n\t\t\t/* In case the number of reported losses is higher than the number of expected packets\n\t\t\t * set media link quality to 0, otherwise the value will overflow */\n\t\t\tmedia_link_q = 0.0;\n\t\telse\n\t\t\tmedia_link_q = 100.0 * (1.0 - ((double)lost / (double)expect));\n\t\tctx->out_media_link_quality = janus_rtcp_link_quality_filter(ctx->out_media_link_quality, media_link_q);\n\t\tJANUS_LOG(LOG_HUGE, \"Out link quality=%\"SCNu32\", media link quality=%\"SCNu32\"\\n\", janus_rtcp_context_get_out_link_quality(ctx), janus_rtcp_context_get_out_media_link_quality(ctx));\n\t}\n\tctx->rr_last_ts = ts;\n\tctx->rr_last_ehsnr = ntohl(rb.ehsnr);\n\tctx->rr_last_lost = total_lost;\n\tctx->rr_last_nack_count = g_atomic_int_get(&ctx->nack_count);\n\tg_atomic_int_set(&ctx->sent_packets_since_last_rr, 0);\n}\n\n/* Helper to handle an incoming RR: triggered by a call to janus_rtcp_fix_ssrc with fixssrc=0 */\nstatic void janus_rtcp_incoming_rr(janus_rtcp_context *ctx, janus_rtcp_rr *rr) {\n\tif(ctx == NULL)\n\t\treturn;\n\t/* FIXME Check the Record Blocks */\n\tif(rr->header.rc > 0) {\n\t\tdouble jitter = (double)ntohl(rr->rb[0].jitter);\n\t\tuint32_t fraction = ntohl(rr->rb[0].flcnpl) >> 24;\n\t\tint32_t total = ntohl(rr->rb[0].flcnpl) & 0x00FFFFFF;\n\t\t/* Sign extend from 24 to 32 bits */\n\t\ttotal = (int32_t)(((uint32_t)total << 8)) >> 8;\n\t\tJANUS_LOG(LOG_HUGE, \"jitter=%f, fraction=%\"SCNu32\", loss=%d\\n\", jitter, fraction, total);\n\t\tctx->lost_remote = total;\n\t\tctx->jitter_remote = jitter;\n\t\tjanus_rtcp_rr_update_stats(ctx, rr->rb[0]);\n\t\t/* FIXME Compute round trip time */\n\t\tuint32_t lsr = ntohl(rr->rb[0].lsr);\n\t\tuint32_t dlsr = ntohl(rr->rb[0].delay);\n\t\tif(lsr == 0)\t/* Not enough info yet */\n\t\t\treturn;\n\t\tstruct timeval tv;\n\t\tgettimeofday(&tv, NULL);\n\t\tuint32_t s = tv.tv_sec + 2208988800u;\n\t\tuint32_t u = tv.tv_usec;\n\t\tuint32_t f = (u << 12) + (u << 8) - ((u * 3650) >> 6);\n\t\tuint32_t ntp_ts_msw = s;\n\t\tuint32_t ntp_ts_lsw = f;\n\t\tuint64_t temp = ((uint64_t)ntp_ts_msw << 32 ) | ntp_ts_lsw;\n\t\tuint32_t a = (uint32_t)(temp >> 16);\n\t\tuint32_t rtt = a - lsr - dlsr;\n\t\tuint32_t rtt_msw = (rtt & 0xFFFF0000) >> 16;\n\t\tuint32_t rtt_lsw = rtt & 0x0000FFFF;\n\t\ttv.tv_sec = rtt_msw;\n\t\ttv.tv_usec = (rtt_lsw * 15625) >> 10;\n\t\tctx->rtt = tv.tv_sec*1000 + tv.tv_usec/1000;\t/* We need milliseconds */\n\t\tctx->rtt_ntp = a;\n\t\tctx->rtt_lsr = lsr;\n\t\tctx->rtt_dlsr = dlsr;\n\t\tJANUS_LOG(LOG_HUGE, \"rtt=%\"SCNu32\"\\n\", ctx->rtt);\n\t}\n}\n\ngboolean janus_rtcp_check_len(janus_rtcp_header *rtcp, int len) {\n\tif (len < (int)sizeof(janus_rtcp_header) + (int)sizeof(uint32_t)) {\n\t\tJANUS_LOG(LOG_VERB, \"Packet size is too small (%d bytes) to contain RTCP\\n\", len);\n\t\treturn FALSE;\n\t}\n\tint header_def_len = 4*(int)ntohs(rtcp->length) + 4;\n\tif (len < header_def_len) {\n\t\tJANUS_LOG(LOG_VERB, \"Invalid RTCP packet defined length, expected %d bytes > actual %d bytes\\n\", header_def_len, len);\n\t\treturn FALSE;\n\t}\n\treturn TRUE;\n}\n\ngboolean janus_rtcp_check_sr(janus_rtcp_header *rtcp, int len) {\n\tif (len < (int)sizeof(janus_rtcp_header) + (int)sizeof(uint32_t) + (int)sizeof(sender_info)) {\n\t\tJANUS_LOG(LOG_VERB, \"RTCP Packet is too small (%d bytes) to contain SR\\n\", len);\n\t\treturn FALSE;\n\t}\n\tint header_rb_len = (int)(rtcp->rc)*(int)sizeof(report_block);\n\tint actual_rb_len = len - (int)sizeof(janus_rtcp_header) - (int)sizeof(uint32_t) - (int)sizeof(sender_info);\n\tif (actual_rb_len < header_rb_len) {\n\t\tJANUS_LOG(LOG_VERB, \"SR got %d RB count, expected %d bytes > actual %d bytes\\n\", rtcp->rc, header_rb_len, actual_rb_len);\n\t\treturn FALSE;\n\t}\n\treturn TRUE;\n}\n\ngboolean janus_rtcp_check_rr(janus_rtcp_header *rtcp, int len) {\n\tint header_rb_len = (int)(rtcp->rc)*(int)sizeof(report_block);\n\tint actual_rb_len = len - (int)sizeof(janus_rtcp_header) - (int)sizeof(uint32_t);\n\tif (actual_rb_len < header_rb_len) {\n\t\tJANUS_LOG(LOG_VERB, \"RR got %d RB count, expected %d bytes > actual %d bytes\\n\", rtcp->rc, header_rb_len, actual_rb_len);\n\t\treturn FALSE;\n\t}\n\treturn TRUE;\n}\n\ngboolean janus_rtcp_check_fci(janus_rtcp_header *rtcp, int len, int sizeof_fci) {\n\t/* At least one sizeof_fci bytes FCI */\n\tif (len < (int)sizeof(janus_rtcp_header) + 2*(int)sizeof(uint32_t) + sizeof_fci) {\n\t\tJANUS_LOG(LOG_VERB, \"RTCP Packet is too small (%d bytes) to contain at least one %d bytes FCI\\n\", len, sizeof_fci);\n\t\treturn FALSE;\n\t}\n\t/* Evaluate fci total size */\n\tint fci_size = len - (int)sizeof(janus_rtcp_header) - 2*(int)sizeof(uint32_t);\n\t/*  The length of the feedback message is set to 2+(sizeof_fci/4)*N where\n\t\tN is the number of FCI entries */\n\tint fcis;\n\tswitch(sizeof_fci) {\n\t\tcase 0:\n\t\t\tfcis = 0;\n\t\t\tbreak;\n\t\tcase 4:\n\t\t\tfcis = (int)ntohs(rtcp->length) - 2;\n\t\t\tbreak;\n\t\tcase 8:\n\t\t\tfcis = ((int)ntohs(rtcp->length) - 2) >> 1;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tfcis = ((int)ntohs(rtcp->length)- 2) / (sizeof_fci >> 2);\n\t\t\tbreak;\n\t}\n\t/* Every FCI is sizeof_fci bytes */\n\tif (fci_size < sizeof_fci*fcis) {\n\t\tJANUS_LOG(LOG_VERB, \"Got %d FCI count, expected %d bytes > actual %d bytes\\n\", fcis, sizeof_fci*fcis, fci_size);\n\t\treturn FALSE;\n\t}\n\treturn TRUE;\n}\n\ngboolean janus_rtcp_check_remb(janus_rtcp_header *rtcp, int len) {\n\t/* At least 1 SSRC feedback */\n\tif (len < (int)sizeof(janus_rtcp_header) + 2*(int)sizeof(uint32_t) + 3*(int)sizeof(uint32_t)) {\n\t\tJANUS_LOG(LOG_VERB, \"Packet is too small (%d bytes) to contain REMB\\n\", len);\n\t\treturn FALSE;\n\t}\n\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\tuint8_t numssrc = *(rtcpfb->fci+4);\n\t/* Evaluate ssrcs total size */\n\tint ssrc_size = len - (int)sizeof(janus_rtcp_header) - 2*(int)sizeof(uint32_t);\n\t/* Every SSRC is 4 bytes */\n\tif (ssrc_size < 4*numssrc) {\n\t\tJANUS_LOG(LOG_VERB, \"REMB got %d SSRC count, expected %d bytes > actual %d bytes\\n\", numssrc, 4*numssrc, ssrc_size);\n\t\treturn FALSE;\n\t}\n\treturn TRUE;\n}\n\nint janus_rtcp_fix_ssrc(janus_rtcp_context *ctx, char *packet, int len, int fixssrc, uint32_t newssrcl, uint32_t newssrcr) {\n\tif(packet == NULL || len <= 0)\n\t\treturn -1;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint pno = 0, total = len;\n\tJANUS_LOG(LOG_HUGE, \"   Parsing compound packet (total of %d bytes)\\n\", total);\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\treturn -2;\n\t\tif(rtcp->version != 2)\n\t\t\treturn -2;\n\t\tpno++;\n\t\t/* TODO Should we handle any of these packets ourselves, or just relay them? */\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_SR: {\n\t\t\t\t/* SR, sender report */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d SR (200)\\n\", pno);\n\t\t\t\tif (!janus_rtcp_check_sr(rtcp, total))\n\t\t\t\t\treturn -2;\n\t\t\t\tjanus_rtcp_sr *sr = (janus_rtcp_sr *)rtcp;\n\t\t\t\t/* If an RTCP context was provided, update it with info on this SR */\n\t\t\t\tjanus_rtcp_incoming_sr(ctx, sr);\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\tsr->ssrc = htonl(newssrcl);\n\t\t\t\t\tif (sr->header.rc > 0) {\n\t\t\t\t\t\tsr->rb[0].ssrc = htonl(newssrcr);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_RR: {\n\t\t\t\t/* RR, receiver report */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d RR (201)\\n\", pno);\n\t\t\t\tif (!janus_rtcp_check_rr(rtcp, total))\n\t\t\t\t\treturn -2;\n\t\t\t\tjanus_rtcp_rr *rr = (janus_rtcp_rr *)rtcp;\n\t\t\t\t/* If an RTCP context was provided, update it with info on this RR */\n\t\t\t\tjanus_rtcp_incoming_rr(ctx, rr);\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\trr->ssrc = htonl(newssrcl);\n\t\t\t\t\tif (rr->header.rc > 0) {\n\t\t\t\t\t\trr->rb[0].ssrc = htonl(newssrcr);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_SDES: {\n\t\t\t\t/* SDES, source description */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d SDES (202)\\n\", pno);\n\t\t\t\tjanus_rtcp_sdes *sdes = (janus_rtcp_sdes *)rtcp;\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- SSRC: %u\\n\", ntohl(sdes->chunk.ssrc));\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\tsdes->chunk.ssrc = htonl(newssrcl);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_BYE: {\n\t\t\t\t/* BYE, goodbye */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d BYE (203)\\n\", pno);\n\t\t\t\tjanus_rtcp_bye *bye = (janus_rtcp_bye *)rtcp;\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- SSRC: %u\\n\", ntohl(bye->ssrc[0]));\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\tbye->ssrc[0] = htonl(newssrcl);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_APP: {\n\t\t\t\t/* APP, application-defined */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d APP (204)\\n\", pno);\n\t\t\t\tjanus_rtcp_app *app = (janus_rtcp_app *)rtcp;\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- SSRC: %u\\n\", ntohl(app->ssrc));\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\tapp->ssrc = htonl(newssrcl);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_FIR: {\n\t\t\t\t/* FIR, rfc2032 */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d FIR (192)\\n\", pno);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_RTPFB: {\n\t\t\t\t/* RTPFB, Transport layer FB message (rfc4585) */\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"     #%d RTPFB (205)\\n\", pno);\n\t\t\t\tgint fmt = rtcp->rc;\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- FMT: %u\\n\", fmt);\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- SSRC: %u\\n\", ntohl(rtcpfb->ssrc));\n\t\t\t\tif(fmt == 1) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d NACK -- RTPFB (205)\\n\", pno);\n\t\t\t\t\t/* NACK FCI size is 4 bytes */\n\t\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 4))\n\t\t\t\t\t\treturn -2;\n\t\t\t\t\tif(fixssrc && newssrcr) {\n\t\t\t\t\t\trtcpfb->media = htonl(newssrcr);\n\t\t\t\t\t}\n\t\t\t\t\tint nacks = ntohs(rtcp->length)-2;\t/* Skip SSRCs */\n\t\t\t\t\tif(nacks > 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_DBG, \"        Got %d nacks\\n\", nacks);\n\t\t\t\t\t\tjanus_rtcp_nack *nack = NULL;\n\t\t\t\t\t\tuint16_t pid = 0;\n\t\t\t\t\t\tuint16_t blp = 0;\n\t\t\t\t\t\tint i=0, j=0;\n\t\t\t\t\t\tchar bitmask[20];\n\t\t\t\t\t\tfor(i=0; i< nacks; i++) {\n\t\t\t\t\t\t\tnack = (janus_rtcp_nack *)rtcpfb->fci + i;\n\t\t\t\t\t\t\tpid = ntohs(nack->pid);\n\t\t\t\t\t\t\tblp = ntohs(nack->blp);\n\t\t\t\t\t\t\tmemset(bitmask, 0, 20);\n\t\t\t\t\t\t\tfor(j=0; j<16; j++) {\n\t\t\t\t\t\t\t\tbitmask[j] = (blp & ( 1 << j )) >> j ? '1' : '0';\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbitmask[16] = '\\n';\n\t\t\t\t\t\t\tJANUS_LOG(LOG_DBG, \"[%d] %\"SCNu16\" / %s\\n\", i, pid, bitmask);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if(fmt == 3) {\t/* rfc5104 */\n\t\t\t\t\t/* TMMBR: http://tools.ietf.org/html/rfc5104#section-4.2.1.1 */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d TMMBR -- RTPFB (205)\\n\", pno);\n\t\t\t\t\tif(fixssrc && newssrcr) {\n\t\t\t\t\t\t/* TMMBR FCI size is 8 bytes */\n\t\t\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 8))\n\t\t\t\t\t\t\treturn -2;\n\t\t\t\t\t\tuint32_t *ssrc = (uint32_t *)rtcpfb->fci;\n\t\t\t\t\t\t*ssrc = htonl(newssrcr);\n\t\t\t\t\t}\n\t\t\t\t} else if(fmt == 15) {\t/* transport-cc */\n\t\t\t\t\t/* If an RTCP context was provided, parse this transport-cc feedback */\n\t\t\t\t\tjanus_rtcp_incoming_transport_cc(ctx, rtcpfb, total);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d ??? -- RTPFB (205, fmt=%d)\\n\", pno, fmt);\n\t\t\t\t}\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\trtcpfb->ssrc = htonl(newssrcl);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_PSFB: {\n\t\t\t\t/* PSFB, Payload-specific FB message (rfc4585) */\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"     #%d PSFB (206)\\n\", pno);\n\t\t\t\tgint fmt = rtcp->rc;\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- FMT: %u\\n\", fmt);\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- SSRC: %u\\n\", ntohl(rtcpfb->ssrc));\n\t\t\t\tif(fmt == 1) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d PLI -- PSFB (206)\\n\", pno);\n\t\t\t\t\t/* PLI does not require parameters.  Therefore, the length field MUST be\n\t\t\t\t\t\t2, and there MUST NOT be any Feedback Control Information. */\n\t\t\t\t\tif(fixssrc && newssrcr) {\n\t\t\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 0))\n\t\t\t\t\t\t\treturn -2;\n\t\t\t\t\t\trtcpfb->media = htonl(newssrcr);\n\t\t\t\t\t}\n\t\t\t\t} else if(fmt == 2) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d SLI -- PSFB (206)\\n\", pno);\n\t\t\t\t} else if(fmt == 3) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d RPSI -- PSFB (206)\\n\", pno);\n\t\t\t\t} else if(fmt == 4) {\t/* rfc5104 */\n\t\t\t\t\t/* FIR: http://tools.ietf.org/html/rfc5104#section-4.3.1.1 */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d FIR -- PSFB (206)\\n\", pno);\n\t\t\t\t\tif(fixssrc && newssrcr) {\n\t\t\t\t\t\t/* FIR FCI size is 8 bytes */\n\t\t\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 8))\n\t\t\t\t\t\t\treturn -2;\n\t\t\t\t\t\trtcpfb->media = htonl(newssrcr);\n\t\t\t\t\t\tuint32_t *ssrc = (uint32_t *)rtcpfb->fci;\n\t\t\t\t\t\t*ssrc = htonl(newssrcr);\n\t\t\t\t\t}\n\t\t\t\t} else if(fmt == 5) {\t/* rfc5104 */\n\t\t\t\t\t/* TSTR: http://tools.ietf.org/html/rfc5104#section-4.3.2.1 */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d PLI -- TSTR (206)\\n\", pno);\n\t\t\t\t} else if(fmt == 15) {\n\t\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \"       -- This is a AFB!\\n\");\n\t\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\t\tif(fixssrc && newssrcr) {\n\t\t\t\t\t\t/* AFB FCI size is variable, check just media SSRC */\n\t\t\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 0))\n\t\t\t\t\t\t\treturn -2;\n\t\t\t\t\t\trtcpfb->ssrc = htonl(newssrcr);\n\t\t\t\t\t\trtcpfb->media = 0;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_rtcp_fb_remb *remb = (janus_rtcp_fb_remb *)rtcpfb->fci;\n\t\t\t\t\tif(janus_rtcp_check_remb(rtcp, total) && remb->id[0] == 'R' && remb->id[1] == 'E' && remb->id[2] == 'M' && remb->id[3] == 'B') {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d REMB -- PSFB (206)\\n\", pno);\n\t\t\t\t\t\tif(fixssrc && newssrcr) {\n\t\t\t\t\t\t\tremb->ssrc[0] = htonl(newssrcr);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* FIXME From rtcp_utility.cc */\n\t\t\t\t\t\tunsigned char *_ptrRTCPData = (unsigned char *)remb;\n\t\t\t\t\t\t_ptrRTCPData += 4;\t// Skip unique identifier and num ssrc\n\t\t\t\t\t\t//~ JANUS_LOG(LOG_HUGE, \" %02X %02X %02X %02X\\n\", _ptrRTCPData[0], _ptrRTCPData[1], _ptrRTCPData[2], _ptrRTCPData[3]);\n\t\t\t\t\t\tuint8_t numssrc = (_ptrRTCPData[0]);\n\t\t\t\t\t\tuint8_t brExp = (_ptrRTCPData[1] >> 2) & 0x3F;\n\t\t\t\t\t\tuint32_t brMantissa = (_ptrRTCPData[1] & 0x03) << 16;\n\t\t\t\t\t\tbrMantissa += (_ptrRTCPData[2] << 8);\n\t\t\t\t\t\tbrMantissa += (_ptrRTCPData[3]);\n\t\t\t\t\t\tuint32_t bitRate = (uint64_t)brMantissa << brExp;\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"       -- -- -- REMB: %u * 2^%u = %\"SCNu32\" (%d SSRCs, %u)\\n\",\n\t\t\t\t\t\t\tbrMantissa, brExp, bitRate, numssrc, ntohl(remb->ssrc[0]));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d AFB ?? -- PSFB (206)\\n\", pno);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"     #%d ?? -- PSFB (206, fmt=%d)\\n\", pno, fmt);\n\t\t\t\t}\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\trtcpfb->ssrc = htonl(newssrcl);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_XR: {\n\t\t\t\t/* XR, extended reports (rfc3611) */\n\t\t\t\tjanus_rtcp_xr *xr = (janus_rtcp_xr *)rtcp;\n\t\t\t\tif(fixssrc && newssrcl) {\n\t\t\t\t\txr->ssrc = htonl(newssrcl);\n\t\t\t\t}\n\t\t\t\t/* TODO Fix report blocks too, once we support them */\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tJANUS_LOG(LOG_ERR, \"     Unknown RTCP PT %d\\n\", rtcp->type);\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tJANUS_LOG(LOG_HUGE, \"       RTCP PT %d, length: %d bytes\\n\", rtcp->type, length*4+4);\n\t\tif(length == 0) {\n\t\t\t//~ JANUS_LOG(LOG_HUGE, \"  0-length, end of compound packet\\n\");\n\t\t\tbreak;\n\t\t}\n\t\ttotal -= length*4+4;\n\t\t//~ JANUS_LOG(LOG_HUGE, \"     Packet has length %d (%d bytes, %d remaining), moving to next one...\\n\", length, length*4+4, total);\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn 0;\n}\n\nchar *janus_rtcp_filter(char *packet, int len, int *newlen) {\n\tif(packet == NULL || len <= 0 || newlen == NULL)\n\t\treturn NULL;\n\t*newlen = 0;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tchar *filtered = NULL;\n\tint total = len, length = 0, bytes = 0;\n\t/* Iterate on the compound packets */\n\tgboolean keep = TRUE;\n\tgboolean error = FALSE;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total)) {\n\t\t\terror = TRUE;\n\t\t\tbreak;\n\t\t}\n\t\tif(rtcp->version != 2) {\n\t\t\terror = TRUE;\n\t\t\tbreak;\n\t\t}\n\t\tkeep = TRUE;\n\t\tlength = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\tbytes = length*4+4;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_SR:\n\t\t\tcase RTCP_RR:\n\t\t\tcase RTCP_SDES:\n\t\t\t\t/* These are packets we generate ourselves, so remove them */\n\t\t\t\tkeep = FALSE;\n\t\t\t\tbreak;\n\t\t\tcase RTCP_BYE:\n\t\t\tcase RTCP_APP:\n\t\t\tcase RTCP_FIR:\n\t\t\tcase RTCP_PSFB:\n\t\t\t\tbreak;\n\t\t\tcase RTCP_RTPFB:\n\t\t\t\tif(rtcp->rc == 1) {\n\t\t\t\t\t/* We handle NACKs ourselves as well, remove this too */\n\t\t\t\t\tkeep = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if(rtcp->rc == 15) {\n\t\t\t\t\t/* We handle Transport Wide CC ourselves as well, remove this too */\n\t\t\t\t\tkeep = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase RTCP_XR:\n\t\t\t\t/* FIXME We generate RR/SR ourselves, so remove XR */\n\t\t\t\tkeep = FALSE;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unknown RTCP PT %d\\n\", rtcp->type);\n\t\t\t\t/* FIXME Should we allow this to go through instead? */\n\t\t\t\tkeep = FALSE;\n\t\t\t\tbreak;\n\t\t}\n\t\tif(keep) {\n\t\t\t/* Keep this packet */\n\t\t\tif(filtered == NULL)\n\t\t\t\tfiltered = g_malloc0(total);\n\t\t\tmemcpy(filtered+*newlen, (char *)rtcp, bytes);\n\t\t\t*newlen += bytes;\n\t\t}\n\t\ttotal -= bytes;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\tif (error) {\n\t\tg_free(filtered);\n\t\tfiltered = NULL;\n\t\t*newlen = 0;\n\t}\n\treturn filtered;\n}\n\n\nint janus_rtcp_process_incoming_rtp(janus_rtcp_context *ctx, char *packet, int len,\n\t\tgboolean rfc4588_pkt, gboolean rfc4588_enabled, gboolean retransmissions_disabled,\n\t\tGHashTable *clock_rates) {\n\tif(ctx == NULL || packet == NULL || len < 1)\n\t\treturn -1;\n\n\t/* First of all, let's check if we need to change the timestamp base */\n\tjanus_rtp_header *rtp = (janus_rtp_header *)packet;\n\tint pt = rtp->type;\n\tuint32_t clock_rate = clock_rates ?\n\t\tGPOINTER_TO_UINT(g_hash_table_lookup(clock_rates, GINT_TO_POINTER(pt))) : 0;\n\tif(clock_rate > 0 && ctx->tb != clock_rate)\n\t\tctx->tb = clock_rate;\n\t/* Now parse this RTP packet header and update the rtcp_context instance */\n\tuint16_t seq_number = ntohs(rtp->seq_number);\n\tgboolean first_pkt = FALSE;\n\tif(ctx->base_seq == 0 && ctx->seq_cycle == 0) {\n\t\tctx->base_seq = seq_number;\n\t\tfirst_pkt = TRUE;\n\t}\n\n\tint64_t now = janus_get_monotonic_time();\n\tif (!rfc4588_pkt) {\n\t\t/* Non-RTX packet */\n\t\tif ((int16_t)(seq_number - ctx->max_seq_nr) > 0 || first_pkt) {\n\t\t\t/* In-order packet */\n\t\t\tctx->received++;\n\n\t\t\tif(seq_number < ctx->max_seq_nr)\n\t\t\t\tctx->seq_cycle++;\n\t\t\tctx->max_seq_nr = seq_number;\n\t\t\tuint32_t rtp_expected = 0x0;\n\t\t\tif(ctx->seq_cycle > 0) {\n\t\t\t\trtp_expected = ctx->seq_cycle;\n\t\t\t\trtp_expected = rtp_expected << 16;\n\t\t\t}\n\t\t\trtp_expected = rtp_expected + 1 + ctx->max_seq_nr - ctx->base_seq;\n\t\t\tctx->expected = rtp_expected;\n\n\t\t\tint64_t arrival = (now * ctx->tb) / 1000000;\n\t\t\tint64_t transit = arrival - ntohl(rtp->timestamp);\n\t\t\tif (ctx->transit != 0) {\n\t\t\t\tint64_t d = transit - ctx->transit;\n\t\t\t\tif (d < 0) d = -d;\n\t\t\t\tctx->jitter += (1./16.) * ((double)d  - ctx->jitter);\n\t\t\t}\n\n\t\t\tctx->transit = transit;\n\t\t\tctx->rtp_last_inorder_ts = ntohl(rtp->timestamp);\n\t\t\tctx->rtp_last_inorder_time = now;\n\t\t} else {\n\t\t\t/* Out-of-order packet */\n\t\t\tif (rfc4588_enabled) {\n\t\t\t\t/* Just an out-of-order packet in a stream that is using a dedicated RTX channel */\n\t\t\t\tctx->received++;\n\t\t\t} else {\n\t\t\t\t/* The stream does not have a rtx channel dedicated to retransmissions */\n\t\t\t\tif (!retransmissions_disabled) {\n\t\t\t\t\t/* The stream does have a retransmission mechanism (e.g. video w/ NACKs) */\n\t\t\t\t\t/* Try to detect the retransmissions */\n\t\t\t\t\t/* TODO We have to accomplish this in a smarter way */\n\t\t\t\t\tint32_t rtp_diff = ntohl(rtp->timestamp) - ctx->rtp_last_inorder_ts;\n\t\t\t\t\tint64_t ms_diff = ((int64_t)abs(rtp_diff) * 1000) / ctx->tb;\n\t\t\t\t\tif (ms_diff > 120)\n\t\t\t\t\t\tctx->retransmitted++;\n\t\t\t\t\telse\n\t\t\t\t\t\tctx->received++;\n\t\t\t\t} else {\n\t\t\t\t\t/* The stream does not have a retransmission mechanism (e.g. audio wo/ NACKs) */\n\t\t\t\t\tctx->received++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t/* RTX packet, just increase retransmitted count */\n\t\tctx->retransmitted++;\n\t}\n\n\t/* RTP packet received: it means we can start sending RR */\n\tctx->rtp_recvd = 1;\n\n\treturn 0;\n}\n\n\nuint32_t janus_rtcp_context_get_rtt(janus_rtcp_context *ctx) {\n\treturn ctx ? ctx->rtt : 0;\n}\n\nuint32_t janus_rtcp_context_get_in_link_quality(janus_rtcp_context *ctx) {\n\treturn ctx ? (uint32_t)(ctx->in_link_quality + 0.5) : 0;\n}\n\nuint32_t janus_rtcp_context_get_in_media_link_quality(janus_rtcp_context *ctx) {\n\treturn ctx ? (uint32_t)(ctx->in_media_link_quality + 0.5) : 0;\n}\n\nuint32_t janus_rtcp_context_get_out_link_quality(janus_rtcp_context *ctx) {\n\treturn ctx ? (uint32_t)(ctx->out_link_quality + 0.5) : 0;\n}\n\nuint32_t janus_rtcp_context_get_out_media_link_quality(janus_rtcp_context *ctx) {\n\treturn ctx ? (uint32_t)(ctx->out_media_link_quality + 0.5) : 0;\n}\n\nint32_t janus_rtcp_context_get_lost_all(janus_rtcp_context *ctx, gboolean remote) {\n\tif(ctx == NULL)\n\t\treturn 0;\n\treturn remote ? ctx->lost_remote : ctx->lost;\n}\n\nstatic int32_t janus_rtcp_context_get_lost(janus_rtcp_context *ctx) {\n\tif(ctx == NULL)\n\t\treturn 0;\n\tint32_t lost;\n\tif(ctx->lost > 0x7FFFFF) {\n\t\tlost = 0x7FFFFF;\n\t} else if(ctx->lost < -0x800000) {\n\t\tlost = -0x800000;\n\t} else {\n\t\tlost = ctx->lost;\n\t}\n\treturn lost & 0x00FFFFFF;\n}\n\nstatic uint32_t janus_rtcp_context_get_lost_fraction(janus_rtcp_context *ctx) {\n\tif(ctx == NULL)\n\t\treturn 0;\n\tuint32_t expected_interval = ctx->expected - ctx->expected_prior;\n\tuint32_t received_interval = ctx->received - ctx->received_prior;\n\tint32_t lost_interval = expected_interval - received_interval;\n\tuint32_t fraction;\n\tif(expected_interval == 0 || lost_interval <=0)\n\t\tfraction = 0;\n\telse\n\t\tfraction = (lost_interval << 8) / expected_interval;\n\treturn fraction << 24;\n}\n\nuint32_t janus_rtcp_context_get_jitter(janus_rtcp_context *ctx, gboolean remote) {\n\tif(ctx == NULL || ctx->tb == 0)\n\t\treturn 0;\n\treturn (uint32_t) floor((remote ? ctx->jitter_remote : ctx->jitter) / (ctx->tb/1000));\n}\n\nstatic void janus_rtcp_estimate_in_link_quality(janus_rtcp_context *ctx) {\n\tuint32_t expected_interval = ctx->expected - ctx->expected_prior;\n\tuint32_t received_interval = ctx->received - ctx->received_prior;\n\tuint32_t retransmitted_interval = ctx->retransmitted - ctx->retransmitted_prior;\n\n\t/* Link lost is calculated without considering any retransmission */\n\tint32_t link_lost = expected_interval - received_interval;\n\tif (link_lost < 0) {\n\t\tlink_lost = 0;\n\t}\n\tdouble link_q = !expected_interval ? 0 : 100.0 - (100.0 * (double)link_lost / (double)expected_interval);\n\tctx->in_link_quality = janus_rtcp_link_quality_filter(ctx->in_link_quality, link_q);\n\n\t/* Media lost is calculated considering also retransmitted packets */\n\tint32_t media_lost = expected_interval - (received_interval + retransmitted_interval);\n\tif (media_lost < 0) {\n\t\tmedia_lost = 0;\n\t}\n\tdouble media_link_q = !expected_interval ? 0 : 100.0 - (100.0 * (double)media_lost / (double)expected_interval);\n\tctx->in_media_link_quality = janus_rtcp_link_quality_filter(ctx->in_media_link_quality, media_link_q);\n\n\tJANUS_LOG(LOG_HUGE, \"In link quality=%\"SCNu32\", media link quality=%\"SCNu32\"\\n\", janus_rtcp_context_get_in_link_quality(ctx), janus_rtcp_context_get_in_media_link_quality(ctx));\n}\n\nint janus_rtcp_report_block(janus_rtcp_context *ctx, janus_report_block *rb) {\n\tif(ctx == NULL || rb == NULL)\n\t\treturn -1;\n\tgint64 now = janus_get_monotonic_time();\n\trb->jitter = htonl((uint32_t) ctx->jitter);\n\trb->ehsnr = htonl((((uint32_t) 0x0 + ctx->seq_cycle) << 16) + ctx->max_seq_nr);\n\tuint32_t expected_interval = ctx->expected - ctx->expected_prior;\n\tuint32_t received_interval = ctx->received - ctx->received_prior;\n\tint32_t lost_interval = 0;\n\tif (expected_interval > received_interval) {\n\t\tlost_interval = expected_interval - received_interval;\n\t}\n\tctx->lost += lost_interval;\n\tint32_t reported_lost = janus_rtcp_context_get_lost(ctx);\n\tuint32_t reported_fraction = janus_rtcp_context_get_lost_fraction(ctx);\n\tjanus_rtcp_estimate_in_link_quality(ctx);\n\tctx->expected_prior = ctx->expected;\n\tctx->received_prior = ctx->received;\n\tctx->retransmitted_prior = ctx->retransmitted;\n\trb->flcnpl = htonl(reported_lost | reported_fraction);\n\tif(ctx->lsr > 0) {\n\t\trb->lsr = htonl(ctx->lsr);\n\t\trb->delay = htonl(((now - ctx->lsr_ts) << 16) / 1000000);\n\t} else {\n\t\trb->lsr = 0;\n\t\trb->delay = 0;\n\t}\n\tctx->last_sent = now;\n\treturn 0;\n}\n\nint janus_rtcp_fix_report_data(char *packet, int len, uint32_t base_ts, uint32_t base_ts_prev, uint32_t ssrc_peer, uint32_t ssrc_local, uint32_t ssrc_expected, gboolean video) {\n\tif(packet == NULL || len <= 0)\n\t\treturn -1;\n\t/* Parse RTCP compound packet */\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint total = len, status = 0;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\treturn -2;\n\t\tif(rtcp->version != 2)\n\t\t\treturn -2;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_RR: {\n\t\t\t\tif (!janus_rtcp_check_rr(rtcp, total))\n\t\t\t\t\treturn -2;\n\t\t\t\tjanus_rtcp_rr *rr = (janus_rtcp_rr *)rtcp;\n\t\t\t\trr->ssrc = htonl(ssrc_peer);\n\t\t\t\tstatus++;\n\t\t\t\tif (rr->header.rc > 0) {\n\t\t\t\t\trr->rb[0].ssrc = htonl(ssrc_local);\n\t\t\t\t\tstatus++;\n\t\t\t\t\t/* FIXME we need to fix the extended highest sequence number received */\n\t\t\t\t\t/* FIXME we need to fix the cumulative number of packets lost */\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase RTCP_SR: {\n\t\t\t\tif (!janus_rtcp_check_sr(rtcp, total))\n\t\t\t\t\treturn -2;\n\t\t\t\tjanus_rtcp_sr *sr = (janus_rtcp_sr *)rtcp;\n\t\t\t\tuint32_t recv_ssrc = ntohl(sr->ssrc);\n\t\t\t\tif (recv_ssrc != ssrc_expected) {\n\t\t\t\t\tif(ssrc_expected != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB,\"Incoming RTCP SR SSRC (%\"SCNu32\") does not match the expected one (%\"SCNu32\") video=%d\\n\",\n\t\t\t\t\t\t\trecv_ssrc, ssrc_expected, video);\n\t\t\t\t\t}\n\t\t\t\t\treturn -3;\n\t\t\t\t}\n\t\t\t\tsr->ssrc = htonl(ssrc_peer);\n\t\t\t\t/* FIXME we need to fix the sender's packet count */\n\t\t\t\t/* FIXME we need to fix the sender's octet count */\n\t\t\t\tuint32_t sr_ts = ntohl(sr->si.rtp_ts);\n\t\t\t\tuint32_t fix_ts = (sr_ts - base_ts) + base_ts_prev;\n\t\t\t\tsr->si.rtp_ts = htonl(fix_ts);\n\t\t\t\tstatus++;\n\t\t\t\tif (sr->header.rc > 0) {\n\t\t\t\t\tsr->rb[0].ssrc = htonl(ssrc_local);\n\t\t\t\t\tstatus++;\n\t\t\t\t\t/* FIXME we need to fix the extended highest sequence number received */\n\t\t\t\t\t/* FIXME we need to fix the cumulative number of packets lost */\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn status;\n}\n\ngboolean janus_rtcp_has_bye(char *packet, int len) {\n\t/* Parse RTCP compound packet */\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\tbreak;\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_BYE:\n\t\t\t\treturn TRUE;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn FALSE;\n}\n\ngboolean janus_rtcp_has_fir(char *packet, int len) {\n\t/* Parse RTCP compound packet */\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\tbreak;\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_FIR:\n\t\t\t\treturn TRUE;\n\t\t\tcase RTCP_PSFB: {\n\t\t\t\tgint fmt = rtcp->rc;\n\t\t\t\tif(fmt == 4)\n\t\t\t\t\treturn TRUE;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn FALSE;\n}\n\ngboolean janus_rtcp_has_pli(char *packet, int len) {\n\t/* Parse RTCP compound packet */\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\tbreak;\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tswitch(rtcp->type) {\n\t\t\tcase RTCP_PSFB: {\n\t\t\t\tgint fmt = rtcp->rc;\n\t\t\t\tif(fmt == 1)\n\t\t\t\t\treturn TRUE;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn FALSE;\n}\n\nvoid janus_rtcp_get_nacks(char *packet, int len, GQueue *nacks_queue) {\n\tif(packet == NULL || len == 0 || nacks_queue == NULL)\n\t\treturn;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tg_queue_clear(nacks_queue);\n\tint total = len;\n\tgboolean error = FALSE;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total)) {\n\t\t\terror = TRUE;\n\t\t\tbreak;\n\t\t}\n\t\tif (rtcp->version != 2) {\n\t\t\terror = TRUE;\n\t\t\tbreak;\n\t\t}\n\t\tif(rtcp->type == RTCP_RTPFB) {\n\t\t\tgint fmt = rtcp->rc;\n\t\t\tif(fmt == 1) {\n\t\t\t\t/* NACK FCI size is 4 bytes */\n\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 4)) {\n\t\t\t\t\terror = TRUE;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\tint nacks = ntohs(rtcp->length)-2;\t/* Skip SSRCs */\n\t\t\t\tif(nacks > 0) {\n\t\t\t\t\tJANUS_LOG(LOG_DBG, \"        Got %d nacks\\n\", nacks);\n\t\t\t\t\tjanus_rtcp_nack *nack = NULL;\n\t\t\t\t\tuint16_t pid = 0;\n\t\t\t\t\tuint16_t blp = 0;\n\t\t\t\t\tint i=0, j=0;\n\t\t\t\t\tchar bitmask[20];\n\t\t\t\t\tfor(i=0; i< nacks; i++) {\n\t\t\t\t\t\tnack = (janus_rtcp_nack *)rtcpfb->fci + i;\n\t\t\t\t\t\tpid = ntohs(nack->pid);\n\t\t\t\t\t\tg_queue_push_head(nacks_queue, GUINT_TO_POINTER(pid));\n\t\t\t\t\t\tblp = ntohs(nack->blp);\n\t\t\t\t\t\tmemset(bitmask, 0, 20);\n\t\t\t\t\t\tfor(j=0; j<16; j++) {\n\t\t\t\t\t\t\tbitmask[j] = (blp & ( 1 << j )) >> j ? '1' : '0';\n\t\t\t\t\t\t\tif((blp & ( 1 << j )) >> j)\n\t\t\t\t\t\t\t\tg_queue_push_head(nacks_queue, GUINT_TO_POINTER(pid+j+1));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbitmask[16] = '\\n';\n\t\t\t\t\t\tJANUS_LOG(LOG_DBG, \"[%d] %\"SCNu16\" / %s\\n\", i, pid, bitmask);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\tif(error)\n\t\tg_queue_clear(nacks_queue);\n}\n\nint janus_rtcp_remove_nacks(char *packet, int len) {\n\tif(packet == NULL || len == 0)\n\t\treturn len;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t/* Find the NACK message */\n\tchar *nacks = NULL;\n\tint total = len, nacks_len = 0;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total)) {\n\t\t\tbreak;\n\t\t}\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tif(rtcp->type == RTCP_RTPFB) {\n\t\t\tgint fmt = rtcp->rc;\n\t\t\tif(fmt == 1) {\n\t\t\t\tnacks = (char *)rtcp;\n\t\t\t\tif (!janus_rtcp_check_fci(rtcp, total, 4)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\tif(nacks != NULL) {\n\t\t\tnacks_len = length*4+4;\n\t\t\tbreak;\n\t\t}\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\tif(nacks != NULL) {\n\t\ttotal = len - ((nacks-packet)+nacks_len);\n\t\tif(total < 0) {\n\t\t\t/* FIXME Should never happen, but you never know: do nothing */\n\t\t\treturn len;\n\t\t} else if(total == 0) {\n\t\t\t/* NACK was the last compound packet, easy enough */\n\t\t\treturn len-nacks_len;\n\t\t} else {\n\t\t\t/* NACK is between two compound packets, move them around */\n\t\t\tint i=0;\n\t\t\tfor(i=0; i<total; i++)\n\t\t\t\t*(nacks+i) = *(nacks+nacks_len+i);\n\t\t\treturn len-nacks_len;\n\t\t}\n\t}\n\treturn len;\n}\n\n/* Query an existing REMB message */\nuint32_t janus_rtcp_get_remb(char *packet, int len) {\n\tif(packet == NULL || len == 0)\n\t\treturn 0;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t/* Get REMB bitrate, if any */\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\tbreak;\n\t\tif(rtcp->version != 2)\n\t\t\tbreak;\n\t\tif(rtcp->type == RTCP_PSFB) {\n\t\t\tgint fmt = rtcp->rc;\n\t\t\tif(fmt == 15) {\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\tjanus_rtcp_fb_remb *remb = (janus_rtcp_fb_remb *)rtcpfb->fci;\n\t\t\t\tif(janus_rtcp_check_remb(rtcp, total) && remb->id[0] == 'R' && remb->id[1] == 'E' && remb->id[2] == 'M' && remb->id[3] == 'B') {\n\t\t\t\t\t/* FIXME From rtcp_utility.cc */\n\t\t\t\t\tunsigned char *_ptrRTCPData = (unsigned char *)remb;\n\t\t\t\t\t_ptrRTCPData += 4;\t/* Skip unique identifier and num ssrc */\n\t\t\t\t\t//~ JANUS_LOG(LOG_VERB, \" %02X %02X %02X %02X\\n\", _ptrRTCPData[0], _ptrRTCPData[1], _ptrRTCPData[2], _ptrRTCPData[3]);\n\t\t\t\t\tuint8_t brExp = (_ptrRTCPData[1] >> 2) & 0x3F;\n\t\t\t\t\tuint32_t brMantissa = (_ptrRTCPData[1] & 0x03) << 16;\n\t\t\t\t\tbrMantissa += (_ptrRTCPData[2] << 8);\n\t\t\t\t\tbrMantissa += (_ptrRTCPData[3]);\n\t\t\t\t\tuint32_t bitrate = (uint64_t)brMantissa << brExp;\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got REMB bitrate %\"SCNu32\"\\n\", bitrate);\n\t\t\t\t\treturn bitrate;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn 0;\n}\n\n/* Change an existing REMB message */\nint janus_rtcp_cap_remb(char *packet, int len, uint32_t bitrate) {\n\tif(packet == NULL || len == 0)\n\t\treturn -1;\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\tif(bitrate == 0)\n\t\treturn 0;\t/* No need to cap */\n\t/* Cap REMB bitrate */\n\tint total = len;\n\twhile(rtcp) {\n\t\tif (!janus_rtcp_check_len(rtcp, total))\n\t\t\treturn -2;\n\t\tif(rtcp->version != 2)\n\t\t\treturn -2;\n\t\tif(rtcp->type == RTCP_PSFB) {\n\t\t\tgint fmt = rtcp->rc;\n\t\t\tif(fmt == 15) {\n\t\t\t\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\t\t\t\tjanus_rtcp_fb_remb *remb = (janus_rtcp_fb_remb *)rtcpfb->fci;\n\t\t\t\tif(janus_rtcp_check_remb(rtcp, total) && remb->id[0] == 'R' && remb->id[1] == 'E' && remb->id[2] == 'M' && remb->id[3] == 'B') {\n\t\t\t\t\t/* FIXME From rtcp_utility.cc */\n\t\t\t\t\tunsigned char *_ptrRTCPData = (unsigned char *)remb;\n\t\t\t\t\t_ptrRTCPData += 4;\t/* Skip unique identifier and num ssrc */\n\t\t\t\t\t//~ JANUS_LOG(LOG_VERB, \" %02X %02X %02X %02X\\n\", _ptrRTCPData[0], _ptrRTCPData[1], _ptrRTCPData[2], _ptrRTCPData[3]);\n\t\t\t\t\tuint8_t brExp = (_ptrRTCPData[1] >> 2) & 0x3F;\n\t\t\t\t\tuint32_t brMantissa = (_ptrRTCPData[1] & 0x03) << 16;\n\t\t\t\t\tbrMantissa += (_ptrRTCPData[2] << 8);\n\t\t\t\t\tbrMantissa += (_ptrRTCPData[3]);\n\t\t\t\t\tuint32_t origbitrate = (uint64_t)brMantissa << brExp;\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got REMB bitrate %\"SCNu32\", need to cap it to %\"SCNu32\"\\n\", origbitrate, bitrate);\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  >> %u * 2^%u = %\"SCNu32\"\\n\", brMantissa, brExp, origbitrate);\n\t\t\t\t\t/* bitrate --> brexp/brmantissa */\n\t\t\t\t\tuint8_t b = 0;\n\t\t\t\t\tuint8_t newbrexp = 0;\n\t\t\t\t\tuint32_t newbrmantissa = 0;\n\t\t\t\t\tfor(b=0; b<32; b++) {\n\t\t\t\t\t\tif(bitrate <= ((uint32_t) 0x3FFFF << b)) {\n\t\t\t\t\t\t\tnewbrexp = b;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(b > 31)\n\t\t\t\t\t\tb = 31;\n\t\t\t\t\tnewbrmantissa = bitrate >> b;\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"new brexp:      %\"SCNu8\"\\n\", newbrexp);\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"new brmantissa: %\"SCNu32\"\\n\", newbrmantissa);\n\t\t\t\t\t/* FIXME From rtcp_sender.cc */\n\t\t\t\t\t_ptrRTCPData[1] = (uint8_t)((newbrexp << 2) + ((newbrmantissa >> 16) & 0x03));\n\t\t\t\t\t_ptrRTCPData[2] = (uint8_t)(newbrmantissa >> 8);\n\t\t\t\t\t_ptrRTCPData[3] = (uint8_t)(newbrmantissa);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Is this a compound packet? */\n\t\tint length = ntohs(rtcp->length);\n\t\tif(length == 0)\n\t\t\tbreak;\n\t\ttotal -= length*4+4;\n\t\tif(total <= 0)\n\t\t\tbreak;\n\t\trtcp = (janus_rtcp_header *)((uint32_t*)rtcp + length + 1);\n\t}\n\treturn 0;\n}\n\n/* Generate a new SDES message */\nint janus_rtcp_sdes_cname(char *packet, int len, const char *cname, int cnamelen) {\n\tif(packet == NULL || len <= 0 || cname == NULL || cnamelen <= 0)\n\t\treturn -1;\n\tmemset(packet, 0, len);\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t/* Set header */\n\trtcp->version = 2;\n\trtcp->type = RTCP_SDES;\n\trtcp->rc = 1;\n\tint plen = 8;\t/* Header + chunk + item header */\n\tplen += cnamelen+3; /* cname item header(2) + cnamelen + terminator(1) */\n\t/* calculate padding length. assume that plen is shorter than 65535 */\n\tplen = (plen + 3) & 0xFFFC;\n\tif(len < plen) {\n\t\tJANUS_LOG(LOG_ERR, \"Buffer too small for SDES message: %d < %d\\n\", len, plen);\n\t\treturn -1;\n\t}\n\trtcp->length = htons((plen/4)-1);\n\t/* Now set SDES stuff */\n\tjanus_rtcp_sdes *rtcpsdes = (janus_rtcp_sdes *)rtcp;\n\trtcpsdes->item.type = 1;\n\trtcpsdes->item.len = cnamelen;\n\tmemcpy(rtcpsdes->item.content, cname, cnamelen);\n\treturn plen;\n}\n\n/* Generate a new REMB message */\nint janus_rtcp_remb(char *packet, int len, uint32_t bitrate) {\n\t/* By default we assume a single SSRC will be set */\n\treturn janus_rtcp_remb_ssrcs(packet, len, bitrate, 1);\n}\n\nint janus_rtcp_remb_ssrcs(char *packet, int len, uint32_t bitrate, uint8_t numssrc) {\n\tif(packet == NULL || numssrc == 0)\n\t\treturn -1;\n\tint min_len = 20 + numssrc*4;\n\tif(len < min_len)\n\t\treturn -1;\n\tmemset(packet, 0, len);\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t/* Set header */\n\trtcp->version = 2;\n\trtcp->type = RTCP_PSFB;\n\trtcp->rc = 15;\n\trtcp->length = htons((min_len/4)-1);\n\t/* Now set REMB stuff */\n\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\tjanus_rtcp_fb_remb *remb = (janus_rtcp_fb_remb *)rtcpfb->fci;\n\tremb->id[0] = 'R';\n\tremb->id[1] = 'E';\n\tremb->id[2] = 'M';\n\tremb->id[3] = 'B';\n\t/* bitrate --> brexp/brmantissa */\n\tuint8_t b = 0;\n\tuint8_t newbrexp = 0;\n\tuint32_t newbrmantissa = 0;\n\tfor(b=0; b<32; b++) {\n\t\tif(bitrate <= ((uint32_t) 0x3FFFF << b)) {\n\t\t\tnewbrexp = b;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif(b > 31)\n\t\tb = 31;\n\tnewbrmantissa = bitrate >> b;\n\t/* FIXME From rtcp_sender.cc */\n\tunsigned char *_ptrRTCPData = (unsigned char *)remb;\n\t_ptrRTCPData += 4;\t/* Skip unique identifier */\n\t_ptrRTCPData[0] = numssrc;\n\t_ptrRTCPData[1] = (uint8_t)((newbrexp << 2) + ((newbrmantissa >> 16) & 0x03));\n\t_ptrRTCPData[2] = (uint8_t)(newbrmantissa >> 8);\n\t_ptrRTCPData[3] = (uint8_t)(newbrmantissa);\n\tJANUS_LOG(LOG_HUGE, \"[REMB] bitrate=%\"SCNu32\" (%d bytes)\\n\", bitrate, 4*(ntohs(rtcp->length)+1));\n\treturn min_len;\n}\n\n/* Generate a new FIR message */\nint janus_rtcp_fir(char *packet, int len, int *seqnr) {\n\tif(packet == NULL || len != 20 || seqnr == NULL)\n\t\treturn -1;\n\tmemset(packet, 0, len);\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t*seqnr = *seqnr + 1;\n\tif(*seqnr < 0 || *seqnr >= 256)\n\t\t*seqnr = 0;\t/* Reset sequence number */\n\t/* Set header */\n\trtcp->version = 2;\n\trtcp->type = RTCP_PSFB;\n\trtcp->rc = 4;\t/* FMT=4 */\n\trtcp->length = htons((len/4)-1);\n\t/* Now set FIR stuff */\n\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\tjanus_rtcp_fb_fir *fir = (janus_rtcp_fb_fir *)rtcpfb->fci;\n\tfir->seqnr = htonl(*seqnr << 24);\t/* FCI: Sequence number */\n\tJANUS_LOG(LOG_HUGE, \"[FIR] seqnr=%d (%d bytes)\\n\", *seqnr, 4*(ntohs(rtcp->length)+1));\n\treturn 20;\n}\n\n/* Generate a new PLI message */\nint janus_rtcp_pli(char *packet, int len) {\n\tif(packet == NULL || len != 12)\n\t\treturn -1;\n\tmemset(packet, 0, len);\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t/* Set header */\n\trtcp->version = 2;\n\trtcp->type = RTCP_PSFB;\n\trtcp->rc = 1;\t/* FMT=1 */\n\trtcp->length = htons((len/4)-1);\n\treturn 12;\n}\n\n/* Generate a new NACK message */\nint janus_rtcp_nacks(char *packet, int len, GSList *nacks) {\n\tif(packet == NULL || len < 16 || nacks == NULL)\n\t\treturn -1;\n\tmemset(packet, 0, len);\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t/* Set header */\n\trtcp->version = 2;\n\trtcp->type = RTCP_RTPFB;\n\trtcp->rc = 1;\t/* FMT=1 */\n\t/* Now set NACK stuff */\n\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\tjanus_rtcp_nack *nack = (janus_rtcp_nack *)rtcpfb->fci;\n\t/* FIXME We assume the GSList list is already ordered... */\n\tguint16 pid = GPOINTER_TO_UINT(nacks->data);\n\tnack->pid = htons(pid);\n\tnacks = nacks->next;\n\tint words = 3;\n\twhile(nacks) {\n\t\tguint16 npid = GPOINTER_TO_UINT(nacks->data);\n\t\tif(npid-pid < 1) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"Skipping PID to NACK (%\"SCNu16\" already added)...\\n\", npid);\n\t\t} else if(npid-pid > 16) {\n\t\t\t/* We need a new block: this sequence number will be its root PID */\n\t\t\tJANUS_LOG(LOG_HUGE, \"Adding another block of NACKs (%\"SCNu16\"-%\"SCNu16\" > 16)...\\n\", npid, pid);\n\t\t\twords++;\n\t\t\tif(len < (words*4+4)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Buffer too small: %d < %d (at least %d NACK blocks needed)\\n\", len, words*4+4, words);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tchar *new_block = packet + words*4;\n\t\t\tnack = (janus_rtcp_nack *)new_block;\n\t\t\tpid = GPOINTER_TO_UINT(nacks->data);\n\t\t\tnack->pid = htons(pid);\n\t\t} else {\n\t\t\tuint16_t blp = ntohs(nack->blp);\n\t\t\tblp |= 1 << (npid-pid-1);\n\t\t\tnack->blp = htons(blp);\n\t\t}\n\t\tnacks = nacks->next;\n\t}\n\trtcp->length = htons(words);\n\treturn words*4+4;\n}\n\nint janus_rtcp_transport_wide_cc_feedback(char *packet, size_t size, guint32 ssrc, guint32 media, guint8 feedback_packet_count, GQueue *transport_wide_cc_stats) {\n\tif(packet == NULL || size < sizeof(janus_rtcp_header) || transport_wide_cc_stats == NULL || g_queue_is_empty(transport_wide_cc_stats))\n\t\treturn -1;\n\n\tmemset(packet, 0, size);\n\tjanus_rtcp_header *rtcp = (janus_rtcp_header *)packet;\n\t/* Set header */\n\trtcp->version = 2;\n\trtcp->type = RTCP_RTPFB;\n\trtcp->rc = 15;\n\t/* Now set FB stuff */\n\tjanus_rtcp_fb *rtcpfb = (janus_rtcp_fb *)rtcp;\n\trtcpfb->ssrc = htonl(ssrc);\n\trtcpfb->media = htonl(media);\n\n\t/* Get first packet */\n\tjanus_rtcp_transport_wide_cc_stats *stat = (janus_rtcp_transport_wide_cc_stats *) g_queue_pop_head (transport_wide_cc_stats);\n\t/* Calculate temporal info */\n\tguint16 base_seq_num = stat->transport_seq_num;\n\tgboolean first_received\t= FALSE;\n\tguint64 reference_time = 0;\n\tguint packet_status_count = g_queue_get_length(transport_wide_cc_stats) + 1;\n\n\t/*\n\t\t0                   1                   2                   3\n\t\t0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t|      base sequence number     |      packet status count      |\n\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t|                 reference time                | fb pkt. count |\n\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t */\n\n\t/* The packet as unsigned */\n\tguint8 *data = (guint8 *)packet;\n\t/* The start of the feedback data */\n\tsize_t len = sizeof(janus_rtcp_header) + 8;\n\n\t/* Set header data */\n\tjanus_set2(data, len, base_seq_num);\n\tjanus_set2(data, len+2, packet_status_count);\n\t/* Set3 referenceTime when first received */\n\tsize_t reference_time_pos = len + 4;\n\tjanus_set1(data, len+7, feedback_packet_count);\n\n\t/* Next byte */\n\tlen += 8;\n\n\t/* Initial time in us */\n\tguint64 timestamp = 0;\n\n\t/* Store delta array */\n\tGQueue *deltas = g_queue_new();\n\tGQueue *statuses = g_queue_new();\n\tjanus_rtp_packet_status last_status = janus_rtp_packet_status_reserved;\n\tjanus_rtp_packet_status max_status = janus_rtp_packet_status_notreceived;\n\tgboolean all_same = TRUE;\n\n\t/* For each packet  */\n\twhile (stat != NULL) {\n\t\tjanus_rtp_packet_status status = janus_rtp_packet_status_notreceived;\n\n\t\t/* If got packet */\n\t\tif (stat->timestamp) {\n\t\t\tint delta = 0;\n\t\t\t/* If first received */\n\t\t\tif (!first_received) {\n\t\t\t\t/* Got it  */\n\t\t\t\tfirst_received = TRUE;\n\t\t\t\t/* Set it */\n\t\t\t\treference_time = stat->timestamp / 64000;\n\t\t\t\t/* Get initial time */\n\t\t\t\ttimestamp = reference_time * 64000;\n\t\t\t\t/* also in buffer */\n\t\t\t\t/* (use only 23 bits of reference_time) */\n\t\t\t\tjanus_set3(data, reference_time_pos, (reference_time & 0x007FFFFF));\n\t\t\t}\n\n\t\t\t/* Get delta */\n\t\t\tif (stat->timestamp>timestamp)\n\t\t\t\tdelta = (stat->timestamp-timestamp)/250;\n\t\t\telse\n\t\t\t\tdelta = -(int)((timestamp-stat->timestamp)/250);\n\t\t\t/* If it is negative or too big */\n\t\t\tif (delta<0 || delta> 255) {\n\t\t\t\t/* Big one */\n\t\t\t\tstatus = janus_rtp_packet_status_largeornegativedelta;\n\t\t\t} else {\n\t\t\t\t/* Small */\n\t\t\t\tstatus = janus_rtp_packet_status_smalldelta;\n\t\t\t}\n\t\t\t/* Store delta */\n\t\t\t/* Overflows are possible here */\n\t\t\tg_queue_push_tail(deltas, GINT_TO_POINTER(delta));\n\t\t\t/* Set last time */\n\t\t\ttimestamp = stat->timestamp;\n\t\t}\n\n\t\t/* Check if all previoues ones were equal and this one the first different */\n\t\tif (all_same && last_status!=janus_rtp_packet_status_reserved && status!=last_status) {\n\t\t\t/* How big was the same run */\n\t\t\tif (g_queue_get_length(statuses)>7) {\n\t\t\t\tguint32 word = 0;\n\t\t\t\t/* Write run! */\n\t\t\t\t/*\n\t\t\t\t\t0                   1\n\t\t\t\t\t0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5\n\t\t\t\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t\t\t\t|T| S |       Run Length        |\n\t\t\t\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t\t\t\tT = 0\n\t\t\t\t */\n\t\t\t\tword = janus_push_bits(word, 1, 0);\n\t\t\t\tword = janus_push_bits(word, 2, last_status);\n\t\t\t\tword = janus_push_bits(word, 13, g_queue_get_length(statuses));\n\t\t\t\t/* Write word */\n\t\t\t\tjanus_set2(data, len, word);\n\t\t\t\tlen += 2;\n\t\t\t\t/* Remove all statuses */\n\t\t\t\tg_queue_clear(statuses);\n\t\t\t\t/* Reset status */\n\t\t\t\tlast_status = janus_rtp_packet_status_reserved;\n\t\t\t\tmax_status = janus_rtp_packet_status_notreceived;\n\t\t\t\tall_same = TRUE;\n\t\t\t} else {\n\t\t\t\t/* Not same */\n\t\t\t\tall_same = FALSE;\n\t\t\t}\n\t\t}\n\n\t\t/* Push back statuses, it will be handled later */\n\t\tg_queue_push_tail(statuses, GUINT_TO_POINTER(status));\n\n\t\t/* If it is bigger */\n\t\tif (status>max_status) {\n\t\t\t/* Store it */\n\t\t\tmax_status = status;\n\t\t}\n\t\t/* Store las status */\n\t\tlast_status = status;\n\n\t\t/* Check if we can still be enqueuing for a run */\n\t\tif (!all_same) {\n\t\t\t/* Check  */\n\t\t\tif (!all_same && max_status==janus_rtp_packet_status_largeornegativedelta && g_queue_get_length(statuses)>6) {\n\t\t\t\tguint32 word = 0;\n\t\t\t\t/*\n\t\t\t\t\t0                   1\n\t\t\t\t\t0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5\n\t\t\t\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t\t\t\t|T|S|        Symbols            |\n\t\t\t\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t\t\t\tT = 1\n\t\t\t\t\tS = 1\n\t\t\t\t */\n\t\t\t\tword = janus_push_bits(word, 1, 1);\n\t\t\t\tword = janus_push_bits(word, 1, 1);\n\t\t\t\t/* Set next 7 */\n\t\t\t\tsize_t i = 0;\n\t\t\t\tfor (i=0;i<7;++i) {\n\t\t\t\t\t/* Get status */\n\t\t\t\t\tjanus_rtp_packet_status status = (janus_rtp_packet_status) GPOINTER_TO_UINT(g_queue_pop_head (statuses));\n\t\t\t\t\t/* Write */\n\t\t\t\t\tword = janus_push_bits(word, 2, (guint8)status);\n\t\t\t\t}\n\t\t\t\t/* Write word */\n\t\t\t\tjanus_set2(data, len, word);\n\t\t\t\tlen += 2;\n\t\t\t\t/* Reset */\n\t\t\t\tlast_status = janus_rtp_packet_status_reserved;\n\t\t\t\tmax_status = janus_rtp_packet_status_notreceived;\n\t\t\t\tall_same = TRUE;\n\n\t\t\t\t/* We need to restore the values, as there may be more elements on the buffer */\n\t\t\t\tfor (i=0; i<g_queue_get_length(statuses); ++i) {\n\t\t\t\t\t/* Get status */\n\t\t\t\t\tstatus = (janus_rtp_packet_status) GPOINTER_TO_UINT(g_queue_peek_nth(statuses, i));\n\t\t\t\t\t/* If it is bigger */\n\t\t\t\t\tif (status>max_status) {\n\t\t\t\t\t\t/* Store it */\n\t\t\t\t\t\tmax_status = status;\n\t\t\t\t\t}\n\t\t\t\t\t//Check if it is the same */\n\t\t\t\t\tif (all_same && last_status!=janus_rtp_packet_status_reserved && status!=last_status) {\n\t\t\t\t\t\t/* Not the same */\n\t\t\t\t\t\tall_same = FALSE;\n\t\t\t\t\t}\n\t\t\t\t\t/* Store las status */\n\t\t\t\t\tlast_status = status;\n\t\t\t\t}\n\t\t\t} else if (!all_same && g_queue_get_length(statuses)>13) {\n\t\t\t\tguint32 word = 0;\n\t\t\t\t/*\n\t\t\t\t\t0                   1\n\t\t\t\t\t0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5\n\t\t\t\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t\t\t\t|T|S|       symbol list         |\n\t\t\t\t\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t\t\t\t\tT = 1\n\t\t\t\t\tS = 0\n\t\t\t\t */\n\t\t\t\tword = janus_push_bits(word, 1, 1);\n\t\t\t\tword = janus_push_bits(word, 1, 0);\n\t\t\t\t/* Set next 7 */\n\t\t\t\tguint32 i = 0;\n\t\t\t\tfor (i=0;i<14;++i) {\n\t\t\t\t\t/* Get status */\n\t\t\t\t\tjanus_rtp_packet_status status = (janus_rtp_packet_status) GPOINTER_TO_UINT(g_queue_pop_head (statuses));\n\t\t\t\t\t/* Write */\n\t\t\t\t\tword = janus_push_bits(word, 1, (guint8)status);\n\t\t\t\t}\n\t\t\t\t/* Write word */\n\t\t\t\tjanus_set2(data, len, word);\n\t\t\t\tlen += 2;\n\t\t\t\t/* Reset */\n\t\t\t\tlast_status = janus_rtp_packet_status_reserved;\n\t\t\t\tmax_status = janus_rtp_packet_status_notreceived;\n\t\t\t\tall_same = TRUE;\n\t\t\t}\n\t\t}\n\t\t/* Free mem */\n\t\tg_free(stat);\n\n\t\t/* Get next packet stat */\n\t\tstat = (janus_rtcp_transport_wide_cc_stats *) g_queue_pop_head (transport_wide_cc_stats);\n\t}\n\n\t/* Get status len */\n\tsize_t statuses_len = g_queue_get_length(statuses);\n\n\t/* If not finished yet */\n\tif (statuses_len>0) {\n\t\t/* How big was the same run */\n\t\tif (all_same) {\n\t\t\tguint32 word = 0;\n\t\t\t/* Write run! */\n\t\t\tword = janus_push_bits(word, 1, 0);\n\t\t\tword = janus_push_bits(word, 2, last_status);\n\t\t\tword = janus_push_bits(word, 13, statuses_len);\n\t\t\t/* Write word */\n\t\t\tjanus_set2(data, len, word);\n\t\t\tlen += 2;\n\t\t} else if (max_status == janus_rtp_packet_status_largeornegativedelta) {\n\t\t\tguint32 word = 0;\n\t\t\t/* Write chunk */\n\t\t\tword = janus_push_bits(word, 1, 1);\n\t\t\tword = janus_push_bits(word, 1, 1);\n\t\t\t/* Write all the statuses */\n\t\t\tunsigned int i = 0;\n\t\t\tfor (i=0;i<statuses_len;i++) {\n\t\t\t\t/* Get each status */\n\t\t\t\tjanus_rtp_packet_status status = (janus_rtp_packet_status) GPOINTER_TO_UINT(g_queue_pop_head (statuses));\n\t\t\t\t/* Write */\n\t\t\t\tword = janus_push_bits(word, 2, (guint8)status);\n\t\t\t}\n\t\t\t/* Write pending */\n\t\t\tword = janus_push_bits(word, 14-statuses_len*2, 0);\n\t\t\t/* Write word */\n\t\t\tjanus_set2(data , len, word);\n\t\t\tlen += 2;\n\t\t} else {\n\t\t\tguint32 word = 0;\n\t\t\t/* Write chunk */\n\t\t\tword = janus_push_bits(word, 1, 1);\n\t\t\tword = janus_push_bits(word, 1, 0);\n\t\t\t/* Write all the statuses */\n\t\t\tunsigned int i = 0;\n\t\t\tfor (i=0;i<statuses_len;i++) {\n\t\t\t\t/* Get each status */\n\t\t\t\tjanus_rtp_packet_status status = (janus_rtp_packet_status) GPOINTER_TO_UINT(g_queue_pop_head (statuses));\n\t\t\t\t/* Write */\n\t\t\t\tword = janus_push_bits(word, 1, (guint8)status);\n\t\t\t}\n\t\t\t/* Write pending */\n\t\t\tword = janus_push_bits(word, 14-statuses_len, 0);\n\t\t\t/* Write word */\n\t\t\tjanus_set2(data, len, word);\n\t\t\tlen += 2;\n\t\t}\n\t}\n\n\t/* Write now the deltas */\n\twhile (!g_queue_is_empty(deltas)) {\n\t\t/* Get next delta */\n\t\tgint delta = GPOINTER_TO_INT(g_queue_pop_head (deltas));\n\t\t/* Check size */\n\t\tif (delta<0 || delta>255) {\n\t\t\tshort reported_delta = (short)delta;\n\t\t\t/* Overflow */\n\t\t\tif (reported_delta != delta) {\n\t\t\t\treported_delta = delta > 0 ? SHRT_MAX : SHRT_MIN;\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Delta value (%d) too large, reporting it as %d\\n\", delta, reported_delta);\n\t\t\t}\n\t\t\t/* 2 bytes */\n\t\t\tjanus_set2(data, len, reported_delta);\n\t\t\t/* Inc */\n\t\t\tlen += 2;\n\t\t} else {\n\t\t\t/* 1 byte */\n\t\t\tjanus_set1(data, len, (guint8)delta);\n\t\t\t/* Inc */\n\t\t\tlen ++;\n\t\t}\n\t}\n\n\t/* Clean mem */\n\tg_queue_free(statuses);\n\tg_queue_free(deltas);\n\n\t/* Add zero padding */\n\twhile (len%4) {\n\t\t/* Add padding */\n\t\tjanus_set1(data, len++, 0);\n\t}\n\n\t/* Set RTCP Len */\n\trtcp->length = htons((len/4)-1);\n\n\t/* Done */\n\treturn len;\n}\n"
  },
  {
    "path": "src/rtcp.h",
    "content": "/*! \\file    rtcp.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    RTCP processing (headers)\n * \\details  Implementation of the RTCP messages. RTCP messages coming\n * through the server are parsed and, if needed (according to\n * http://tools.ietf.org/html/draft-ietf-straw-b2bua-rtcp-00),\n * fixed before they are sent to the peers (e.g., to fix SSRCs that may\n * have been changed by the server). Methods to generate FIR messages\n * and generate/cap REMB messages are provided as well.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_RTCP_H\n#define JANUS_RTCP_H\n\n#include <arpa/inet.h>\n#ifdef __MACH__\n#include <machine/endian.h>\n#elif defined(__FreeBSD__)\n#include <sys/endian.h>\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n\n/*! \\brief RTCP Packet Types (http://www.networksorcery.com/enp/protocol/rtcp.htm) */\ntypedef enum {\n    RTCP_FIR = 192,\n    RTCP_SR = 200,\n    RTCP_RR = 201,\n    RTCP_SDES = 202,\n    RTCP_BYE = 203,\n    RTCP_APP = 204,\n    RTCP_RTPFB = 205,\n    RTCP_PSFB = 206,\n    RTCP_XR = 207,\n} rtcp_type;\ntypedef rtcp_type janus_rtcp_type;\n\n\n/*! \\brief RTCP Header (http://tools.ietf.org/html/rfc3550#section-6.1) */\ntypedef struct rtcp_header\n{\n#if __BYTE_ORDER == __BIG_ENDIAN\n\tuint16_t version:2;\n\tuint16_t padding:1;\n\tuint16_t rc:5;\n\tuint16_t type:8;\n#elif __BYTE_ORDER == __LITTLE_ENDIAN\n\tuint16_t rc:5;\n\tuint16_t padding:1;\n\tuint16_t version:2;\n\tuint16_t type:8;\n#endif\n\tuint16_t length:16;\n} rtcp_header;\ntypedef rtcp_header janus_rtcp_header;\n\n/*! \\brief RTCP Sender Information (http://tools.ietf.org/html/rfc3550#section-6.4.1) */\ntypedef struct sender_info\n{\n\tuint32_t ntp_ts_msw;\n\tuint32_t ntp_ts_lsw;\n\tuint32_t rtp_ts;\n\tuint32_t s_packets;\n\tuint32_t s_octets;\n} sender_info;\ntypedef sender_info janus_sender_info;\n\n/*! \\brief RTCP Report Block (http://tools.ietf.org/html/rfc3550#section-6.4.1) */\ntypedef struct report_block\n{\n\tuint32_t ssrc;\n\tuint32_t flcnpl;\n\tuint32_t ehsnr;\n\tuint32_t jitter;\n\tuint32_t lsr;\n\tuint32_t delay;\n} report_block;\ntypedef report_block janus_report_block;\n\n/*! \\brief RTCP Sender Report (http://tools.ietf.org/html/rfc3550#section-6.4.1) */\ntypedef struct rtcp_sr\n{\n\trtcp_header header;\n\tuint32_t ssrc;\n\tsender_info si;\n\treport_block rb[1];\n} rtcp_sr;\ntypedef rtcp_sr janus_rtcp_sr;\n\n/*! \\brief RTCP Receiver Report (http://tools.ietf.org/html/rfc3550#section-6.4.2) */\ntypedef struct rtcp_rr\n{\n\trtcp_header header;\n\tuint32_t ssrc;\n\treport_block rb[1];\n} rtcp_rr;\ntypedef rtcp_rr janus_rtcp_rr;\n\n/*! \\brief RTCP SDES (http://tools.ietf.org/html/rfc3550#section-6.5) */\ntypedef struct rtcp_sdes_chunk\n{\n\tuint32_t ssrc;\n} rtcp_sdes_chunk;\ntypedef rtcp_sdes_chunk janus_rtcp_sdes_chunk;\n\ntypedef struct rtcp_sdes_item\n{\n\tuint8_t type;\n\tuint8_t len;\n\tchar content[1];\n} rtcp_sdes_item;\ntypedef rtcp_sdes_item janus_rtcp_sdes_item;\n\ntypedef struct rtcp_sdes\n{\n\trtcp_header header;\n\trtcp_sdes_chunk chunk;\n\trtcp_sdes_item item;\n} rtcp_sdes;\ntypedef rtcp_sdes janus_rtcp_sdes;\n\n/*! \\brief RTCP BYE (http://tools.ietf.org/html/rfc3550#section-6.6) */\ntypedef struct rtcp_bye\n{\n\trtcp_header header;\n\tuint32_t ssrc[1];\n} rtcp_bye;\ntypedef rtcp_bye janus_rtcp_bye;\n\n/*! \\brief RTCP APP (http://tools.ietf.org/html/rfc3550#section-6.7) */\ntypedef struct rtcp_app\n{\n\trtcp_header header;\n\tuint32_t ssrc;\n\tchar name[4];\n} rtcp_app;\ntypedef rtcp_app janus_rtcp_app;\n\n/*! \\brief RTCP NACK (http://tools.ietf.org/html/rfc4585#section-6.2.1) */\ntypedef struct rtcp_nack\n{\n\t/*! \\brief Packet ID */\n\tuint16_t pid;\n\t/*! \\brief bitmask of following lost packets */\n\tuint16_t blp;\n} rtcp_nack;\ntypedef rtcp_nack janus_rtcp_nack;\n\n/*! \\brief Janus representation (linked list) of sequence numbers to send again */\ntypedef struct janus_nack {\n\t/*! \\brief Sequence number to send again */\n\tuint16_t seq_no;\n\t/*! \\brief Next element in the linked list */\n\tstruct janus_nack *next;\n} janus_nack;\n\n\n/*! \\brief RTCP REMB (http://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03) */\ntypedef struct rtcp_remb\n{\n\t/*! \\brief Unique identifier ('R' 'E' 'M' 'B') */\n\tchar id[4];\n\t/*! \\brief Num SSRC, Br Exp, Br Mantissa (bit mask) */\n\tuint32_t bitrate;\n\t/*! \\brief SSRC feedback (we expect at max three SSRCs in there) */\n\tuint32_t ssrc[3];\n} rtcp_remb;\ntypedef rtcp_remb janus_rtcp_fb_remb;\n\n\n/*! \\brief RTCP FIR (http://tools.ietf.org/search/rfc5104#section-4.3.1.1) */\ntypedef struct rtcp_fir\n{\n\t/*! \\brief SSRC of the media sender that needs to send a key frame */\n\tuint32_t ssrc;\n\t/*! \\brief Sequence number (only the first 8 bits are used, the other 24 are reserved) */\n\tuint32_t seqnr;\n} rtcp_fir;\ntypedef rtcp_fir janus_rtcp_fb_fir;\n\n\n/*! \\brief RTCP-FB (http://tools.ietf.org/html/rfc4585) */\ntypedef struct rtcp_fb\n{\n\t/*! \\brief Common header */\n\trtcp_header header;\n\t/*! \\brief Sender SSRC */\n\tuint32_t ssrc;\n\t/*! \\brief Media source */\n\tuint32_t media;\n\t/*! \\brief Feedback Control Information */\n\tchar fci[1];\n} rtcp_fb;\ntypedef rtcp_fb janus_rtcp_fb;\n\n/*! \\brief RTCP Extended Report Block (https://tools.ietf.org/html/rfc3611#section-3) */\ntypedef struct extended_report_block\n{\n\t/*! \\brief Block type (BT) */\n\tuint8_t blocktype;\n\t/*! \\brief Type-specific */\n\tuint8_t typesp;\n\t/*! \\brief Block length */\n\tuint16_t length;\n\t/*! \\brief Content (variable length) */\n\tchar content[1];\n\n} extended_report_block;\ntypedef extended_report_block janus_extended_report_block;\n\n/*! \\brief RTCP Extended Report (https://tools.ietf.org/html/rfc3611#section-2) */\ntypedef struct rtcp_xr\n{\n\trtcp_header header;\n\tuint32_t ssrc;\n\textended_report_block erb[1];\n} rtcp_xr;\ntypedef rtcp_xr janus_rtcp_xr;\n\n\n/*! \\brief Internal RTCP state context (for RR/SR) */\ntypedef struct rtcp_context\n{\n\t/* Whether we received any RTP packet at all (don't send RR otherwise) */\n\tuint8_t rtp_recvd:1;\n\tuint32_t rtp_last_inorder_ts;\n\tint64_t rtp_last_inorder_time;\n\n\tuint16_t max_seq_nr;\n\tuint16_t seq_cycle;\n\tuint16_t base_seq;\n\t/* Payload type */\n\tuint16_t pt;\n\n\t/* RFC 3550 A.8 Interarrival Jitter */\n\tint64_t transit;\n\tdouble jitter, jitter_remote;\n\t/* Timestamp base (e.g., 48000 for opus audio, or 90000 for video) */\n\tuint32_t tb;\n\n\t/* Last SR received */\n\tuint32_t lsr;\n\t/* Monotonic time of last SR received */\n\tint64_t lsr_ts;\n\n\t/* Last RR/SR we sent */\n\tint64_t last_sent;\n\n\t/* Estimated round-trip time */\n\tuint32_t rtt;\n\t/* Fields that led to RTT calculation (for stats) */\n\tuint32_t rtt_ntp, rtt_lsr, rtt_dlsr;\n\n\t/* RFC 3550 A.3 */\n\tuint32_t received;\n\tuint32_t received_prior;\n\tuint32_t expected;\n\tuint32_t expected_prior;\n\tint32_t lost, lost_remote;\n\n\tuint32_t retransmitted;\n\tuint32_t retransmitted_prior;\n\n\t/* Inbound RR process */\n\tint64_t rr_last_ts;\n\tuint32_t rr_last_ehsnr;\n\tint32_t rr_last_lost;\n\tuint32_t rr_last_nack_count;\n\tgint sent_packets_since_last_rr;\n\tgint nack_count;\n\n\t/* Link quality estimations */\n\tdouble in_link_quality;\n\tdouble in_media_link_quality;\n\tdouble out_link_quality;\n\tdouble out_media_link_quality;\n\n\t/* TODO Incoming transport-wide CC feedback*/\n\n} rtcp_context;\ntypedef rtcp_context janus_rtcp_context;\n\n/*! \\brief Stores transport wide packet reception statistics */\ntypedef struct rtcp_transport_wide_cc_stats\n{\n\t/*! \\brief Transwport wide sequence number */\n\tguint32 transport_seq_num;\n\t/*! \\brief Reception time */\n\tguint64 timestamp;\n} rtcp_transport_wide_cc_stats;\ntypedef rtcp_transport_wide_cc_stats janus_rtcp_transport_wide_cc_stats;\n\n/*! \\brief Method to retrieve the estimated round-trip time from an existing RTCP context\n * @param[in] ctx The RTCP context to query\n * @returns The estimated round-trip time */\nuint32_t janus_rtcp_context_get_rtt(janus_rtcp_context *ctx);\n/*! \\brief Method to retrieve the total number of lost packets from an existing RTCP context\n * @param[in] ctx The RTCP context to query\n * @param[in] remote Whether we're querying the remote (provided by peer) or local (computed by Janus) info\n * @returns The total number of lost packets */\nint32_t janus_rtcp_context_get_lost_all(janus_rtcp_context *ctx, gboolean remote);\n/*! \\brief Method to retrieve the jitter from an existing RTCP context\n * @param[in] ctx The RTCP context to query\n * @param[in] remote Whether we're querying the remote (provided by peer) or local (computed by Janus) info\n * @returns The computed jitter */\nuint32_t janus_rtcp_context_get_jitter(janus_rtcp_context *ctx, gboolean remote);\n/*! \\brief Method to retrieve inbound link quality from an existing RTCP context\n * @param[in] ctx The RTCP context to query\n * @returns Inbound link quality estimation */\nuint32_t janus_rtcp_context_get_in_link_quality(janus_rtcp_context *ctx);\n/*! \\brief Method to retrieve inbound media link quality from an existing RTCP context\n * @param[in] ctx The RTCP context to query\n * @returns Inbound media link quality estimation */\nuint32_t janus_rtcp_context_get_in_media_link_quality(janus_rtcp_context *ctx);\n/*! \\brief Method to retrieve outbound link quality from an existing RTCP context\n * @param[in] ctx The RTCP context to query\n * @returns Outbound link quality estimation */\nuint32_t janus_rtcp_context_get_out_link_quality(janus_rtcp_context *ctx);\n/*! \\brief Method to retrieve outbound media link quality from an existing RTCP context\n * @param[in] ctx The RTCP context to query\n * @returns Outbound media link quality estimation */\nuint32_t janus_rtcp_context_get_out_media_link_quality(janus_rtcp_context *ctx);\n/*! \\brief Method to swap Report Blocks and move media RB in first position in case rtx SSRC comes first\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @param[in] rtx_ssrc The rtx SSRC */\nvoid janus_rtcp_swap_report_blocks(char *packet, int len, uint32_t rtx_ssrc);\n/*! \\brief Method to quickly retrieve the sender SSRC (needed for demuxing RTCP in BUNDLE)\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns The sender SSRC, or 0 in case of error */\nguint32 janus_rtcp_get_sender_ssrc(char *packet, int len);\n/*! \\brief Method to quickly retrieve the received SSRC (needed for demuxing RTCP in BUNDLE)\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns The receiver SSRC, or 0 in case of error */\nguint32 janus_rtcp_get_receiver_ssrc(char *packet, int len);\n\n/*! \\brief Method to check that a RTCP packet size is at least the minimum necessary (8 bytes)\n *  and to validate the length field against the actual size\n * @param[in] rtcp The RTCP message\n * @param[in] len The message data length in bytes\n * @returns TRUE if packet is OK, or FALSE in case of error */\ngboolean janus_rtcp_check_len(janus_rtcp_header *rtcp, int len);\n/*! \\brief Method to check if a RTCP packet could contain a Receiver Report\n * @param[in] rtcp The RTCP message\n * @param[in] len The message data length in bytes\n * @returns TRUE if packet is OK, or FALSE in case of error */\ngboolean janus_rtcp_check_rr(janus_rtcp_header *rtcp, int len);\n/*! \\brief Method to check if a RTCP packet could contain a Sender Report\n * @param[in] rtcp The RTCP message\n * @param[in] len The message data length in bytes\n * @returns TRUE if packet is OK, or FALSE in case of error */\ngboolean janus_rtcp_check_sr(janus_rtcp_header *rtcp, int len);\n/*! \\brief Method to check if a RTCP packet could contain a Feedback Message\n * with a defined FCI size.\n * @param[in] rtcp The RTCP message\n * @param[in] len The message data length in bytes\n * @param[in] sizeof_fci The size of a FCI entry\n * @returns TRUE if packet is OK, or FALSE in case of error */\ngboolean janus_rtcp_check_fci(janus_rtcp_header *rtcp, int len, int sizeof_fci);\n/*! \\brief Method to check if a RTCP packet could contain an AFB REMB Message\n * @param[in] rtcp The RTCP message\n * @param[in] len The message data length in bytes\n * @returns TRUE if packet is OK, or FALSE in case of error */\ngboolean janus_rtcp_check_remb(janus_rtcp_header *rtcp, int len);\n\n/*! \\brief Helper method to demultiplex RTCP from other protocols\n * @param[in] buf Buffer to inspect\n * @param[in] len Length of the buffer to inspect */\ngboolean janus_is_rtcp(char *buf, guint len);\n\n/*! \\brief Method to parse/validate an RTCP message\n * @param[in] ctx RTCP context to update, if needed (optional)\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns 0 in case of success, -1 on errors */\nint janus_rtcp_parse(janus_rtcp_context *ctx, char *packet, int len);\n\n/*! \\brief Method to fix incoming RTCP SR and RR data\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @param[in] base_ts RTP context base timestamp to compute offset\n * @param[in] base_ts_prev RTP context base timestamp to compute offset\n * @param[in] ssrc_peer The remote SSRC in usage for this stream\n * @param[in] ssrc_local The local SSRC in usage for this stream\n * @param[in] ssrc_expected The expected SSRC for this RTCP packet\n * @param[in] video Whether the RTCP packet contains report for video data\n * @returns The number of fields updated, negative values on errors */\nint janus_rtcp_fix_report_data(char *packet, int len, uint32_t base_ts, uint32_t base_ts_prev, uint32_t ssrc_peer, uint32_t ssrc_local, uint32_t ssrc_expected, gboolean video);\n\n/*! \\brief Method to fix an RTCP message (http://tools.ietf.org/html/draft-ietf-straw-b2bua-rtcp-00)\n * @param[in] ctx RTCP context to update, if needed (optional)\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @param[in] fixssrc Whether the method needs to fix the message or just parse it\n * @param[in] newssrcl The SSRC of the sender to put in the message\n * @param[in] newssrcr The SSRC of the receiver to put in the message\n * @returns 0 in case of success, -1 on errors */\nint janus_rtcp_fix_ssrc(janus_rtcp_context *ctx, char *packet, int len, int fixssrc, uint32_t newssrcl, uint32_t newssrcr);\n\n/*! \\brief Method to filter an outgoing RTCP message (http://tools.ietf.org/html/draft-ietf-straw-b2bua-rtcp-00)\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @param[in,out] newlen The data length of the filtered RTCP message\n * @returns A pointer to the new RTCP message data, NULL in case all messages have been filtered out */\nchar *janus_rtcp_filter(char *packet, int len, int *newlen);\n\n/*! \\brief Method to quickly process the header of an incoming RTP packet to update the associated RTCP context\n * @param[in] ctx RTCP context to update, if needed (optional)\n * @param[in] packet The RTP packet\n * @param[in] len The packet data length in bytes\n * @param[in] rfc4588_pkt True if this is a RTX packet\n * @param[in] rfc4588_enabled True if this packet comes from a RTX enabled stream\n * @param[in] retransmissions_disabled True if retransmissions are not supported at all for this stream\n * @param[in] clock_rates Mapping between payload types and clock rates, if available\n * @returns 0 in case of success, -1 on errors */\nint janus_rtcp_process_incoming_rtp(janus_rtcp_context *ctx, char *packet, int len,\n\tgboolean rfc4588_pkt, gboolean rfc4588_enabled, gboolean retransmissions_disabled,\n\tGHashTable *clock_rates);\n\n/*! \\brief Method to fill in a Report Block in a Receiver Report\n * @param[in] ctx The RTCP context to use for the report\n * @param[in] rb Pointer to a valid report_block area of the RTCP data\n * @returns 0 in case of success, -1 on errors */\nint janus_rtcp_report_block(janus_rtcp_context *ctx, janus_report_block *rb);\n\n/*! \\brief Method to check whether an RTCP message contains a BYE message\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns TRUE in case of success, FALSE otherwise */\ngboolean janus_rtcp_has_bye(char *packet, int len);\n\n/*! \\brief Method to check whether an RTCP message contains a FIR request\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns TRUE in case of success, FALSE otherwise */\ngboolean janus_rtcp_has_fir(char *packet, int len);\n\n/*! \\brief Method to check whether an RTCP message contains a PLI request\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns TRUE in case of success, FALSE otherwise */\ngboolean janus_rtcp_has_pli(char *packet, int len);\n\n/*! \\brief Method to parse an RTCP NACK message\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @param[in,out] nacks_queue The queue containing the sequence numbers to send again */\nvoid janus_rtcp_get_nacks(char *packet, int len, GQueue *nacks_queue);\n\n/*! \\brief Method to remove an RTCP NACK message\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns The new message data length in bytes\n * @note This is mostly a placeholder: for the sake of simplicity, whenever we handle\n * some sequence numbers in a NACK, we remove the NACK as a whole before forwarding the\n * RTCP message. Future versions will only selectively remove the sequence numbers that\n * have been handled. */\nint janus_rtcp_remove_nacks(char *packet, int len);\n\n/*! \\brief Inspect an existing RTCP REMB message to retrieve the reported bitrate\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @returns The reported bitrate if successful, 0 if no REMB packet was available */\nuint32_t janus_rtcp_get_remb(char *packet, int len);\n\n/*! \\brief Method to modify an existing RTCP REMB message to cap the reported bitrate\n * @param[in] packet The message data\n * @param[in] len The message data length in bytes\n * @param[in] bitrate The new bitrate to report (e.g., 128000)\n * @returns 0 in case of success, -1 on errors */\nint janus_rtcp_cap_remb(char *packet, int len, uint32_t bitrate);\n\n/*! \\brief Method to generate a new RTCP SDES message\n * @param[in] packet The buffer data\n * @param[in] len The buffer data length in bytes\n * @param[in] cname The CNAME to write\n * @param[in] cnamelen The CNAME data length in bytes\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_sdes_cname(char *packet, int len, const char *cname, int cnamelen);\n\n/*! \\brief Method to generate a new RTCP REMB message to cap the reported bitrate\n * @param[in] packet The buffer data (MUST be at least 24 chars)\n * @param[in] len The message data length in bytes (MUST be 24)\n * @param[in] bitrate The bitrate to report (e.g., 128000)\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_remb(char *packet, int len, uint32_t bitrate);\n\n/*! \\brief Method to generate a new RTCP REMB message to cap the reported bitrate, but for more SSRCs\n * @param[in] packet The buffer data (MUST be at least 24 chars)\n * @param[in] len The message data length in bytes (MUST be 24)\n * @param[in] bitrate The bitrate to report (e.g., 128000)\n * @param[in] numssrc The number of SSRCs to include in the request\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_remb_ssrcs(char *packet, int len, uint32_t bitrate, uint8_t numssrc);\n\n/*! \\brief Method to generate a new RTCP FIR message to request a key frame\n * @param[in] packet The buffer data (MUST be at least 20 chars)\n * @param[in] len The message data length in bytes (MUST be 20)\n * @param[in,out] seqnr The current FIR sequence number (will be incremented by the method)\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_fir(char *packet, int len, int *seqnr);\n\n/*! \\brief Method to generate a new legacy RTCP FIR (RFC2032) message to request a key frame\n * \\note This is actually identical to janus_rtcp_fir(), with the difference that we set 192 as packet type\n * @param[in] packet The buffer data (MUST be at least 20 chars)\n * @param[in] len The message data length in bytes (MUST be 20)\n * @param[in,out] seqnr The current FIR sequence number (will be incremented by the method)\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_fir_legacy(char *packet, int len, int *seqnr);\n\n/*! \\brief Method to generate a new RTCP PLI message to request a key frame\n * @param[in] packet The buffer data (MUST be at least 12 chars)\n * @param[in] len The message data length in bytes (MUST be 12)\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_pli(char *packet, int len);\n\n/*! \\brief Method to generate a new RTCP NACK message to report lost packets\n * @param[in] packet The buffer data (MUST be at least 16 chars)\n * @param[in] len The message data length in bytes (MUST be 16)\n * @param[in] nacks List of packets to NACK\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_nacks(char *packet, int len, GSList *nacks);\n\n/*! \\brief Method to generate a new RTCP transport wide message to report reception stats\n * @param[in] packet The buffer data (MUST be at least 16 chars)\n * @param[in] len The message data length in bytes\n * @param[in] ssrc SSRC of the origin stream\n * @param[in] media SSRC of the destination stream\n * @param[in] feedback_packet_count Feedback paccket count\n * @param[in] transport_wide_cc_stats List of rtp packet reception stats\n * @returns The message data length in bytes, if successful, -1 on errors */\nint janus_rtcp_transport_wide_cc_feedback(char *packet, size_t len, guint32 ssrc, guint32 media, guint8 feedback_packet_count, GQueue *transport_wide_cc_stats);\n\n#endif\n"
  },
  {
    "path": "src/rtp.c",
    "content": "/*! \\file    rtp.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    RTP processing\n * \\details  Implementation of the RTP header. Since the server does not\n * much more than relaying frames around, the only thing we're interested\n * in is the RTP header and how to get its payload, and parsing extensions.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#include <string.h>\n#include \"rtp.h\"\n#include \"rtpsrtp.h\"\n#include \"debug.h\"\n\n/* Local, private, structures for parsing video-layers-allocation extensions */\ntypedef struct janus_rtp_vla_spatial_layer {\n\tuint8_t id;\n\tuint8_t tls;\n} janus_rtp_vla_spatial_layer;\n\ntypedef struct janus_rtp_vla_rtp_stream {\n\tuint8_t rid;\n\tuint8_t sl_bm;\n\tjanus_rtp_vla_spatial_layer sl[4];\n} janus_rtp_vla_rtp_stream;\n\n/* Public methods */\ngboolean janus_is_rtp(char *buf, guint len) {\n\tif (len < 12)\n\t\treturn FALSE;\n\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\treturn ((header->type < 64) || (header->type >= 96));\n}\n\nchar *janus_rtp_payload(char *buf, int len, int *plen) {\n\tif(!buf || len < 12)\n\t\treturn NULL;\n\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\tif (rtp->version != 2) {\n\t\treturn NULL;\n\t}\n\tint hlen = 12;\n\tif(rtp->csrccount)\t/* Skip CSRC if needed */\n\t\thlen += rtp->csrccount*4;\n\n\tif(rtp->extension) {\n\t\tjanus_rtp_header_extension *ext = (janus_rtp_header_extension *)(buf+hlen);\n\t\tint extlen = ntohs(ext->length)*4;\n\t\thlen += 4;\n\t\tif(len > (hlen + extlen))\n\t\t\thlen += extlen;\n\t}\n\tif (len-hlen <= 0) {\n\t\treturn NULL;\n\t}\n\tif(plen)\n\t\t*plen = len-hlen;\n\treturn buf+hlen;\n}\n\nint janus_rtp_header_extension_get_id(const char *sdp, const char *extension) {\n\tif(!sdp || !extension)\n\t\treturn -1;\n\tchar extmap[100];\n\tg_snprintf(extmap, 100, \"a=extmap:%%d %s\", extension);\n\t/* Look for the extmap */\n\tconst char *line = strstr(sdp, \"m=\");\n\twhile(line) {\n\t\tchar *next = strchr(line, '\\n');\n\t\tif(next) {\n\t\t\t*next = '\\0';\n\t\t\tif(strstr(line, \"a=extmap\") && strstr(line, extension)) {\n\t\t\t\t/* Gotcha! */\n\t\t\t\tint id = 0;\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wformat-nonliteral\"\n\t\t\t\tif(sscanf(line, extmap, &id) == 1) {\n#pragma GCC diagnostic pop\n\t\t\t\t\t*next = '\\n';\n\t\t\t\t\treturn id;\n\t\t\t\t}\n\t\t\t}\n\t\t\t*next = '\\n';\n\t\t}\n\t\tline = next ? (next+1) : NULL;\n\t}\n\treturn -2;\n}\n\nconst char *janus_rtp_header_extension_get_from_id(const char *sdp, int id) {\n\tif(!sdp || id < 0)\n\t\treturn NULL;\n\t/* Look for the mapping */\n\tchar extmap[100];\n\tg_snprintf(extmap, 100, \"a=extmap:%d \", id);\n\tconst char *line = strstr(sdp, \"m=\");\n\twhile(line) {\n\t\tchar *next = strchr(line, '\\n');\n\t\tif(next) {\n\t\t\t*next = '\\0';\n\t\t\tif(strstr(line, extmap)) {\n\t\t\t\t/* Gotcha! */\n\t\t\t\tchar extension[100];\n\t\t\t\tif(sscanf(line, \"a=extmap:%d %99s\", &id, extension) == 2) {\n\t\t\t\t\t*next = '\\n';\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_AUDIO_LEVEL))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_AUDIO_LEVEL;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_VIDEO_ORIENTATION;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_PLAYOUT_DELAY))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_PLAYOUT_DELAY;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_TOFFSET))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_TOFFSET;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_ABS_SEND_TIME))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_ABS_SEND_TIME;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_MID))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_MID;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_RID))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_RID;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_REPAIRED_RID))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_REPAIRED_RID;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_DEPENDENCY_DESC))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_DEPENDENCY_DESC;\n\t\t\t\t\tif(strstr(extension, JANUS_RTP_EXTMAP_VIDEO_LAYERS))\n\t\t\t\t\t\treturn JANUS_RTP_EXTMAP_VIDEO_LAYERS;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported extension '%s'\\n\", extension);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t\t*next = '\\n';\n\t\t}\n\t\tline = next ? (next+1) : NULL;\n\t}\n\treturn NULL;\n}\n\n/* Static helper to quickly find the extension data */\nstatic int janus_rtp_header_extension_find(char *buf, int len, int id,\n\t\tuint8_t *byte, uint32_t *word, char **ref, uint8_t *idlen) {\n\tif(idlen == NULL)\n\t\treturn -1;\n\t*idlen = 0;\n\tif(!buf || len < 12)\n\t\treturn -2;\n\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\tif(rtp->version != 2) {\n\t\treturn -3;\n\t}\n\tint hlen = 12;\n\tif(rtp->csrccount)\t/* Skip CSRC if needed */\n\t\thlen += rtp->csrccount*4;\n\tif(rtp->extension && (len > hlen + (int)sizeof(janus_rtp_header_extension))) {\n\t\tjanus_rtp_header_extension *ext = (janus_rtp_header_extension *)(buf+hlen);\n\t\tint extlen = ntohs(ext->length)*4;\n\t\thlen += 4;\n\t\tif(len > (hlen + extlen)) {\n\t\t\tif(ntohs(ext->type) == 0xBEDE) {\n\t\t\t\t/* 1-Byte extension */\n\t\t\t\tconst uint8_t padding = 0x00, reserved = 0xF;\n\t\t\t\tuint8_t extid = 0;\n\t\t\t\tint i = 0;\n\t\t\t\twhile(i < extlen) {\n\t\t\t\t\textid = (uint8_t)buf[hlen+i] >> 4;\n\t\t\t\t\tif(extid == reserved) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(extid == padding) {\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t*idlen = ((uint8_t)buf[hlen+i] & 0xF)+1;\n\t\t\t\t\ti++;\n\t\t\t\t\tif(extid == id && ((i+*idlen) <= extlen)) {\n\t\t\t\t\t\t/* Found! */\n\t\t\t\t\t\tif(byte)\n\t\t\t\t\t\t\t*byte = (uint8_t)buf[hlen+i];\n\t\t\t\t\t\tif(word && *idlen >= 4 && (i+4) < extlen) {\n\t\t\t\t\t\t\tmemcpy(word, buf+hlen+i, sizeof(uint32_t));\n\t\t\t\t\t\t\t*word = ntohl(*word);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ref)\n\t\t\t\t\t\t\t*ref = &buf[hlen+i];\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\ti += *idlen;\n\t\t\t\t}\n\t\t\t} else if(ntohs(ext->type) == 0x1000) {\n\t\t\t\t/* 2-Byte extension */\n\t\t\t\tconst uint8_t padding = 0x00;\n\t\t\t\tuint8_t extid = 0;\n\t\t\t\tint i = 0;\n\t\t\t\twhile(i < extlen) {\n\t\t\t\t\tif((extlen-i) < 2)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\textid = buf[hlen+i];\n\t\t\t\t\tif(extid == padding) {\n\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\t*idlen = buf[hlen+i];\n\t\t\t\t\ti++;\n\t\t\t\t\tif(extid == id && ((i+*idlen) <= extlen)) {\n\t\t\t\t\t\t/* Found! */\n\t\t\t\t\t\tif(byte)\n\t\t\t\t\t\t\t*byte = (uint8_t)buf[hlen+i];\n\t\t\t\t\t\tif(word && *idlen >= 4 && (i+4) < extlen) {\n\t\t\t\t\t\t\tmemcpy(word, buf+hlen+i, sizeof(uint32_t));\n\t\t\t\t\t\t\t*word = ntohl(*word);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(ref)\n\t\t\t\t\t\t\t*ref = &buf[hlen+i];\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\ti += *idlen;\n\t\t\t\t}\n\t\t\t}\n\t\t\thlen += extlen;\n\t\t}\n\t}\n\treturn -1;\n}\n\nint janus_rtp_header_extension_parse_audio_level(char *buf, int len, int id, gboolean *vad, int *level) {\n\tuint8_t byte = 0, idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, &byte, NULL, NULL, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level */\n\tgboolean v = (byte & 0x80) >> 7;\n\tint value = byte & 0x7F;\n\tJANUS_LOG(LOG_DBG, \"%02x --> v=%d, level=%d\\n\", byte, v, value);\n\tif(vad)\n\t\t*vad = v;\n\tif(level)\n\t\t*level = value;\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_video_orientation(char *buf, int len, int id,\n\t\tgboolean *c, gboolean *f, gboolean *r1, gboolean *r0) {\n\tuint8_t byte = 0, idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, &byte, NULL, NULL, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:4 urn:3gpp:video-orientation */\n\tgboolean cbit = (byte & 0x08) >> 3;\n\tgboolean fbit = (byte & 0x04) >> 2;\n\tgboolean r1bit = (byte & 0x02) >> 1;\n\tgboolean r0bit = byte & 0x01;\n\tJANUS_LOG(LOG_DBG, \"%02x --> c=%d, f=%d, r1=%d, r0=%d\\n\", byte, cbit, fbit, r1bit, r0bit);\n\tif(c)\n\t\t*c = cbit;\n\tif(f)\n\t\t*f = fbit;\n\tif(r1)\n\t\t*r1 = r1bit;\n\tif(r0)\n\t\t*r0 = r0bit;\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_playout_delay(char *buf, int len, int id,\n\t\tuint16_t *min_delay, uint16_t *max_delay) {\n\tuint32_t bytes = 0;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, &bytes, NULL, &idlen) < 0)\n\t\treturn -1;\n\tif(idlen < 3)\n\t\treturn -2;\n\t/* a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay */\n\tuint16_t min = (bytes & 0x00FFF000) >> 12;\n\tuint16_t max = bytes & 0x00000FFF;\n\tJANUS_LOG(LOG_DBG, \"%\"SCNu32\"x --> min=%\"SCNu16\", max=%\"SCNu16\"\\n\", bytes, min, max);\n\tif(min_delay)\n\t\t*min_delay = min;\n\tif(max_delay)\n\t\t*max_delay = max;\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_mid(char *buf, int len, int id,\n\t\tchar *sdes_item, int sdes_len) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid */\n\tif(ext == NULL || idlen < 1)\n\t\treturn -2;\n\tif(idlen > (sdes_len-1)) {\n\t\tJANUS_LOG(LOG_WARN, \"Buffer is too small (%d > %d), MID will be cut\\n\", idlen, sdes_len);\n\t\tidlen = sdes_len-1;\n\t}\n\tif(idlen > len-(ext-buf)-1) {\n\t\treturn -3;\n\t}\n\tmemcpy(sdes_item, ext, idlen);\n\t*(sdes_item+idlen) = '\\0';\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_rid(char *buf, int len, int id,\n\t\tchar *sdes_item, int sdes_len) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id */\n\t/* a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id */\n\tif(ext == NULL || idlen < 1)\n\t\treturn -2;\n\tif(idlen > (sdes_len-1)) {\n\t\tJANUS_LOG(LOG_WARN, \"Buffer is too small (%d > %d), RTP stream ID will be cut\\n\", idlen, sdes_len);\n\t\tidlen = sdes_len-1;\n\t}\n\tif(idlen > len-(ext-buf)-1) {\n\t\treturn -3;\n\t}\n\tmemcpy(sdes_item, ext, idlen);\n\t*(sdes_item+idlen) = '\\0';\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_dependency_desc(char *buf, int len, int id,\n\t\tuint8_t *dd_item, int *dd_len) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tint buflen = *dd_len;\n\t*dd_len = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:10 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension */\n\tif(ext == NULL || idlen < 1)\n\t\treturn -2;\n\tif(idlen > buflen) {\n\t\tJANUS_LOG(LOG_WARN, \"Buffer is too small (%d > %d), dependency descriptor will be cut\\n\", idlen, buflen);\n\t\tidlen = buflen;\n\t}\n\tif(idlen > len-(ext-buf)-1) {\n\t\treturn -3;\n\t}\n\tmemcpy(dd_item, ext, idlen);\n\t*dd_len = idlen;\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_abs_send_time(char *buf, int len, int id, uint32_t *abs_ts) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:4 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time */\n\tif(ext == NULL)\n\t\treturn -2;\n\tif(idlen < 3 || idlen > len-(ext-buf)-1)\n\t\treturn -3;\n\tuint32_t abs24 = 0;\n\tmemcpy(&abs24, ext, 3);\n\tif(abs_ts)\n\t\t*abs_ts = ntohl(abs24 << 8);\n\treturn 0;\n}\n\nint janus_rtp_header_extension_set_abs_send_time(char *buf, int len, int id, uint32_t abs_ts) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\tif(ext == NULL)\n\t\treturn -2;\n\tif(idlen < 3 || idlen > len-(ext-buf)-1)\n\t\treturn -3;\n\tuint32_t abs24 = htonl(abs_ts) >> 8;\n\tmemcpy(ext, &abs24, 3);\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_abs_capture_time(char *buf, int len, int id, uint64_t *abs_ts) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time */\n\tif(ext == NULL)\n\t\treturn -2;\n\tif(idlen < 8 || idlen > len-(ext-buf)-1)\n\t\treturn -3;\n\tuint64_t abs64 = 0;\n\tmemcpy(&abs64, ext, 8);\n\tif(abs_ts)\n\t\t*abs_ts = ntohll(abs64);\n\treturn 0;\n}\n\nint janus_rtp_header_extension_set_abs_capture_time(char *buf, int len, int id, uint64_t abs_ts) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\tif(ext == NULL)\n\t\treturn -2;\n\tif(idlen < 8 || idlen > len-(ext-buf)-1)\n\t\treturn -3;\n\tuint64_t abs64 = htonll(abs_ts);\n\tmemcpy(ext, &abs64, 8);\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_transport_wide_cc(char *buf, int len, int id, uint16_t *transSeqNum) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\t/*  0                   1                   2                   3\n\t    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n\t   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t   |  ID   | L=1   |transport-wide sequence number | zero padding  |\n\t   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\t*/\n\tif(ext == NULL)\n\t\treturn -2;\n\tif(idlen < 2 || idlen > len-(ext-buf)-1)\n\t\treturn -3;\n\tmemcpy(transSeqNum, ext, sizeof(uint16_t));\n\t*transSeqNum = ntohs(*transSeqNum);\n\treturn 0;\n}\n\nint janus_rtp_header_extension_set_transport_wide_cc(char *buf, int len, int id, uint16_t transSeqNum) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\tif(ext == NULL)\n\t\treturn -2;\n\tif(idlen < 2 || idlen > len-(ext-buf)-1)\n\t\treturn -3;\n\ttransSeqNum = htons(transSeqNum);\n\tmemcpy(ext, &transSeqNum, sizeof(uint16_t));\n\treturn 0;\n}\n\nint janus_rtp_header_extension_parse_video_layers_allocation(char *buf, int len, int id,\n\t\tint8_t *spatial_layers, int8_t *temporal_layers) {\n\tchar *ext = NULL;\n\tuint8_t idlen = 0;\n\tif(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext, &idlen) < 0)\n\t\treturn -1;\n\t/* a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00 */\n\tif(ext == NULL || idlen < 2)\n\t\treturn -2;\n\t/* Parse the extension to reconstruct the layers topology */\n\tjanus_rtp_vla_rtp_stream streams[4] = { 0 };\n\t/* First byte */\n\tuint8_t offset = 0;\n\tuint8_t ns = (ext[offset] & 0x30) >> 4;\n\tuint8_t sl_bm = ext[offset] & 0x0F;\n\toffset++;\n\t/* Spatial layer bitmasks (up to two bytes) */\n\tuint8_t i = 0;\n\tfor(i=0; i<=ns; i++) {\n\t\tif(sl_bm > 0) {\n\t\t\t/* Copy the shared value */\n\t\t\tstreams[i].sl_bm = sl_bm;\n\t\t} else {\n\t\t\tif(i == 2) {\n\t\t\t\toffset++;\n\t\t\t\tif(offset == idlen)\n\t\t\t\t\treturn -3;\n\t\t\t}\n\t\t\tif(i % 2 == 0) {\n\t\t\t\tstreams[i].sl_bm = (ext[offset] & 0xF0) >> 4;\n\t\t\t} else {\n\t\t\t\tstreams[i].sl_bm = ext[offset] & 0x0F;\n\t\t\t}\n\t\t}\n\t}\n\tif(sl_bm == 0)\n\t\toffset++;\n\tif(offset == idlen)\n\t\treturn -3;\n\t/* Temporal layers (one byte) */\n\tuint8_t j = 0, boff = 8, sl = 0, tl = 0;\n\tfor(i=0; i<=ns; i++) {\n\t\tsl = 0;\n\t\tfor(j=0; j<4; j++) {\n\t\t\tif((streams[i].sl_bm & (1 << j)) == 0)\n\t\t\t\tcontinue;\n\t\t\tsl++;\n\t\t\tboff -= 2;\n\t\t\tstreams[i].sl[j].id = j;\n\t\t\tstreams[i].sl[j].tls = (ext[offset] >> boff) & 0x03;\n\t\t\ttl = streams[i].sl[j].tls + 1;\n\t\t\tif(temporal_layers && tl > *temporal_layers)\n\t\t\t\t*temporal_layers = tl;\n\t\t\tif(boff == 0) {\n\t\t\t\tboff = 8;\n\t\t\t\toffset++;\n\t\t\t\tif(offset == idlen)\n\t\t\t\t\treturn -3;\n\t\t\t}\n\t\t}\n\t\tif(spatial_layers && sl > *spatial_layers)\n\t\t\t*spatial_layers = sl;\n\t}\n\t/* Done, we don't care about bitrates and resolutions for now */\n\treturn 0;\n}\n\nint janus_rtp_header_extension_replace_id(char *buf, int len, int id, int new_id) {\n\tif(!buf || len < 12)\n\t\treturn -1;\n\tjanus_rtp_header *rtp = (janus_rtp_header *)buf;\n\tif (rtp->version != 2) {\n\t\treturn -2;\n\t}\n\tint hlen = 12;\n\tif(rtp->csrccount)\t/* Skip CSRC if needed */\n\t\thlen += rtp->csrccount*4;\n\tif(rtp->extension) {\n\t\tjanus_rtp_header_extension *ext = (janus_rtp_header_extension *)(buf+hlen);\n\t\tint extlen = ntohs(ext->length)*4;\n\t\thlen += 4;\n\t\tif(len > (hlen + extlen)) {\n\t\t\tif(ntohs(ext->type) == 0xBEDE) {\n\t\t\t\t/* 1-Byte extension */\n\t\t\t\tconst uint8_t padding = 0x00, reserved = 0xF;\n\t\t\t\tuint8_t extid = 0, idlen = 0;\n\t\t\t\tint i = 0;\n\t\t\t\twhile(i < extlen) {\n\t\t\t\t\textid = buf[hlen+i] >> 4;\n\t\t\t\t\tif(extid == reserved) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if(extid == padding) {\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tidlen = (buf[hlen+i] & 0xF)+1;\n\t\t\t\t\tif(extid == id) {\n\t\t\t\t\t\t/* Found! */\n\t\t\t\t\t\tbuf[hlen+i] = (new_id << 4) + (idlen - 1);\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\ti += 1 + idlen;\n\t\t\t\t}\n\t\t\t} if(ntohs(ext->type) == 0x1000) {\n\t\t\t\t/* 2-Byte extension */\n\t\t\t\tconst uint8_t padding = 0x00;\n\t\t\t\tuint8_t extid = 0, idlen = 0;\n\t\t\t\tint i = 0;\n\t\t\t\twhile(i < extlen) {\n\t\t\t\t\tif((extlen-i) < 2)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\textid = buf[hlen+i];\n\t\t\t\t\tif(extid == padding) {\n\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(extid == id) {\n\t\t\t\t\t\t/* Found! */\n\t\t\t\t\t\tbuf[hlen+i] = new_id;\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tidlen = buf[hlen+i];\n\t\t\t\t\ti += 1 + idlen;\n\t\t\t\t}\n\t\t\t}\n\t\t\thlen += extlen;\n\t\t}\n\t}\n\treturn -3;\n}\n\nint janus_rtp_extension_id(const char *type) {\n\tif(type == NULL)\n\t\treturn 0;\n\tif(!strcasecmp(type, JANUS_RTP_EXTMAP_AUDIO_LEVEL))\n\t\treturn 1;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_TOFFSET))\n\t\treturn 14;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_ABS_SEND_TIME))\n\t\treturn 2;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME))\n\t\treturn 7;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_VIDEO_ORIENTATION))\n\t\treturn 13;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC))\n\t\treturn 3;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_PLAYOUT_DELAY))\n\t\treturn 12;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_MID))\n\t\treturn 4;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_RID))\n\t\treturn 5;\n\telse if(!strcasecmp(type, JANUS_RTP_EXTMAP_REPAIRED_RID))\n\t\treturn 6;\n\treturn 0;\n}\n\n/* RTP context related methods */\nvoid janus_rtp_switching_context_reset(janus_rtp_switching_context *context) {\n\tif(context == NULL)\n\t\treturn;\n\t/* Reset the context values */\n\tmemset(context, 0, sizeof(*context));\n}\n\nint janus_rtp_skew_compensate_audio(janus_rtp_header *header, janus_rtp_switching_context *context, gint64 now) {\n\t/* Reset values if a new ssrc has been detected */\n\tif(context->new_ssrc) {\n\t\tJANUS_LOG(LOG_VERB, \"audio skew SSRC=%\"SCNu32\" resetting status\\n\", context->last_ssrc);\n\t\tcontext->reference_time = now;\n\t\tcontext->start_time = 0;\n\t\tcontext->evaluating_start_time = 0;\n\t\tcontext->start_ts = 0;\n\t\tcontext->active_delay = 0;\n\t\tcontext->prev_delay = 0;\n\t\tcontext->seq_offset = 0;\n\t\tcontext->ts_offset = 0;\n\t\tcontext->target_ts = 0;\n\t\tcontext->new_ssrc = FALSE;\n\t}\n\n\t/* N \t: a N sequence number jump has been performed */\n\t/* 0  \t: any new skew compensation has been applied */\n\t/* -N  \t: a N packet drop must be performed */\n\tint exit_status = 0;\n\n\t/* Do not execute skew analysis in the first seconds */\n\tif(now-context->reference_time < SKEW_DETECTION_WAIT_TIME_SECS/2 * G_USEC_PER_SEC) {\n\t\treturn 0;\n\t} else if(!context->start_time) {\n\t\tJANUS_LOG(LOG_VERB, \"audio skew SSRC=%\"SCNu32\" evaluation phase start\\n\", context->last_ssrc);\n\t\tcontext->start_time = now;\n\t\tcontext->evaluating_start_time = now;\n\t\tcontext->start_ts = context->last_ts;\n\t}\n\n\t/* Skew analysis */\n\t/* Are we waiting for a target timestamp? (a negative skew has been evaluated in a previous iteration) */\n\tif(context->target_ts > 0 && (gint32)(context->target_ts - context->last_ts) > 0) {\n\t\tcontext->seq_offset--;\n\t\texit_status = -1;\n\t} else {\n\t\tcontext->target_ts = 0;\n\t\t/* Do not execute analysis for out of order packets or multi-packets frame */\n\t\tif(context->last_seq == context->prev_seq + 1 && context->last_ts != context->prev_ts) {\n\t\t\t/* Set the sample rate according to the header */\n\t\t\tguint32 akhz = 48; /* 48khz for Opus */\n\t\t\tif(header->type == 0 || header->type == 8 || header->type == 9)\n\t\t\t\takhz = 8;\n\t\t\t/* Evaluate the local RTP timestamp according to the local clock */\n\t\t\tguint32 expected_ts = ((now - context->start_time)*akhz)/1000 + context->start_ts;\n\t\t\t/* Evaluate current delay */\n\t\t\tgint32 delay_now = context->last_ts - expected_ts;\n\t\t\t/* Exponentially weighted moving average estimation */\n\t\t\tgint32 delay_estimate = (63*context->prev_delay + delay_now)/64;\n\t\t\t/* Save previous delay for the next iteration*/\n\t\t\tcontext->prev_delay = delay_estimate;\n\t\t\t/* Evaluate the distance between active delay and current delay estimate */\n\t\t\tgint32 offset = context->active_delay - delay_estimate;\n\t\t\tJANUS_LOG(LOG_HUGE, \"audio skew status SSRC=%\"SCNu32\" RECVD_TS=%\"SCNu32\" EXPTD_TS=%\"SCNu32\" OFFSET=%\"SCNi32\" TS_OFFSET=%\"SCNi32\" SEQ_OFFSET=%\"SCNi16\"\\n\", context->last_ssrc, context->last_ts, expected_ts, offset, context->ts_offset, context->seq_offset);\n\t\t\tgint32 skew_th = RTP_AUDIO_SKEW_TH_MS*akhz;\n\t\t\t/* Evaluation phase */\n\t\t\tif(context->evaluating_start_time > 0) {\n\t\t\t\t/* Check if the offset has surpassed half the threshold during the evaluating phase */\n\t\t\t\tif(now-context->evaluating_start_time <= SKEW_DETECTION_WAIT_TIME_SECS/2 * G_USEC_PER_SEC) {\n\t\t\t\t\tif(abs(offset) <= skew_th/2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"audio skew SSRC=%\"SCNu32\" evaluation phase continue\\n\", context->last_ssrc);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"audio skew SSRC=%\"SCNu32\" evaluation phase reset\\n\", context->last_ssrc);\n\t\t\t\t\t\tcontext->start_time = now;\n\t\t\t\t\t\tcontext->evaluating_start_time = now;\n\t\t\t\t\t\tcontext->start_ts = context->last_ts;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"audio skew SSRC=%\"SCNu32\" evaluation phase stop\\n\", context->last_ssrc);\n\t\t\t\t\tcontext->evaluating_start_time = 0;\n\t\t\t\t}\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\t/* Check if the offset has surpassed the threshold */\n\t\t\tif(offset >= skew_th) {\n\t\t\t\t/* The source is slowing down */\n\t\t\t\t/* Update active delay */\n\t\t\t\tcontext->active_delay = delay_estimate;\n\t\t\t\t/* Adjust ts offset */\n\t\t\t\tcontext->ts_offset += skew_th;\n\t\t\t\t/* Calculate last ts increase */\n\t\t\t\tguint32 ts_incr = context->last_ts-context->prev_ts;\n\t\t\t\t/* Evaluate sequence number jump */\n\t\t\t\tguint16 jump = (skew_th+ts_incr-1)/ts_incr;\n\t\t\t\t/* Adjust seq num offset */\n\t\t\t\tcontext->seq_offset += jump;\n\t\t\t\texit_status = jump;\n\t\t\t} else if(offset <= -skew_th) {\n\t\t\t\t/* The source is speeding up*/\n\t\t\t\t/* Update active delay */\n\t\t\t\tcontext->active_delay = delay_estimate;\n\t\t\t\t/* Adjust ts offset */\n\t\t\t\tcontext->ts_offset -= skew_th;\n\t\t\t\t/* Set target ts */\n\t\t\t\tcontext->target_ts = context->last_ts + skew_th;\n\t\t\t\tif (context->target_ts == 0)\n\t\t\t\t\tcontext->target_ts = 1;\n\t\t\t\t/* Adjust seq num offset */\n\t\t\t\tcontext->seq_offset--;\n\t\t\t\texit_status = -1;\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Skew compensation */\n\t/* Fix header timestamp considering the active offset */\n\tguint32 fixed_rtp_ts = context->last_ts + context->ts_offset;\n\theader->timestamp = htonl(fixed_rtp_ts);\n\t/* Fix header sequence number considering the total offset */\n\tguint16 fixed_rtp_seq = context->last_seq + context->seq_offset;\n\theader->seq_number = htons(fixed_rtp_seq);\n\n\treturn exit_status;\n}\n\nint janus_rtp_skew_compensate_video(janus_rtp_header *header, janus_rtp_switching_context *context, gint64 now) {\n\t/* Reset values if a new ssrc has been detected */\n\tif(context->new_ssrc) {\n\t\tJANUS_LOG(LOG_VERB, \"video skew SSRC=%\"SCNu32\" resetting status\\n\", context->last_ssrc);\n\t\tcontext->reference_time = now;\n\t\tcontext->start_time = 0;\n\t\tcontext->evaluating_start_time = 0;\n\t\tcontext->start_ts = 0;\n\t\tcontext->active_delay = 0;\n\t\tcontext->prev_delay = 0;\n\t\tcontext->seq_offset = 0;\n\t\tcontext->ts_offset = 0;\n\t\tcontext->target_ts = 0;\n\t\tcontext->new_ssrc = FALSE;\n\t}\n\n\t/* N \t: a N sequence numbers jump has been performed */\n\t/* 0  \t: any new skew compensation has been applied */\n\t/* -N  \t: a N packets drop must be performed */\n\tint exit_status = 0;\n\n\t/* Do not execute skew analysis in the first seconds */\n\tif(now-context->reference_time < SKEW_DETECTION_WAIT_TIME_SECS/2 *G_USEC_PER_SEC) {\n\t\treturn 0;\n\t} else if(!context->start_time) {\n\t\tJANUS_LOG(LOG_VERB, \"video skew SSRC=%\"SCNu32\" evaluation phase start\\n\", context->last_ssrc);\n\t\tcontext->start_time = now;\n\t\tcontext->evaluating_start_time = now;\n\t\tcontext->start_ts = context->last_ts;\n\t}\n\n\t/* Skew analysis */\n\t/* Are we waiting for a target timestamp? (a negative skew has been evaluated in a previous iteration) */\n\tif(context->target_ts > 0 && (gint32)(context->target_ts - context->last_ts) > 0) {\n\t\tcontext->seq_offset--;\n\t\texit_status = -1;\n\t} else {\n\t\tcontext->target_ts = 0;\n\t\t/* Do not execute analysis for out of order packets or multi-packets frame */\n\t\tif(context->last_seq == context->prev_seq + 1 && context->last_ts != context->prev_ts) {\n\t\t\t/* Set the sample rate */\n\t\t\tguint32 vkhz = 90; /* 90khz */\n\t\t\t/* Evaluate the local RTP timestamp according to the local clock */\n\t\t\tguint32 expected_ts = ((now - context->start_time)*vkhz)/1000 + context->start_ts;\n\t\t\t/* Evaluate current delay */\n\t\t\tgint32 delay_now = context->last_ts - expected_ts;\n\t\t\t/* Exponentially weighted moving average estimation */\n\t\t\tgint32 delay_estimate = (63*context->prev_delay + delay_now)/64;\n\t\t\t/* Save previous delay for the next iteration*/\n\t\t\tcontext->prev_delay = delay_estimate;\n\t\t\t/* Evaluate the distance between active delay and current delay estimate */\n\t\t\tgint32 offset = context->active_delay - delay_estimate;\n\t\t\tJANUS_LOG(LOG_HUGE, \"video skew status SSRC=%\"SCNu32\" RECVD_TS=%\"SCNu32\" EXPTD_TS=%\"SCNu32\" OFFSET=%\"SCNi32\" TS_OFFSET=%\"SCNi32\" SEQ_OFFSET=%\"SCNi16\"\\n\", context->last_ssrc, context->last_ts, expected_ts, offset, context->ts_offset, context->seq_offset);\n\t\t\tgint32 skew_th = RTP_VIDEO_SKEW_TH_MS*vkhz;\n\t\t\t/* Evaluation phase */\n\t\t\tif(context->evaluating_start_time > 0) {\n\t\t\t\t/* Check if the offset has surpassed half the threshold during the evaluating phase */\n\t\t\t\tif(now-context->evaluating_start_time <= SKEW_DETECTION_WAIT_TIME_SECS/2 * G_USEC_PER_SEC) {\n\t\t\t\t\tif(abs(offset) <= skew_th/2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"video skew SSRC=%\"SCNu32\" evaluation phase continue\\n\", context->last_ssrc);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"video skew SSRC=%\"SCNu32\" evaluation phase reset\\n\", context->last_ssrc);\n\t\t\t\t\t\tcontext->start_time = now;\n\t\t\t\t\t\tcontext->evaluating_start_time = now;\n\t\t\t\t\t\tcontext->start_ts = context->last_ts;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"video skew SSRC=%\"SCNu32\" evaluation phase stop\\n\", context->last_ssrc);\n\t\t\t\t\tcontext->evaluating_start_time = 0;\n\t\t\t\t}\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\t/* Check if the offset has surpassed the threshold */\n\t\t\tif(offset >= skew_th) {\n\t\t\t\t/* The source is slowing down */\n\t\t\t\t/* Update active delay */\n\t\t\t\tcontext->active_delay = delay_estimate;\n\t\t\t\t/* Adjust ts offset */\n\t\t\t\tcontext->ts_offset += skew_th;\n\t\t\t\t/* Calculate last ts increase */\n\t\t\t\tguint32 ts_incr = context->last_ts-context->prev_ts;\n\t\t\t\t/* Evaluate sequence number jump */\n\t\t\t\tguint16 jump = (skew_th+ts_incr-1)/ts_incr;\n\t\t\t\t/* Adjust seq num offset */\n\t\t\t\tcontext->seq_offset += jump;\n\t\t\t\texit_status = jump;\n\t\t\t} else if(offset <= -skew_th) {\n\t\t\t\t/* The source is speeding up*/\n\t\t\t\t/* Update active delay */\n\t\t\t\tcontext->active_delay = delay_estimate;\n\t\t\t\t/* Adjust ts offset */\n\t\t\t\tcontext->ts_offset -= skew_th;\n\t\t\t\t/* Set target ts */\n\t\t\t\tcontext->target_ts = context->last_ts + skew_th;\n\t\t\t\tif(context->target_ts == 0)\n\t\t\t\t\tcontext->target_ts = 1;\n\t\t\t\t/* Adjust seq num offset */\n\t\t\t\tcontext->seq_offset--;\n\t\t\t\texit_status = -1;\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Skew compensation */\n\t/* Fix header timestamp considering the active offset */\n\tguint32 fixed_rtp_ts = context->last_ts + context->ts_offset;\n\theader->timestamp = htonl(fixed_rtp_ts);\n\t/* Fix header sequence number considering the total offset */\n\tguint16 fixed_rtp_seq = context->last_seq + context->seq_offset;\n\theader->seq_number = htons(fixed_rtp_seq);\n\n\treturn exit_status;\n}\n\nvoid janus_rtp_header_update(janus_rtp_header *header, janus_rtp_switching_context *context, gboolean video, int step) {\n\tif(header == NULL || context == NULL)\n\t\treturn;\n\t/* Note: while the step property is still there for compatibility reasons, to\n\t * keep the signature as it was before, it's ignored: whenever there's a switch\n\t * to take into account, we compute how much time passed between the last RTP\n\t * packet with the old SSRC and this new one, and prepare a timestamp accordingly */\n\tuint32_t ssrc = ntohl(header->ssrc);\n\tuint32_t timestamp = ntohl(header->timestamp);\n\tuint16_t seq = ntohs(header->seq_number);\n\tif(ssrc != context->last_ssrc) {\n\t\t/* Audio SSRC changed: update both sequence number and timestamp */\n\t\tJANUS_LOG(LOG_VERB, \"SSRC changed, %\"SCNu32\" --> %\"SCNu32\"\\n\",\n\t\t\tcontext->last_ssrc, ssrc);\n\t\tcontext->last_ssrc = ssrc;\n\t\tcontext->ts_reset = TRUE;\n\t\tcontext->seq_reset = TRUE;\n\t\t/* Reset skew compensation data */\n\t\tcontext->new_ssrc = TRUE;\n\t}\n\tif(context->ts_reset) {\n\t\t/* RTP timestamp was paused for a while */\n\t\tJANUS_LOG(LOG_HUGE, \"RTP timestamp reset requested\\n\");\n\t\tcontext->ts_reset = FALSE;\n\t\tcontext->base_ts_prev = context->last_ts;\n\t\tcontext->base_ts = timestamp;\n\t\t/* How much time since the last audio RTP packet? We compute an offset accordingly */\n\t\tif(context->last_time > 0) {\n\t\t\tgint64 time_diff = janus_get_monotonic_time() - context->last_time;\n\t\t\t/* We're assuming 90khz for video and 48khz for audio, here */\n\t\t\tint khz = video ? 90 : 48;\n\t\t\tif(!video && (header->type == 0 || header->type == 8 || header->type == 9))\n\t\t\t\tkhz = 8;\t/* We're assuming 48khz here (Opus), unless it's G.711/G.722 (8khz) */\n\t\t\ttime_diff = (time_diff*khz)/1000;\n\t\t\tif(time_diff == 0)\n\t\t\t\ttime_diff = 1;\n\t\t\tcontext->base_ts_prev += (guint32)time_diff;\n\t\t\tcontext->prev_ts += (guint32)time_diff;\n\t\t\tcontext->last_ts += (guint32)time_diff;\n\t\t\tJANUS_LOG(LOG_VERB, \"Computed offset for RTP timestamp: %\"SCNu32\"\\n\", (guint32)time_diff);\n\t\t}\n\t}\n\tif(context->seq_reset) {\n\t\t/* Audio sequence number was paused for a while: just update that */\n\t\tcontext->seq_reset = FALSE;\n\t\tcontext->base_seq_prev = context->last_seq;\n\t\tcontext->base_seq = seq;\n\t}\n\t/* Compute a coherent timestamp and sequence number */\n\tcontext->prev_ts = context->last_ts;\n\tcontext->last_ts = (timestamp-context->base_ts) + context->base_ts_prev;\n\tcontext->prev_seq = context->last_seq;\n\tcontext->last_seq = (seq-context->base_seq)+context->base_seq_prev+1;\n\t/* Update the timestamp and sequence number in the RTP packet */\n\theader->timestamp = htonl(context->last_ts);\n\theader->seq_number = htons(context->last_seq);\n\t/* Take note of when we last handled this RTP packet */\n\tcontext->last_time = janus_get_monotonic_time();\n}\n\n\n/* SRTP stuff: we may need our own randomizer */\n#ifdef HAVE_SRTP_2\nint srtp_crypto_get_random(uint8_t *key, int len) {\n#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION\n\t/* libsrtp 2.0 doesn't have crypto_get_random, we use OpenSSL's RAND_* to replace it:\n\t * \t\thttps://wiki.openssl.org/index.php/Random_Numbers */\n\tint rc = RAND_bytes(key, len);\n\tif(rc != 1) {\n\t\t/* Error generating */\n\t\treturn -1;\n\t}\n#endif\n\treturn 0;\n}\n#endif\n/* SRTP error codes as a string array */\nstatic const char *janus_srtp_error[] =\n{\n#ifdef HAVE_SRTP_2\n\t\"srtp_err_status_ok\",\n\t\"srtp_err_status_fail\",\n\t\"srtp_err_status_bad_param\",\n\t\"srtp_err_status_alloc_fail\",\n\t\"srtp_err_status_dealloc_fail\",\n\t\"srtp_err_status_init_fail\",\n\t\"srtp_err_status_terminus\",\n\t\"srtp_err_status_auth_fail\",\n\t\"srtp_err_status_cipher_fail\",\n\t\"srtp_err_status_replay_fail\",\n\t\"srtp_err_status_replay_old\",\n\t\"srtp_err_status_algo_fail\",\n\t\"srtp_err_status_no_such_op\",\n\t\"srtp_err_status_no_ctx\",\n\t\"srtp_err_status_cant_check\",\n\t\"srtp_err_status_key_expired\",\n\t\"srtp_err_status_socket_err\",\n\t\"srtp_err_status_signal_err\",\n\t\"srtp_err_status_nonce_bad\",\n\t\"srtp_err_status_read_fail\",\n\t\"srtp_err_status_write_fail\",\n\t\"srtp_err_status_parse_err\",\n\t\"srtp_err_status_encode_err\",\n\t\"srtp_err_status_semaphore_err\",\n\t\"srtp_err_status_pfkey_err\",\n#else\n\t\"err_status_ok\",\n\t\"err_status_fail\",\n\t\"err_status_bad_param\",\n\t\"err_status_alloc_fail\",\n\t\"err_status_dealloc_fail\",\n\t\"err_status_init_fail\",\n\t\"err_status_terminus\",\n\t\"err_status_auth_fail\",\n\t\"err_status_cipher_fail\",\n\t\"err_status_replay_fail\",\n\t\"err_status_replay_old\",\n\t\"err_status_algo_fail\",\n\t\"err_status_no_such_op\",\n\t\"err_status_no_ctx\",\n\t\"err_status_cant_check\",\n\t\"err_status_key_expired\",\n\t\"err_status_socket_err\",\n\t\"err_status_signal_err\",\n\t\"err_status_nonce_bad\",\n\t\"err_status_read_fail\",\n\t\"err_status_write_fail\",\n\t\"err_status_parse_err\",\n\t\"err_status_encode_err\",\n\t\"err_status_semaphore_err\",\n\t\"err_status_pfkey_err\",\n#endif\n};\nconst char *janus_srtp_error_str(int error) {\n\tif(error < 0 || error > 24)\n\t\treturn NULL;\n\treturn janus_srtp_error[error];\n}\n\n/* Payload types we'll offer internally */\n#define OPUS_PT\t\t111\n#define MULTIOPUS_PT\tOPUS_PT\n#define OPUSRED_PT\t120\n#define ISAC32_PT\t104\n#define ISAC16_PT\t103\n#define PCMU_PT\t\t0\n#define PCMA_PT\t\t8\n#define G722_PT\t\t9\n#define L16_48_PT\t105\n#define L16_PT\t\t106\n#define VP8_PT\t\t96\n#define VP9_PT\t\t101\n#define H264_PT\t\t107\n#define AV1_PT\t\t98\n#define H265_PT\t\t100\nconst char *janus_audiocodec_name(janus_audiocodec acodec) {\n\tswitch(acodec) {\n\t\tcase JANUS_AUDIOCODEC_NONE:\n\t\t\treturn \"none\";\n\t\tcase JANUS_AUDIOCODEC_OPUS:\n\t\t\treturn \"opus\";\n\t\tcase JANUS_AUDIOCODEC_MULTIOPUS:\n\t\t\treturn \"multiopus\";\n\t\tcase JANUS_AUDIOCODEC_OPUSRED:\n\t\t\treturn \"red\";\n\t\tcase JANUS_AUDIOCODEC_PCMU:\n\t\t\treturn \"pcmu\";\n\t\tcase JANUS_AUDIOCODEC_PCMA:\n\t\t\treturn \"pcma\";\n\t\tcase JANUS_AUDIOCODEC_G722:\n\t\t\treturn \"g722\";\n\t\tcase JANUS_AUDIOCODEC_ISAC_32K:\n\t\t\treturn \"isac32\";\n\t\tcase JANUS_AUDIOCODEC_ISAC_16K:\n\t\t\treturn \"isac16\";\n\t\tcase JANUS_AUDIOCODEC_L16_48K:\n\t\t\treturn \"l16-48\";\n\t\tcase JANUS_AUDIOCODEC_L16_16K:\n\t\t\treturn \"l16\";\n\t\tdefault:\n\t\t\t/* Shouldn't happen */\n\t\t\treturn \"opus\";\n\t}\n}\njanus_audiocodec janus_audiocodec_from_name(const char *name) {\n\tif(name == NULL)\n\t\treturn JANUS_AUDIOCODEC_NONE;\n\telse if(!strcasecmp(name, \"opus\"))\n\t\treturn JANUS_AUDIOCODEC_OPUS;\n\telse if(!strcasecmp(name, \"multiopus\"))\n\t\treturn JANUS_AUDIOCODEC_MULTIOPUS;\n\telse if(!strcasecmp(name, \"red\"))\n\t\treturn JANUS_AUDIOCODEC_OPUSRED;\n\telse if(!strcasecmp(name, \"isac32\"))\n\t\treturn JANUS_AUDIOCODEC_ISAC_32K;\n\telse if(!strcasecmp(name, \"isac16\"))\n\t\treturn JANUS_AUDIOCODEC_ISAC_16K;\n\telse if(!strcasecmp(name, \"pcmu\"))\n\t\treturn JANUS_AUDIOCODEC_PCMU;\n\telse if(!strcasecmp(name, \"pcma\"))\n\t\treturn JANUS_AUDIOCODEC_PCMA;\n\telse if(!strcasecmp(name, \"g722\"))\n\t\treturn JANUS_AUDIOCODEC_G722;\n\telse if(!strcasecmp(name, \"l16-48\"))\n\t\treturn JANUS_AUDIOCODEC_L16_48K;\n\telse if(!strcasecmp(name, \"l16\"))\n\t\treturn JANUS_AUDIOCODEC_L16_16K;\n\tJANUS_LOG(LOG_WARN, \"Unsupported audio codec '%s'\\n\", name);\n\treturn JANUS_AUDIOCODEC_NONE;\n}\nint janus_audiocodec_pt(janus_audiocodec acodec) {\n\tswitch(acodec) {\n\t\tcase JANUS_AUDIOCODEC_NONE:\n\t\t\treturn -1;\n\t\tcase JANUS_AUDIOCODEC_OPUS:\n\t\t\treturn OPUS_PT;\n\t\tcase JANUS_AUDIOCODEC_MULTIOPUS:\n\t\t\treturn MULTIOPUS_PT;\n\t\tcase JANUS_AUDIOCODEC_OPUSRED:\n\t\t\treturn OPUSRED_PT;\n\t\tcase JANUS_AUDIOCODEC_ISAC_32K:\n\t\t\treturn ISAC32_PT;\n\t\tcase JANUS_AUDIOCODEC_ISAC_16K:\n\t\t\treturn ISAC16_PT;\n\t\tcase JANUS_AUDIOCODEC_PCMU:\n\t\t\treturn PCMU_PT;\n\t\tcase JANUS_AUDIOCODEC_PCMA:\n\t\t\treturn PCMA_PT;\n\t\tcase JANUS_AUDIOCODEC_G722:\n\t\t\treturn G722_PT;\n\t\tcase JANUS_AUDIOCODEC_L16_48K:\n\t\t\treturn L16_48_PT;\n\t\tcase JANUS_AUDIOCODEC_L16_16K:\n\t\t\treturn L16_PT;\n\t\tdefault:\n\t\t\t/* Shouldn't happen */\n\t\t\treturn OPUS_PT;\n\t}\n}\n\nconst char *janus_videocodec_name(janus_videocodec vcodec) {\n\tswitch(vcodec) {\n\t\tcase JANUS_VIDEOCODEC_NONE:\n\t\t\treturn \"none\";\n\t\tcase JANUS_VIDEOCODEC_VP8:\n\t\t\treturn \"vp8\";\n\t\tcase JANUS_VIDEOCODEC_VP9:\n\t\t\treturn \"vp9\";\n\t\tcase JANUS_VIDEOCODEC_H264:\n\t\t\treturn \"h264\";\n\t\tcase JANUS_VIDEOCODEC_AV1:\n\t\t\treturn \"av1\";\n\t\tcase JANUS_VIDEOCODEC_H265:\n\t\t\treturn \"h265\";\n\t\tdefault:\n\t\t\t/* Shouldn't happen */\n\t\t\treturn \"vp8\";\n\t}\n}\njanus_videocodec janus_videocodec_from_name(const char *name) {\n\tif(name == NULL)\n\t\treturn JANUS_VIDEOCODEC_NONE;\n\telse if(!strcasecmp(name, \"vp8\"))\n\t\treturn JANUS_VIDEOCODEC_VP8;\n\telse if(!strcasecmp(name, \"vp9\"))\n\t\treturn JANUS_VIDEOCODEC_VP9;\n\telse if(!strcasecmp(name, \"h264\"))\n\t\treturn JANUS_VIDEOCODEC_H264;\n\telse if(!strcasecmp(name, \"av1\"))\n\t\treturn JANUS_VIDEOCODEC_AV1;\n\telse if(!strcasecmp(name, \"h265\"))\n\t\treturn JANUS_VIDEOCODEC_H265;\n\tJANUS_LOG(LOG_WARN, \"Unsupported video codec '%s'\\n\", name);\n\treturn JANUS_VIDEOCODEC_NONE;\n}\nint janus_videocodec_pt(janus_videocodec vcodec) {\n\tswitch(vcodec) {\n\t\tcase JANUS_VIDEOCODEC_NONE:\n\t\t\treturn -1;\n\t\tcase JANUS_VIDEOCODEC_VP8:\n\t\t\treturn VP8_PT;\n\t\tcase JANUS_VIDEOCODEC_VP9:\n\t\t\treturn VP9_PT;\n\t\tcase JANUS_VIDEOCODEC_H264:\n\t\t\treturn H264_PT;\n\t\tcase JANUS_VIDEOCODEC_AV1:\n\t\t\treturn AV1_PT;\n\t\tcase JANUS_VIDEOCODEC_H265:\n\t\t\treturn H265_PT;\n\t\tdefault:\n\t\t\t/* Shouldn't happen */\n\t\t\treturn VP8_PT;\n\t}\n}\n\nvoid janus_rtp_simulcasting_context_reset(janus_rtp_simulcasting_context *context) {\n\tif(context == NULL)\n\t\treturn;\n\t/* Reset the context values */\n\tjanus_av1_svc_context_reset(&context->av1_context[0]);\n\tjanus_av1_svc_context_reset(&context->av1_context[1]);\n\tjanus_av1_svc_context_reset(&context->av1_context[2]);\n\tmemset(context, 0, sizeof(*context));\n\tcontext->rid_ext_id = -1;\n\tcontext->substream = -1;\n\tcontext->substream_target_temp = -1;\n\tcontext->templayer = -1;\n}\n\nvoid janus_rtp_simulcasting_prepare(json_t *simulcast, int *rid_ext_id, uint32_t *ssrcs, char **rids) {\n\tif(simulcast == NULL)\n\t\treturn;\n\tjson_t *r = json_object_get(simulcast, \"rids\");\n\tjson_t *s = json_object_get(simulcast, \"ssrcs\");\n\tif(r && json_array_size(r) > 0) {\n\t\tJANUS_LOG(LOG_VERB, \"  -- Simulcasting is rid based\\n\");\n\t\tsize_t i = 0;\n\t\tint count = json_array_size(r);\n\t\tfor(i=count; i > 0; i--) {\n\t\t\tjson_t *rid = json_array_get(r, i-1);\n\t\t\tif(rid && json_is_string(rid) && rids)\n\t\t\t\trids[count-i] = g_strdup(json_string_value(rid));\n\t\t}\n\t\tjson_t *rid_ext = json_object_get(simulcast, \"rid-ext\");\n\t\tif(rid_ext_id != NULL)\n\t\t\t*rid_ext_id = json_integer_value(rid_ext);\n\t} else if(s && json_array_size(s) > 0) {\n\t\tJANUS_LOG(LOG_VERB, \"  -- Simulcasting is SSRC based\\n\");\n\t\tsize_t i = 0;\n\t\tfor(i=0; i<json_array_size(s); i++) {\n\t\t\tif(i == 3)\n\t\t\t\tbreak;\n\t\t\tjson_t *ssrc = json_array_get(s, i);\n\t\t\tif(ssrc && json_is_integer(ssrc) && ssrcs)\n\t\t\t\tssrcs[i] = json_integer_value(ssrc);\n\t\t}\n\t}\n}\n\nvoid janus_rtp_simulcasting_cleanup(int *rid_ext_id, uint32_t *ssrcs, char **rids, janus_mutex *rid_mutex) {\n\tif(rid_mutex != NULL)\n\t\tjanus_mutex_lock(rid_mutex);\n\tif(rid_ext_id)\n\t\t*rid_ext_id = -1;\n\tif(ssrcs || rids) {\n\t\tint i = 0;\n\t\tfor(i=0; i<3; i++) {\n\t\t\tif(ssrcs)\n\t\t\t\t*(ssrcs+i) = 0;\n\t\t\tif(rids) {\n\t\t\t\tg_free(rids[i]);\n\t\t\t\trids[i] = NULL;\n\t\t\t}\n\t\t}\n\t}\n\tif(rid_mutex != NULL)\n\t\tjanus_mutex_unlock(rid_mutex);\n}\n\ngboolean janus_rtp_simulcasting_context_process_rtp(janus_rtp_simulcasting_context *context,\n\t\tchar *buf, int len, uint8_t *dd_content, int dd_len, uint32_t *ssrcs, char **rids,\n\t\tjanus_videocodec vcodec, janus_rtp_switching_context *sc, janus_mutex *rid_mutex) {\n\tif(!context || !buf || len < 1)\n\t\treturn FALSE;\n\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\tuint32_t ssrc = ntohl(header->ssrc);\n\tint substream = -1;\n\tif(ssrc == *(ssrcs)) {\n\t\tsubstream = 0;\n\t} else if(ssrc == *(ssrcs+1)) {\n\t\tsubstream = 1;\n\t} else if(ssrc == *(ssrcs+2)) {\n\t\tsubstream = 2;\n\t} else {\n\t\t/* We don't recognize this SSRC, check if rid can help us */\n\t\tif(context->rid_ext_id < 1 || rids == NULL)\n\t\t\treturn FALSE;\n\t\tchar sdes_item[16];\n\t\tif(janus_rtp_header_extension_parse_rid(buf, len, context->rid_ext_id, sdes_item, sizeof(sdes_item)) != 0)\n\t\t\treturn FALSE;\n\t\tif(rid_mutex != NULL)\n\t\t\tjanus_mutex_lock(rid_mutex);\n\t\tif(rids[0] != NULL && !strcmp(rids[0], sdes_item)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Simulcasting: rid=%s --> ssrc=%\"SCNu32\"\\n\", sdes_item, ssrc);\n\t\t\t*(ssrcs) = ssrc;\n\t\t\tsubstream = 0;\n\t\t} else if(rids[1] != NULL && !strcmp(rids[1], sdes_item)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Simulcasting: rid=%s --> ssrc=%\"SCNu32\"\\n\", sdes_item, ssrc);\n\t\t\t*(ssrcs+1) = ssrc;\n\t\t\tsubstream = 1;\n\t\t} else if(rids[2] != NULL && !strcmp(rids[2], sdes_item)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Simulcasting: rid=%s --> ssrc=%\"SCNu32\"\\n\", sdes_item, ssrc);\n\t\t\t*(ssrcs+2) = ssrc;\n\t\t\tsubstream = 2;\n\t\t}\n\t\tif(rid_mutex != NULL)\n\t\t\tjanus_mutex_unlock(rid_mutex);\n\t\tif(substream == -1) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Simulcasting: unknown rid '%s'...\\n\", sdes_item);\n\t\t\treturn FALSE;\n\t\t}\n\t}\n\t/* Reset the flags */\n\tcontext->changed_substream = FALSE;\n\tcontext->changed_temporal = FALSE;\n\tcontext->need_pli = FALSE;\n\tgint64 now = janus_get_monotonic_time();\n\t/* Access the packet payload */\n\tint plen = 0;\n\tchar *payload = janus_rtp_payload(buf, len, &plen);\n\tif(payload == NULL)\n\t\treturn FALSE;\n\t/* Check what's our target */\n\tif(context->substream_target_temp != -1 && (substream > context->substream_target_temp ||\n\t\t\tcontext->substream_target <= context->substream_target_temp)) {\n\t\t/* We either just received media on a substream that is higher than\n\t\t * the target we dropped to (which means the one we want is now flowing\n\t\t * again) or we've been requested a lower substream target instead */\n\t\tcontext->substream_target_temp = -1;\n\t}\n\tint target = (context->substream_target_temp == -1) ? context->substream_target : context->substream_target_temp;\n\t/* Check what we need to do with the packet */\n\tif(context->substream == -1) {\n\t\tif((vcodec == JANUS_VIDEOCODEC_VP8 && janus_vp8_is_keyframe(payload, plen)) ||\n\t\t\t\t(vcodec == JANUS_VIDEOCODEC_VP9 && janus_vp9_is_keyframe(payload, plen)) ||\n\t\t\t\t(vcodec == JANUS_VIDEOCODEC_H264 && janus_h264_is_keyframe(payload, plen)) ||\n\t\t\t\t(vcodec == JANUS_VIDEOCODEC_AV1 && janus_av1_is_keyframe(payload, plen)) ||\n\t\t\t\t(vcodec == JANUS_VIDEOCODEC_H265 && janus_h265_is_keyframe(payload, plen))) {\n\t\t\tcontext->substream = substream;\n\t\t\t/* Notify the caller that the substream changed */\n\t\t\tcontext->changed_substream = TRUE;\n\t\t\tcontext->last_relayed = now;\n\t\t} else {\n\t\t\t/* Don't relay anything until we get a keyframe */\n\t\t\treturn FALSE;\n\t\t}\n\t} else if(context->substream != target) {\n\t\t/* We're not on the substream we'd like: let's wait for a keyframe on the target */\n\t\tif(((context->substream < target && substream > context->substream) ||\n\t\t\t\t(context->substream > target && substream < context->substream)) &&\n\t\t\t\t\t((vcodec == JANUS_VIDEOCODEC_VP8 && janus_vp8_is_keyframe(payload, plen)) ||\n\t\t\t\t\t(vcodec == JANUS_VIDEOCODEC_VP9 && janus_vp9_is_keyframe(payload, plen)) ||\n\t\t\t\t\t(vcodec == JANUS_VIDEOCODEC_H264 && janus_h264_is_keyframe(payload, plen)) ||\n\t\t\t\t\t(vcodec == JANUS_VIDEOCODEC_AV1 && janus_av1_is_keyframe(payload, plen)) ||\n\t\t\t\t\t(vcodec == JANUS_VIDEOCODEC_H265 && janus_h265_is_keyframe(payload, plen)))) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Received keyframe on #%d (SSRC %\"SCNu32\"), switching (was #%d/%\"SCNu32\")\\n\",\n\t\t\t\tsubstream, ssrc, context->substream, *(ssrcs + context->substream));\n\t\t\tcontext->substream = substream;\n\t\t\t/* Notify the caller that the substream changed */\n\t\t\tcontext->changed_substream = TRUE;\n\t\t\tcontext->last_relayed = now;\n\t\t}\n\t}\n\t/* If we haven't received our desired substream yet, let's drop temporarily */\n\tif(context->last_relayed == 0) {\n\t\t/* Let's start slow */\n\t\tcontext->last_relayed = now;\n\t} else if(context->substream > 0) {\n\t\t/* Check if too much time went by with no packet relayed */\n\t\tgint64 delay_us = (now - context->last_relayed);\n\t\tif(delay_us > (context->drop_trigger ? context->drop_trigger : 250000)) {\n\t\t\tcontext->last_relayed = now;\n\t\t\tif(context->substream != substream && context->substream_target_temp != 0) {\n\t\t\t\tif(context->substream_target > substream) {\n\t\t\t\t\tint prev_target = context->substream_target_temp;\n\t\t\t\t\tif(context->substream_target_temp == -1)\n\t\t\t\t\t\tcontext->substream_target_temp = context->substream_target - 1;\n\t\t\t\t\telse\n\t\t\t\t\t\tcontext->substream_target_temp--;\n\t\t\t\t\tif(context->substream_target_temp < 0)\n\t\t\t\t\t\tcontext->substream_target_temp = 0;\n\t\t\t\t\tif(context->substream_target_temp != prev_target) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"No packet received on substream %d for %\"SCNi64\"ms, falling back to %d\\n\",\n\t\t\t\t\t\t\tcontext->substream, (delay_us / 1000), context->substream_target_temp);\n\t\t\t\t\t\t/* Notify the caller that we (still) need a PLI */\n\t\t\t\t\t\tcontext->need_pli = TRUE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* Do we need to drop this? */\n\tif(context->substream < 0)\n\t\treturn FALSE;\n\tif(substream != context->substream) {\n\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (it's from SSRC %\"SCNu32\", but we're only relaying SSRC %\"SCNu32\" now\\n\",\n\t\t\tssrc, *(ssrcs + context->substream));\n\t\treturn FALSE;\n\t}\n\tcontext->last_relayed = janus_get_monotonic_time();\n\t/* Temporal layers are only easily available for some codecs */\n\tif(vcodec == JANUS_VIDEOCODEC_VP8) {\n\t\t/* Check if there's any temporal scalability to take into account */\n\t\tgboolean m = FALSE;\n\t\tuint16_t picid = 0;\n\t\tuint8_t tlzi = 0;\n\t\tuint8_t tid = 0;\n\t\tuint8_t ybit = 0;\n\t\tuint8_t keyidx = 0;\n\t\tif(janus_vp8_parse_descriptor(payload, plen, &m, &picid, &tlzi, &tid, &ybit, &keyidx) == 0) {\n\t\t\t//~ JANUS_LOG(LOG_WARN, \"%\"SCNu16\", %u, %u, %u, %u\\n\", picid, tlzi, tid, ybit, keyidx);\n\t\t\tif(context->templayer != context->templayer_target && tid == context->templayer_target) {\n\t\t\t\t/* FIXME We should be smarter in deciding when to switch */\n\t\t\t\tcontext->templayer = context->templayer_target;\n\t\t\t\t/* Notify the caller that the temporal layer changed */\n\t\t\t\tcontext->changed_temporal = TRUE;\n\t\t\t}\n\t\t\tif(context->templayer != -1 && tid > context->templayer) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (it's temporal layer %d, but we're capping at %d)\\n\",\n\t\t\t\t\ttid, context->templayer);\n\t\t\t\t/* We increase the base sequence number, or there will be gaps when delivering later */\n\t\t\t\tif(sc)\n\t\t\t\t\tsc->base_seq++;\n\t\t\t\treturn FALSE;\n\t\t\t}\n\t\t}\n\t} else if(vcodec == JANUS_VIDEOCODEC_VP9) {\n\t\t/* We use the VP9 SVC parser to extract info on temporal layers */\n\t\tgboolean found = FALSE;\n\t\tjanus_vp9_svc_info svc_info = { 0 };\n\t\tif(janus_vp9_parse_svc(payload, plen, &found, &svc_info) == 0 && found) {\n\t\t\tint temporal_layer = context->templayer;\n\t\t\tif(context->templayer_target > context->templayer) {\n\t\t\t\t/* We need to upscale */\n\t\t\t\tif(svc_info.ubit && svc_info.bbit &&\n\t\t\t\t\t\tsvc_info.temporal_layer > context->templayer &&\n\t\t\t\t\t\tsvc_info.temporal_layer <= context->templayer_target) {\n\t\t\t\t\tcontext->templayer = svc_info.temporal_layer;\n\t\t\t\t\ttemporal_layer = context->templayer;\n\t\t\t\t\tcontext->changed_temporal = TRUE;\n\t\t\t\t}\n\t\t\t} else if(context->templayer_target < context->templayer) {\n\t\t\t\t/* We need to downscale */\n\t\t\t\tif(svc_info.ebit && svc_info.temporal_layer == context->templayer_target) {\n\t\t\t\t\tcontext->templayer = context->templayer_target;\n\t\t\t\t\tcontext->changed_temporal = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(temporal_layer < svc_info.temporal_layer) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (it's temporal layer %d, but we're capping at %d)\\n\",\n\t\t\t\t\tsvc_info.temporal_layer, context->templayer);\n\t\t\t\t/* We increase the base sequence number, or there will be gaps when delivering later */\n\t\t\t\tif(sc)\n\t\t\t\t\tsc->base_seq++;\n\t\t\t\treturn FALSE;\n\t\t\t}\n\t\t}\n\t} else if(vcodec == JANUS_VIDEOCODEC_AV1 && dd_content != NULL && dd_len > 0) {\n\t\t/* Use the Dependency Descriptor to check temporal layers */\n\t\tjanus_av1_svc_context *av1ctx = NULL;\n\t\tif(context->substream >= 0 && context->substream <= 2)\n\t\t\tav1ctx = &context->av1_context[context->substream];\n\t\tif(av1ctx != NULL) {\n\t\t\tuint8_t template = 0;\n\t\t\tif(janus_av1_svc_context_process_dd(av1ctx, dd_content, dd_len, &template, NULL)) {\n\t\t\t\tjanus_av1_svc_template *t = g_hash_table_lookup(av1ctx->templates, GUINT_TO_POINTER(template));\n\t\t\t\tif(t) {\n\t\t\t\t\tint temporal_layer = context->templayer;\n\t\t\t\t\tif(context->templayer_target > context->templayer) {\n\t\t\t\t\t\t/* We need to upscale */\n\t\t\t\t\t\tif(t->temporal > context->templayer && t->temporal <= context->templayer_target) {\n\t\t\t\t\t\t\tcontext->templayer = t->temporal;\n\t\t\t\t\t\t\ttemporal_layer = context->templayer;\n\t\t\t\t\t\t\tcontext->changed_temporal = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(context->templayer_target < context->templayer) {\n\t\t\t\t\t\t/* We need to downscale */\n\t\t\t\t\t\tif(t->temporal == context->templayer_target) {\n\t\t\t\t\t\t\tcontext->templayer = context->templayer_target;\n\t\t\t\t\t\t\tcontext->changed_temporal = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(temporal_layer < t->temporal) {\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (it's temporal layer %d, but we're capping at %d)\\n\",\n\t\t\t\t\t\t\tt->temporal, context->templayer);\n\t\t\t\t\t\t/* We increase the base sequence number, or there will be gaps when delivering later */\n\t\t\t\t\t\tif(sc)\n\t\t\t\t\t\t\tsc->base_seq++;\n\t\t\t\t\t\treturn FALSE;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* If we got here, the packet can be relayed */\n\treturn TRUE;\n}\n\n/* VP9 SVC */\nvoid janus_rtp_svc_context_reset(janus_rtp_svc_context *context) {\n\tif(context == NULL)\n\t\treturn;\n\t/* Reset the context values */\n\tjanus_av1_svc_context_reset(&context->dd_context);\n\tmemset(context, 0, sizeof(*context));\n\tcontext->spatial = -1;\n\tcontext->temporal = -1;\n}\n\ngboolean janus_rtp_svc_context_process_rtp(janus_rtp_svc_context *context,\n\t\tchar *buf, int len, uint8_t *dd_content, int dd_len,\n\t\tjanus_videocodec vcodec, janus_vp9_svc_info *info, janus_rtp_switching_context *sc) {\n\tif(!context || !buf || len < 1 || (vcodec != JANUS_VIDEOCODEC_VP9 && vcodec != JANUS_VIDEOCODEC_AV1))\n\t\treturn FALSE;\n\tjanus_rtp_header *header = (janus_rtp_header *)buf;\n\t/* Reset the flags */\n\tcontext->changed_spatial = FALSE;\n\tcontext->changed_temporal = FALSE;\n\tcontext->need_pli = FALSE;\n\tgint64 now = janus_get_monotonic_time();\n\t/* Access the packet payload */\n\tint plen = 0;\n\tchar *payload = janus_rtp_payload(buf, len, &plen);\n\tif(payload == NULL)\n\t\treturn FALSE;\n\t/* Check if we should use the Dependency Descriptor */\n\tif(vcodec == JANUS_VIDEOCODEC_AV1) {\n\t\t/* We do, make sure the data is there */\n\t\tif(dd_content == NULL || dd_len < 1) {\n\t\t\t/* No Dependency Descriptor, relay as it is */\n\t\t\treturn TRUE;\n\t\t}\n\t\tuint8_t template = 0, ebit = 0;\n\t\tif(!janus_av1_svc_context_process_dd(&context->dd_context, dd_content, dd_len, &template, &ebit)) {\n\t\t\t/* We couldn't parse the Dependency Descriptor, relay as it is */\n\t\t\treturn TRUE;\n\t\t}\n\t\tjanus_av1_svc_template *t = g_hash_table_lookup(context->dd_context.templates, GUINT_TO_POINTER(template));\n\t\tif(t == NULL) {\n\t\t\t/* We couldn't find the template, relay as it is */\n\t\t\treturn TRUE;\n\t\t}\n\t\t/* Now let's check if we should let the packet through or not */\n\t\tgboolean keyframe = janus_av1_is_keyframe((const char *)payload, plen);\n\t\tgboolean override_mark_bit = FALSE, has_marker_bit = header->markerbit;\n\t\tint spatial_layer = context->spatial;\n\t\tif(t->spatial >= 0 && t->spatial <= 2)\n\t\t\tcontext->last_spatial_layer[t->spatial] = now;\n\t\tif(context->spatial_target > context->spatial) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"We need to upscale spatially: (%d < %d)\\n\",\n\t\t\t\tcontext->spatial, context->spatial_target);\n\t\t\t/* We need to upscale: wait for a keyframe */\n\t\t\tif(keyframe) {\n\t\t\t\tint new_spatial_layer = context->spatial_target;\n\t\t\t\twhile(new_spatial_layer > context->spatial && new_spatial_layer > 0) {\n\t\t\t\t\tif(now - context->last_spatial_layer[new_spatial_layer] >= (context->drop_trigger ? context->drop_trigger : 250000)) {\n\t\t\t\t\t\t/* We haven't received packets from this layer for a while, try a lower layer */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Haven't received packets from layer %d for a while, trying %d instead...\\n\",\n\t\t\t\t\t\t\tnew_spatial_layer, new_spatial_layer-1);\n\t\t\t\t\t\tnew_spatial_layer--;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(new_spatial_layer > context->spatial) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Upscaling spatial layer: %d --> %d (need %d)\\n\",\n\t\t\t\t\t\tcontext->spatial, new_spatial_layer, context->spatial_target);\n\t\t\t\t\tcontext->spatial = new_spatial_layer;\n\t\t\t\t\tspatial_layer = context->spatial;\n\t\t\t\t\tcontext->changed_spatial = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(context->spatial_target < context->spatial) {\n\t\t\t/* We need to scale: wait for a keyframe */\n\t\t\tJANUS_LOG(LOG_HUGE, \"We need to downscale spatially: (%d > %d)\\n\",\n\t\t\t\tcontext->spatial, context->spatial_target);\n\t\t\t/* Check the E bit to see if this is an end-of-frame */\n\t\t\tif(ebit) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Downscaling spatial layer: %d --> %d\\n\",\n\t\t\t\t\tcontext->spatial, context->spatial_target);\n\t\t\t\tcontext->spatial = context->spatial_target;\n\t\t\t\tcontext->changed_spatial = TRUE;\n\t\t\t}\n\t\t}\n\t\tif(spatial_layer < t->spatial) {\n\t\t\t/* Drop the packet: update the context to make sure sequence number is increased normally later */\n\t\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (spatial layer %d < %d)\\n\", spatial_layer, t->spatial);\n\t\t\tif(sc)\n\t\t\t\tsc->base_seq++;\n\t\t\treturn FALSE;\n\t\t} else if(ebit && spatial_layer == t->spatial) {\n\t\t\t/* If we stop at layer 0, we need a marker bit now, as the one from layer 1 will not be received */\n\t\t\toverride_mark_bit = TRUE;\n\t\t}\n\t\tint temporal = context->temporal;\n\t\tif(context->temporal_target > context->temporal) {\n\t\t\t/* We need to upscale */\n\t\t\tif(t->temporal > context->temporal && t->temporal <= context->temporal_target) {\n\t\t\t\tcontext->temporal = t->temporal;\n\t\t\t\ttemporal = context->temporal;\n\t\t\t\tcontext->changed_temporal = TRUE;\n\t\t\t}\n\t\t} else if(context->temporal_target < context->temporal) {\n\t\t\t/* We need to downscale */\n\t\t\tif(t->temporal == context->temporal_target) {\n\t\t\t\tcontext->temporal = context->temporal_target;\n\t\t\t\tcontext->changed_temporal = TRUE;\n\t\t\t}\n\t\t}\n\t\tif(temporal < t->temporal) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (it's temporal layer %d, but we're capping at %d)\\n\",\n\t\t\t\tt->temporal, context->temporal);\n\t\t\t/* We increase the base sequence number, or there will be gaps when delivering later */\n\t\t\tif(sc)\n\t\t\t\tsc->base_seq++;\n\t\t\treturn FALSE;\n\t\t}\n\t\t/* If we got here, we can send the frame: this doesn't necessarily mean it's\n\t\t * one of the layers the user wants, as there may be dependencies involved */\n\t\tJANUS_LOG(LOG_HUGE, \"Sending packet (spatial=%d, temporal=%d)\\n\",\n\t\t\tt->spatial, t->temporal);\n\t\tif(override_mark_bit && !has_marker_bit)\n\t\t\theader->markerbit = 1;\n\t\treturn TRUE;\n\t}\n\t/* If we got here, it's VP9, for which we parse the payload manually:\n\t * if we don't have any info parsed from the VP9 payload header, get it now */\n\tjanus_vp9_svc_info svc_info = { 0 };\n\tif(!info) {\n\t\tgboolean found = FALSE;\n\t\tif(janus_vp9_parse_svc(payload, plen, &found, &svc_info) < 0) {\n\t\t\t/* Error parsing, relay as it is */\n\t\t\treturn TRUE;\n\t\t}\n\t\tif(!found) {\n\t\t\t/* No SVC info, maybe a generic VP9 payload? Relay as it is */\n\t\t\treturn TRUE;\n\t\t}\n\t} else {\n\t\tsvc_info = *info;\n\t}\n\t/* Note: Following code inspired by the excellent job done by Sergio Garcia Murillo here:\n\t * https://github.com/medooze/media-server/blob/master/src/vp9/VP9LayerSelector.cpp */\n\tgboolean keyframe = janus_vp9_is_keyframe((const char *)payload, plen);\n\tgboolean override_mark_bit = FALSE, has_marker_bit = header->markerbit;\n\tint spatial_layer = context->spatial;\n\tif(svc_info.spatial_layer >= 0 && svc_info.spatial_layer <= 2)\n\t\tcontext->last_spatial_layer[svc_info.spatial_layer] = now;\n\tif(context->spatial_target > context->spatial) {\n\t\tJANUS_LOG(LOG_HUGE, \"We need to upscale spatially: (%d < %d)\\n\",\n\t\t\tcontext->spatial, context->spatial_target);\n\t\t/* We need to upscale: wait for a keyframe */\n\t\tif(keyframe) {\n\t\t\tint new_spatial_layer = context->spatial_target;\n\t\t\twhile(new_spatial_layer > context->spatial && new_spatial_layer > 0) {\n\t\t\t\tif(now - context->last_spatial_layer[new_spatial_layer] >= (context->drop_trigger ? context->drop_trigger : 250000)) {\n\t\t\t\t\t/* We haven't received packets from this layer for a while, try a lower layer */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Haven't received packets from layer %d for a while, trying %d instead...\\n\",\n\t\t\t\t\t\tnew_spatial_layer, new_spatial_layer-1);\n\t\t\t\t\tnew_spatial_layer--;\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(new_spatial_layer > context->spatial) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Upscaling spatial layer: %d --> %d (need %d)\\n\",\n\t\t\t\t\tcontext->spatial, new_spatial_layer, context->spatial_target);\n\t\t\t\tcontext->spatial = new_spatial_layer;\n\t\t\t\tspatial_layer = context->spatial;\n\t\t\t\tcontext->changed_spatial = TRUE;\n\t\t\t}\n\t\t}\n\t} else if(context->spatial_target < context->spatial) {\n\t\t/* We need to downscale */\n\t\tJANUS_LOG(LOG_HUGE, \"We need to downscale spatially: (%d > %d)\\n\",\n\t\t\tcontext->spatial, context->spatial_target);\n\t\tgboolean downscaled = FALSE;\n\t\tif(!svc_info.fbit && keyframe) {\n\t\t\t/* Non-flexible mode: wait for a keyframe */\n\t\t\tdownscaled = TRUE;\n\t\t} else if(svc_info.fbit && svc_info.ebit) {\n\t\t\t/* Flexible mode: check the E bit */\n\t\t\tdownscaled = TRUE;\n\t\t}\n\t\tif(downscaled) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Downscaling spatial layer: %d --> %d\\n\",\n\t\t\t\tcontext->spatial, context->spatial_target);\n\t\t\tcontext->spatial = context->spatial_target;\n\t\t\tcontext->changed_spatial = TRUE;\n\t\t}\n\t}\n\tif(spatial_layer < svc_info.spatial_layer) {\n\t\t/* Drop the packet: update the context to make sure sequence number is increased normally later */\n\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (spatial layer %d < %d)\\n\", spatial_layer, svc_info.spatial_layer);\n\t\tif(sc)\n\t\t\tsc->base_seq++;\n\t\treturn FALSE;\n\t} else if(svc_info.ebit && spatial_layer == svc_info.spatial_layer) {\n\t\t/* If we stop at layer 0, we need a marker bit now, as the one from layer 1 will not be received */\n\t\toverride_mark_bit = TRUE;\n\t}\n\tint temporal_layer = context->temporal;\n\tif(context->temporal_target > context->temporal) {\n\t\t/* We need to upscale */\n\t\tJANUS_LOG(LOG_HUGE, \"We need to upscale temporally: (%d < %d)\\n\",\n\t\t\tcontext->temporal, context->temporal_target);\n\t\tif(svc_info.ubit && svc_info.bbit &&\n\t\t\t\tsvc_info.temporal_layer > context->temporal &&\n\t\t\t\tsvc_info.temporal_layer <= context->temporal_target) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Upscaling temporal layer: %d --> %d (want %d)\\n\",\n\t\t\t\tcontext->temporal, svc_info.temporal_layer, context->temporal_target);\n\t\t\tcontext->temporal = svc_info.temporal_layer;\n\t\t\ttemporal_layer = context->temporal;\n\t\t\tcontext->changed_temporal = TRUE;\n\t\t}\n\t} else if(context->temporal_target < context->temporal) {\n\t\t/* We need to downscale */\n\t\tJANUS_LOG(LOG_HUGE, \"We need to downscale temporally: (%d > %d)\\n\",\n\t\t\tcontext->temporal, context->temporal_target);\n\t\tif(svc_info.ebit && svc_info.temporal_layer == context->temporal_target) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- Downscaling temporal layer: %d --> %d\\n\",\n\t\t\t\tcontext->temporal, context->temporal_target);\n\t\t\tcontext->temporal = context->temporal_target;\n\t\t\tcontext->changed_temporal = TRUE;\n\t\t}\n\t}\n\tif(temporal_layer < svc_info.temporal_layer) {\n\t\t/* Drop the packet: update the context to make sure sequence number is increased normally later */\n\t\tJANUS_LOG(LOG_HUGE, \"Dropping packet (temporal layer %d < %d)\\n\", temporal_layer, svc_info.temporal_layer);\n\t\tif(sc)\n\t\t\tsc->base_seq++;\n\t\treturn FALSE;\n\t}\n\t/* If we got here, we can send the frame: this doesn't necessarily mean it's\n\t * one of the layers the user wants, as there may be dependencies involved */\n\tJANUS_LOG(LOG_HUGE, \"Sending packet (spatial=%d, temporal=%d)\\n\",\n\t\tsvc_info.spatial_layer, svc_info.temporal_layer);\n\tif(override_mark_bit && !has_marker_bit)\n\t\theader->markerbit = 1;\n\t/* If we got here, the packet can be relayed */\n\treturn TRUE;\n}\n\n/* AV1 SVC (still WIP) */\nvoid janus_av1_svc_context_reset(janus_av1_svc_context *context) {\n\tif(context == NULL)\n\t\treturn;\n\t/* Reset the context values */\n\tif(context->templates != NULL)\n\t\tg_hash_table_destroy(context->templates);\n\tmemset(context, 0, sizeof(*context));\n}\n\ngboolean janus_av1_svc_context_process_dd(janus_av1_svc_context *context,\n\t\tuint8_t *dd, int dd_len, uint8_t *template_id, uint8_t *ebit) {\n\tif(!context || !dd || dd_len < 3)\n\t\treturn FALSE;\n\n\t/* First of all, let's parse the Dependency Descriptor */\n\tsize_t blen = dd_len*8;\n\tuint32_t offset = 0;\n\t/* mandatory_descriptor_fields() */\n\tuint8_t start = janus_bitstream_getbit(dd, offset++);\n\tuint8_t end = janus_bitstream_getbit(dd, offset++);\n\tif(ebit)\n\t\t*ebit = end;\n\tuint8_t template = janus_bitstream_getbits(dd, 6, &offset);\n\tuint16_t frame = janus_bitstream_getbits(dd, 16, &offset);\n\tJANUS_LOG(LOG_HUGE, \"  -- s=%u, e=%u, t=%u, f=%u\\n\",\n\t\tstart, end, template, frame);\n\tif(blen > 24) {\n\t\t/* extended_descriptor_fields() */\n\t\tuint8_t tdeps = janus_bitstream_getbit(dd, offset++);\n\t\t(void)janus_bitstream_getbit(dd, offset++);\n\t\t(void)janus_bitstream_getbit(dd, offset++);\n\t\t(void)janus_bitstream_getbit(dd, offset++);\n\t\t(void)janus_bitstream_getbit(dd, offset++);\n\t\t/* template_dependency_structure() */\n\t\tif(tdeps) {\n\t\t\tuint8_t tioff = janus_bitstream_getbits(dd, 6, &offset);\n\t\t\t(void)janus_bitstream_getbits(dd, 5, &offset);\n\t\t\t/* template_layers() */\n\t\t\tuint32_t nlidc = 0;\n\t\t\tuint8_t tcnt = 0;\n\t\t\tint spatial_layers = 0;\n\t\t\tint temporal_layers = 0;\n\t\t\tdo {\n\t\t\t\tnlidc = janus_bitstream_getbits(dd, 2, &offset);\n\t\t\t\tif(context->templates == NULL)\n\t\t\t\t\tcontext->templates = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)g_free);\n\t\t\t\tjanus_av1_svc_template *t = g_hash_table_lookup(context->templates,\n\t\t\t\t\tGUINT_TO_POINTER(tcnt));\n\t\t\t\tif(t == NULL) {\n\t\t\t\t\tt = g_malloc0(sizeof(janus_av1_svc_template));\n\t\t\t\t\tt->id = tcnt;\n\t\t\t\t\tg_hash_table_insert(context->templates, GUINT_TO_POINTER(t->id), t);\n\t\t\t\t\tcontext->updated = TRUE;\n\t\t\t\t}\n\t\t\t\tt->spatial = spatial_layers;\n\t\t\t\tt->temporal = temporal_layers;\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- -- -- [%u] spatial=%u, temporal=%u\\n\",\n\t\t\t\t\ttcnt, t->spatial, t->temporal);\n\t\t\t\tif(nlidc == 1) {\n\t\t\t\t\ttemporal_layers++;\n\t\t\t\t} else if(nlidc == 2) {\n\t\t\t\t\ttemporal_layers = 0;\n\t\t\t\t\tspatial_layers++;\n\t\t\t\t}\n\t\t\t\ttcnt++;\n\t\t\t} while(nlidc != 3);\n\t\t\t/* Check if anything changed since the latest update */\n\t\t\tif(context->tcnt != tcnt || context->tioff != tioff ||\n\t\t\t\t\tcontext->spatial_layers != spatial_layers ||\n\t\t\t\t\tcontext->temporal_layers != temporal_layers)\n\t\t\t\tcontext->updated = TRUE;\n\t\t\tcontext->tcnt = tcnt;\n\t\t\tcontext->tioff = tioff;\n\t\t\tcontext->spatial_layers = spatial_layers;\n\t\t\tcontext->temporal_layers = temporal_layers;\n\t\t\t/* FIXME We currently don't care about the other fields */\n\t\t}\n\t}\n\t/* frame_dependency_definition() */\n\tuint8_t tindex = (template + 64 - context->tioff) % 64;\n\tjanus_av1_svc_template *t = context->templates ? g_hash_table_lookup(context->templates,\n\t\tGUINT_TO_POINTER(tindex)) : NULL;\n\tif(t == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid template ID '%u' (count is %u), ignoring packet...\\n\",\n\t\t\ttindex, context->tcnt);\n\t\treturn FALSE;\n\t}\n\tJANUS_LOG(LOG_HUGE, \"  -- spatial=%u, temporal=%u (tindex %u)\\n\",\n\t\tt->spatial, t->temporal, t->id);\n\t/* FIXME We currently don't care about the other fields */\n\n\t/* If we got here, the packet is fine */\n\tif(template_id != NULL)\n\t\t*template_id = tindex;\n\treturn TRUE;\n}\n"
  },
  {
    "path": "src/rtp.h",
    "content": "/*! \\file    rtp.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    RTP processing (headers)\n * \\details  Implementation of the RTP header. Since the server does not\n * much more than relaying frames around, the only thing we're interested\n * in is the RTP header and how to get its payload, and parsing extensions.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_RTP_H\n#define JANUS_RTP_H\n\n#include <arpa/inet.h>\n#if defined (__MACH__) || defined(__FreeBSD__)\n#include <machine/endian.h>\n#define __BYTE_ORDER BYTE_ORDER\n#define __BIG_ENDIAN BIG_ENDIAN\n#define __LITTLE_ENDIAN LITTLE_ENDIAN\n#else\n#include <endian.h>\n#endif\n#include <inttypes.h>\n#include <string.h>\n#include <glib.h>\n#include <jansson.h>\n\n#include \"plugins/plugin.h\"\n#include \"utils.h\"\n\n#define RTP_HEADER_SIZE\t12\n\n/*! \\brief RTP Header (http://tools.ietf.org/html/rfc3550#section-5.1) */\ntypedef struct rtp_header\n{\n#if __BYTE_ORDER == __BIG_ENDIAN\n\tuint16_t version:2;\n\tuint16_t padding:1;\n\tuint16_t extension:1;\n\tuint16_t csrccount:4;\n\tuint16_t markerbit:1;\n\tuint16_t type:7;\n#elif __BYTE_ORDER == __LITTLE_ENDIAN\n\tuint16_t csrccount:4;\n\tuint16_t extension:1;\n\tuint16_t padding:1;\n\tuint16_t version:2;\n\tuint16_t type:7;\n\tuint16_t markerbit:1;\n#endif\n\tuint16_t seq_number;\n\tuint32_t timestamp;\n\tuint32_t ssrc;\n\tuint32_t csrc[0];\n} rtp_header;\ntypedef rtp_header janus_rtp_header;\n\n/*! \\brief RTP packet */\ntypedef struct janus_rtp_packet {\n\tchar *data;\n\tgint length;\n\tgint64 created;\n\tgint64 last_retransmit;\n\tgint64 current_backoff;\n\tjanus_plugin_rtp_extensions extensions;\n} janus_rtp_packet;\n\n/*! \\brief RTP extension */\ntypedef struct janus_rtp_header_extension {\n\tuint16_t type;\n\tuint16_t length;\n} janus_rtp_header_extension;\n\n/*! \\brief RTP RFC2833 payload */\ntypedef struct janus_rtp_rfc2833_payload {\n#if __BYTE_ORDER == __BIG_ENDIAN\n\tuint8_t event;\n\tuint8_t end:1;\n\tuint8_t reserved:1;\n\tuint8_t volume:6;\n\tuint16_t duration;\n#elif __BYTE_ORDER == __LITTLE_ENDIAN\n\tuint8_t event;\n\tuint8_t volume:6;\n\tuint8_t reserved:1;\n\tuint8_t end:1;\n\tuint16_t duration;\n#endif\n} janus_rtp_rfc2833_payload;\n\n/*! \\brief a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level */\n#define JANUS_RTP_EXTMAP_AUDIO_LEVEL\t\t\"urn:ietf:params:rtp-hdrext:ssrc-audio-level\"\n/*! \\brief a=extmap:2 urn:ietf:params:rtp-hdrext:toffset */\n#define JANUS_RTP_EXTMAP_TOFFSET\t\t\t\"urn:ietf:params:rtp-hdrext:toffset\"\n/*! \\brief a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time */\n#define JANUS_RTP_EXTMAP_ABS_SEND_TIME\t\t\"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\"\n/*! \\brief a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time */\n#define JANUS_RTP_EXTMAP_ABS_CAPTURE_TIME\t\"http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time\"\n/*! \\brief a=extmap:4 urn:3gpp:video-orientation */\n#define JANUS_RTP_EXTMAP_VIDEO_ORIENTATION\t\"urn:3gpp:video-orientation\"\n/*! \\brief a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 */\n#define JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC\t\"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\"\n/*! \\brief a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay */\n#define JANUS_RTP_EXTMAP_PLAYOUT_DELAY\t\t\"http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\"\n/*! \\brief a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid */\n#define JANUS_RTP_EXTMAP_MID\t\t\t\t\"urn:ietf:params:rtp-hdrext:sdes:mid\"\n/*! \\brief a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id */\n#define JANUS_RTP_EXTMAP_RID\t\t\t\t\"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\"\n/*! \\brief a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id */\n#define JANUS_RTP_EXTMAP_REPAIRED_RID\t\t\"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\"\n/*! \\brief a=extmap:10 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension */\n#define JANUS_RTP_EXTMAP_DEPENDENCY_DESC\t\"https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\"\n/*! \\brief a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00 */\n#define JANUS_RTP_EXTMAP_VIDEO_LAYERS\t\t\"http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00\"\n/*! \\brief \\note Note: We don't support encrypted extensions yet */\n#define JANUS_RTP_EXTMAP_ENCRYPTED\t\t\t\"urn:ietf:params:rtp-hdrext:encrypt\"\nint janus_rtp_extension_id(const char *type);\n\n\ntypedef enum janus_audiocodec {\n\tJANUS_AUDIOCODEC_NONE,\n\tJANUS_AUDIOCODEC_OPUS,\n\tJANUS_AUDIOCODEC_MULTIOPUS,\n\tJANUS_AUDIOCODEC_OPUSRED,\n\tJANUS_AUDIOCODEC_PCMU,\n\tJANUS_AUDIOCODEC_PCMA,\n\tJANUS_AUDIOCODEC_G722,\n\tJANUS_AUDIOCODEC_ISAC_32K,\n\tJANUS_AUDIOCODEC_ISAC_16K,\n\tJANUS_AUDIOCODEC_L16_48K,\n\tJANUS_AUDIOCODEC_L16_16K\n} janus_audiocodec;\nconst char *janus_audiocodec_name(janus_audiocodec acodec);\njanus_audiocodec janus_audiocodec_from_name(const char *name);\nint janus_audiocodec_pt(janus_audiocodec acodec);\n\ntypedef enum janus_videocodec {\n\tJANUS_VIDEOCODEC_NONE,\n\tJANUS_VIDEOCODEC_VP8,\n\tJANUS_VIDEOCODEC_VP9,\n\tJANUS_VIDEOCODEC_H264,\n\tJANUS_VIDEOCODEC_AV1,\n\tJANUS_VIDEOCODEC_H265\n} janus_videocodec;\nconst char *janus_videocodec_name(janus_videocodec vcodec);\njanus_videocodec janus_videocodec_from_name(const char *name);\nint janus_videocodec_pt(janus_videocodec vcodec);\n\n\n/*! \\brief Helper method to demultiplex RTP from other protocols\n * @param[in] buf Buffer to inspect\n * @param[in] len Length of the buffer to inspect */\ngboolean janus_is_rtp(char *buf, guint len);\n\n/*! \\brief Helper to quickly access the RTP payload, skipping header and extensions\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[out] plen The payload data length in bytes\n * @returns A pointer to where the payload data starts, or NULL otherwise; plen is also set accordingly */\nchar *janus_rtp_payload(char *buf, int len, int *plen);\n\n/*! \\brief Ugly and dirty helper to quickly get the id associated with an RTP extension (extmap) in an SDP\n * @param sdp The SDP to parse\n * @param extension The extension namespace to look for\n * @returns The extension id, if found, -1 otherwise */\nint janus_rtp_header_extension_get_id(const char *sdp, const char *extension);\n\n/*! \\brief Ugly and dirty helper to quickly get the RTP extension namespace associated with an id (extmap) in an SDP\n * @note This only looks for the extensions we know about, those defined in rtp.h\n * @param sdp The SDP to parse\n * @param id The extension id to look for\n * @returns The extension namespace, if found, NULL otherwise */\nconst char *janus_rtp_header_extension_get_from_id(const char *sdp, int id);\n\n/*! \\brief Helper to parse a ssrc-audio-level RTP extension (https://tools.ietf.org/html/rfc6464)\n * @note Browsers apparently always set the VAD to 1, so it's unreliable and should be ignored:\n * only use this method if you're interested in the audio-level value itself.\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] vad Whether the encoder thinks there's voice activity\n * @param[out] level The level value in dBov (0=max, 127=min)\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_audio_level(char *buf, int len, int id, gboolean *vad, int *level);\n\n/*! \\brief Helper to parse a video-orientation RTP extension (http://www.3gpp.org/ftp/Specs/html-info/26114.htm)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] c The value of the Camera (C) bit\n * @param[out] f The value of the Flip (F) bit\n * @param[out] r1 The value of the first Rotation (R1) bit\n * @param[out] r0 The value of the second Rotation (R0) bit\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_video_orientation(char *buf, int len, int id,\n\tgboolean *c, gboolean *f, gboolean *r1, gboolean *r0);\n\n/*! \\brief Helper to parse a playout-delay RTP extension (https://webrtc.org/experiments/rtp-hdrext/playout-delay)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] min_delay The minimum delay value\n * @param[out] max_delay The maximum delay value\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_playout_delay(char *buf, int len, int id,\n\tuint16_t *min_delay, uint16_t *max_delay);\n\n/*! \\brief Helper to parse a sdes-mid RTP extension (https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] sdes_item Buffer where the RTP stream ID will be written\n * @param[in] sdes_len Size of the input/output buffer\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_mid(char *buf, int len, int id,\n\tchar *sdes_item, int sdes_len);\n\n/*! \\brief Helper to parse a rtp-stream-id RTP extension (https://tools.ietf.org/html/draft-ietf-avtext-rid-09)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] sdes_item Buffer where the RTP stream ID will be written\n * @param[in] sdes_len Size of the input/output buffer\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_rid(char *buf, int len, int id,\n\tchar *sdes_item, int sdes_len);\n\n/*! \\brief Helper to parse a dependency descriptor RTP extension (https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] dd_item Buffer where the dependency descriptor will be written\n * @param[out] dd_len Size of the input/output buffer, will be updated with the size of the data\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_dependency_desc(char *buf, int len, int id,\n\tuint8_t *dd_item, int *dd_len);\n\n/*! \\brief Helper to parse an abs-send-time RTP extension (http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] abs_ts Variable where the parsed abs-send-time value will be stored\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_abs_send_time(char *buf, int len, int id, uint32_t *abs_ts);\n\n/*! \\brief Helper to set an abs-send-time RTP extension (http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] abs_ts Absolute Send Time value to set\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_set_abs_send_time(char *buf, int len, int id, uint32_t abs_ts);\n\n/*! \\brief Helper to parse an abs-capture-time RTP extension (http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] abs_ts Variable where the parsed abs-capture-time value will be stored\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_abs_capture_time(char *buf, int len, int id, uint64_t *abs_ts);\n\n/*! \\brief Helper to set an abs-capture-time RTP extension (http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] abs_ts Absolute Send Time value to set\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_set_abs_capture_time(char *buf, int len, int id, uint64_t abs_ts);\n\n/*! \\brief Helper to parse a transport wide sequence number (https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] transSeqNum Variable to read the transport wide sequence number in\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_transport_wide_cc(char *buf, int len, int id, uint16_t *transSeqNum);\n\n/*! \\brief Helper to set a transport wide sequence number (https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] transSeqNum Transport wide sequence number to set\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_set_transport_wide_cc(char *buf, int len, int id, uint16_t transSeqNum);\n\n/*! \\brief Helper to parse a video layers allocation (http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for\n * @param[out] spatial_layers Variable where the parsed spatial layers value will be stored\n * @param[out] temporal_layers Variable where the parsed temporal layers value will be stored\n * @returns 0 if found, -1 otherwise */\nint janus_rtp_header_extension_parse_video_layers_allocation(char *buf, int len, int id,\n\tint8_t *spatial_layers, int8_t *temporal_layers);\n\n/*! \\brief Helper to replace the ID of an RTP extension with a different one (e.g.,\n * to turn a repaired-rtp-stream-id into a rtp-stream-id after a successful rtx)\n * @param[in] buf The packet data\n * @param[in] len The packet data length in bytes\n * @param[in] id The extension ID to look for and replace\n * @param[in] new_id The new value for the extension ID\n * @returns 0 if found, a negative integer otherwise */\nint janus_rtp_header_extension_replace_id(char *buf, int len, int id, int new_id);\n\n/*! \\brief RTP context, in order to make sure SSRC changes result in coherent seq/ts increases */\ntypedef struct janus_rtp_switching_context {\n\tuint32_t last_ssrc, last_ts, base_ts, base_ts_prev, prev_ts, target_ts, start_ts;\n\tuint16_t last_seq, prev_seq, base_seq, base_seq_prev;\n\tgboolean ts_reset, seq_reset, new_ssrc;\n\tgint16 seq_offset;\n\tgint32 prev_delay, active_delay, ts_offset;\n\tgint64 last_time, reference_time, start_time, evaluating_start_time;\n} janus_rtp_switching_context;\n\n/*! \\brief Set (or reset) the context fields to their default values\n * @param[in] context The context to (re)set */\nvoid janus_rtp_switching_context_reset(janus_rtp_switching_context *context);\n\n/*! \\brief Use the context info to update the RTP header of a packet, if needed\n * @param[in] header The RTP header to update\n * @param[in] context The context to use as a reference\n * @param[in] video Whether this is an audio or a video packet\n * @param[in] step \\b deprecated The expected timestamp step */\nvoid janus_rtp_header_update(janus_rtp_header *header, janus_rtp_switching_context *context, gboolean video, int step);\n\n#define RTP_AUDIO_SKEW_TH_MS 120\n#define RTP_VIDEO_SKEW_TH_MS 120\n#define SKEW_DETECTION_WAIT_TIME_SECS 10\n\n/*! \\brief Use the context info to compensate for audio source skew, if needed\n * @param[in] header The RTP header to update\n * @param[in] context The context to use as a reference\n * @param[in] now \\b The packet arrival monotonic time\n * @returns 0 if no compensation is needed, -N if a N packets drop must be performed, N if a N sequence numbers jump has been performed */\nint janus_rtp_skew_compensate_audio(janus_rtp_header *header, janus_rtp_switching_context *context, gint64 now);\n/*! \\brief Use the context info to compensate for video source skew, if needed\n * @param[in] header The RTP header to update\n * @param[in] context The context to use as a reference\n * @param[in] now \\b The packet arrival monotonic time\n * @returns 0 if no compensation is needed, -N if a N packets drop must be performed, N if a N sequence numbers jump has been performed */\nint janus_rtp_skew_compensate_video(janus_rtp_header *header, janus_rtp_switching_context *context, gint64 now);\n\n\n/** @name Janus AV1-SVC processing methods\n */\n///@{\n/*! \\brief Helper struct for processing and tracking AV1-SVC streams */\ntypedef struct janus_av1_svc_context {\n\t/*! \\brief Number of templates advertised via Dependency Descriptor */\n\tuint8_t tcnt;\n\t/*! \\brief Template ID offset, as advertised via Dependency Descriptor */\n\tuint8_t tioff;\n\t/*! \\brief Map of templates advertised via Dependency Descriptor, indexed by ID */\n\tGHashTable *templates;\n\t/*! \\brief How many spatial and temporal layers are available */\n\tint spatial_layers, temporal_layers;\n\t/*! \\brief Whether this context changed since the last update */\n\tgboolean updated;\n} janus_av1_svc_context;\n\n/*! \\brief Helper struct to track SVC templates\n * \\note This is very incomplete, since we only track the spatial and\n * temporal layer associated with a specific template ID for now */\ntypedef struct janus_av1_svc_template {\n\t/*! \\brief Template ID */\n\tuint8_t id;\n\t/*! \\brief Spatial layer associated to this template */\n\tint spatial;\n\t/*! \\brief Temporal layer associated to this template */\n\tint temporal;\n} janus_av1_svc_template;\n\n/*! \\brief Set (or reset) the context fields to their default values\n * @param[in] context The context to (re)set */\nvoid janus_av1_svc_context_reset(janus_av1_svc_context *context);\n\n/*! \\brief Process a Dependency Descriptor payload, updating the SVC context accordingly\n * \\note At the moment, this code is quite naive, as it mostly looks at the target\n * spatial/temporal layers, and the one written in the Dependency Descriptor data.\n * In the future, this should become more sophisticated, and use additional\n * information like dependency chains and stuff like that\n * @param[in] context The av1svc context to use\n * @param[in] dd Pointer to the Dependency Descriptor data\n * @param[in] dd_len The length of the Dependendy Descriptor data\n * @param[out] template_id Pointer to the ID of the template referenced in this packet\n * @param[out] ebit Whether this packet is an end of frame or not\n * @returns TRUE if the packet is valid, FALSE if it should be dropped instead */\ngboolean janus_av1_svc_context_process_dd(janus_av1_svc_context *context,\n\tuint8_t *dd, int dd_len, uint8_t *template_id, uint8_t *ebit);\n///@}\n\n/** @name Janus simulcast processing methods\n */\n///@{\n/*! \\brief Helper struct for processing and tracking simulcast streams */\ntypedef struct janus_rtp_simulcasting_context {\n\t/*! \\brief RTP Stream extension ID, if any */\n\tgint rid_ext_id;\n\t/*! \\brief Dependency Descriptors contexts, if any */\n\tjanus_av1_svc_context av1_context[3];\n\t/*! \\brief Which simulcast substream we should forward back */\n\tint substream;\n\t/*! \\brief As above, but to handle transitions (e.g., wait for keyframe, or get this if available) */\n\tint substream_target, substream_target_temp;\n\t/*! \\brief Which simulcast temporal layer we should forward back */\n\tint templayer;\n\t/*! \\brief As above, but to handle transitions (e.g., wait for keyframe) */\n\tint templayer_target;\n\t/*! \\brief How much time (in us, default 250000) without receiving packets will make us drop to the substream below */\n\tguint32 drop_trigger;\n\t/*! \\brief When we relayed the last packet (used to detect when substreams become unavailable) */\n\tgint64 last_relayed;\n\t/*! \\brief Whether the substream has changed after processing a packet */\n\tgboolean changed_substream;\n\t/*! \\brief Whether the temporal layer has changed after processing a packet */\n\tgboolean changed_temporal;\n\t/*! \\brief Whether we need to send the user a keyframe request (PLI) */\n\tgboolean need_pli;\n} janus_rtp_simulcasting_context;\n\n/*! \\brief Set (or reset) the context fields to their default values\n * @param[in] context The context to (re)set */\nvoid janus_rtp_simulcasting_context_reset(janus_rtp_simulcasting_context *context);\n\n/*! \\brief Helper method to prepare the simulcasting info (rids and/or SSRCs) from\n * the simulcast object the core passes to plugins for new PeerConnections\n * @param[in] simulcast JSON object containing SSRCs and rids\n * @param[in] rid_ext_id The rid RTP extension ID to set, if any\n * @param[in] ssrcs The list of simulcast SSRCs to update, if any\n * @param[in] rids The list of rids to update, if any (items will be allocated) */\nvoid janus_rtp_simulcasting_prepare(json_t *simulcast, int *rid_ext_id, uint32_t *ssrcs, char **rids);\n\n/*! \\brief Helper method to cleanup some or all of the simulcasting info\n * (rids and/or SSRCs) we may have prepared before via janus_rtp_simulcasting_prepare\n * @param[in] rid_ext_id The rid RTP extension ID to cleanup, if any\n * @param[in] ssrcs The list of simulcast SSRCs to cleanup, if any\n * @param[in] rids The list of rids to cleanup, if any (items will be freed and NULL-ed)\n * @param[in] rid_mutex A mutex that must be acquired before cleaning up, if any */\nvoid janus_rtp_simulcasting_cleanup(int *rid_ext_id, uint32_t *ssrcs, char **rids, janus_mutex *rid_mutex);\n\n/*! \\brief Process an RTP packet, and decide whether this should be relayed or not, updating the context accordingly\n * \\note Calling this method resets the \\c changed_substream , \\c changed_temporal and \\c need_pli\n * properties, and updates them according to the decisions made after processing the packet\n * @param[in] context The simulcasting context to use\n * @param[in] buf The RTP packet to process\n * @param[in] len The length of the RTP packet (header, extension and payload)\n * @param[in] dd_content The Dependency Descriptor RTP extension data, if available\n * @param[in] dd_len Length of the Dependency Descriptor data, if available\n * @param[in] ssrcs The simulcast SSRCs to refer to (may be updated if rids are involved)\n * @param[in] rids The simulcast rids to refer to, if any\n * @param[in] vcodec Video codec of the RTP payload\n * @param[in] sc RTP switching context to refer to, if any (only needed for VP8 and dropping temporal layers)\n * @param[in] rid_mutex A mutex that must be acquired before reading the rids array, if any\n * @returns TRUE if the packet should be relayed, FALSE if it should be dropped instead */\ngboolean janus_rtp_simulcasting_context_process_rtp(janus_rtp_simulcasting_context *context,\n\tchar *buf, int len, uint8_t *dd_content, int dd_len, uint32_t *ssrcs, char **rids,\n\tjanus_videocodec vcodec, janus_rtp_switching_context *sc, janus_mutex *rid_mutex);\n///@}\n\n/** @name Janus SVC processing methods\n */\n///@{\n/*! \\brief Helper struct for processing and tracking VP9-SVC streams */\ntypedef struct janus_rtp_svc_context {\n\t/*! \\brief Dependency Descriptor context, in case it's needed */\n\tstruct janus_av1_svc_context dd_context;\n\t/*! \\brief Which SVC spatial layer we should forward back */\n\tint spatial;\n\t/*! \\brief As above, but to handle transitions (e.g., wait for keyframe, or get this if available) */\n\tint spatial_target;\n\t/*! \\brief Which SVC temporal layer we should forward back */\n\tint temporal;\n\t/*! \\brief As above, but to handle transitions (e.g., wait for keyframe) */\n\tint temporal_target;\n\t/*! \\brief How much time (in us, default 250000) without receiving packets will make us drop to the substream below */\n\tguint32 drop_trigger;\n\t/*! \\brief When we relayed the last packet (used to detect when layers become unavailable) */\n\tgint64 last_spatial_layer[3];\n\t/*! \\brief Whether the spatial layer has changed after processing a packet */\n\tgboolean changed_spatial;\n\t/*! \\brief Whether the temporal layer has changed after processing a packet */\n\tgboolean changed_temporal;\n\t/*! \\brief Whether we need to send the user a keyframe request (PLI) */\n\tgboolean need_pli;\n} janus_rtp_svc_context;\n\n/*! \\brief Set (or reset) the context fields to their default values\n * @param[in] context The context to (re)set */\nvoid janus_rtp_svc_context_reset(janus_rtp_svc_context *context);\n\n/*! \\brief Process an RTP packet, and decide whether this should be relayed or not, updating the context accordingly\n * \\note Calling this method resets the \\c changed_spatial , \\c changed_temporal and \\c need_pli\n * properties, and updates them according to the decisions made after processing the packet\n * @param[in] context The VP9 SVC context to use\n * @param[in] buf The RTP packet to process\n * @param[in] len The length of the RTP packet (header, extension and payload)\n * @param[in] dd_content The Dependency Descriptor RTP extension data, if available\n * @param[in] dd_len Length of the Dependency Descriptor data, if available\n * @param[in] vcodec Video codec of the RTP payload\n * @param[in] info Parsed info on VP9-SVC, if any\n * @param[in] sc RTP switching context to refer to, if any\n * @returns TRUE if the packet should be relayed, FALSE if it should be dropped instead */\ngboolean janus_rtp_svc_context_process_rtp(janus_rtp_svc_context *context,\n\tchar *buf, int len, uint8_t *dd_content, int dd_len,\n\tjanus_videocodec vcodec, janus_vp9_svc_info *info, janus_rtp_switching_context *sc);\n///@}\n\n#endif\n"
  },
  {
    "path": "src/rtpfwd.c",
    "content": "/*! \\file    rtpfwd.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    RTP forwarders\n * \\details  Implementation of the so called RTP forwarders, that is an\n * helper mechanism that core and/or plugins can make use of to quickly\n * and simply forward RTP streams to a separate UDP address out of the\n * context of any signalling. Such a mechanism can be used, for instance,\n * for scalabiloty purposes, monitoring, or feeding external applications\n * with media traffic handled by Janus..\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#include \"rtpfwd.h\"\n#include \"rtcp.h\"\n#include \"utils.h\"\n\n/* Local resources */\nstatic janus_mutex rtpfwds_mutex = JANUS_MUTEX_INITIALIZER;\nstatic GHashTable *rtpfwds = NULL;\nstatic gboolean ipv6_disabled = FALSE;\n/* RTCP stuff */\nstatic GMainContext *rtcpfwd_ctx = NULL;\nstatic GMainLoop *rtcpfwd_loop = NULL;\nstatic GThread *rtcpfwd_thread = NULL;\nstatic void *janus_rtp_forwarder_rtcp_thread(void *data) {\n\tJANUS_LOG(LOG_VERB, \"Joining RTCP thread for RTP forwarders...\\n\");\n\t/* Run the main loop */\n\tg_main_loop_run(rtcpfwd_loop);\n\t/* When the loop ends, we're done */\n\tJANUS_LOG(LOG_VERB, \"Leaving RTCP thread for RTP forwarders...\\n\");\n\treturn NULL;\n}\n\n/* Static helper to quickly unref an RTP forwarder instance */\nstatic void janus_rtp_forwarder_unref(janus_rtp_forwarder *rf);\n/* Static helper to free an RTP forwarder instance when the reference goes to 0 */\nstatic void janus_rtp_forwarder_free(const janus_refcount *f_ref);\n\n/* \\brief RTP forwarders code initialization\n * @returns 0 in case of success, a negative integer on errors */\nint janus_rtp_forwarders_init(void) {\n\t/* Initialize the forwarders table and muted */\n\trtpfwds = g_hash_table_new_full(g_str_hash, g_str_equal,\n\t\t(GDestroyNotify)g_free, (GDestroyNotify)janus_rtp_forwarder_unref);\n\t/* Let's check if IPv6 is disabled, as we may need to know for forwarders */\n\tint fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);\n\tif(fd < 0) {\n\t\tipv6_disabled = TRUE;\n\t} else {\n\t\tint v6only = 0;\n\t\tif(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0)\n\t\t\tipv6_disabled = TRUE;\n\t}\n\tif(fd >= 0)\n\t\tclose(fd);\n\tif(ipv6_disabled) {\n\t\tJANUS_LOG(LOG_WARN, \"IPv6 disabled, will only create RTP forwarders to IPv4 addresses\\n\");\n\t}\n\t/* Spawn the thread for handling incoming RTCP packets from RTP forwarders, if any */\n\trtcpfwd_ctx = g_main_context_new();\n\trtcpfwd_loop = g_main_loop_new(rtcpfwd_ctx, FALSE);\n\tGError *error = NULL;\n\trtcpfwd_thread = g_thread_try_new(\"rtcpfwd\", janus_rtp_forwarder_rtcp_thread, NULL, &error);\n\tif(error != NULL) {\n\t\t/* We show the error but it's not fatal */\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the RTCP thread for RTP forwarders...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\t/* Donw */\n\treturn 0;\n}\n\n/* \\brief RTP forwarders code de-initialization */\nvoid janus_rtp_forwarders_deinit(void) {\n\t/* Stop the RTCP receiver thread */\n\tif(rtcpfwd_thread != NULL) {\n\t\tif(g_main_loop_is_running(rtcpfwd_loop)) {\n\t\t\tg_main_loop_quit(rtcpfwd_loop);\n\t\t\tg_main_context_wakeup(rtcpfwd_ctx);\n\t\t}\n\t\tg_thread_join(rtcpfwd_thread);\n\t\trtcpfwd_thread = NULL;\n\t}\n\t/* Get rid of the table */\n\tjanus_mutex_lock(&rtpfwds_mutex);\n\tg_hash_table_destroy(rtpfwds);\n\trtpfwds = NULL;\n\tjanus_mutex_unlock(&rtpfwds_mutex);\n}\n\n/* RTCP support in RTP forwarders */\ntypedef struct janus_rtcp_receiver {\n\tGSource parent;\n\tjanus_rtp_forwarder *rf;\n\tGDestroyNotify destroy;\n} janus_rtcp_receiver;\nstatic void janus_rtp_forwarder_rtcp_receive(janus_rtp_forwarder *rf) {\n\tchar buffer[1500];\n\tstruct sockaddr_storage remote_addr;\n\tsocklen_t addrlen = sizeof(remote_addr);\n\tint len = recvfrom(rf->rtcp_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&remote_addr, &addrlen);\n\tif(len > 0 && janus_is_rtcp(buffer, len)) {\n\t\tJANUS_LOG(LOG_HUGE, \"Got %s RTCP packet: %d bytes\\n\", rf->is_video ? \"video\" : \"audio\", len);\n\t\t/* Invoke the callback function for RTCP feedback, if any */\n\t\tif(rf->rtcp_callback)\n\t\t\trf->rtcp_callback(rf, buffer, len);\n\t}\n}\nstatic gboolean janus_rtp_forwarder_rtcp_prepare(GSource *source, gint *timeout) {\n\t*timeout = -1;\n\treturn FALSE;\n}\nstatic gboolean janus_rtp_forwarder_rtcp_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) {\n\tjanus_rtcp_receiver *rr = (janus_rtcp_receiver *)source;\n\t/* Receive the packet */\n\tif(rr)\n\t\tjanus_rtp_forwarder_rtcp_receive(rr->rf);\n\treturn G_SOURCE_CONTINUE;\n}\nstatic void janus_rtp_forwarder_rtcp_finalize(GSource *source) {\n\tjanus_rtcp_receiver *rr = (janus_rtcp_receiver *)source;\n\t/* Remove the reference to the forwarder */\n\tif(rr && rr->rf) {\n\t\tif(rr->rf->source) {\n\t\t\t//~ janus_publisher_stream_dereference_void(r->forward->source);\n\t\t\trr->rf->source = NULL;\n\t\t}\n\t\tjanus_rtp_forwarder_unref(rr->rf);\n\t}\n}\nstatic GSourceFuncs janus_rtp_forwarder_rtcp_funcs = {\n\tjanus_rtp_forwarder_rtcp_prepare,\n\tNULL,\n\tjanus_rtp_forwarder_rtcp_dispatch,\n\tjanus_rtp_forwarder_rtcp_finalize,\n\tNULL, NULL\n};\n\n/* Create a new forwarder */\njanus_rtp_forwarder *janus_rtp_forwarder_create(const char *ctx,\n\t\tuint32_t stream_id, int udp_fd, const char *host, int port,\n\t\tuint32_t ssrc, int pt, int srtp_suite, const char *srtp_crypto,\n\t\tgboolean simulcast, int substream, gboolean is_video, gboolean is_data) {\n\tjanus_mutex_lock(&rtpfwds_mutex);\n\tif(ctx == NULL)\n\t\tctx = \"default\";\n\tchar id[1024];\n\tif(stream_id > 0) {\n\t\t/* Make sure the provided ID isn't already in use */\n\t\tg_snprintf(id, sizeof(id), \"%s-%\"SCNu32, ctx, stream_id);\n\t\tif(g_hash_table_lookup(rtpfwds, id) != NULL) {\n\t\t\tjanus_mutex_unlock(&rtpfwds_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"RTP forwarder with ID %\"SCNu32\" already exists in context '%s'\\n\",\n\t\t\t\tstream_id, ctx);\n\t\t\treturn NULL;\n\t\t}\n\t} else {\n\t\t/* Autogenerate an ID within the provided context */\n\t\tstream_id = janus_random_uint32();\n\t\tg_snprintf(id, sizeof(id), \"%s-%\"SCNu32, ctx, stream_id);\n\t\twhile(g_hash_table_lookup(rtpfwds, id)) {\n\t\t\tstream_id = janus_random_uint32();\n\t\t\tg_snprintf(id, sizeof(id), \"%s-%\"SCNu32, ctx, stream_id);\n\t\t}\n\t}\n\tjanus_rtp_forwarder *rf = g_malloc0(sizeof(janus_rtp_forwarder));\n\trf->udp_fd = udp_fd;\t/* FIXME Should we create one ourselves, if not provided? */\n\t/* RTCP may be added later */\n\trf->rtcp_fd = -1;\n\trf->local_rtcp_port = 0;\n\trf->remote_rtcp_port = 0;\n\t/* First of all, let's check if we need to setup an SRTP forwarder */\n\tif(!is_data && srtp_suite > 0 && srtp_crypto != NULL) {\n\t\t/* Base64 decode the crypto string and set it as the SRTP context */\n\t\tgsize len = 0;\n\t\tguchar *decoded = g_base64_decode(srtp_crypto, &len);\n\t\tif(len < SRTP_MASTER_LENGTH) {\n\t\t\tjanus_mutex_unlock(&rtpfwds_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid SRTP crypto (%s)\\n\", srtp_crypto);\n\t\t\tg_free(decoded);\n\t\t\tg_free(rf);\n\t\t\treturn NULL;\n\t\t}\n\t\t/* Set SRTP policy */\n\t\tsrtp_policy_t *policy = &rf->srtp_policy;\n\t\tsrtp_crypto_policy_set_rtp_default(&(policy->rtp));\n\t\tif(srtp_suite == 32) {\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(policy->rtp));\n\t\t} else if(srtp_suite == 80) {\n\t\t\tsrtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(policy->rtp));\n\t\t}\n\t\tpolicy->ssrc.type = ssrc_any_outbound;\n\t\tpolicy->key = decoded;\n\t\tpolicy->next = NULL;\n\t\t/* Create SRTP context */\n\t\tsrtp_err_status_t res = srtp_create(&rf->srtp_ctx, policy);\n\t\tif(res != srtp_err_status_ok) {\n\t\t\t/* Something went wrong... */\n\t\t\tjanus_mutex_unlock(&rtpfwds_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating forwarder SRTP session: %d (%s)\\n\", res, janus_srtp_error_str(res));\n\t\t\tg_free(decoded);\n\t\t\tpolicy->key = NULL;\n\t\t\tg_free(rf);\n\t\t\treturn NULL;\n\t\t}\n\t\trf->is_srtp = TRUE;\n\t}\n\trf->is_video = is_video;\n\trf->payload_type = pt;\n\trf->ssrc = ssrc;\n\trf->substream = substream;\n\trf->is_data = is_data;\n\t/* Check if the host address is IPv4 or IPv6 */\n\tif(strstr(host, \":\") != NULL) {\n\t\trf->serv_addr6.sin6_family = AF_INET6;\n\t\tinet_pton(AF_INET6, host, &(rf->serv_addr6.sin6_addr));\n\t\trf->serv_addr6.sin6_port = htons(port);\n\t} else {\n\t\trf->serv_addr.sin_family = AF_INET;\n\t\tinet_pton(AF_INET, host, &(rf->serv_addr.sin_addr));\n\t\trf->serv_addr.sin_port = htons(port);\n\t}\n\tif(is_video && simulcast) {\n\t\trf->simulcast = TRUE;\n\t\tjanus_rtp_switching_context_reset(&rf->rtp_context);\n\t\tjanus_rtp_simulcasting_context_reset(&rf->sim_context);\n\t\trf->sim_context.substream_target = 2;\n\t\trf->sim_context.templayer_target = 2;\n\t}\n\tjanus_refcount_init(&rf->ref, janus_rtp_forwarder_free);\n\trf->context = g_strdup(ctx);\n\trf->stream_id = stream_id;\n\tjanus_refcount_increase(&rf->ref);\n\tg_hash_table_insert(rtpfwds, g_strdup(id), rf);\n\tjanus_mutex_unlock(&rtpfwds_mutex);\n\t/* Done */\n\treturn rf;\n}\n\n/* Add RTCP support to an existing RTP forwarder */\nint janus_rtp_forwarder_add_rtcp(janus_rtp_forwarder *rf, int rtcp_port,\n\t\tvoid (*rtcp_callback)(janus_rtp_forwarder *rf, char *buffer, int len)) {\n\tif(rf == NULL || g_atomic_int_get(&rf->destroyed) || rf->rtcp_fd > 0 || rtcp_port < 1 || rf->is_data)\n\t\treturn -1;\n\t/* Bind to a port for RTCP */\n\tuint16_t local_rtcp_port = 0;\n\tint fd = socket(!ipv6_disabled ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n\tif(fd < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error creating RTCP socket for new RTP forwarder... %d (%s)\\n\",\n\t\t\terrno, g_strerror(errno));\n\t\treturn -4;\n\t}\n\tstruct sockaddr *address = NULL;\n\tstruct sockaddr_in addr4 = { 0 };\n\tstruct sockaddr_in6 addr6 = { 0 };\n\tsocklen_t len = 0;\n\tif(!ipv6_disabled) {\n\t\t/* Configure the socket so that it can be used both on IPv4 and IPv6 */\n\t\tint v6only = 0;\n\t\tif(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error configuring RTCP socket for new RTP forwarder... %d (%s)\\n\",\n\t\t\t\terrno, g_strerror(errno));\n\t\t\tclose(fd);\n\t\t\treturn -5;\n\t\t}\n\t\tlen = sizeof(addr6);\n\t\taddr6.sin6_family = AF_INET6;\n\t\taddr6.sin6_port = htons(0);\t\t/* The RTCP port we received is the remote one */\n\t\taddr6.sin6_addr = in6addr_any;\n\t\taddress = (struct sockaddr *)&addr6;\n\t} else {\n\t\t/* IPv6 is disabled, only do IPv4 */\n\t\tlen = sizeof(addr4);\n\t\taddr4.sin_family = AF_INET;\n\t\taddr4.sin_port = htons(0);\t\t/* The RTCP port we received is the remote one */\n\t\taddr4.sin_addr.s_addr = INADDR_ANY;\n\t\taddress = (struct sockaddr *)&addr4;\n\t}\n\tif(bind(fd, (struct sockaddr *)address, len) < 0 ||\n\t\t\tgetsockname(fd, (struct sockaddr *)address, &len) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error binding RTCP socket for new RTP forwarder... %d (%s)\\n\",\n\t\t\terrno, g_strerror(errno));\n\t\tclose(fd);\n\t\treturn -6;\n\t}\n\tlocal_rtcp_port = ntohs(!ipv6_disabled ? addr6.sin6_port : addr4.sin_port);\n\tJANUS_LOG(LOG_HUGE, \"Bound RTP forwarder's local %s RTCP port: %\"SCNu16\"\\n\",\n\t\trf->is_video ? \"video\" : \"audio\", local_rtcp_port);\n\t/* Update the forwarder, and create a source for the loop */\n\trf->rtcp_fd = fd;\n\trf->remote_rtcp_port = rtcp_port;\n\trf->local_rtcp_port = local_rtcp_port;\n\trf->rtcp_callback = rtcp_callback;\n\trf->rtcp_recv = g_source_new(&janus_rtp_forwarder_rtcp_funcs, sizeof(janus_rtcp_receiver));\n\tjanus_rtcp_receiver *rr = (janus_rtcp_receiver *)rf->rtcp_recv;\n\tjanus_refcount_increase(&rf->ref);\n\trr->rf = rf;\n\tg_source_set_priority(rf->rtcp_recv, G_PRIORITY_DEFAULT);\n\tg_source_add_unix_fd(rf->rtcp_recv, fd, G_IO_IN | G_IO_ERR);\n\tg_source_attach((GSource *)rf->rtcp_recv, rtcpfwd_ctx);\n\t/* Send a couple of empty RTP packets to the remote port to do latching */\n\tJANUS_LOG(LOG_HUGE, \"Latching to remote %s RTCP port: %\"SCNu16\"\\n\",\n\t\trf->is_video ? \"video\" : \"audio\", local_rtcp_port);\n\tsocklen_t addrlen = 0;\n\tif(rf->serv_addr.sin_family == AF_INET) {\n\t\taddr4.sin_family = AF_INET;\n\t\taddr4.sin_addr.s_addr = rf->serv_addr.sin_addr.s_addr;\n\t\taddr4.sin_port = htons(rf->remote_rtcp_port);\n\t\taddress = (struct sockaddr *)&addr4;\n\t\taddrlen = sizeof(addr4);\n\t} else {\n\t\taddr6.sin6_family = AF_INET6;\n\t\tmemcpy(&addr6.sin6_addr, &rf->serv_addr6.sin6_addr, sizeof(struct in6_addr));\n\t\taddr6.sin6_port = htons(rf->remote_rtcp_port);\n\t\taddress = (struct sockaddr *)&addr6;\n\t\taddrlen = sizeof(addr6);\n\t}\n\tjanus_rtp_header rtp = { 0 };\n\trtp.version = 2;\n\t(void)sendto(fd, &rtp, 12, 0, address, addrlen);\n\t(void)sendto(fd, &rtp, 12, 0, address, addrlen);\n\t/* Done */\n\treturn 0;\n}\n\n/* Simplified frontend to the forwarder function */\nvoid janus_rtp_forwarder_send_rtp(janus_rtp_forwarder *rf, char *buffer, int len, int substream) {\n\tjanus_rtp_forwarder_send_rtp_full(rf, buffer, len, substream, NULL, NULL, JANUS_VIDEOCODEC_NONE, NULL);\n}\n\n/* Helper function to forward an RTP packet within the context of a forwarder */\nvoid janus_rtp_forwarder_send_rtp_full(janus_rtp_forwarder *rf, char *buffer, int len, int substream,\n\t\tuint32_t *ssrcs, char **rids, janus_videocodec vcodec, janus_mutex *rid_mutex) {\n\tif(!rf || g_atomic_int_get(&rf->destroyed) || !buffer || !janus_is_rtp(buffer, len))\n\t\treturn;\n\t/* Access the RTP header */\n\tjanus_rtp_header *rtp = (janus_rtp_header *)buffer;\n\t/* Backup the RTP header info, as we may rewrite part of it */\n\tuint32_t seq_number = ntohs(rtp->seq_number);\n\tuint32_t timestamp = ntohl(rtp->timestamp);\n\tint pt = rtp->type;\n\tuint32_t ssrc = ntohl(rtp->ssrc);\n\t/* First of all, check if we're simulcasting and if we need to forward or ignore this frame */\n\tif(rf->is_video && !rf->simulcast && rf->substream != substream) {\n\t\t/* We're being asked to forward a specific substream, and it's not it */\n\t\treturn;\n\t}\n\tif(rf->is_video && rf->simulcast) {\n\t\t/* This is video and we're simulcasting, check if we need to forward this frame */\n\t\tif(!janus_rtp_simulcasting_context_process_rtp(&rf->sim_context,\n\t\t\t\tbuffer, len, NULL, 0, ssrcs, rids, vcodec, &rf->rtp_context, rid_mutex)) {\n\t\t\t/* There was an error processing simulcasting for this packet */\n\t\t\treturn;\n\t\t}\n\t\tjanus_rtp_header_update(rtp, &rf->rtp_context, TRUE, 0);\n\t\t/* By default we use a fixed SSRC (it may be overwritten later) */\n\t\trtp->ssrc = htonl(rf->stream_id);\n\t}\n\t/* Check if payload type and/or SSRC need to be overwritten for this forwarder */\n\tif(rf->payload_type > 0)\n\t\trtp->type = rf->payload_type;\n\tif(rf->ssrc > 0)\n\t\trtp->ssrc = htonl(rf->ssrc);\n\t/* Check if this is an RTP or SRTP forwarder */\n\tif(!rf->is_srtp) {\n\t\t/* Plain RTP */\n\t\tstruct sockaddr *address = (rf->serv_addr.sin_family == AF_INET ?\n\t\t\t(struct sockaddr *)&rf->serv_addr : (struct sockaddr *)&rf->serv_addr6);\n\t\tsize_t addrlen = (rf->serv_addr.sin_family == AF_INET ? sizeof(rf->serv_addr) : sizeof(rf->serv_addr6));\n\t\tif(sendto(rf->udp_fd, buffer, len, 0, address, addrlen) < 0) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"Error forwarding RTP %s packet... %s (len=%d)...\\n\",\n\t\t\t\t(rf->is_video ? \"video\" : \"audio\"), g_strerror(errno), len);\n\t\t}\n\t} else {\n\t\t/* SRTP: encrypt the packet before sending it */\n\t\tchar sbuf[1500];\n\t\tmemcpy(sbuf, buffer, len);\n\t\tint protected = len;\n\t\tint res = srtp_protect(rf->srtp_ctx, sbuf, &protected);\n\t\tif(res != srtp_err_status_ok) {\n\t\t\tjanus_rtp_header *header = (janus_rtp_header *)sbuf;\n\t\t\tguint32 timestamp = ntohl(header->timestamp);\n\t\t\tguint16 seq = ntohs(header->seq_number);\n\t\t\tJANUS_LOG(LOG_ERR, \"Error encrypting %s packet... %s (len=%d-->%d, ts=%\"SCNu32\", seq=%\"SCNu16\")...\\n\",\n\t\t\t\t(rf->is_video ? \"Video\" : \"Audio\"), janus_srtp_error_str(res), len, protected, timestamp, seq);\n\t\t} else {\n\t\t\tstruct sockaddr *address = (rf->serv_addr.sin_family == AF_INET ?\n\t\t\t\t(struct sockaddr *)&rf->serv_addr : (struct sockaddr *)&rf->serv_addr6);\n\t\t\tsize_t addrlen = (rf->serv_addr.sin_family == AF_INET ? sizeof(rf->serv_addr) : sizeof(rf->serv_addr6));\n\t\t\tif(sendto(rf->udp_fd, sbuf, protected, 0, address, addrlen) < 0) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Error forwarding SRTP %s packet... %s (len=%d)...\\n\",\n\t\t\t\t\t(rf->is_video ? \"video\" : \"audio\"), g_strerror(errno), protected);\n\t\t\t}\n\t\t}\n\t}\n\t/* Restore original values of the RTP payload before returning */\n\trtp->type = pt;\n\trtp->ssrc = htonl(ssrc);\n\trtp->timestamp = htonl(timestamp);\n\trtp->seq_number = htons(seq_number);\n}\n\n/* Mark an RTP forwarder instance as destroyed */\nvoid janus_rtp_forwarder_destroy(janus_rtp_forwarder *rf) {\n\tif(rf && g_atomic_int_compare_and_exchange(&rf->destroyed, 0, 1)) {\n\t\tif(rf->rtcp_fd > -1 && rf->rtcp_recv != NULL) {\n\t\t\tg_source_destroy(rf->rtcp_recv);\n\t\t\tg_source_unref(rf->rtcp_recv);\n\t\t}\n\t\tchar id[1024];\n\t\tg_snprintf(id, sizeof(id), \"%s-%\"SCNu32, rf->context, rf->stream_id);\n\t\tjanus_mutex_lock(&rtpfwds_mutex);\n\t\tif(rtpfwds != NULL)\n\t\t\tg_hash_table_remove(rtpfwds, id);\n\t\tjanus_mutex_unlock(&rtpfwds_mutex);\n\t\tjanus_refcount_decrease(&rf->ref);\n\t}\n}\n\n/* Static helper to quickly unref an RTP forwarder instance */\nstatic void janus_rtp_forwarder_unref(janus_rtp_forwarder *rf) {\n\tif(rf)\n\t\tjanus_refcount_decrease(&rf->ref);\n}\n\n/* Static helper to free an RTP forwarder instance when the reference goes to 0 */\nstatic void janus_rtp_forwarder_free(const janus_refcount *f_ref) {\n\tjanus_rtp_forwarder *rf = janus_refcount_containerof(f_ref, janus_rtp_forwarder, ref);\n\tif(rf->rtcp_fd > -1)\n\t\tclose(rf->rtcp_fd);\n\tif(rf->is_srtp) {\n\t\tsrtp_dealloc(rf->srtp_ctx);\n\t\tg_free(rf->srtp_policy.key);\n\t}\n\tg_free(rf->context);\n\tg_free(rf->metadata);\n\tg_free(rf);\n}\n"
  },
  {
    "path": "src/rtpfwd.h",
    "content": "/*! \\file    rtpfwd.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    RTP forwarders (headers)\n * \\details  Implementation of the so called RTP forwarders, that is an\n * helper mechanism that core and/or plugins can make use of to quickly\n * and simply forward RTP streams to a separate UDP address out of the\n * context of any signalling. Such a mechanism can be used, for instance,\n * for scalabiloty purposes, monitoring, or feeding external applications\n * with media traffic handled by Janus..\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_RTPFWD_H\n#define JANUS_RTPFWD_H\n\n#include \"rtp.h\"\n#include \"rtpsrtp.h\"\n\n\n/*! \\brief RTP forwarders code initialization\n * @returns 0 in case of success, a negative integer on errors */\nint janus_rtp_forwarders_init(void);\n/*! \\brief RTP forwarders code de-initialization */\nvoid janus_rtp_forwarders_deinit(void);\n\n/*! \\brief Helper struct for implementing RTP forwarders */\ntypedef struct janus_rtp_forwarder {\n\t/* \\brief Opaque pointer to the owner of this forwarder */\n\tvoid *source;\n\t/* \\brief Context of the forwarder */\n\tchar *context;\n\t/* \\brief Unique ID (within the context) of the forwarder */\n\tuint32_t stream_id;\n\t/* \\brief Socket used for sending RTP packets */\n\tint udp_fd;\n\t/* \\brief Whether this is a video forwarder */\n\tgboolean is_video;\n\t/* \\brief Whether this is an audio forwarder */\n\tgboolean is_data;\n\t/* \\brief SSRC to put in forwarded RTP packets */\n\tuint32_t ssrc;\n\t/* \\brief Payload type to put in forwarded RTP packets */\n\tint payload_type;\n\t/* \\brief Substream to forward, in case this is part of a simulcast stream */\n\tint substream;\n\t/* \\brief Recipient address (IPv4) */\n\tstruct sockaddr_in serv_addr;\n\t/* \\brief Recipient address (IPv6) */\n\tstruct sockaddr_in6 serv_addr6;\n\t/* \\brief RTCP socket, if needed */\n\tint rtcp_fd;\n\t/* \\brief RTCP local and remote ports, if needed */\n\tuint16_t local_rtcp_port, remote_rtcp_port;\n\t/* \\brief Callback to invoke when receiving RTCP messages, if any */\n\tvoid (*rtcp_callback)(struct janus_rtp_forwarder *rf, char *buffer, int len);\n\t/* \\brief RTCP GSource, if needed */\n\tGSource *rtcp_recv;\n\t/* \\brief Whether simulcast automatic selection is enabled for this forwarder */\n\tgboolean simulcast;\n\t/* \\brief RTP switching context, if needed */\n\tjanus_rtp_switching_context rtp_context;\n\t/* \\brief Simulcast context, if needed */\n\tjanus_rtp_simulcasting_context sim_context;\n\t/* \\brief Whether SRTP is enabled for this forwarder */\n\tgboolean is_srtp;\n\t/* \\brief The SRTP context, in case SRTP is enabled */\n\tsrtp_t srtp_ctx;\n\t/* \\brief The SRTP policy, in case SRTP is enabled */\n\tsrtp_policy_t srtp_policy;\n\t/* \\brief Opaque metadata property, in case it's useful to the owner\n\t * \\note This can be anything (e.g., a string, an allocated struct, etc.),\n\t * as long as it can be freed with a single call to g_free(), as\n\t * that's all that will be done when getting rid of the forwarder */\n\tvoid *metadata;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_rtp_forwarder;\n/*! \\brief Helper method to create a new janus_rtp_forwarder instance\n * @param[in] ctx The context of this forwarder (e.g., the plugin name)\n * @param[in] stream_id The unique forwarder ID to assign as part of the context (0=autogenerate)\n * @param[in] udp_fd The socket to use for sending RTP packets\n * @param[in] host The address to forward the RTP packets to\n * @param[in] port The port to forward the RTP packets to\n * @param[in] ssrc The SSRC to put in outgoing RTP packets\n * @param[in] pt The payload type to put in outgoing RTP packets\n * @param[in] srtp_suite In case SRTP must be enabled, the SRTP suite to use\n * @param[in] srtp_crypto In case SRTP must be enabled, the base64-encoded SRTP crypto material to use\n * @param[in] simulcast Whether the RTP forwarder should act as a simulcast viewer\n * \t\t(meaning it will only forward the highest quality available substream)\n * @param[in] substream In case we want to forward a specific simulcast substream, which substream it is\n * \t\\note Do NOT mix the simulcast and substream properties, as they implement different behaviours\n * @param[in] is_video Whether this a video forwarder\n * @param[in] is_data Whether this a data channel forwarder\n * @returns A pointer to a valid janus_rtp_forwarder instance, if successful, NULL otherwise */\njanus_rtp_forwarder *janus_rtp_forwarder_create(const char *ctx,\n\tuint32_t stream_id, int udp_fd, const char *host, int port,\n\tuint32_t ssrc, int pt, int srtp_suite, const char *srtp_crypto,\n\tgboolean simulcast, int substream, gboolean is_video, gboolean is_data);\n/*! \\brief Helper method to add RTCP support to an existing forwarder\n * @note Notice that only a single RTCP handler can be added to a forwarder,\n * and once added it cannot be removed until the forwarder is destroyed\n * @param[in] rf The janus_rtp_forwarder instance to add RTCP to\n * @param[in] rtcp_port The port to latch to for RTCP purposes\n * @param[in] rtcp_callback The function to invoke when RTCP feedback is received\n * @returns 0 if successful, a negative integer otherwise */\nint janus_rtp_forwarder_add_rtcp(janus_rtp_forwarder *rf, int rtcp_port,\n\tvoid (*rtcp_callback)(janus_rtp_forwarder *rf, char *buffer, int len));\n/*! \\brief Helper method to forward an RTP packet within the context of a forwarder\n * @note This is equivalent to calling janus_rtp_forwarder_send_rtp_full\n * with all the extra arguments that are usually not required set to NULL\n * @param[in] rf The janus_rtp_forwarder instance to use\n * @param[in] buffer The RTP packet buffer\n * @param[in] len The length of the RTP packet buffer\n * @param[in] substream In case the forwarder is relaying a single simulcast\n * \t\tsubstream, the substream the packet belongs to (pass -1 to ignore) */\nvoid janus_rtp_forwarder_send_rtp(janus_rtp_forwarder *rf, char *buffer, int len, int substream);\n/*! \\brief Extended version of janus_rtp_forwarder_send_rtp, to be used when the forwarder\n * is configured to act as a simulcast receiver, and so will call janus_rtp_simulcasting_context_process_rtp\n * @param[in] rf The janus_rtp_forwarder instance to use\n * @param[in] buffer The RTP packet buffer\n * @param[in] len The length of the RTP packet buffer\n * @param[in] substream In case the forwarder is relaying a single simulcast\n * \t\tsubstream, the substream the packet belongs to (pass -1 to ignore)\n * @param[in] ssrcs The simulcast SSRCs to refer to (may be updated if rids are involved)\n * @param[in] rids The simulcast rids to refer to, if any\n * @param[in] vcodec Video codec of the RTP payload\n * @param[in] rid_mutex A mutex that must be acquired before reading the rids array, if any */\nvoid janus_rtp_forwarder_send_rtp_full(janus_rtp_forwarder *rf, char *buffer, int len, int substream,\n\tuint32_t *ssrcs, char **rids, janus_videocodec vcodec, janus_mutex *rid_mutex);\n/*! \\brief Helper method to free a janus_rtp_forwarder instance\n * @param[in] rf The janus_rtp_forwarder instance to free */\nvoid janus_rtp_forwarder_destroy(janus_rtp_forwarder *rf);\n\n#endif\n"
  },
  {
    "path": "src/rtpsrtp.h",
    "content": "/*! \\file    rtpsrtp.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    SRTP definitions (headers)\n * \\details  Definitions of the SRTP usage. This header tries to abstract\n * the differences there may be between libsrtp and libsrtp2, with respect\n * to the structs and defines (e.g., errors), plus adding some helpers.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_RTPSRTP_H\n#define JANUS_RTPSRTP_H\n\n#ifdef HAVE_SRTP_2\n#include <srtp2/srtp.h>\n#include <openssl/rand.h>\n#include <openssl/err.h>\n#include <openssl/ssl.h>\n#include <openssl/srtp.h>\nint srtp_crypto_get_random(uint8_t *key, int len);\n#else\n#include <srtp/srtp.h>\n#include <srtp/crypto_kernel.h>\n#define srtp_err_status_t err_status_t\n#define srtp_err_status_ok err_status_ok\n#define srtp_err_status_replay_fail err_status_replay_fail\n#define srtp_err_status_replay_old err_status_replay_old\n#define srtp_crypto_policy_set_rtp_default crypto_policy_set_rtp_default\n#define srtp_crypto_policy_set_rtcp_default crypto_policy_set_rtcp_default\n#define srtp_crypto_policy_set_aes_cm_128_hmac_sha1_32 crypto_policy_set_aes_cm_128_hmac_sha1_32\n#define srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80 crypto_policy_set_aes_cm_128_hmac_sha1_80\n#define srtp_crypto_policy_set_aes_gcm_256_16_auth crypto_policy_set_aes_gcm_256_16_auth\n#define srtp_crypto_policy_set_aes_gcm_128_16_auth crypto_policy_set_aes_gcm_128_16_auth\n#define srtp_crypto_get_random crypto_get_random\n#endif\n\n/* SRTP stuff (http://tools.ietf.org/html/rfc3711) */\n#define SRTP_MASTER_KEY_LENGTH\t16\n#define SRTP_MASTER_SALT_LENGTH\t14\n#define SRTP_MASTER_LENGTH (SRTP_MASTER_KEY_LENGTH + SRTP_MASTER_SALT_LENGTH)\n/* AES-GCM stuff (http://tools.ietf.org/html/rfc7714) */\n#define SRTP_AESGCM128_MASTER_KEY_LENGTH\t16\n#define SRTP_AESGCM128_MASTER_SALT_LENGTH\t12\n#define SRTP_AESGCM128_MASTER_LENGTH (SRTP_AESGCM128_MASTER_KEY_LENGTH + SRTP_AESGCM128_MASTER_SALT_LENGTH)\n#define SRTP_AESGCM256_MASTER_KEY_LENGTH\t32\n#define SRTP_AESGCM256_MASTER_SALT_LENGTH\t12\n#define SRTP_AESGCM256_MASTER_LENGTH (SRTP_AESGCM256_MASTER_KEY_LENGTH + SRTP_AESGCM256_MASTER_SALT_LENGTH)\n\n/* SRTP profiles */\ntypedef enum janus_srtp_profile {\n\tJANUS_SRTP_AES128_CM_SHA1_32 = 1,\n\tJANUS_SRTP_AES128_CM_SHA1_80,\n\tJANUS_SRTP_AEAD_AES_128_GCM,\n\tJANUS_SRTP_AEAD_AES_256_GCM\n} janus_srtp_profile;\n\n#ifndef SRTP_AEAD_AES_256_GCM\n\t#undef HAVE_SRTP_AESGCM\n#endif\n\n/*! \\brief Helper method to get a string representation of a libsrtp error code\n * @param[in] error The libsrtp error code\n * @returns A string representation of the error code */\nconst char *janus_srtp_error_str(int error);\n\n#endif\n"
  },
  {
    "path": "src/sctp.c",
    "content": "/*! \\file    sctp.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    SCTP processing for data channels\n * \\details  Implementation (based on libusrsctp) of the SCTP Data Channels.\n * The code takes care of the SCTP association between peers and the server,\n * and allows for sending and receiving text messages (binary stuff yet to\n * be implemented) after that.\n *\n * \\note Right now, the code is heavily based on the rtcweb.c sample code\n * provided in the \\c usrsctp library code, and as such the copyright notice\n * that appears at the beginning of that code is ideally present here as\n * well: http://code.google.com/p/sctp-refimpl/source/browse/trunk/KERN/usrsctp/programs/rtcweb.c\n *\n * \\note If you want/need to debug SCTP messages for any reason, you can\n * do so by uncommenting the definition of \\c DEBUG_SCTP in sctp.h. This\n * will force this code to save all the SCTP messages being exchanged to\n * a separate file for each session. You must choose what folder to save\n * these files in by modifying the \\c debug_folder variable. Once a file\n * has been saved, you need to process it using the \\c text2pcap tool\n * that is usually shipped with Wireshark, e.g.:\n *\n\\verbatim\ncd /path/to/sctp\n/usr/sbin/text2pcap -n -l 248 -D -t '%H:%M:%S.' sctp-debug-XYZ.txt sctp-debug-XYZ.pcapng\n/usr/sbin/wireshark sctp-debug-XYZ.pcapng\n\\endverbatim\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifdef HAVE_SCTP\n\n#include \"sctp.h\"\n#include \"dtls.h\"\n#include \"janus.h\"\n#include \"ice.h\"\n#include \"debug.h\"\n\n#ifdef DEBUG_SCTP\n/* If we're debugging the SCTP messaging, save the files here (edit path) */\nconst char *debug_folder = \"/path/to/sctp\";\n#endif\n\nstatic const char *default_label = \"JanusDataChannel\";\n\n\n#define SCTP_MAX_PACKET_SIZE (1<<16)\n\n/* Events we're interested in */\nstatic uint16_t event_types[] = {\n\tSCTP_ASSOC_CHANGE,\n\tSCTP_PEER_ADDR_CHANGE,\n\tSCTP_REMOTE_ERROR,\n\tSCTP_SHUTDOWN_EVENT,\n\tSCTP_ADAPTATION_INDICATION,\n\tSCTP_SEND_FAILED_EVENT,\n\tSCTP_SENDER_DRY_EVENT,\n\tSCTP_STREAM_RESET_EVENT,\n\tSCTP_STREAM_CHANGE_EVENT\n};\n\n/* Buffered message (in case we can't send right away) */\ntypedef struct janus_sctp_pending_message {\n\tuint16_t id;\n\tgboolean textdata;\n\tchar *buf;\n\tsize_t len;\n} janus_sctp_pending_message;\nstatic janus_sctp_pending_message *janus_sctp_pending_message_create(uint16_t id, gboolean textdata, char *buf, size_t len) {\n\tjanus_sctp_pending_message *m = g_malloc(sizeof(janus_sctp_pending_message));\n\tm->id = id;\n\tm->textdata = textdata;\n\tif(buf != NULL && len > 0) {\n\t\tm->buf = g_malloc(len);\n\t\tmemcpy(m->buf, buf, len);\n\t\tm->len = len;\n\t} else {\n\t\tm->buf = NULL;\n\t\tm->len = 0;\n\t}\n\treturn m;\n}\nstatic void janus_sctp_pending_message_free(janus_sctp_pending_message *m) {\n\tif(m != NULL) {\n\t\tg_free(m->buf);\n\t\tg_free(m);\n\t}\n}\n\n/* usrsctp callbacks and methods */\nint janus_sctp_data_to_dtls(void *instance, void *buffer, size_t length, uint8_t tos, uint8_t set_df);\nstatic int janus_sctp_incoming_data(struct socket *sock, union sctp_sockstore addr, void *data, size_t datalen, struct sctp_rcvinfo rcv, int flags, void *ulp_info);\njanus_sctp_channel *janus_sctp_find_channel_by_stream(janus_sctp_association *sctp, uint16_t stream);\njanus_sctp_channel *janus_sctp_find_free_channel(janus_sctp_association *sctp);\nuint16_t janus_sctp_find_free_stream(janus_sctp_association *sctp);\nvoid janus_sctp_request_more_streams(janus_sctp_association *sctp);\nint janus_sctp_send_open_request_message(struct socket *sock, uint16_t stream, char *label, char *protocol, uint8_t unordered, uint16_t pr_policy, uint32_t pr_value);\nint janus_sctp_send_open_response_message(struct socket *sock, uint16_t stream);\nint janus_sctp_send_open_ack_message(struct socket *sock, uint16_t stream);\nvoid janus_sctp_send_deferred_messages(janus_sctp_association *sctp);\nint janus_sctp_open_channel(janus_sctp_association *sctp, char *label, char *protocol, uint8_t unordered, uint16_t pr_policy, uint32_t pr_value);\nint janus_sctp_send_text_or_binary(janus_sctp_association *sctp, uint16_t id, gboolean textdata, char *text, size_t length);\nvoid janus_sctp_reset_outgoing_stream(janus_sctp_association *sctp, uint16_t stream);\nvoid janus_sctp_send_outgoing_stream_reset(janus_sctp_association *sctp);\nint janus_sctp_close_channel(janus_sctp_association *sctp, uint16_t id);\nvoid janus_sctp_data_ready(janus_sctp_association *sctp);\nvoid janus_sctp_handle_open_request_message(janus_sctp_association *sctp, janus_datachannel_open_request *req, size_t length, uint16_t stream);\nvoid janus_sctp_handle_open_response_message(janus_sctp_association *sctp, janus_datachannel_open_response *rsp, size_t length, uint16_t stream);\nvoid janus_sctp_handle_open_ack_message(janus_sctp_association *sctp, janus_datachannel_ack *ack, size_t length, uint16_t stream);\nvoid janus_sctp_handle_unknown_message(char *msg, size_t length, uint16_t stream);\nvoid janus_sctp_handle_data_message(janus_sctp_association *sctp, gboolean textdata, char *buffer, size_t length, uint16_t stream);\nvoid janus_sctp_handle_message(janus_sctp_association *sctp, char *buffer, size_t length, uint32_t ppid, uint16_t stream, int flags);\nvoid janus_sctp_handle_association_change_event(struct sctp_assoc_change *sac);\nvoid janus_sctp_handle_peer_address_change_event(struct sctp_paddr_change *spc);\nvoid janus_sctp_handle_adaptation_indication(struct sctp_adaptation_event *sai);\nvoid janus_sctp_handle_shutdown_event(struct sctp_shutdown_event *sse);\nvoid janus_sctp_handle_stream_reset_event(janus_sctp_association *sctp, struct sctp_stream_reset_event *strrst);\nvoid janus_sctp_handle_remote_error_event(struct sctp_remote_error *sre);\nvoid janus_sctp_handle_send_failed_event(struct sctp_send_failed_event *ssfe);\nvoid janus_sctp_handle_notification(janus_sctp_association *sctp, union sctp_notification *notif, size_t n);\n\n/* We need to keep a map of associations with random IDs, as usrsctp will\n * use the pointer to our structures in the actual messages instead */\nstatic janus_mutex sctp_mutex = JANUS_MUTEX_INITIALIZER;\nstatic GHashTable *sctp_ids = NULL;\nstatic void janus_sctp_association_unref(janus_sctp_association *sctp);\n\n/* SCTP management code */\nstatic gboolean sctp_running;\nint janus_sctp_init(void) {\n\t/* Initialize the SCTP stack */\n\tusrsctp_init(0, janus_sctp_data_to_dtls, NULL);\n\tsctp_running = TRUE;\n\n#ifdef DEBUG_SCTP\n\tJANUS_LOG(LOG_WARN, \"SCTP debugging to files enabled: going to save them in %s\\n\", debug_folder);\n\tif(janus_mkdir(debug_folder, 0755) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Error creating folder %s, expect problems...\\n\", debug_folder);\n\t}\n#endif\n\n\t/* Create a map of local IDs too, to map them to our SCTP associations */\n\tsctp_ids = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_sctp_association_unref);\n\n\treturn 0;\n}\n\nvoid janus_sctp_deinit(void) {\n\tusrsctp_finish();\n\tsctp_running = FALSE;\n\tjanus_mutex_lock(&sctp_mutex);\n\tg_clear_pointer(&sctp_ids, g_hash_table_destroy);\n\tjanus_mutex_unlock(&sctp_mutex);\n}\n\nstatic void janus_sctp_association_unref(janus_sctp_association *sctp) {\n\tif(sctp)\n\t\tjanus_refcount_decrease(&sctp->ref);\n}\n\nstatic void janus_sctp_association_free(const janus_refcount *sctp_ref) {\n\tjanus_sctp_association *sctp = janus_refcount_containerof(sctp_ref, janus_sctp_association, ref);\n\t/* This association can be destroyed, free all the resources */\n\tjanus_refcount_decrease(&sctp->handle->ref);\n\tjanus_refcount_decrease(&sctp->dtls->ref);\n\tif(sctp->pending_messages != NULL)\n\t\tg_queue_free_full(sctp->pending_messages, (GDestroyNotify)janus_sctp_pending_message_free);\n#ifdef DEBUG_SCTP\n\tif(sctp->debug_dump != NULL)\n\t\tfclose(sctp->debug_dump);\n\tsctp->debug_dump = NULL;\n#endif\n\tg_free(sctp->buffer);\n\tg_free(sctp);\n\tsctp = NULL;\n}\n\njanus_sctp_association *janus_sctp_association_create(janus_dtls_srtp *dtls, janus_ice_handle *handle, uint16_t udp_port) {\n\tif(dtls == NULL || handle == NULL || udp_port == 0)\n\t\treturn NULL;\n\n\t/* usrsctp provides UDP encapsulation of SCTP, but we need these messages to\n\t * be encapsulated in DTLS and actually sent/received by libnice, and not by\n\t * usrsctp itself... as such, we make use of the AF_CONN approach */\n\n\tjanus_sctp_association *sctp = g_malloc0(sizeof(janus_sctp_association));\n\tjanus_refcount_init(&sctp->ref, janus_sctp_association_free);\n\tg_atomic_int_set(&sctp->destroyed, 0);\n\tsctp->dtls = dtls;\n\tjanus_refcount_increase(&dtls->ref);\n\tsctp->handle = handle;\n\tjanus_refcount_increase(&handle->ref);\n\tsctp->handle_id = handle->handle_id;\n\tsctp->local_port = 5000;\t/* FIXME We always use this one */\n\tsctp->remote_port = udp_port;\n\tsctp->buffer = NULL;\n\tsctp->buflen = 0;\n\tsctp->offset = 0;\n\tsctp->pending_messages = NULL;\n#ifdef DEBUG_SCTP\n\tsctp->debug_dump = NULL;\n#endif\n\n\tstruct socket *sock = NULL;\n\tunsigned int i = 0;\n\tstruct sockaddr_conn sconn = { 0 };\n\n\t/* Now go on with SCTP */\n\tjanus_sctp_channel *channel = NULL;\n\n\tfor(i = 0; i < NUMBER_OF_CHANNELS; i++) {\n\t\tchannel = &(sctp->channels[i]);\n\t\tchannel->id = i;\n\t\tchannel->label[0] = '\\0';\n\t\tchannel->state = DATA_CHANNEL_CLOSED;\n\t\tchannel->pr_policy = SCTP_PR_SCTP_NONE;\n\t\tchannel->pr_value = 0;\n\t\tchannel->stream = 0;\n\t\tchannel->unordered = 0;\n\t\tchannel->flags = 0;\n\t}\n\tfor(i = 0; i < NUMBER_OF_STREAMS; i++) {\n\t\tsctp->stream_channel[i] = NULL;\n\t\tsctp->stream_buffer[i] = 0;\n\t}\n\tsctp->stream_buffer_counter = 0;\n\n\t/* Create a unique ID to map locally: this is what we'll pass to\n\t * usrsctp_socket, which means that's what we'll get in callbacks\n\t * too: we can then use the map to retrieve the actual struct */\n\tjanus_mutex_lock(&sctp_mutex);\n\twhile(sctp->map_id == 0) {\n\t\tsctp->map_id = janus_random_uint32();\n\t\tif(g_hash_table_lookup(sctp_ids, GUINT_TO_POINTER(sctp->map_id)) != NULL) {\n\t\t\t/* ID already taken, try another one */\n\t\t\tsctp->map_id = 0;\n\t\t}\n\t}\n\tjanus_refcount_increase(&sctp->ref);\n\tg_hash_table_insert(sctp_ids, GUINT_TO_POINTER(sctp->map_id), sctp);\n\tjanus_mutex_unlock(&sctp_mutex);\n\n\tusrsctp_register_address(GUINT_TO_POINTER(sctp->map_id));\n\tusrsctp_sysctl_set_sctp_ecn_enable(0);\n\tif((sock = usrsctp_socket(AF_CONN, SOCK_STREAM, IPPROTO_SCTP, janus_sctp_incoming_data, NULL, 0,\n\t\t\tGUINT_TO_POINTER(sctp->map_id))) == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error creating usrsctp socket... (%d, %s)\\n\",\n\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\t/* Store the socket handle to make sure it is closed in any case if the association creation fails */\n\tsctp->sock = sock;\n\n\t/* Make the socket non-blocking. Connect, close, shutdown etc will not block\n\t * the thread waiting for the socket operation to complete. */\n\tif (usrsctp_set_non_blocking(sock, 1) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error setting socket to non-blocking... (%d, %s)\\n\",\n\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\t/* Set SO_LINGER */\n\tstruct linger linger_opt;\n\tlinger_opt.l_onoff = 1;\n\tlinger_opt.l_linger = 0;\n\tif(usrsctp_setsockopt(sock, SOL_SOCKET, SO_LINGER, &linger_opt, sizeof(linger_opt))) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] setsockopt error: SO_LINGER (%d, %s)\\n\",\n\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\t/* Allow resetting streams */\n\tstruct sctp_assoc_value av;\n\tav.assoc_id = SCTP_ALL_ASSOC;\n\tav.assoc_value = SCTP_ENABLE_RESET_STREAM_REQ | SCTP_ENABLE_CHANGE_ASSOC_REQ;\n\tif(usrsctp_setsockopt(sock, IPPROTO_SCTP, SCTP_ENABLE_STREAM_RESET, &av, sizeof(struct sctp_assoc_value)) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] setsockopt error: SCTP_ENABLE_STREAM_RESET (%d, %s)\\n\",\n\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\t/* Disable Nagle */\n\tuint32_t nodelay = 1;\n\tif(usrsctp_setsockopt(sock, IPPROTO_SCTP, SCTP_NODELAY, &nodelay, sizeof(nodelay))) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] setsockopt error: SCTP_NODELAY (%d, %s)\\n\",\n\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\t/* Enable the events of interest */\n\tstruct sctp_event event;\n\tmemset(&event, 0, sizeof(event));\n\tevent.se_assoc_id = SCTP_ALL_ASSOC;\n\tevent.se_on = 1;\n\tfor(i = 0; i < sizeof(event_types)/sizeof(uint16_t); i++) {\n\t\tevent.se_type = event_types[i];\n\t\tif(usrsctp_setsockopt(sock, IPPROTO_SCTP, SCTP_EVENT, &event, sizeof(event)) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] setsockopt error: SCTP_EVENT (%d, %s)\\n\",\n\t\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t\t\tjanus_sctp_association_destroy(sctp);\n\t\t\treturn NULL;\n\t\t}\n\t}\n\t/* Configure our INIT message */\n\tstruct sctp_initmsg initmsg;\n\tmemset(&initmsg, 0, sizeof(struct sctp_initmsg));\n\tinitmsg.sinit_num_ostreams = NUMBER_OF_STREAMS;\n\tinitmsg.sinit_max_instreams = NUMBER_OF_STREAMS;\n\tif(usrsctp_setsockopt(sock, IPPROTO_SCTP, SCTP_INITMSG, &initmsg, sizeof(struct sctp_initmsg)) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] setsockopt error: SCTP_INITMSG (%d, %s)\\n\",\n\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\t/* Bind our side of the communication, using AF_CONN as we're doing the actual delivery ourselves */\n\tmemset(&sconn, 0, sizeof(struct sockaddr_conn));\n\tsconn.sconn_family = AF_CONN;\n\tsconn.sconn_port = htons(sctp->local_port);\n\tsconn.sconn_addr = GUINT_TO_POINTER(sctp->map_id);\n\tif(usrsctp_bind(sock, (struct sockaddr *)&sconn, sizeof(struct sockaddr_conn)) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error binding client on port %\"SCNu16\" (%d, %s)\\n\",\n\t\t\tsctp->handle_id, sctp->local_port, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\n#ifdef DEBUG_SCTP\n\tchar debug_file[1024];\n\tg_snprintf(debug_file, 1024, \"%s/sctp-debug-%\"SCNu64\".txt\", debug_folder, sctp->handle_id);\n\tsctp->debug_dump = fopen(debug_file, \"wt\");\n#endif\n\n\t/* Operating as client */\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Connecting the SCTP association\\n\", sctp->handle_id);\n\tstruct sockaddr_conn rconn = { 0 };\n\tmemset(&rconn, 0, sizeof(struct sockaddr_conn));\n\trconn.sconn_family = AF_CONN;\n\trconn.sconn_port = htons(sctp->remote_port);\n\trconn.sconn_addr = GUINT_TO_POINTER(sctp->map_id);\n#ifdef HAVE_SCONN_LEN\n\trconn.sconn_len = sizeof(struct sockaddr_conn);\n#endif\n\tint res = usrsctp_connect(sock, (struct sockaddr *)&rconn, sizeof(struct sockaddr_conn));\n\tif(res < 0 && errno != EINPROGRESS) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Error connecting to SCTP server at port %\"SCNu16\" (%d, %s)\\n\",\n\t\t\tsctp->handle_id, sctp->remote_port, errno, g_strerror(errno));\n\t\tjanus_sctp_association_destroy(sctp);\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Connected to the DataChannel peer\\n\", sctp->handle_id);\n\treturn sctp;\n}\n\nvoid janus_sctp_association_destroy(janus_sctp_association *sctp) {\n\tif(sctp == NULL || !g_atomic_int_compare_and_exchange(&sctp->destroyed, 0, 1))\n\t\treturn;\n\n\tif(sctp->map_id != 0) {\n\t\tusrsctp_deregister_address(GUINT_TO_POINTER(sctp->map_id));\n\t\tjanus_mutex_lock(&sctp_mutex);\n\t\tg_hash_table_remove(sctp_ids, GUINT_TO_POINTER(sctp->map_id));\n\t\tjanus_mutex_unlock(&sctp_mutex);\n\t}\n\tif(sctp->sock != NULL) {\n\t\tusrsctp_shutdown(sctp->sock, SHUT_RDWR);\n\t\tusrsctp_close(sctp->sock);\n\t}\n\tjanus_refcount_decrease(&sctp->ref);\n}\n\nvoid janus_sctp_data_from_dtls(janus_sctp_association *sctp, char *buf, int len) {\n\tif(sctp == NULL || sctp->handle == NULL || buf == NULL || len <= 0)\n\t\treturn;\n\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Data from DTLS to SCTP stack: %d bytes\\n\", sctp->handle_id, len);\n#ifdef DEBUG_SCTP\n\tif(sctp->debug_dump != NULL) {\n\t\t/* Dump incoming message */\n\t\tchar *dump = usrsctp_dumppacket(buf, len, SCTP_DUMP_INBOUND);\n\t\tif(dump != NULL) {\n\t\t\tfwrite(dump, sizeof(char), strlen(dump), sctp->debug_dump);\n\t\t\tfflush(sctp->debug_dump);\n\t\t\tusrsctp_freedumpbuffer(dump);\n\t\t}\n\t}\n#endif\n\tusrsctp_conninput(GUINT_TO_POINTER(sctp->map_id), buf, len, 0);\n}\n\nint janus_sctp_data_to_dtls(void *instance, void *buffer, size_t length, uint8_t tos, uint8_t set_df) {\n\tjanus_mutex_lock(&sctp_mutex);\n\tjanus_sctp_association *sctp = (janus_sctp_association *)g_hash_table_lookup(sctp_ids, instance);\n\tjanus_mutex_unlock(&sctp_mutex);\n\tif(sctp == NULL || sctp->handle == NULL)\n\t\treturn -1;\n\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Data from SCTP to DTLS stack: %zu bytes\\n\", sctp->handle_id, length);\n#ifdef DEBUG_SCTP\n\tif(sctp->debug_dump != NULL) {\n\t\t/* Dump outgoing message */\n\t\tchar *dump = usrsctp_dumppacket(buffer, length, SCTP_DUMP_OUTBOUND);\n\t\tif(dump != NULL) {\n\t\t\tfwrite(dump, sizeof(char), strlen(dump), sctp->debug_dump);\n\t\t\tfflush(sctp->debug_dump);\n\t\t\tusrsctp_freedumpbuffer(dump);\n\t\t}\n\t}\n#endif\n\tjanus_ice_relay_sctp(sctp->handle, buffer, length);\n\treturn 0;\n}\n\nstatic int janus_sctp_incoming_data(struct socket *sock, union sctp_sockstore addr, void *data, size_t datalen, struct sctp_rcvinfo rcv, int flags, void *ulp_info) {\n\tjanus_mutex_lock(&sctp_mutex);\n\tjanus_sctp_association *sctp = (janus_sctp_association *)g_hash_table_lookup(sctp_ids, ulp_info);\n\tjanus_mutex_unlock(&sctp_mutex);\n\tif(sctp == NULL || sctp->dtls == NULL) {\n\t\tfree(data);\n\t\treturn 0;\n\t}\n\tif(data) {\n\t\tif(flags & MSG_NOTIFICATION) {\n\t\t\tjanus_sctp_handle_notification(sctp, (union sctp_notification *)data, datalen);\n\t\t} else {\n\t\t\tjanus_sctp_handle_message(sctp, data, datalen, ntohl(rcv.rcv_ppid), rcv.rcv_sid, flags);\n\t\t}\n\t\tfree(data);\n\t}\n\treturn 1;\n}\n\nvoid janus_sctp_send_data(janus_sctp_association *sctp, char *label, char *protocol, gboolean textdata, char *buf, int len) {\n\tif(sctp == NULL)\n\t\treturn;\n\n\tif(buf == NULL || len <= 0)\n\t\treturn;\n\tif(label == NULL)\n\t\tlabel = (char *)default_label;\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] SCTP data to send (label=%s, %d bytes) coming from a plugin.\\n\",\n\t\t  sctp->handle_id, label, len);\n\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Outgoing SCTP contents: %.*s\\n\",\n\t\t  sctp->handle_id, len, buf);\n\t/* FIXME Is there any open channel we can use? */\n\tint i = 0, found = 0;\n\tfor(i = 0; i < NUMBER_OF_CHANNELS; i++) {\n\t\tif(sctp->channels[i].state != DATA_CHANNEL_CLOSED && !strcmp(sctp->channels[i].label, label)) {\n\t\t\tfound = 1;\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- Using open channel %i\\n\", sctp->handle_id, i);\n\t\t\tbreak;\n\t\t}\n\t}\n\tif(!found) {\n\t\t/* There's no open channel, try opening one now */\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Creating channel '%s'...\\n\", sctp->handle_id, label);\n\t\tif(janus_sctp_open_channel(sctp, label, protocol, 0, 0, 0) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Couldn't open channel...\\n\", sctp->handle_id);\n\t\t\treturn;\n\t\t}\n\t\tfor(i = 0; i < NUMBER_OF_CHANNELS; i++) {\n\t\t\tif(sctp->channels[i].state != DATA_CHANNEL_CLOSED && !strcmp(sctp->channels[i].label, label)) {\n\t\t\t\tfound = 1;\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- Using open channel %i\\n\", sctp->handle_id, i);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif(!found) {\n\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Channel opened but not found?? giving up...\\n\", sctp->handle_id);\n\t\t\treturn;\n\t\t}\n\t}\n\t/* Send the data, whether it's text or binary */\n\tif(sctp->pending_messages != NULL && !g_queue_is_empty(sctp->pending_messages)) {\n\t\t/* We couldn't send all pending messages, queue the new one as well */\n\t\tif(buf != NULL && len > 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Couldn't send all pending messages, queueing new message\\n\",\n\t\t\t\tsctp->handle_id);\n\t\t\tjanus_sctp_pending_message *m = janus_sctp_pending_message_create(i, textdata, buf, len);\n\t\t\tif(sctp->pending_messages == NULL)\n\t\t\t\tsctp->pending_messages = g_queue_new();\n\t\t\tg_queue_push_tail(sctp->pending_messages, m);\n\t\t}\n\t\treturn;\n\t}\n\tint res = janus_sctp_send_text_or_binary(sctp, i, textdata, buf, len);\n\tif(res == -2) {\n\t\t/* Delivery failed with an EAGAIN, queue and retry later */\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Got EAGAIN when trying to send message on channel %d, retrying later\\n\",\n\t\t\tsctp->handle_id, i);\n\t\tjanus_sctp_pending_message *m = janus_sctp_pending_message_create(i, textdata, buf, len);\n\t\tif(sctp->pending_messages == NULL)\n\t\t\tsctp->pending_messages = g_queue_new();\n\t\tg_queue_push_tail(sctp->pending_messages, m);\n\t}\n}\n\n\n/* From now on, it's SCTP stuff */\njanus_sctp_channel *janus_sctp_find_channel_by_stream(janus_sctp_association *sctp, uint16_t stream) {\n\tif(sctp == NULL)\n\t\treturn NULL;\n\tif(stream < NUMBER_OF_STREAMS) {\n\t\treturn (sctp->stream_channel[stream]);\n\t} else {\n\t\treturn NULL;\n\t}\n}\n\njanus_sctp_channel *janus_sctp_find_free_channel(janus_sctp_association *sctp) {\n\tuint32_t i;\n\n\tfor(i = 0; i < NUMBER_OF_CHANNELS; i++) {\n\t\tif(sctp->channels[i].state == DATA_CHANNEL_CLOSED) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif(i == NUMBER_OF_CHANNELS) {\n\t\treturn NULL;\n\t} else {\n\t\treturn (&(sctp->channels[i]));\n\t}\n}\n\nuint16_t janus_sctp_find_free_stream(janus_sctp_association *sctp) {\n\tstruct sctp_status status;\n\tuint32_t i, limit;\n\tsocklen_t len;\n\n\tlen = (socklen_t)sizeof(struct sctp_status);\n\tif(usrsctp_getsockopt(sctp->sock, IPPROTO_SCTP, SCTP_STATUS, &status, &len) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] getsockopt error: SCTP_STATUS\\n\", sctp->handle_id);\n\t\treturn 0;\n\t}\n\tif(status.sstat_outstrms < NUMBER_OF_STREAMS) {\n\t\tlimit = status.sstat_outstrms;\n\t} else {\n\t\tlimit = NUMBER_OF_STREAMS;\n\t}\n\t/* stream id 0 is reserved */\n\tfor(i = 1; i < limit; i++) {\n\t\tif(sctp->stream_channel[i] == NULL) {\n\t\t\tbreak;\n\t\t}\n\t}\n\tif(i == limit) {\n\t\treturn 0;\n\t} else {\n\t\treturn ((uint16_t)i);\n\t}\n}\n\nvoid janus_sctp_request_more_streams(janus_sctp_association *sctp) {\n\tstruct sctp_status status;\n\tstruct sctp_add_streams sas;\n\tuint32_t i, streams_needed;\n\tsocklen_t len;\n\n\tstreams_needed = 0;\n\tfor(i = 0; i < NUMBER_OF_CHANNELS; i++) {\n\t\tif((sctp->channels[i].state == DATA_CHANNEL_CONNECTING) &&\n\t\t\t(sctp->channels[i].stream == 0)) {\n\t\t\tstreams_needed++;\n\t\t}\n\t}\n\tlen = (socklen_t)sizeof(struct sctp_status);\n\tif(usrsctp_getsockopt(sctp->sock, IPPROTO_SCTP, SCTP_STATUS, &status, &len) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] getsockopt error: SCTP_STATUS\\n\", sctp->handle_id);\n\t\treturn;\n\t}\n\tif(status.sstat_outstrms + streams_needed > NUMBER_OF_STREAMS) {\n\t\tstreams_needed = NUMBER_OF_STREAMS - status.sstat_outstrms;\n\t}\n\tif(streams_needed == 0) {\n\t\treturn;\n\t}\n\tmemset(&sas, 0, sizeof(struct sctp_add_streams));\n\tsas.sas_instrms = 0;\n\tsas.sas_outstrms = (uint16_t)streams_needed; /* XXX error handling */\n\tif(usrsctp_setsockopt(sctp->sock, IPPROTO_SCTP, SCTP_ADD_STREAMS, &sas, (socklen_t)sizeof(struct sctp_add_streams)) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] setsockopt error: SCTP_ADD_STREAMS\\n\", sctp->handle_id);\n\t}\n\treturn;\n}\n\nint janus_sctp_send_open_request_message(struct socket *sock, uint16_t stream, char *label, char *protocol, uint8_t unordered, uint16_t pr_policy, uint32_t pr_value) {\n\t/* XXX: This should be encoded in a better way */\n\tjanus_datachannel_open_request *req = NULL;\n\tstruct sctp_sndinfo sndinfo;\n\n\t/* Use the default label, if none was provided */\n\tif(label == NULL)\n\t\tlabel = (char *)default_label;\n\tsize_t label_size = strlen(label);\n\tsize_t protocol_size = protocol ? strlen(protocol) : 0;\n\tJANUS_LOG(LOG_VERB, \"Opening channel with label '%s' (%zu, protocol %s)\\n\",\n\t\tlabel, label_size, (protocol ? protocol : \"unknown\"));\n\n\treq = g_malloc0(sizeof(janus_datachannel_open_request) + label_size + protocol_size);\n\treq->msg_type = DATA_CHANNEL_OPEN_REQUEST;\n\tswitch (pr_policy) {\n\t\tcase SCTP_PR_SCTP_NONE:\n\t\t\t/* XXX: What about DATA_CHANNEL_RELIABLE_STREAM */\n\t\t\treq->channel_type = unordered ? DATA_CHANNEL_RELIABLE_UNORDERED : DATA_CHANNEL_RELIABLE;\n\t\t\tbreak;\n\t\tcase SCTP_PR_SCTP_TTL:\n\t\t\t/* XXX: What about DATA_CHANNEL_UNRELIABLE */\n\t\t\treq->channel_type = unordered ? DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED : DATA_CHANNEL_PARTIAL_RELIABLE_TIMED;\n\t\t\tbreak;\n\t\tcase SCTP_PR_SCTP_RTX:\n\t\t\treq->channel_type = unordered ? DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED : DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tg_free(req);\n\t\t\treturn 0;\n\t}\n\treq->priority = htons(0); /* XXX: add support */\n\treq->reliability_params = htonl((uint32_t)pr_value);\n\treq->label_length = htons(label_size);\n\treq->protocol_length = htons(protocol_size);\n\tmemcpy(req->label, label, label_size);\n\tif(protocol != NULL)\n\t\tmemcpy(req->label + label_size, protocol, protocol_size);\n\n\tmemset(&sndinfo, 0, sizeof(struct sctp_sndinfo));\n\tsndinfo.snd_sid = stream;\n\tsndinfo.snd_flags = SCTP_EOR;\n\tsndinfo.snd_ppid = htonl(DATA_CHANNEL_PPID_CONTROL);\n\n\tif(usrsctp_sendv(sock,\n\t\t\treq, sizeof(janus_datachannel_open_request) + label_size + protocol_size,\n\t\t\tNULL, 0,\n\t\t\t&sndinfo, (socklen_t)sizeof(struct sctp_sndinfo),\n\t\t\tSCTP_SENDV_SNDINFO, 0) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"usrsctp_sendv error (%d, %s)\\n\", errno, g_strerror(errno));\n\t\tg_free(req);\n\t\treq = NULL;\n\t\treturn 0;\n\t} else {\n\t\tg_free(req);\n\t\treq = NULL;\n\t\treturn 1;\n\t}\n}\n\nint janus_sctp_send_open_response_message(struct socket *sock, uint16_t stream) {\n\t/* XXX: This should be encoded in a better way */\n\tjanus_datachannel_open_response rsp;\n\tstruct sctp_sndinfo sndinfo;\n\n\tmemset(&rsp, 0, sizeof(janus_datachannel_open_response));\n\trsp.msg_type = DATA_CHANNEL_OPEN_RESPONSE;\n\trsp.error = 0;\n\trsp.flags = htons(0);\n\trsp.reverse_stream = htons(stream);\n\tmemset(&sndinfo, 0, sizeof(struct sctp_sndinfo));\n\tsndinfo.snd_sid = stream;\n\tsndinfo.snd_flags = SCTP_EOR;\n\tsndinfo.snd_ppid = htonl(DATA_CHANNEL_PPID_CONTROL);\n\tif(usrsctp_sendv(sock,\n\t\t\t&rsp, sizeof(janus_datachannel_open_response),\n\t\t\tNULL, 0,\n\t\t\t&sndinfo, (socklen_t)sizeof(struct sctp_sndinfo),\n\t\t\tSCTP_SENDV_SNDINFO, 0) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"usrsctp_sendv error (%d, %s)\\n\", errno, g_strerror(errno));\n\t\treturn 0;\n\t} else {\n\t\treturn 1;\n\t}\n}\n\nint janus_sctp_send_open_ack_message(struct socket *sock, uint16_t stream) {\n\t/* XXX: This should be encoded in a better way */\n\tjanus_datachannel_ack ack;\n\tstruct sctp_sndinfo sndinfo;\n\n\tmemset(&ack, 0, sizeof(janus_datachannel_ack));\n\tack.msg_type = DATA_CHANNEL_ACK;\n\tmemset(&sndinfo, 0, sizeof(struct sctp_sndinfo));\n\tsndinfo.snd_sid = stream;\n\tsndinfo.snd_flags = SCTP_EOR;\n\tsndinfo.snd_ppid = htonl(DATA_CHANNEL_PPID_CONTROL);\n\tif(usrsctp_sendv(sock,\n\t\t\t&ack, sizeof(janus_datachannel_ack),\n\t\t\tNULL, 0,\n\t\t\t&sndinfo, (socklen_t)sizeof(struct sctp_sndinfo),\n\t\t\tSCTP_SENDV_SNDINFO, 0) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"usrsctp_sendv error (%d, %s)\\n\", errno, g_strerror(errno));\n\t\treturn 0;\n\t} else {\n\t\treturn 1;\n\t}\n}\n\nvoid janus_sctp_send_deferred_messages(janus_sctp_association *sctp) {\n\tuint32_t i;\n\tjanus_sctp_channel *channel;\n\n\tfor(i = 0; i < NUMBER_OF_CHANNELS; i++) {\n\t\tchannel = &(sctp->channels[i]);\n\t\tif(channel->flags & DATA_CHANNEL_FLAGS_SEND_REQ) {\n\t\t\tif(janus_sctp_send_open_request_message(sctp->sock, channel->stream,\n\t\t\t\t\tchannel->label, channel->protocol, channel->unordered, channel->pr_policy, channel->pr_value)) {\n\t\t\t\tchannel->flags &= ~DATA_CHANNEL_FLAGS_SEND_REQ;\n\t\t\t} else {\n\t\t\t\tif(errno != EAGAIN) {\n\t\t\t\t\t/* XXX: error handling */\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(channel->flags & DATA_CHANNEL_FLAGS_SEND_RSP) {\n\t\t\tif(janus_sctp_send_open_response_message(sctp->sock, channel->stream)) {\n\t\t\t\tchannel->flags &= ~DATA_CHANNEL_FLAGS_SEND_RSP;\n\t\t\t} else {\n\t\t\t\tif(errno != EAGAIN) {\n\t\t\t\t\t/* XXX: error handling */\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(channel->flags & DATA_CHANNEL_FLAGS_SEND_ACK) {\n\t\t\tif(janus_sctp_send_open_ack_message(sctp->sock, channel->stream)) {\n\t\t\t\tchannel->flags &= ~DATA_CHANNEL_FLAGS_SEND_ACK;\n\t\t\t} else {\n\t\t\t\tif(errno != EAGAIN) {\n\t\t\t\t\t/* XXX: error handling */\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn;\n}\n\nint janus_sctp_open_channel(janus_sctp_association *sctp, char *label, char *protocol, uint8_t unordered, uint16_t pr_policy, uint32_t pr_value) {\n\tif(sctp == NULL)\n\t\treturn -1;\n\tjanus_sctp_channel *channel;\n\tuint16_t stream;\n\n\tif((pr_policy != SCTP_PR_SCTP_NONE) &&\n\t\t\t(pr_policy != SCTP_PR_SCTP_TTL) &&\n\t\t\t(pr_policy != SCTP_PR_SCTP_RTX)) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid pr_policy %\"SCNu32\"\\n\", sctp->handle_id, pr_policy);\n\t\treturn -1;\n\t}\n\tif((pr_policy == SCTP_PR_SCTP_NONE) && (pr_value != 0)) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid pr_value %\"SCNu32\" for SCTP_PR_SCTP_NONE\\n\", sctp->handle_id, pr_value);\n\t\treturn -1;\n\t}\n\tif((channel = janus_sctp_find_free_channel(sctp)) == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No more free channels available\\n\", sctp->handle_id);\n\t\treturn -1;\n\t}\n\tstream = janus_sctp_find_free_stream(sctp);\n\tchannel->state = DATA_CHANNEL_CONNECTING;\n\tchannel->unordered = unordered ? 1 : 0;\n\tchannel->pr_policy = pr_policy;\n\tchannel->pr_value = pr_value;\n\tchannel->stream = stream;\n\tchannel->flags = 0;\n\tg_snprintf(channel->label, sizeof(channel->label), \"%s\", (label ? label : default_label));\n\tchannel->protocol[0] = '\\0';\n\tif(protocol != NULL)\n\t\tg_snprintf(channel->protocol, sizeof(channel->protocol), \"%s\", protocol);\n\tif(stream == 0) {\n\t\tjanus_sctp_request_more_streams(sctp);\n\t} else {\n\t\tif(janus_sctp_send_open_request_message(sctp->sock, stream, channel->label, channel->protocol, unordered, pr_policy, pr_value)) {\n\t\t\tsctp->stream_channel[stream] = channel;\n\t\t} else {\n\t\t\tif(errno == EAGAIN) {\n\t\t\t\tsctp->stream_channel[stream] = channel;\n\t\t\t\tchannel->flags |= DATA_CHANNEL_FLAGS_SEND_REQ;\n\t\t\t} else {\n\t\t\t\tchannel->label[0] = '\\0';\n\t\t\t\tchannel->protocol[0] = '\\0';\n\t\t\t\tchannel->state = DATA_CHANNEL_CLOSED;\n\t\t\t\tchannel->unordered = 0;\n\t\t\t\tchannel->pr_policy = 0;\n\t\t\t\tchannel->pr_value = 0;\n\t\t\t\tchannel->stream = 0;\n\t\t\t\tchannel->flags = 0;\n\t\t\t\tchannel = NULL;\n\t\t\t}\n\t\t}\n\t}\n\treturn 0;\n}\n\nint janus_sctp_send_text_or_binary(janus_sctp_association *sctp, uint16_t id, gboolean textdata, char *text, size_t length) {\n\tif(id >= NUMBER_OF_CHANNELS || text == NULL)\n\t\treturn -1;\n\tstruct sctp_sendv_spa spa;\n\tjanus_sctp_channel *channel = &sctp->channels[id];\n\tif(channel == NULL) {\n\t\t/* No such channel */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] No such channel %\"SCNu16\"...\\n\", sctp->handle_id, id);\n\t\treturn -1;\n\t}\n\tif((channel->state != DATA_CHANNEL_OPEN) && (channel->state != DATA_CHANNEL_CONNECTING)) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Channel %\"SCNu16\" is neither open nor connecting (state=%d)...\\n\", sctp->handle_id, id, channel->state);\n\t\treturn -1;\n\t}\n\n\tmemset(&spa, 0, sizeof(struct sctp_sendv_spa));\n\tspa.sendv_sndinfo.snd_sid = channel->stream;\n\tif((channel->state == DATA_CHANNEL_OPEN) && (channel->unordered)) {\n\t\tspa.sendv_sndinfo.snd_flags = SCTP_EOR | SCTP_UNORDERED;\n\t} else {\n\t\tspa.sendv_sndinfo.snd_flags = SCTP_EOR;\n\t}\n\tspa.sendv_sndinfo.snd_ppid = htonl(textdata ? DATA_CHANNEL_PPID_DOMSTRING : DATA_CHANNEL_PPID_BINARY);\n\tspa.sendv_flags = SCTP_SEND_SNDINFO_VALID;\n\tif((channel->pr_policy == SCTP_PR_SCTP_TTL) || (channel->pr_policy == SCTP_PR_SCTP_RTX)) {\n\t\tspa.sendv_prinfo.pr_policy = channel->pr_policy;\n\t\tspa.sendv_prinfo.pr_value = channel->pr_value;\n\t\tspa.sendv_flags |= SCTP_SEND_PRINFO_VALID;\n\t}\n\tif(usrsctp_sendv(sctp->sock, text, length, NULL, 0,\n\t\t\t&spa, (socklen_t)sizeof(struct sctp_sendv_spa),\n\t\t\tSCTP_SENDV_SPA, 0) < 0) {\n\t\tint res = errno;\n\t\tif(res == EAGAIN) {\n\t\t\t/* Couldn't send the message right away, add to the queue and retry later */\n\t\t\treturn -2;\n\t\t}\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] sctp_sendv error (%d, %s)\\n\",\n\t\t\tsctp->handle_id, res, g_strerror(res));\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Message sent on channel %\"SCNu16\"\\n\", sctp->handle_id, id);\n\treturn 0;\n}\n\nvoid janus_sctp_reset_outgoing_stream(janus_sctp_association *sctp, uint16_t stream) {\n\tuint32_t i;\n\n\tfor(i = 0; i < sctp->stream_buffer_counter; i++) {\n\t\tif(sctp->stream_buffer[i] == stream) {\n\t\t\treturn;\n\t\t}\n\t}\n\tsctp->stream_buffer[sctp->stream_buffer_counter++] = stream;\n\treturn;\n}\n\nvoid janus_sctp_send_outgoing_stream_reset(janus_sctp_association *sctp) {\n\tstruct sctp_reset_streams *srs;\n\tuint32_t i;\n\tsize_t len;\n\n\tif(sctp->stream_buffer_counter == 0) {\n\t\treturn;\n\t}\n\tlen = sizeof(sctp_assoc_t) + (2 + sctp->stream_buffer_counter) * sizeof(uint16_t);\n\tsrs = g_malloc0(len);\n\tsrs->srs_flags = SCTP_STREAM_RESET_OUTGOING;\n\tsrs->srs_number_streams = sctp->stream_buffer_counter;\n\tfor(i = 0; i < sctp->stream_buffer_counter; i++) {\n\t\tsrs->srs_stream_list[i] = sctp->stream_buffer[i];\n\t}\n\tif(usrsctp_setsockopt(sctp->sock, IPPROTO_SCTP, SCTP_RESET_STREAMS, srs, (socklen_t)len) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] setsockopt error: SCTP_RESET_STREAMS (%d, %s)\\n\",\n\t\t\tsctp->handle_id, errno, g_strerror(errno));\n\t} else {\n\t\tfor(i = 0; i < sctp->stream_buffer_counter; i++) {\n\t\t\tsrs->srs_stream_list[i] = 0;\n\t\t}\n\t\tsctp->stream_buffer_counter = 0;\n\t}\n\tg_free(srs);\n\treturn;\n}\n\nint janus_sctp_close_channel(janus_sctp_association *sctp, uint16_t id) {\n\tif(id >= NUMBER_OF_CHANNELS)\n\t\treturn -1;\n\tjanus_sctp_channel *channel = &sctp->channels[id];\n\tif(channel == NULL) {\n\t\treturn -1;\n\t}\n\tif(channel->state != DATA_CHANNEL_OPEN) {\n\t\treturn -1;\n\t}\n\tjanus_sctp_reset_outgoing_stream(sctp, channel->stream);\n\tjanus_sctp_send_outgoing_stream_reset(sctp);\n\tchannel->state = DATA_CHANNEL_CLOSING;\n\treturn 0;\n}\n\nvoid janus_sctp_data_ready(janus_sctp_association *sctp) {\n\tif(sctp == NULL || g_atomic_int_get(&sctp->destroyed))\n\t\treturn;\n\n\tif(sctp->pending_messages != NULL && !g_queue_is_empty(sctp->pending_messages)) {\n\t\t/* Messages waiting in the queue, send those first */\n\t\tjanus_sctp_pending_message *m = g_queue_peek_head(sctp->pending_messages);\n\t\twhile(m != NULL) {\n\t\t\tint res = janus_sctp_send_text_or_binary(sctp, m->id, m->textdata, m->buf, m->len);\n\t\t\tif(res == -2) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Got EAGAIN when trying to resend pending message on channel %\"SCNu16\"\\n\",\n\t\t\t\t\tsctp->handle_id, m->id);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t(void)g_queue_pop_head(sctp->pending_messages);\n\t\t\tjanus_sctp_pending_message_free(m);\n\t\t\tm = g_queue_peek_head(sctp->pending_messages);\n\t\t}\n\t}\n\n\tjanus_dtls_sctp_data_ready(sctp->dtls);\n}\n\nvoid janus_sctp_handle_open_request_message(janus_sctp_association *sctp, janus_datachannel_open_request *req, size_t length, uint16_t stream) {\n\tjanus_sctp_channel *channel;\n\tuint32_t pr_value;\n\tuint16_t pr_policy;\n\tuint8_t unordered;\n\n\tif(stream >= NUMBER_OF_STREAMS) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Exceeded number of allowed streams (%u > %u).\\n\", sctp->handle_id, (stream+1), NUMBER_OF_STREAMS);\n\t\t/* XXX: some error handling */\n\t\treturn;\n\t}\n\n\tif((channel = janus_sctp_find_channel_by_stream(sctp, stream))) {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] channel %d is in state %d instead of CLOSED.\\n\", sctp->handle_id, channel->id, channel->state);\n\t\tJANUS_LOG(LOG_ERR, \"%.*s\\n\", ntohs(req->label_length), req->label);\n\t\t/* XXX: some error handling */\n\t\treturn;\n\t}\n\tif((channel = janus_sctp_find_free_channel(sctp)) == NULL) {\n\t\t/* XXX: some error handling */\n\t\treturn;\n\t}\n\tswitch (req->channel_type) {\n\t\tcase DATA_CHANNEL_RELIABLE:\n\t\t\tpr_policy = SCTP_PR_SCTP_NONE;\n\t\t\tunordered = 0;\n\t\t\tbreak;\n\t\tcase DATA_CHANNEL_RELIABLE_UNORDERED:\n\t\t\tpr_policy = SCTP_PR_SCTP_NONE;\n\t\t\tunordered = 1;\n\t\t\tbreak;\n\t\tcase DATA_CHANNEL_PARTIAL_RELIABLE_TIMED:\n\t\t\tpr_policy = SCTP_PR_SCTP_TTL;\n\t\t\tunordered = 0;\n\t\t\tbreak;\n\t\tcase DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED:\n\t\t\tpr_policy = SCTP_PR_SCTP_TTL;\n\t\t\tunordered = 1;\n\t\t\tbreak;\n\t\tcase DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT:\n\t\t\tpr_policy = SCTP_PR_SCTP_RTX;\n\t\t\tunordered = 1;\n\t\t\tbreak;\n\t\tcase DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED:\n\t\t\tpr_policy = SCTP_PR_SCTP_RTX;\n\t\t\tunordered = 1;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tpr_policy = SCTP_PR_SCTP_NONE;\n\t\t\tunordered = 0;\n\t\t\t/* FIXME Should we handle some error, here? */\n\t\t\tbreak;\n\t}\n\tpr_value = ntohs(req->reliability_params);\n\tchannel->state = DATA_CHANNEL_CONNECTING;\n\tchannel->unordered = unordered;\n\tchannel->pr_policy = pr_policy;\n\tchannel->pr_value = pr_value;\n\tchannel->stream = stream;\n\tchannel->flags = 0;\n\tsctp->stream_channel[stream] = channel;\n\tif(janus_sctp_send_open_ack_message(sctp->sock, stream)) {\n\t\tsctp->stream_channel[stream] = channel;\n\t} else {\n\t\tif(errno == EAGAIN) {\n\t\t\tchannel->flags |= DATA_CHANNEL_FLAGS_SEND_ACK;\n\t\t\tsctp->stream_channel[stream] = channel;\n\t\t} else {\n\t\t\t/* XXX: Signal error to the other end */\n\t\t\tsctp->stream_channel[stream] = NULL;\n\t\t\tchannel->label[0] = '\\0';\n\t\t\tchannel->protocol[0] = '\\0';\n\t\t\tchannel->state = DATA_CHANNEL_CLOSED;\n\t\t\tchannel->unordered = 0;\n\t\t\tchannel->pr_policy = 0;\n\t\t\tchannel->pr_value = 0;\n\t\t\tchannel->stream = 0;\n\t\t\tchannel->flags = 0;\n\t\t}\n\t}\n\t/* Read label, if available */\n\tchar *label = NULL;\n\tguint len = ntohs(req->label_length);\n\tif(len > 0 && len < length) {\n\t\tlabel = g_malloc(len+1);\n\t\tmemcpy(label, req->label, len);\n\t\tlabel[len] = '\\0';\n\t\tg_snprintf(channel->label, sizeof(channel->label), \"%s\", label);\n\t}\n\tchar *protocol = NULL;\n\tguint plen = ntohs(req->protocol_length);\n\tif(plen > 0 && plen < length) {\n\t\tprotocol = g_malloc(plen+1);\n\t\tmemcpy(protocol, req->label+len, plen);\n\t\tprotocol[plen] = '\\0';\n\t\tg_snprintf(channel->protocol, sizeof(channel->protocol), \"%s\", protocol);\n\t}\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Opened channel '%s' (protocol=%s, id=%\"SCNu16\") (%d/%d/%d)\\n\",\n\t\tsctp->handle_id, label ? label : \"??\", protocol ? protocol : \"??\",\n\t\tchannel->stream, channel->unordered, channel->pr_policy, channel->pr_value);\n\tg_free(label);\n\tg_free(protocol);\n}\n\nvoid janus_sctp_handle_open_response_message(janus_sctp_association *sctp, janus_datachannel_open_response *rsp, size_t length, uint16_t stream) {\n\tjanus_sctp_channel *channel;\n\n\tchannel = janus_sctp_find_channel_by_stream(sctp, stream);\n\tif(channel == NULL) {\n\t\t/* XXX: improve error handling */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Can't find channel for outgoing steam %d.\\n\", sctp->handle_id, stream);\n\t\treturn;\n\t}\n\tif(channel->state != DATA_CHANNEL_CONNECTING) {\n\t\t/* XXX: improve error handling */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Channel with id %d for outgoing steam %d is in state %d.\\n\", sctp->handle_id, channel->id, stream, channel->state);\n\t\treturn;\n\t}\n\tif(janus_sctp_find_channel_by_stream(sctp, stream)) {\n\t\t/* XXX: improve error handling */\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Channel collision for channel with id %d and streams (in/out) = (%d/%d).\\n\", sctp->handle_id, channel->id, stream, stream);\n\t\treturn;\n\t}\n\tchannel->stream = stream;\n\tchannel->state = DATA_CHANNEL_OPEN;\n\tsctp->stream_channel[stream] = channel;\n\tif(janus_sctp_send_open_ack_message(sctp->sock, stream)) {\n\t\tchannel->flags = 0;\n\t} else {\n\t\tchannel->flags |= DATA_CHANNEL_FLAGS_SEND_ACK;\n\t}\n\treturn;\n}\n\nvoid janus_sctp_handle_open_ack_message(janus_sctp_association *sctp, janus_datachannel_ack *ack, size_t length, uint16_t stream) {\n\tjanus_sctp_channel *channel;\n\n\tchannel = janus_sctp_find_channel_by_stream(sctp, stream);\n\tif(channel == NULL) {\n\t\t/* XXX: some error handling */\n\t\tJANUS_LOG(LOG_ERR, \"Ops, no channel with stream %\"SCNu16\"?\\n\", stream);\n\t\treturn;\n\t}\n\tif(channel->state == DATA_CHANNEL_OPEN) {\n\t\treturn;\n\t}\n\tif(channel->state != DATA_CHANNEL_CONNECTING) {\n\t\t/* XXX: error handling */\n\t\treturn;\n\t}\n\tchannel->state = DATA_CHANNEL_OPEN;\n\treturn;\n}\n\nvoid janus_sctp_handle_unknown_message(char *msg, size_t length, uint16_t stream) {\n\t/* XXX: Send an error message */\n\treturn;\n}\n\nvoid janus_sctp_handle_data_message(janus_sctp_association *sctp, gboolean textdata, char *buffer, size_t length, uint16_t stream) {\n\tjanus_sctp_channel *channel;\n\n\tchannel = janus_sctp_find_channel_by_stream(sctp, stream);\n\tif(channel == NULL) {\n\t\t/* XXX: Some error handling */\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Got data from this SCTP association but there is no channel with stream %\"SCNu16\"...\\n\", sctp->handle_id, stream);\n\t\treturn;\n\t}\n\tif(channel->state == DATA_CHANNEL_CONNECTING) {\n\t\t/* Implicit ACK */\n\t\tchannel->state = DATA_CHANNEL_OPEN;\n\t}\n\tif(channel->state != DATA_CHANNEL_OPEN) {\n\t\t/* XXX: What about other states? */\n\t\t/* XXX: Some error handling */\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Got data from this SCTP association but channel isn't open yet...\\n\", sctp->handle_id);\n\t\treturn;\n\t} else {\n\t\t/* XXX: Protect for non 0 terminated buffer */\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] SCTP data received of length %zu on channel with id %d.\\n\",\n\t\t\tsctp->handle_id, length, channel->id);\n\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Incoming SCTP contents: %.*s\\n\",\n\t\t\tsctp->handle_id, (int)length, buffer);\n\t\t/* Pass this to the core */\n\t\tjanus_dtls_notify_sctp_data(sctp->dtls, channel->label,\n\t\t\tstrlen(channel->protocol) ? channel->protocol : NULL,\n\t\t\ttextdata, buffer, (int)length);\n\t}\n\treturn;\n}\n\nvoid janus_sctp_handle_message(janus_sctp_association *sctp, char *buffer, size_t length, uint32_t ppid, uint16_t stream, int flags) {\n\tjanus_datachannel_open_request *req;\n\tjanus_datachannel_open_response *rsp;\n\tjanus_datachannel_ack *ack, *msg;\n\n\tswitch (ppid) {\n\t\tcase DATA_CHANNEL_PPID_CONTROL:\n\t\t\tif(length < sizeof(janus_datachannel_ack)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tmsg = (janus_datachannel_ack *)buffer;\n\t\t\tswitch (msg->msg_type) {\n\t\t\t\tcase DATA_CHANNEL_OPEN_REQUEST:\n\t\t\t\t\tif(length < sizeof(janus_datachannel_open_request)) {\n\t\t\t\t\t\t/* XXX: error handling? */\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\treq = (janus_datachannel_open_request *)buffer;\n\t\t\t\t\tjanus_sctp_handle_open_request_message(sctp, req, length, stream);\n\t\t\t\t\tbreak;\n\t\t\t\tcase DATA_CHANNEL_OPEN_RESPONSE:\n\t\t\t\t\tif(length < sizeof(janus_datachannel_open_response)) {\n\t\t\t\t\t\t/* XXX: error handling? */\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\trsp = (janus_datachannel_open_response *)buffer;\n\t\t\t\t\tjanus_sctp_handle_open_response_message(sctp, rsp, length, stream);\n\t\t\t\t\tbreak;\n\t\t\t\tcase DATA_CHANNEL_ACK:\n\t\t\t\t\tif(length < sizeof(janus_datachannel_ack)) {\n\t\t\t\t\t\t/* XXX: error handling? */\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tack = (janus_datachannel_ack *)buffer;\n\t\t\t\t\tjanus_sctp_handle_open_ack_message(sctp, ack, length, stream);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tjanus_sctp_handle_unknown_message(buffer, length, stream);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tbreak;\n\t\tcase DATA_CHANNEL_PPID_DOMSTRING:\n\t\tcase DATA_CHANNEL_PPID_BINARY:\n\t\tcase DATA_CHANNEL_PPID_DOMSTRING_PARTIAL:\n\t\tcase DATA_CHANNEL_PPID_BINARY_PARTIAL:\n\t\t\tif((flags & MSG_EOR) &&\n\t\t\t\t\tppid != DATA_CHANNEL_PPID_DOMSTRING_PARTIAL &&\n\t\t\t\t\tppid != DATA_CHANNEL_PPID_BINARY_PARTIAL) {\n\t\t\t\t/* Message is complete, send it */\n\t\t\t\tgboolean textdata = (ppid == DATA_CHANNEL_PPID_DOMSTRING || ppid == DATA_CHANNEL_PPID_DOMSTRING_PARTIAL);\n\t\t\t\tif(sctp->offset > 0) {\n\t\t\t\t\t/* We buffered multiple partial messages */\n\t\t\t\t\tjanus_sctp_handle_data_message(sctp, textdata, sctp->buffer, sctp->offset, stream);\n\t\t\t\t\tsctp->offset = 0;\n\t\t\t\t} else {\n\t\t\t\t\t/* No buffering done, send this message as it is */\n\t\t\t\t\tjanus_sctp_handle_data_message(sctp, textdata, buffer, length, stream);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Partial message, buffer only for now */\n\t\t\t\tif(length > (sctp->buflen - sctp->offset)) {\n\t\t\t\t\t/* (re)Allocate the buffer */\n\t\t\t\t\tint newlen = sctp->buflen + (length - (sctp->buflen - sctp->offset));\n\t\t\t\t\tsctp->buffer = g_realloc(sctp->buffer, newlen);\n\t\t\t\t\tsctp->buflen = newlen;\n\t\t\t\t}\n\t\t\t\tmemcpy(sctp->buffer + sctp->offset, buffer, length);\n\t\t\t\tsctp->offset += length;\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Message of length %zu, PPID %u on stream %u received.\\n\",\n\t\t\t\tsctp->handle_id, length, ppid, stream);\n\t\t\tbreak;\n\t}\n}\n\nvoid janus_sctp_handle_association_change_event(struct sctp_assoc_change *sac) {\n\tunsigned int i, n;\n\n\tJANUS_LOG(LOG_VERB, \"Association change \");\n\tswitch (sac->sac_state) {\n\t\tcase SCTP_COMM_UP:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_COMM_UP\");\n\t\t\tbreak;\n\t\tcase SCTP_COMM_LOST:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_COMM_LOST\");\n\t\t\tbreak;\n\t\tcase SCTP_RESTART:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_RESTART\");\n\t\t\tbreak;\n\t\tcase SCTP_SHUTDOWN_COMP:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_SHUTDOWN_COMP\");\n\t\t\tbreak;\n\t\tcase SCTP_CANT_STR_ASSOC:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_CANT_STR_ASSOC\");\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_VERB, \"UNKNOWN\");\n\t\t\tbreak;\n\t}\n\tJANUS_LOG(LOG_VERB, \", streams (in/out) = (%u/%u)\",\n\t\tsac->sac_inbound_streams, sac->sac_outbound_streams);\n\tn = sac->sac_length - sizeof(struct sctp_assoc_change);\n\tif(((sac->sac_state == SCTP_COMM_UP) ||\n\t\t\t(sac->sac_state == SCTP_RESTART)) && (n > 0)) {\n\t\tJANUS_LOG(LOG_VERB, \", supports\");\n\t\tfor(i = 0; i < n; i++) {\n\t\t\tswitch (sac->sac_info[i]) {\n\t\t\t\tcase SCTP_ASSOC_SUPPORTS_PR:\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \" PR\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase SCTP_ASSOC_SUPPORTS_AUTH:\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \" AUTH\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase SCTP_ASSOC_SUPPORTS_ASCONF:\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \" ASCONF\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase SCTP_ASSOC_SUPPORTS_MULTIBUF:\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \" MULTIBUF\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase SCTP_ASSOC_SUPPORTS_RE_CONFIG:\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \" RE-CONFIG\");\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \" UNKNOWN(0x%02x)\", sac->sac_info[i]);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} else if(((sac->sac_state == SCTP_COMM_LOST) ||\n\t\t\t(sac->sac_state == SCTP_CANT_STR_ASSOC)) && (n > 0)) {\n\t\tJANUS_LOG(LOG_VERB, \", ABORT =\");\n\t\tfor(i = 0; i < n; i++) {\n\t\t\tJANUS_LOG(LOG_VERB, \" 0x%02x\", sac->sac_info[i]);\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \".\\n\");\n\tif((sac->sac_state == SCTP_CANT_STR_ASSOC) ||\n\t\t\t(sac->sac_state == SCTP_SHUTDOWN_COMP) ||\n\t\t\t(sac->sac_state == SCTP_COMM_LOST)) {\n\t\t/* FIXME Should we notify the application that data channels were lost? */\n\t}\n\treturn;\n}\n\nvoid janus_sctp_handle_peer_address_change_event(struct sctp_paddr_change *spc) {\n\tchar addr_buf[INET6_ADDRSTRLEN];\n\tconst char *addr;\n\tstruct sockaddr_in *sin;\n\tstruct sockaddr_in6 *sin6;\n\n\tswitch (spc->spc_aaddr.ss_family) {\n\t\tcase AF_INET:\n\t\t\tsin = (struct sockaddr_in *)&spc->spc_aaddr;\n\t\t\taddr = inet_ntop(AF_INET, &sin->sin_addr, addr_buf, INET_ADDRSTRLEN);\n\t\t\tbreak;\n\t\tcase AF_INET6:\n\t\t\tsin6 = (struct sockaddr_in6 *)&spc->spc_aaddr;\n\t\t\taddr = inet_ntop(AF_INET6, &sin6->sin6_addr, addr_buf, INET6_ADDRSTRLEN);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tsnprintf(addr_buf, INET6_ADDRSTRLEN, \"Unknown family %d\", spc->spc_aaddr.ss_family);\n\t\t\taddr = addr_buf;\n\t\t\tbreak;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Peer address %s is now \", addr);\n\tswitch (spc->spc_state) {\n\t\tcase SCTP_ADDR_AVAILABLE:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_ADDR_AVAILABLE\");\n\t\t\tbreak;\n\t\tcase SCTP_ADDR_UNREACHABLE:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_ADDR_UNREACHABLE\");\n\t\t\tbreak;\n\t\tcase SCTP_ADDR_REMOVED:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_ADDR_REMOVED\");\n\t\t\tbreak;\n\t\tcase SCTP_ADDR_ADDED:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_ADDR_ADDED\");\n\t\t\tbreak;\n\t\tcase SCTP_ADDR_MADE_PRIM:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_ADDR_MADE_PRIM\");\n\t\t\tbreak;\n\t\tcase SCTP_ADDR_CONFIRMED:\n\t\t\tJANUS_LOG(LOG_VERB, \"SCTP_ADDR_CONFIRMED\");\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tJANUS_LOG(LOG_VERB, \"UNKNOWN\");\n\t\t\tbreak;\n\t}\n\tJANUS_LOG(LOG_VERB, \" (error = 0x%08x).\\n\", spc->spc_error);\n\treturn;\n}\n\nvoid janus_sctp_handle_adaptation_indication(struct sctp_adaptation_event *sai) {\n\tJANUS_LOG(LOG_VERB, \"Adaptation indication: %x.\\n\", sai-> sai_adaptation_ind);\n\treturn;\n}\n\nvoid janus_sctp_handle_shutdown_event(struct sctp_shutdown_event *sse) {\n\tJANUS_LOG(LOG_VERB, \"Shutdown event.\\n\");\n\t/* XXX: notify all channels */\n\treturn;\n}\n\nvoid janus_sctp_handle_stream_reset_event(janus_sctp_association *sctp, struct sctp_stream_reset_event *strrst) {\n\tuint32_t n, i;\n\tjanus_sctp_channel *channel;\n\n\tn = (strrst->strreset_length - sizeof(struct sctp_stream_reset_event)) / sizeof(uint16_t);\n\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Stream reset event: flags = %x, \", sctp->handle_id, strrst->strreset_flags);\n\tif(strrst->strreset_flags & SCTP_STREAM_RESET_INCOMING_SSN) {\n\t\tif(strrst->strreset_flags & SCTP_STREAM_RESET_OUTGOING_SSN) {\n\t\t\tJANUS_LOG(LOG_VERB, \"incoming/\");\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"incoming \");\n\t}\n\tif(strrst->strreset_flags & SCTP_STREAM_RESET_OUTGOING_SSN) {\n\t\tJANUS_LOG(LOG_VERB, \"outgoing \");\n\t}\n\tJANUS_LOG(LOG_VERB, \"stream ids = \");\n\tfor(i = 0; i < n; i++) {\n\t\tif(i > 0) {\n\t\t\tJANUS_LOG(LOG_VERB, \", \");\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"%d\", strrst->strreset_stream_list[i]);\n\t}\n\tJANUS_LOG(LOG_VERB, \".\\n\");\n\tif(!(strrst->strreset_flags & SCTP_STREAM_RESET_DENIED) &&\n\t\t\t!(strrst->strreset_flags & SCTP_STREAM_RESET_FAILED)) {\n\t\tfor(i = 0; i < n; i++) {\n\t\t\tif(strrst->strreset_flags & SCTP_STREAM_RESET_INCOMING_SSN ||\n\t\t\t\t\tstrrst->strreset_flags & SCTP_STREAM_RESET_OUTGOING_SSN) {\n\t\t\t\tchannel = janus_sctp_find_channel_by_stream(sctp, strrst->strreset_stream_list[i]);\n\t\t\t\tif(channel != NULL) {\n\t\t\t\t\tsctp->stream_channel[channel->stream] = NULL;\n\t\t\t\t\tif(channel->stream == 0) {\n\t\t\t\t\t\tchannel->pr_policy = SCTP_PR_SCTP_NONE;\n\t\t\t\t\t\tchannel->pr_value = 0;\n\t\t\t\t\t\tchannel->unordered = 0;\n\t\t\t\t\t\tchannel->flags = 0;\n\t\t\t\t\t\tchannel->state = DATA_CHANNEL_CLOSED;\n\t\t\t\t\t\tchannel->label[0] = '\\0';\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif(channel->state == DATA_CHANNEL_OPEN) {\n\t\t\t\t\t\t\tjanus_sctp_reset_outgoing_stream(sctp, channel->stream);\n\t\t\t\t\t\t\tchannel->state = DATA_CHANNEL_CLOSING;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* XXX: What to do? */\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn;\n}\n\nvoid janus_sctp_handle_remote_error_event(struct sctp_remote_error *sre) {\n\tsize_t i, n;\n\n\tn = sre->sre_length - sizeof(struct sctp_remote_error);\n\tJANUS_LOG(LOG_VERB, \"Remote Error (error = 0x%04x): \", sre->sre_error);\n\tfor(i = 0; i < n; i++) {\n\t\tJANUS_LOG(LOG_VERB, \" 0x%02x\", sre-> sre_data[i]);\n\t}\n\tJANUS_LOG(LOG_VERB, \".\\n\");\n\treturn;\n}\n\nvoid janus_sctp_handle_send_failed_event(struct sctp_send_failed_event *ssfe) {\n\tsize_t i, n;\n\n\tif(ssfe->ssfe_flags & SCTP_DATA_UNSENT) {\n\t\tJANUS_LOG(LOG_VERB, \"Unsent \");\n\t}\n\tif(ssfe->ssfe_flags & SCTP_DATA_SENT) {\n\t\tJANUS_LOG(LOG_VERB, \"Sent \");\n\t}\n\tif(ssfe->ssfe_flags & ~(SCTP_DATA_SENT | SCTP_DATA_UNSENT)) {\n\t\tJANUS_LOG(LOG_VERB, \"(flags = %x) \", ssfe->ssfe_flags);\n\t}\n\tJANUS_LOG(LOG_VERB, \"message with PPID = %d, SID = %d, flags: 0x%04x due to error = 0x%08x\",\n\t\tntohl(ssfe->ssfe_info.snd_ppid), ssfe->ssfe_info.snd_sid,\n\t\tssfe->ssfe_info.snd_flags, ssfe->ssfe_error);\n\tn = ssfe->ssfe_length - sizeof(struct sctp_send_failed_event);\n\tfor(i = 0; i < n; i++) {\n\t\tJANUS_LOG(LOG_VERB, \" 0x%02x\", ssfe->ssfe_data[i]);\n\t}\n\tJANUS_LOG(LOG_VERB, \".\\n\");\n\treturn;\n}\n\nvoid janus_sctp_handle_notification(janus_sctp_association *sctp, union sctp_notification *notif, size_t n) {\n\tif(notif->sn_header.sn_length != (uint32_t)n) {\n\t\treturn;\n\t}\n\tswitch (notif->sn_header.sn_type) {\n\t\tcase SCTP_ASSOC_CHANGE:\n\t\t\tjanus_sctp_handle_association_change_event(&(notif->sn_assoc_change));\n\t\t\tbreak;\n\t\tcase SCTP_PEER_ADDR_CHANGE:\n\t\t\tjanus_sctp_handle_peer_address_change_event(&(notif->sn_paddr_change));\n\t\t\tbreak;\n\t\tcase SCTP_REMOTE_ERROR:\n\t\t\tjanus_sctp_handle_remote_error_event(&(notif->sn_remote_error));\n\t\t\tbreak;\n\t\tcase SCTP_SHUTDOWN_EVENT:\n\t\t\tjanus_sctp_handle_shutdown_event(&(notif->sn_shutdown_event));\n\t\t\tbreak;\n\t\tcase SCTP_ADAPTATION_INDICATION:\n\t\t\tjanus_sctp_handle_adaptation_indication(&(notif->sn_adaptation_event));\n\t\t\tbreak;\n\t\tcase SCTP_PARTIAL_DELIVERY_EVENT:\n\t\t\tbreak;\n\t\tcase SCTP_AUTHENTICATION_EVENT:\n\t\t\tbreak;\n\t\tcase SCTP_SENDER_DRY_EVENT: {\n\t\t\t/* Internal buffers empty, notify the application they can send again */\n\t\t\tjanus_sctp_data_ready(sctp);\n\t\t\tbreak;\n\t\t}\n\t\tcase SCTP_NOTIFICATIONS_STOPPED_EVENT:\n\t\t\tbreak;\n\t\tcase SCTP_SEND_FAILED_EVENT:\n\t\t\tjanus_sctp_handle_send_failed_event(&(notif->sn_send_failed_event));\n\t\t\tbreak;\n\t\tcase SCTP_STREAM_RESET_EVENT:\n\t\t\tjanus_sctp_handle_stream_reset_event(sctp, &(notif->sn_strreset_event));\n\t\t\tjanus_sctp_send_deferred_messages(sctp);\n\t\t\tjanus_sctp_send_outgoing_stream_reset(sctp);\n\t\t\tjanus_sctp_request_more_streams(sctp);\n\t\t\tbreak;\n\t\tcase SCTP_ASSOC_RESET_EVENT:\n\t\t\tbreak;\n\t\tcase SCTP_STREAM_CHANGE_EVENT:\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Stream change (in/out) = (%u/%u)\\n\", sctp ? sctp->handle_id : 0,\n\t\t\t\tnotif->sn_strchange_event.strchange_instrms, notif->sn_strchange_event.strchange_outstrms);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n}\n\n#endif\n"
  },
  {
    "path": "src/sctp.h",
    "content": "/*! \\file    sctp.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    SCTP processing for data channels (headers)\n * \\details  Implementation (based on libusrsctp) of the SCTP Data Channels.\n * The code takes care of the SCTP association between peers and the server,\n * and allows for sending and receiving text messages (binary stuff yet to\n * be implemented) after that.\n *\n * \\note Right now, the code is heavily based on the rtcweb.c sample code\n * provided in the \\c usrsctp library code, and as such the copyright notice\n * that appears at the beginning of that code is ideally present here as\n * well: http://code.google.com/p/sctp-refimpl/source/browse/trunk/KERN/usrsctp/programs/rtcweb.c\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_SCTP_H\n#define JANUS_SCTP_H\n\n#ifdef HAVE_SCTP\n\n#define INET 1\n#define INET6 1\n\n/* Uncomment the line below to enable SCTP debugging to files */\n//~ #define DEBUG_SCTP\n\n#include <sys/types.h>\n#include <sys/socket.h>\n#include <sys/select.h>\n#include <netinet/in.h>\n#include <arpa/inet.h>\n#include <pthread.h>\n#include <unistd.h>\n#include <stdint.h>\n#include <stdarg.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <errno.h>\n#include <usrsctp.h>\n#include <glib.h>\n\n#include \"mutex.h\"\n#include \"refcount.h\"\n\n\n/*! \\brief SCTP stuff initialization\n * \\returns 0 on success, a negative integer otherwise */\nint janus_sctp_init(void);\n\n/*! \\brief SCTP stuff de-initialization */\nvoid janus_sctp_deinit(void);\n\n\n#define BUFFER_SIZE (1<<16)\n#define NUMBER_OF_CHANNELS (150)\n#define NUMBER_OF_STREAMS (300)\n\n#define DATA_CHANNEL_PPID_CONTROL           50\n#define DATA_CHANNEL_PPID_DOMSTRING         51\n#define DATA_CHANNEL_PPID_BINARY_PARTIAL    52\n#define DATA_CHANNEL_PPID_BINARY            53\n#define DATA_CHANNEL_PPID_DOMSTRING_PARTIAL 54\n\n#define DATA_CHANNEL_CLOSED     0\n#define DATA_CHANNEL_CONNECTING 1\n#define DATA_CHANNEL_OPEN       2\n#define DATA_CHANNEL_CLOSING    3\n\n#define DATA_CHANNEL_FLAGS_SEND_REQ 0x00000001\n#define DATA_CHANNEL_FLAGS_SEND_RSP 0x00000002\n#define DATA_CHANNEL_FLAGS_SEND_ACK 0x00000004\n\nstruct janus_dtls_srtp;\nstruct janus_ice_handle;\n\ntypedef struct janus_sctp_channel {\n\t/*! \\brief SCTP channel ID */\n\tuint32_t id;\n\t/*! \\brief SCTP channel label */\n\tchar label[128];\n\t/*! \\brief SCTP protocol */\n\tchar protocol[64];\n\t/*! \\brief Value of the PR-SCTP policy (http://tools.ietf.org/html/rfc6458) */\n\tuint32_t pr_value;\n\t/*! \\brief PR-SCTP policy to use (http://tools.ietf.org/html/rfc6458) */\n\tuint16_t pr_policy;\n\t/*! \\brief Stream ID (both inbound and outbound) */\n\tuint16_t stream;\n\t/*! \\brief Whether this channel is unordered or not */\n\tuint8_t unordered;\n\t/*! \\brief State of the channel */\n\tuint8_t state;\n\t/*! \\brief Flags for this channel */\n\tuint32_t flags;\n} janus_sctp_channel;\n\ntypedef struct janus_sctp_association {\n\t/*! \\brief Unique (local) ID of this association (needed for an internal map) */\n\tuint32_t map_id;\n\t/*! \\brief Pointer to the DTLS instance related to this SCTP association */\n\tstruct janus_dtls_srtp *dtls;\n\t/*! \\brief Pointer to the ICE handle related to this SCTP association */\n\tstruct janus_ice_handle *handle;\n\t/*! \\brief Identifier of the handle owning this SCTP association (for debugging purposes only) */\n\tuint64_t handle_id;\n\t/*! \\brief Array of SCTP channels */\n\tstruct janus_sctp_channel channels[NUMBER_OF_CHANNELS];\n\t/*! \\brief Array of streams (both inbound and outbound) */\n\tstruct janus_sctp_channel *stream_channel[NUMBER_OF_STREAMS];\n\t/*! \\brief Array of stream buffers */\n\tuint16_t stream_buffer[NUMBER_OF_STREAMS];\n\t/*! \\brief Number of stream buffers */\n\tuint32_t stream_buffer_counter;\n\t/*! \\brief UDP-encapsulation socket related to this association */\n\tstruct socket *sock;\n\t/*! \\brief Local port to be used for SCTP */\n\tuint16_t local_port;\n\t/*! \\brief Remote port to be used for SCTP */\n\tuint16_t remote_port;\n\t/*! \\brief Buffer for handling partial messages */\n\tchar *buffer;\n\t/*! \\brief Current size of the buffer for handling partial messages */\n\tsize_t buflen;\n\t/*! \\brief Current offset of the buffer for handling partial messages */\n\tsize_t offset;\n\t/*! \\brief Buffer of pending messages */\n\tGQueue *pending_messages;\n#ifdef DEBUG_SCTP\n\tFILE *debug_dump;\n#endif\n\t/*! \\brief Mutex to lock/unlock this instance */\n\tjanus_mutex mutex;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_sctp_association;\n\n\n#define DATA_CHANNEL_OPEN_REQUEST  3\t/* FIXME was 0, but should be 3 as per http://tools.ietf.org/html/draft-ietf-rtcweb-data-protocol-05 */\n#define DATA_CHANNEL_OPEN_RESPONSE 1\n#define DATA_CHANNEL_ACK           2\n\n#define DATA_CHANNEL_RELIABLE\t\t\t\t\t\t\t0x00\n#define DATA_CHANNEL_RELIABLE_UNORDERED\t\t\t\t\t0x80\n#define DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT\t\t\t0x01\n#define DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED\t0x81\n#define DATA_CHANNEL_PARTIAL_RELIABLE_TIMED\t\t\t\t0x02\n#define DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED\t0x82\n\n/* http://tools.ietf.org/html/draft-ietf-rtcweb-data-protocol-05 */\ntypedef struct janus_datachannel_open_request {\n\t/*! \\brief Message type (DATA_CHANNEL_OPEN_REQUEST) */\n\tuint8_t msg_type;\n\t/*! \\brief Channel type */\n\tuint8_t channel_type;\n\t/*! \\brief Priority */\n\tuint16_t priority;\n\t/*! \\brief Reliability parameters */\n\tuint32_t reliability_params;\n\t/*! \\brief Label length */\n\tuint16_t label_length;\n\t/*! \\brief Protocol length */\n\tuint16_t protocol_length;\n\t/*! \\brief Optional label */\n\tchar label[0];\n\t/* The Protocol field will come after the label, if available */\n} janus_datachannel_open_request;\n\ntypedef struct janus_datachannel_open_response {\n\t/*! \\brief Message type (DATA_CHANNEL_OPEN_RESPONSE) */\n\tuint8_t msg_type;\n\t/*! \\brief Whether there's an error or not */\n\tuint8_t error;\n\t/*! \\brief Response flags */\n\tuint16_t flags;\n\t/*! \\brief Reverse stream ID */\n\tuint16_t reverse_stream;\n} janus_datachannel_open_response;\n\ntypedef struct janus_datachannel_ack {\n\t/*! \\brief Message type (DATA_CHANNEL_ACK) */\n\tuint8_t msg_type;\n} janus_datachannel_ack;\n\n\n\n/*! \\brief Create and setup a new SCTP association\n * \\param[in] dtls Pointer to the DTLS instance that will encapsulate SCTP messages\n * \\param[in] handle Pointer to the ICE handle that will send out SCTP messages.\n * \\param[in] udp_port The port as negotiated in the sctpmap attribute (http://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-06)\n * \\returns A janus_sctp_association instance if successful, NULL otherwise */\njanus_sctp_association *janus_sctp_association_create(struct janus_dtls_srtp *dtls, struct janus_ice_handle *handle, uint16_t udp_port);\n\n/*! \\brief Destroy an existing SCTP association\n * \\param[in] sctp The SCTP association to get rid of */\nvoid janus_sctp_association_destroy(janus_sctp_association *sctp);\n\n/*! \\brief Callback to notify the SCTP stack when data has been decapsulated from DTLS\n * \\param[in] sctp The SCTP association this data is for\n * \\param[in] buf The data buffer\n * \\param[in] len The buffer length */\nvoid janus_sctp_data_from_dtls(janus_sctp_association *sctp, char *buf, int len);\n\n/*! \\brief Method to send data via SCTP to the peer\n * \\param[in] sctp The SCTP association this data is from\n * @param[in] label The label of the data channel to use\n * @param[in] protocol The protocol of the data channel to use\n * @param[in] textdata Whether the buffer is text (domstring) or binary data\n * \\param[in] buf The data buffer\n * \\param[in] len The buffer length */\nvoid janus_sctp_send_data(janus_sctp_association *sctp, char *label, char *protocol, gboolean textdata, char *buf, int len);\n\n#endif\n\n#endif\n"
  },
  {
    "path": "src/sdp-utils.c",
    "content": "/*! \\file    sdp-utils.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    SDP utilities\n * \\details  Implementation of an internal SDP representation. Allows\n * to parse SDP strings to an internal janus_sdp object, the manipulation\n * of such object by playing with its properties, and a serialization\n * to an SDP string that can be passed around. Since they don't have any\n * core dependencies, these utilities can be used by plugins as well.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <string.h>\n\n#include \"sdp-utils.h\"\n#include \"rtp.h\"\n#include \"utils.h\"\n#include \"debug.h\"\n\n/* Preferred codecs when negotiating audio/video, and number of supported codecs */\nconst char *janus_preferred_audio_codecs[] = {\n\t\"opus\", \"multiopus\", \"pcmu\", \"pcma\", \"g722\", \"l16-48\", \"l16\", \"isac16\", \"isac32\"\n};\nuint janus_audio_codecs = sizeof(janus_preferred_audio_codecs)/sizeof(*janus_preferred_audio_codecs);\nconst char *janus_preferred_video_codecs[] = {\n\t\"vp8\", \"vp9\", \"h264\", \"av1\", \"h265\"\n};\nuint janus_video_codecs = sizeof(janus_preferred_video_codecs)/sizeof(*janus_preferred_video_codecs);\n\n/* Reference counters management */\nvoid janus_sdp_destroy(janus_sdp *sdp) {\n\tif(!sdp || !g_atomic_int_compare_and_exchange(&sdp->destroyed, 0, 1))\n\t\treturn;\n\tjanus_refcount_decrease(&sdp->ref);\n}\n\nvoid janus_sdp_mline_destroy(janus_sdp_mline *m) {\n\tif(!m || !g_atomic_int_compare_and_exchange(&m->destroyed, 0, 1))\n\t\treturn;\n\tjanus_refcount_decrease(&m->ref);\n}\n\nvoid janus_sdp_attribute_destroy(janus_sdp_attribute *a) {\n\tif(!a || !g_atomic_int_compare_and_exchange(&a->destroyed, 0, 1))\n\t\treturn;\n\tjanus_refcount_decrease(&a->ref);\n}\n\n/* Internal frees */\nstatic void janus_sdp_free(const janus_refcount *sdp_ref) {\n\tjanus_sdp *sdp = janus_refcount_containerof(sdp_ref, janus_sdp, ref);\n\t/* This SDP instance can be destroyed, free all the resources */\n\tg_free(sdp->o_name);\n\tg_free(sdp->o_addr);\n\tg_free(sdp->s_name);\n\tg_free(sdp->c_addr);\n\tGList *temp = sdp->attributes;\n\twhile(temp) {\n\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)temp->data;\n\t\tjanus_sdp_attribute_destroy(a);\n\t\ttemp = temp->next;\n\t}\n\tg_list_free(sdp->attributes);\n\tsdp->attributes = NULL;\n\ttemp = sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tjanus_sdp_mline_destroy(m);\n\t\ttemp = temp->next;\n\t}\n\tg_list_free(sdp->m_lines);\n\tsdp->m_lines = NULL;\n\tg_free(sdp);\n}\n\nstatic void janus_sdp_mline_free(const janus_refcount *mline_ref) {\n\tjanus_sdp_mline *mline = janus_refcount_containerof(mline_ref, janus_sdp_mline, ref);\n\t/* This SDP m-line instance can be destroyed, free all the resources */\n\tg_free(mline->type_str);\n\tg_free(mline->proto);\n\tg_free(mline->c_addr);\n\tg_free(mline->b_name);\n\tg_list_free_full(mline->fmts, (GDestroyNotify)g_free);\n\tmline->fmts = NULL;\n\tg_list_free(mline->ptypes);\n\tmline->ptypes = NULL;\n\tGList *temp = mline->attributes;\n\twhile(temp) {\n\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)temp->data;\n\t\tjanus_sdp_attribute_destroy(a);\n\t\ttemp = temp->next;\n\t}\n\tg_list_free(mline->attributes);\n\tg_free(mline);\n}\n\nstatic void janus_sdp_attribute_free(const janus_refcount *attr_ref) {\n\tjanus_sdp_attribute *attr = janus_refcount_containerof(attr_ref, janus_sdp_attribute, ref);\n\t/* This SDP attribute instance can be destroyed, free all the resources */\n\tg_free(attr->name);\n\tg_free(attr->value);\n\tg_free(attr);\n}\n\n\n/* SDP and m-lines/attributes code */\njanus_sdp_mline *janus_sdp_mline_create(janus_sdp_mtype type, guint16 port, const char *proto, janus_sdp_mdirection direction) {\n\tjanus_sdp_mline *m = g_malloc0(sizeof(janus_sdp_mline));\n\tg_atomic_int_set(&m->destroyed, 0);\n\tjanus_refcount_init(&m->ref, janus_sdp_mline_free);\n\tm->type = type;\n\tconst char *type_str = janus_sdp_mtype_str(type);\n\tif(type_str == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Unknown media type, type_str will have to be set manually\\n\");\n\t} else {\n\t\tm->type_str = g_strdup(type_str);\n\t}\n\tm->port = port;\n\tm->proto = proto ? g_strdup(proto) : NULL;\n\tm->direction = direction;\n\treturn m;\n}\n\njanus_sdp_mline *janus_sdp_mline_find(janus_sdp *sdp, janus_sdp_mtype type) {\n\tif(sdp == NULL)\n\t\treturn NULL;\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif(m->type == type)\n\t\t\treturn m;\n\t\tml = ml->next;\n\t}\n\treturn NULL;\n}\n\njanus_sdp_mline *janus_sdp_mline_find_by_index(janus_sdp *sdp, int index) {\n\tif(sdp == NULL || index < 0)\n\t\treturn NULL;\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif(m->index == index)\n\t\t\treturn m;\n\t\tml = ml->next;\n\t}\n\treturn NULL;\n}\n\nint janus_sdp_mline_remove(janus_sdp *sdp, janus_sdp_mtype type) {\n\tif(sdp == NULL)\n\t\treturn -1;\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif(m->type == type) {\n\t\t\t/* Found! */\n\t\t\tsdp->m_lines = g_list_remove(sdp->m_lines, m);\n\t\t\tjanus_sdp_mline_destroy(m);\n\t\t\treturn 0;\n\t\t}\n\t\tml = ml->next;\n\t}\n\t/* If we got here, we couldn't the m-line */\n\treturn -2;\n}\n\njanus_sdp_attribute *janus_sdp_attribute_create(const char *name, const char *value, ...) {\n\tif(!name)\n\t\treturn NULL;\n\tjanus_sdp_attribute *a = g_malloc(sizeof(janus_sdp_attribute));\n\tg_atomic_int_set(&a->destroyed, 0);\n\tjanus_refcount_init(&a->ref, janus_sdp_attribute_free);\n\ta->name = g_strdup(name);\n\ta->direction = JANUS_SDP_DEFAULT;\n\ta->value = NULL;\n\tif(value) {\n\t\tchar buffer[2048];\n\t\tva_list ap;\n\t\tva_start(ap, value);\n\t\tg_vsnprintf(buffer, sizeof(buffer), value, ap);\n\t\tva_end(ap);\n\t\ta->value = g_strdup(buffer);\n\t}\n\treturn a;\n}\n\nint janus_sdp_attribute_add_to_mline(janus_sdp_mline *mline, janus_sdp_attribute *attr) {\n\tif(!mline || !attr)\n\t\treturn -1;\n\tmline->attributes = g_list_append(mline->attributes, attr);\n\treturn 0;\n}\n\njanus_sdp_mtype janus_sdp_parse_mtype(const char *type) {\n\tif(type == NULL)\n\t\treturn JANUS_SDP_OTHER;\n\tif(!strcasecmp(type, \"audio\"))\n\t\treturn JANUS_SDP_AUDIO;\n\tif(!strcasecmp(type, \"video\"))\n\t\treturn JANUS_SDP_VIDEO;\n\tif(!strcasecmp(type, \"application\"))\n\t\treturn JANUS_SDP_APPLICATION;\n\treturn JANUS_SDP_OTHER;\n}\n\nconst char *janus_sdp_mtype_str(janus_sdp_mtype type) {\n\tswitch(type) {\n\t\tcase JANUS_SDP_AUDIO:\n\t\t\treturn \"audio\";\n\t\tcase JANUS_SDP_VIDEO:\n\t\t\treturn \"video\";\n\t\tcase JANUS_SDP_APPLICATION:\n\t\t\treturn \"application\";\n\t\tcase JANUS_SDP_OTHER:\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\njanus_sdp_mdirection janus_sdp_parse_mdirection(const char *direction) {\n\tif(direction == NULL)\n\t\treturn JANUS_SDP_INVALID;\n\tif(!strcasecmp(direction, \"sendrecv\"))\n\t\treturn JANUS_SDP_SENDRECV;\n\tif(!strcasecmp(direction, \"sendonly\"))\n\t\treturn JANUS_SDP_SENDONLY;\n\tif(!strcasecmp(direction, \"recvonly\"))\n\t\treturn JANUS_SDP_RECVONLY;\n\tif(!strcasecmp(direction, \"inactive\"))\n\t\treturn JANUS_SDP_INACTIVE;\n\treturn JANUS_SDP_INVALID;\n}\n\nconst char *janus_sdp_mdirection_str(janus_sdp_mdirection direction) {\n\tswitch(direction) {\n\t\tcase JANUS_SDP_DEFAULT:\n\t\tcase JANUS_SDP_SENDRECV:\n\t\t\treturn \"sendrecv\";\n\t\tcase JANUS_SDP_SENDONLY:\n\t\t\treturn \"sendonly\";\n\t\tcase JANUS_SDP_RECVONLY:\n\t\t\treturn \"recvonly\";\n\t\tcase JANUS_SDP_INACTIVE:\n\t\t\treturn \"inactive\";\n\t\tcase JANUS_SDP_INVALID:\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\nconst char *janus_sdp_oa_type_str(janus_sdp_oa_type type) {\n\tswitch(type) {\n\t\tcase JANUS_SDP_OA_MLINE:\n\t\t\treturn \"JANUS_SDP_OA_MLINE\";\n\t\tcase JANUS_SDP_OA_ENABLED:\n\t\t\treturn \"JANUS_SDP_OA_ENABLED\";\n\t\tcase JANUS_SDP_OA_MID:\n\t\t\treturn \"JANUS_SDP_OA_MID\";\n\t\tcase JANUS_SDP_OA_MSID:\n\t\t\treturn \"JANUS_SDP_OA_MSID\";\n\t\tcase JANUS_SDP_OA_DIRECTION:\n\t\t\treturn \"JANUS_SDP_OA_DIRECTION\";\n\t\tcase JANUS_SDP_OA_CODEC:\n\t\t\treturn \"JANUS_SDP_OA_CODEC\";\n\t\tcase JANUS_SDP_OA_EXTENSION:\n\t\t\treturn \"JANUS_SDP_OA_EXTENSION\";\n\t\tcase JANUS_SDP_OA_EXTENSIONS:\n\t\t\treturn \"JANUS_SDP_OA_EXTENSIONS\";\n\t\tcase JANUS_SDP_OA_ACCEPT_EXTMAP:\n\t\t\treturn \"JANUS_SDP_OA_ACCEPT_EXTMAP\";\n\t\tcase JANUS_SDP_OA_PT:\n\t\t\treturn \"JANUS_SDP_OA_PT\";\n\t\tcase JANUS_SDP_OA_FMTP:\n\t\t\treturn \"JANUS_SDP_OA_FMTP\";\n\t\tcase JANUS_SDP_OA_AUDIO_DTMF:\n\t\t\treturn \"JANUS_SDP_OA_AUDIO_DTMF\";\n\t\tcase JANUS_SDP_OA_VP9_PROFILE:\n\t\t\treturn \"JANUS_SDP_OA_VP9_PROFILE\";\n\t\tcase JANUS_SDP_OA_H264_PROFILE:\n\t\t\treturn \"JANUS_SDP_OA_H264_PROFILE\";\n\t\tcase JANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS:\n\t\t\treturn \"JANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS\";\n\t\tcase JANUS_SDP_OA_DATA_LEGACY:\n\t\t\treturn \"JANUS_SDP_OA_DATA_LEGACY\";\n\t\tcase JANUS_SDP_OA_DONE:\n\t\t\treturn \"JANUS_SDP_OA_DONE\";\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\njanus_sdp *janus_sdp_parse(const char *sdp, char *error, size_t errlen) {\n\tif(!sdp)\n\t\treturn NULL;\n\tif(strstr(sdp, \"v=\") != sdp) {\n\t\tif(error)\n\t\t\tg_snprintf(error, errlen, \"Invalid SDP (doesn't start with v=)\");\n\t\treturn NULL;\n\t}\n\tjanus_sdp *imported = g_malloc0(sizeof(janus_sdp));\n\tg_atomic_int_set(&imported->destroyed, 0);\n\tjanus_refcount_init(&imported->ref, janus_sdp_free);\n\timported->o_ipv4 = TRUE;\n\timported->c_ipv4 = TRUE;\n\n\tgboolean success = TRUE;\n\tjanus_sdp_mline *mline = NULL;\n\tint mlines = 0;\n\tchar *line = NULL, *cr = NULL, *rest = NULL;\n\tchar *sdp_copy = g_strdup(sdp);\n\tgboolean first = TRUE, mline_ended = FALSE;\n\t/* When a m-line has been detected we re-use the previous SDP line */\n\twhile(success && (mline_ended || (line = strtok_r(!first ? NULL: sdp_copy, \"\\n\", &rest)) != NULL)) {\n\t\tfirst = FALSE;\n\t\tmline_ended = FALSE;\n\t\tcr = strchr(line, '\\r');\n\t\tif(cr != NULL)\n\t\t\t*cr = '\\0';\n\t\tif(*line == '\\0') {\n\t\t\tif(cr != NULL)\n\t\t\t\t*cr = '\\r';\n\t\t\tcontinue;\n\t\t}\n\t\tif(strnlen(line, 3) < 3) {\n\t\t\tif(error)\n\t\t\t\tg_snprintf(error, errlen, \"Invalid line (%zu bytes): %s\", strlen(line), line);\n\t\t\tsuccess = FALSE;\n\t\t\tbreak;\n\t\t}\n\t\tif(*(line+1) != '=') {\n\t\t\tif(error)\n\t\t\t\tg_snprintf(error, errlen, \"Invalid line (2nd char is not '='): %s\", line);\n\t\t\tsuccess = FALSE;\n\t\t\tbreak;\n\t\t}\n\t\tchar c = *line;\n\t\tif(mline == NULL) {\n\t\t\t/* Global stuff */\n\t\t\tswitch(c) {\n\t\t\t\tcase 'v': {\n\t\t\t\t\tif(sscanf(line, \"v=%d\", &imported->version) != 1) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid v= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'o': {\n\t\t\t\t\tif(imported->o_name || imported->o_addr) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Multiple o= lines: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tchar name[256], addrtype[6], addr[256];\n\t\t\t\t\tif(sscanf(line, \"o=%255s %\"SCNu64\" %\"SCNu64\" IN %5s %255s\",\n\t\t\t\t\t\t\tname, &imported->o_sessid, &imported->o_version, addrtype, addr) != 5) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid o= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(!strcasecmp(addrtype, \"IP4\"))\n\t\t\t\t\t\timported->o_ipv4 = TRUE;\n\t\t\t\t\telse if(!strcasecmp(addrtype, \"IP6\"))\n\t\t\t\t\t\timported->o_ipv4 = FALSE;\n\t\t\t\t\telse {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid o= line (unsupported protocol %s): %s\", addrtype, line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\timported->o_name = g_strdup(name);\n\t\t\t\t\timported->o_addr = g_strdup(addr);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 's': {\n\t\t\t\t\tif(imported->s_name) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Multiple s= lines: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\timported->s_name = g_strdup(line+2);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 't': {\n\t\t\t\t\tif(sscanf(line, \"t=%\"SCNu64\" %\"SCNu64, &imported->t_start, &imported->t_stop) != 2) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid t= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'c': {\n\t\t\t\t\tif(imported->c_addr) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Multiple global c= lines: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tchar addrtype[6], addr[256];\n\t\t\t\t\tif(sscanf(line, \"c=IN %5s %255s\", addrtype, addr) != 2) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid c= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(!strcasecmp(addrtype, \"IP4\"))\n\t\t\t\t\t\timported->c_ipv4 = TRUE;\n\t\t\t\t\telse if(!strcasecmp(addrtype, \"IP6\"))\n\t\t\t\t\t\timported->c_ipv4 = FALSE;\n\t\t\t\t\telse {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid c= line (unsupported protocol %s): %s\", addrtype, line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\timported->c_addr = g_strdup(addr);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'a': {\n\t\t\t\t\tjanus_sdp_attribute *a = g_malloc0(sizeof(janus_sdp_attribute));\n\t\t\t\t\tjanus_refcount_init(&a->ref, janus_sdp_attribute_free);\n\t\t\t\t\tline += 2;\n\t\t\t\t\tchar *semicolon = strchr(line, ':');\n\t\t\t\t\tif(semicolon == NULL) {\n\t\t\t\t\t\ta->name = g_strdup(line);\n\t\t\t\t\t\ta->value = NULL;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif(*(semicolon+1) == '\\0') {\n\t\t\t\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid a= line: %s\", line);\n\t\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t*semicolon = '\\0';\n\t\t\t\t\t\ta->name = g_strdup(line);\n\t\t\t\t\t\ta->value = g_strdup(semicolon+1);\n\t\t\t\t\t\ta->direction = JANUS_SDP_DEFAULT;\n\t\t\t\t\t\t*semicolon = ':';\n\t\t\t\t\t\tif(strstr(line, \"/sendonly\"))\n\t\t\t\t\t\t\ta->direction = JANUS_SDP_SENDONLY;\n\t\t\t\t\t\telse if(strstr(line, \"/recvonly\"))\n\t\t\t\t\t\t\ta->direction = JANUS_SDP_RECVONLY;\n\t\t\t\t\t\tif(strstr(line, \"/inactive\"))\n\t\t\t\t\t\t\ta->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\t}\n\t\t\t\t\timported->attributes = g_list_prepend(imported->attributes, a);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'm': {\n\t\t\t\t\tjanus_sdp_mline *m = g_malloc0(sizeof(janus_sdp_mline));\n\t\t\t\t\tg_atomic_int_set(&m->destroyed, 0);\n\t\t\t\t\tjanus_refcount_init(&m->ref, janus_sdp_mline_free);\n\t\t\t\t\t/* Start with media type, port and protocol */\n\t\t\t\t\tchar type[32];\n\t\t\t\t\tchar proto[64];\n\t\t\t\t\tif(strnlen(line, 200 + 1) > 200) {\n\t\t\t\t\t\tjanus_sdp_mline_destroy(m);\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid m= line (too long): %zu\", strlen(line));\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(sscanf(line, \"m=%31s %\"SCNu16\" %63s %*s\", type, &m->port, proto) != 3) {\n\t\t\t\t\t\tjanus_sdp_mline_destroy(m);\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid m= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tm->index = mlines;\n\t\t\t\t\tmlines++;\n\t\t\t\t\tm->type = janus_sdp_parse_mtype(type);\n\t\t\t\t\tif(m->type == JANUS_SDP_OTHER) {\n\t\t\t\t\t\tjanus_sdp_mline_destroy(m);\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid m= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tm->type_str = g_strdup(type);\n\t\t\t\t\tm->proto = g_strdup(proto);\n\t\t\t\t\tm->direction = JANUS_SDP_SENDRECV;\n\t\t\t\t\tm->c_ipv4 = TRUE;\n\t\t\t\t\t/* Now let's check the payload types/formats */\n\t\t\t\t\tgchar **mline_parts = g_strsplit(line+2, \" \", -1);\n\t\t\t\t\tif(!mline_parts && (m->port > 0 || m->type == JANUS_SDP_APPLICATION)) {\n\t\t\t\t\t\tjanus_sdp_mline_destroy(m);\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid m= line (no payload types/formats): %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tint mindex = 0;\n\t\t\t\t\t\twhile(mline_parts[mindex]) {\n\t\t\t\t\t\t\tif(mindex < 3) {\n\t\t\t\t\t\t\t\t/* We've parsed these before */\n\t\t\t\t\t\t\t\tmindex++;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/* Add string fmt */\n\t\t\t\t\t\t\tm->fmts = g_list_prepend(m->fmts, g_strdup(mline_parts[mindex]));\n\t\t\t\t\t\t\t/* Add numeric payload type */\n\t\t\t\t\t\t\tint ptype = atoi(mline_parts[mindex]);\n\t\t\t\t\t\t\tif(ptype < 0) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid payload type (%s)\\n\", mline_parts[mindex]);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tm->ptypes = g_list_prepend(m->ptypes, GINT_TO_POINTER(ptype));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmindex++;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg_strfreev(mline_parts);\n\t\t\t\t\t\tif(m->fmts == NULL || m->ptypes == NULL) {\n\t\t\t\t\t\t\tjanus_sdp_mline_destroy(m);\n\t\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid m= line (no payload types/formats): %s\", line);\n\t\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tm->fmts = g_list_reverse(m->fmts);\n\t\t\t\t\t\tm->ptypes = g_list_reverse(m->ptypes);\n\t\t\t\t\t}\n\t\t\t\t\t/* Append to the list of m-lines */\n\t\t\t\t\timported->m_lines = g_list_prepend(imported->m_lines, m);\n\t\t\t\t\t/* From now on, we parse this m-line */\n\t\t\t\t\tmline = m;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring '%c' property\\n\", c);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\t/* m-line stuff */\n\t\t\tswitch(c) {\n\t\t\t\tcase 'c': {\n\t\t\t\t\tif(mline->c_addr) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Multiple m-line c= lines: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tchar addrtype[6], addr[256];\n\t\t\t\t\tif(sscanf(line, \"c=IN %5s %255s\", addrtype, addr) != 2) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid c= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif(!strcasecmp(addrtype, \"IP4\"))\n\t\t\t\t\t\tmline->c_ipv4 = TRUE;\n\t\t\t\t\telse if(!strcasecmp(addrtype, \"IP6\"))\n\t\t\t\t\t\tmline->c_ipv4 = FALSE;\n\t\t\t\t\telse {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid c= line (unsupported protocol %s): %s\", addrtype, line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tmline->c_addr = g_strdup(addr);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'b': {\n\t\t\t\t\tif(mline->b_name) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring extra m-line b= line: %s\\n\", line);\n\t\t\t\t\t\tif(cr != NULL)\n\t\t\t\t\t\t\t*cr = '\\r';\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tline += 2;\n\t\t\t\t\tchar *semicolon = strchr(line, ':');\n\t\t\t\t\tif(semicolon == NULL || (*(semicolon+1) == '\\0')) {\n\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid b= line: %s\", line);\n\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t*semicolon = '\\0';\n\t\t\t\t\tif(strcmp(line, \"AS\") && strcmp(line, \"TIAS\")) {\n\t\t\t\t\t\t/* We only support b=AS and b=TIAS, skip */\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tmline->b_name = g_strdup(line);\n\t\t\t\t\tmline->b_value = atol(semicolon+1);\n\t\t\t\t\t*semicolon = ':';\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'a': {\n\t\t\t\t\tjanus_sdp_attribute *a = g_malloc0(sizeof(janus_sdp_attribute));\n\t\t\t\t\tjanus_refcount_init(&a->ref, janus_sdp_attribute_free);\n\t\t\t\t\tline += 2;\n\t\t\t\t\tchar *semicolon = strchr(line, ':');\n\t\t\t\t\tif(semicolon == NULL) {\n\t\t\t\t\t\t/* Is this a media direction attribute? */\n\t\t\t\t\t\tjanus_sdp_mdirection direction = janus_sdp_parse_mdirection(line);\n\t\t\t\t\t\tif(direction != JANUS_SDP_INVALID) {\n\t\t\t\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\t\t\t\tmline->direction = direction;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ta->name = g_strdup(line);\n\t\t\t\t\t\ta->value = NULL;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif(*(semicolon+1) == '\\0') {\n\t\t\t\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\t\t\t\tif(error)\n\t\t\t\t\t\t\t\tg_snprintf(error, errlen, \"Invalid a= line: %s\", line);\n\t\t\t\t\t\t\tsuccess = FALSE;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t*semicolon = '\\0';\n\t\t\t\t\t\ta->name = g_strdup(line);\n\t\t\t\t\t\ta->value = g_strdup(semicolon+1);\n\t\t\t\t\t\ta->direction = JANUS_SDP_DEFAULT;\n\t\t\t\t\t\t*semicolon = ':';\n\t\t\t\t\t\tif(strstr(line, \"/sendonly\"))\n\t\t\t\t\t\t\ta->direction = JANUS_SDP_SENDONLY;\n\t\t\t\t\t\telse if(strstr(line, \"/recvonly\"))\n\t\t\t\t\t\t\ta->direction = JANUS_SDP_RECVONLY;\n\t\t\t\t\t\tif(strstr(line, \"/inactive\"))\n\t\t\t\t\t\t\ta->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\t}\n\t\t\t\t\tmline->attributes = g_list_prepend(mline->attributes, a);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'm': {\n\t\t\t\t\t/* Current m-line ended, back to global parsing */\n\t\t\t\t\tif(mline && mline->attributes)\n\t\t\t\t\t\tmline->attributes = g_list_reverse(mline->attributes);\n\t\t\t\t\tmline = NULL;\n\t\t\t\t\tmline_ended = TRUE;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring '%c' property (m-line)\\n\", c);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif(cr != NULL)\n\t\t\t*cr = '\\r';\n\t}\n\tif(cr != NULL)\n\t\t*cr = '\\r';\n\tg_free(sdp_copy);\n\t/* FIXME Do a last check: is all the stuff that's supposed to be there available? */\n\tif(success && (imported->o_name == NULL || imported->o_addr == NULL || imported->s_name == NULL || imported->m_lines == NULL)) {\n\t\tsuccess = FALSE;\n\t\tif(error)\n\t\t\tg_snprintf(error, errlen, \"Missing mandatory lines (o=, s= or m=)\");\n\t}\n\t/* If something wrong happened, free and return a failure */\n\tif(!success) {\n\t\tif(error)\n\t\t\tJANUS_LOG(LOG_ERR, \"%s\\n\", error);\n\t\tjanus_sdp_destroy(imported);\n\t\timported = NULL;\n\t} else {\n\t\t/* Reverse lists for efficiency */\n\t\tif(mline && mline->attributes)\n\t\t\tmline->attributes = g_list_reverse(mline->attributes);\n\t\tif(imported->attributes)\n\t\t\timported->attributes = g_list_reverse(imported->attributes);\n\t\tif(imported->m_lines)\n\t\t\timported->m_lines = g_list_reverse(imported->m_lines);\n\t}\n\treturn imported;\n}\n\nint janus_sdp_remove_payload_type(janus_sdp *sdp, int index, int pt) {\n\tif(!sdp || pt < 0)\n\t\treturn -1;\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif(index != -1 && index != m->index) {\n\t\t\tml = ml->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Remove any reference from the m-line */\n\t\tm->ptypes = g_list_remove(m->ptypes, GINT_TO_POINTER(pt));\n\t\t/* Also remove all attributes that reference the same payload type */\n\t\tGList *ma = m->attributes;\n\t\twhile(ma) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\tif(a->value && atoi(a->value) == pt) {\n\t\t\t\tm->attributes = g_list_remove(m->attributes, a);\n\t\t\t\tma = m->attributes;\n\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tma = ma->next;\n\t\t}\n\t\tif(index != -1)\n\t\t\tbreak;\n\t\tml = ml->next;\n\t}\n\treturn 0;\n}\n\nint janus_sdp_get_codec_pt(janus_sdp *sdp, int index, const char *codec) {\n\treturn janus_sdp_get_codec_pt_full(sdp, index, codec, NULL);\n}\n\nint janus_sdp_get_codec_pt_full(janus_sdp *sdp, int index, const char *codec, const char *profile) {\n\tif(sdp == NULL || codec == NULL)\n\t\treturn -1;\n\t/* Check the format string (note that we only parse what browsers can negotiate) */\n\tgboolean video = FALSE, vp9 = FALSE, h264 = FALSE;\n\tconst char *format = NULL, *format2 = NULL;\n\tif(!strcasecmp(codec, \"opus\")) {\n\t\tformat = \"opus/48000/2\";\n\t\tformat2 = \"OPUS/48000/2\";\n\t} else if(!strcasecmp(codec, \"multiopus\")) {\n\t\t/* FIXME We're hardcoding to 6 channels, for now */\n\t\tformat = \"multiopus/48000/6\";\n\t\tformat2 = \"MULTIOPUS/48000/6\";\n\t} else if(!strcasecmp(codec, \"pcmu\")) {\n\t\t/* We know the payload type is 0: we just need to make sure it's there */\n\t\tformat = \"pcmu/8000\";\n\t\tformat2 = \"PCMU/8000\";\n\t} else if(!strcasecmp(codec, \"pcma\")) {\n\t\t/* We know the payload type is 8: we just need to make sure it's there */\n\t\tformat = \"pcma/8000\";\n\t\tformat2 = \"PCMA/8000\";\n\t} else if(!strcasecmp(codec, \"g722\")) {\n\t\t/* We know the payload type is 9: we just need to make sure it's there */\n\t\tformat = \"g722/8000\";\n\t\tformat2 = \"G722/8000\";\n\t} else if(!strcasecmp(codec, \"isac16\")) {\n\t\tformat = \"isac/16000\";\n\t\tformat2 = \"ISAC/16000\";\n\t} else if(!strcasecmp(codec, \"isac32\")) {\n\t\tformat = \"isac/32000\";\n\t\tformat2 = \"ISAC/32000\";\n\t} else if(!strcasecmp(codec, \"l16-48\")) {\n\t\tformat = \"l16/48000\";\n\t\tformat2 = \"L16/48000\";\n\t} else if(!strcasecmp(codec, \"l16\")) {\n\t\tformat = \"l16/16000\";\n\t\tformat2 = \"L16/16000\";\n\t} else if(!strcasecmp(codec, \"dtmf\")) {\n\t\tformat = \"telephone-event/8000\";\n\t\tformat2 = \"TELEPHONE-EVENT/8000\";\n\t} else if(!strcasecmp(codec, \"vp8\")) {\n\t\tvideo = TRUE;\n\t\tformat = \"vp8/90000\";\n\t\tformat2 = \"VP8/90000\";\n\t} else if(!strcasecmp(codec, \"vp9\")) {\n\t\tvideo = TRUE;\n\t\tvp9 = TRUE;\t\t/* We may need to filter on profiles */\n\t\tformat = \"vp9/90000\";\n\t\tformat2 = \"VP9/90000\";\n\t} else if(!strcasecmp(codec, \"h264\")) {\n\t\tvideo = TRUE;\n\t\th264 = TRUE;\t/* We may need to filter on profiles */\n\t\tformat = \"h264/90000\";\n\t\tformat2 = \"H264/90000\";\n\t} else if(!strcasecmp(codec, \"av1\")) {\n\t\tvideo = TRUE;\n\t\tformat = \"av1/90000\";\n\t\tformat2 = \"AV1/90000\";\n\t} else if(!strcasecmp(codec, \"h265\")) {\n\t\tvideo = TRUE;\n\t\tformat = \"h265/90000\";\n\t\tformat2 = \"H265/90000\";\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec '%s'\\n\", codec);\n\t\treturn -1;\n\t}\n\t/* Check all m->lines */\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif((!video && m->type != JANUS_SDP_AUDIO) || (video && m->type != JANUS_SDP_VIDEO)) {\n\t\t\tml = ml->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(index != -1 && index != m->index) {\n\t\t\tml = ml->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Look in all rtpmap attributes first */\n\t\tGList *ma = m->attributes;\n\t\tint pt = -1;\n\t\tGList *pts = NULL;\n\t\twhile(ma) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\tif(a->name != NULL && a->value != NULL && !strcasecmp(a->name, \"rtpmap\")) {\n\t\t\t\tpt = atoi(a->value);\n\t\t\t\tif(pt < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid payload type (%s)\\n\", a->value);\n\t\t\t\t} else if(strstr(a->value, format) || strstr(a->value, format2)) {\n\t\t\t\t\tif(profile != NULL && (vp9 || h264)) {\n\t\t\t\t\t\t/* Let's keep track of this payload type */\n\t\t\t\t\t\tpts = g_list_append(pts, GINT_TO_POINTER(pt));\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Payload type for codec found */\n\t\t\t\t\t\tg_list_free(pts);\n\t\t\t\t\t\treturn pt;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tma = ma->next;\n\t\t}\n\t\tif(profile != NULL) {\n\t\t\t/* Now look for the profile in the fmtp attributes */\n\t\t\tma = m->attributes;\n\t\t\twhile(ma) {\n\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\tif(profile != NULL && a->name != NULL && a->value != NULL && !strcasecmp(a->name, \"fmtp\")) {\n\t\t\t\t\t/* Does this match the payload types we're looking for? */\n\t\t\t\t\tpt = atoi(a->value);\n\t\t\t\t\tif(g_list_find(pts, GINT_TO_POINTER(pt)) == NULL) {\n\t\t\t\t\t\t/* Not what we're looking for */\n\t\t\t\t\t\tma = ma->next;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(vp9) {\n\t\t\t\t\t\tchar profile_id[20];\n\t\t\t\t\t\tg_snprintf(profile_id, sizeof(profile_id), \"profile-id=%s\", profile);\n\t\t\t\t\t\tif(strstr(a->value, profile_id) != NULL) {\n\t\t\t\t\t\t\t/* Found */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"VP9 profile %s found --> %d\\n\", profile, pt);\n\t\t\t\t\t\t\tg_list_free(pts);\n\t\t\t\t\t\t\treturn pt;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(h264 && strstr(a->value, \"packetization-mode=0\") == NULL) {\n\t\t\t\t\t\t/* We only support packetization-mode=1, no matter the profile */\n\t\t\t\t\t\tchar profile_level_id[30];\n\t\t\t\t\t\tchar *profile_lower = g_ascii_strdown(profile, -1);\n\t\t\t\t\t\tg_snprintf(profile_level_id, sizeof(profile_level_id), \"profile-level-id=%s\", profile_lower);\n\t\t\t\t\t\tg_free(profile_lower);\n\t\t\t\t\t\tif(strstr(a->value, profile_level_id) != NULL) {\n\t\t\t\t\t\t\t/* Found */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"H.264 profile %s found --> %d\\n\", profile, pt);\n\t\t\t\t\t\t\tg_list_free(pts);\n\t\t\t\t\t\t\treturn pt;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Not found, try converting the profile to upper case */\n\t\t\t\t\t\tchar *profile_upper = g_ascii_strup(profile, -1);\n\t\t\t\t\t\tg_snprintf(profile_level_id, sizeof(profile_level_id), \"profile-level-id=%s\", profile_upper);\n\t\t\t\t\t\tg_free(profile_upper);\n\t\t\t\t\t\tif(strstr(a->value, profile_level_id) != NULL) {\n\t\t\t\t\t\t\t/* Found */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"H.264 profile %s found --> %d\\n\", profile, pt);\n\t\t\t\t\t\t\tg_list_free(pts);\n\t\t\t\t\t\t\treturn pt;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tma = ma->next;\n\t\t\t}\n\t\t}\n\t\tg_list_free(pts);\n\t\tif(index != -1)\n\t\t\tbreak;\n\t\tml = ml->next;\n\t}\n\treturn -1;\n}\n\nconst char *janus_sdp_get_codec_name(janus_sdp *sdp, int index, int pt) {\n\tif(sdp == NULL || pt < 0)\n\t\treturn NULL;\n\tif(pt == 0)\n\t\treturn \"pcmu\";\n\tif(pt == 8)\n\t\treturn \"pcma\";\n\tif(pt == 9)\n\t\treturn \"g722\";\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif(index != -1 && index != m->index) {\n\t\t\tml = ml->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Look in all rtpmap attributes */\n\t\tGList *ma = m->attributes;\n\t\twhile(ma) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\tif(a->name != NULL && a->value != NULL && !strcasecmp(a->name, \"rtpmap\")) {\n\t\t\t\tint a_pt = atoi(a->value);\n\t\t\t\tif(a_pt == pt) {\n\t\t\t\t\t/* Found! */\n\t\t\t\t\tif(strstr(a->value, \"vp8\") || strstr(a->value, \"VP8\"))\n\t\t\t\t\t\treturn \"vp8\";\n\t\t\t\t\tif(strstr(a->value, \"vp9\") || strstr(a->value, \"VP9\"))\n\t\t\t\t\t\treturn \"vp9\";\n\t\t\t\t\tif(strstr(a->value, \"h264\") || strstr(a->value, \"H264\"))\n\t\t\t\t\t\treturn \"h264\";\n\t\t\t\t\tif(strstr(a->value, \"av1\") || strstr(a->value, \"AV1\"))\n\t\t\t\t\t\treturn \"av1\";\n\t\t\t\t\tif(strstr(a->value, \"h265\") || strstr(a->value, \"H265\"))\n\t\t\t\t\t\treturn \"h265\";\n\t\t\t\t\tif(strstr(a->value, \"multiopus\") || strstr(a->value, \"MULTIOPUS\"))\n\t\t\t\t\t\treturn \"multiopus\";\n\t\t\t\t\tif(strstr(a->value, \"opus\") || strstr(a->value, \"OPUS\"))\n\t\t\t\t\t\treturn \"opus\";\n\t\t\t\t\tif(strstr(a->value, \"pcmu\") || strstr(a->value, \"PCMU\"))\n\t\t\t\t\t\treturn \"pcmu\";\n\t\t\t\t\tif(strstr(a->value, \"pcma\") || strstr(a->value, \"PCMA\"))\n\t\t\t\t\t\treturn \"pcma\";\n\t\t\t\t\tif(strstr(a->value, \"g722\") || strstr(a->value, \"G722\"))\n\t\t\t\t\t\treturn \"g722\";\n\t\t\t\t\tif(strstr(a->value, \"isac/16\") || strstr(a->value, \"ISAC/16\"))\n\t\t\t\t\t\treturn \"isac16\";\n\t\t\t\t\tif(strstr(a->value, \"isac/32\") || strstr(a->value, \"ISAC/32\"))\n\t\t\t\t\t\treturn \"isac32\";\n\t\t\t\t\tif(strstr(a->value, \"l16/48\") || strstr(a->value, \"L16/48\"))\n\t\t\t\t\t\treturn \"l16-48\";\n\t\t\t\t\tif(strstr(a->value, \"l16/16\") || strstr(a->value, \"L16/16\"))\n\t\t\t\t\t\treturn \"l16\";\n\t\t\t\t\tif(strstr(a->value, \"telephone-event/8000\") || strstr(a->value, \"telephone-event/8000\"))\n\t\t\t\t\t\treturn \"dtmf\";\n\t\t\t\t\t/* RED is not really a codec, but we need to detect it anyway */\n\t\t\t\t\tif(strstr(a->value, \"red\") || strstr(a->value, \"RED\"))\n\t\t\t\t\t\treturn \"red\";\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec '%s'\\n\", a->value);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t\tma = ma->next;\n\t\t}\n\t\tif(index != -1)\n\t\t\tbreak;\n\t\tml = ml->next;\n\t}\n\treturn NULL;\n}\n\nconst char *janus_sdp_get_rtpmap_codec(const char *rtpmap) {\n\tif(rtpmap == NULL)\n\t\treturn NULL;\n\tconst char *codec = NULL;\n\tchar *rtpmap_val = g_ascii_strdown(rtpmap, -1);\n\tif(strstr(rtpmap_val, \"opus/\") == rtpmap_val)\n\t\tcodec = \"opus\";\n\telse if(strstr(rtpmap_val, \"multiopus/\") == rtpmap_val)\n\t\tcodec = \"multiopus\";\n\telse if(strstr(rtpmap_val, \"pcmu/\") == rtpmap_val)\n\t\tcodec = \"pcmu\";\n\telse if(strstr(rtpmap_val, \"pcma/\") == rtpmap_val)\n\t\tcodec = \"pcma\";\n\telse if(strstr(rtpmap_val, \"g722/\") == rtpmap_val)\n\t\tcodec = \"g722\";\n\telse if(strstr(rtpmap_val, \"isac/16\") == rtpmap_val)\n\t\tcodec = \"isac16\";\n\telse if(strstr(rtpmap_val, \"isac/32\") == rtpmap_val)\n\t\tcodec = \"isac32\";\n\telse if(strstr(rtpmap_val, \"l16/48\") == rtpmap_val)\n\t\tcodec = \"l16-48\";\n\telse if(strstr(rtpmap_val, \"l16/16\") == rtpmap_val)\n\t\tcodec = \"l16\";\n\telse if(strstr(rtpmap_val, \"telephone-event/\") == rtpmap_val)\n\t\tcodec = \"dtmf\";\n\telse if(strstr(rtpmap_val, \"vp8/\") == rtpmap_val)\n\t\tcodec = \"vp8\";\n\telse if(strstr(rtpmap_val, \"vp9/\") == rtpmap_val)\n\t\tcodec = \"vp9\";\n\telse if(strstr(rtpmap_val, \"h264/\") == rtpmap_val)\n\t\tcodec = \"h264\";\n\telse if(strstr(rtpmap_val, \"av1/\") == rtpmap_val)\n\t\tcodec = \"av1\";\n\telse if(strstr(rtpmap_val, \"h265/\") == rtpmap_val)\n\t\tcodec = \"h265\";\n\tif(codec == NULL)\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported rtpmap '%s'\\n\", rtpmap);\n\tg_free(rtpmap_val);\n\treturn codec;\n}\n\nconst char *janus_sdp_get_codec_rtpmap(const char *codec) {\n\tif(codec == NULL)\n\t\treturn NULL;\n\tif(!strcasecmp(codec, \"opus\"))\n\t\treturn \"opus/48000/2\";\n\tif(!strcasecmp(codec, \"multiopus\"))\n\t\t/* FIXME We're hardcoding to 6 channels, for now */\n\t\treturn \"multiopus/48000/6\";\n\tif(!strcasecmp(codec, \"pcmu\"))\n\t\treturn \"PCMU/8000\";\n\tif(!strcasecmp(codec, \"pcma\"))\n\t\treturn \"PCMA/8000\";\n\tif(!strcasecmp(codec, \"g722\"))\n\t\treturn \"G722/8000\";\n\tif(!strcasecmp(codec, \"isac16\"))\n\t\treturn \"ISAC/16000\";\n\tif(!strcasecmp(codec, \"isac32\"))\n\t\treturn \"ISAC/32000\";\n\tif(!strcasecmp(codec, \"l16-48\"))\n\t\treturn \"L16/48000\";\n\tif(!strcasecmp(codec, \"l16\"))\n\t\treturn \"L16/16000\";\n\tif(!strcasecmp(codec, \"dtmf\"))\n\t\treturn \"telephone-event/8000\";\n\tif(!strcasecmp(codec, \"vp8\"))\n\t\treturn \"VP8/90000\";\n\tif(!strcasecmp(codec, \"vp9\"))\n\t\treturn \"VP9/90000\";\n\tif(!strcasecmp(codec, \"h264\"))\n\t\treturn \"H264/90000\";\n\tif(!strcasecmp(codec, \"av1\"))\n\t\treturn \"AV1/90000\";\n\tif(!strcasecmp(codec, \"h265\"))\n\t\treturn \"H265/90000\";\n\tJANUS_LOG(LOG_ERR, \"Unsupported codec '%s'\\n\", codec);\n\treturn NULL;\n}\n\nconst char *janus_sdp_get_fmtp(janus_sdp *sdp, int index, int pt) {\n\tif(sdp == NULL || pt < 0)\n\t\treturn NULL;\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif(index != -1 && index != m->index) {\n\t\t\tml = ml->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Look in all fmtp attributes */\n\t\tGList *ma = m->attributes;\n\t\twhile(ma) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\tif(a->name != NULL && a->value != NULL && !strcasecmp(a->name, \"fmtp\")) {\n\t\t\t\tint a_pt = atoi(a->value);\n\t\t\t\tif(a_pt == pt) {\n\t\t\t\t\t/* Found! */\n\t\t\t\t\tchar needle[10];\n\t\t\t\t\tg_snprintf(needle, sizeof(needle), \"%d \", pt);\n\t\t\t\t\tif(strstr(a->value, needle) == a->value)\n\t\t\t\t\t\treturn a->value + strlen(needle);\n\t\t\t\t}\n\t\t\t}\n\t\t\tma = ma->next;\n\t\t}\n\t\tif(index != -1)\n\t\t\tbreak;\n\t\tml = ml->next;\n\t}\n\treturn NULL;\n}\n\nchar *janus_sdp_get_video_profile(janus_videocodec codec, const char *fmtp) {\n\tif(fmtp == NULL)\n\t\treturn NULL;\n\tconst char *needle = NULL;\n\tif(codec == JANUS_VIDEOCODEC_H264) {\n\t\tneedle = \"profile-level-id=\";\n\t} else if(codec == JANUS_VIDEOCODEC_VP9) {\n\t\tneedle = \"profile-id=\";\n\t} else {\n\t\treturn NULL;\n\t}\n\tgchar **list = g_strsplit(fmtp, \";\", -1);\n\tint i=0;\n\tgchar *index = list[0];\n\tchar *profile = NULL, *temp = NULL;\n\twhile(index != NULL) {\n\t\tif((temp = strstr(index, needle)) != NULL) {\n\t\t\tprofile = temp + strlen(needle);\n\t\t\tif(strlen(profile) > 0)\n\t\t\t\tprofile = g_strdup(profile);\n\t\t\telse\n\t\t\t\tprofile = NULL;\n\t\t\tbreak;\n\t\t}\n\t\ti++;\n\t\tindex = list[i];\n\t}\n\tg_clear_pointer(&list, g_strfreev);\n\treturn profile;\n}\n\nint janus_sdp_get_opusred_pt(janus_sdp *sdp, int index) {\n\tif(sdp == NULL)\n\t\treturn -1;\n\t/* Check all m->lines */\n\tGList *ml = sdp->m_lines;\n\twhile(ml) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)ml->data;\n\t\tif(m->type != JANUS_SDP_AUDIO) {\n\t\t\tml = ml->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(index != -1 && index != m->index) {\n\t\t\tml = ml->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Look in all rtpmap attributes */\n\t\tGList *ma = m->attributes;\n\t\twhile(ma) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\tif(a->name != NULL && a->value != NULL && !strcasecmp(a->name, \"rtpmap\")) {\n\t\t\t\tint pt = atoi(a->value);\n\t\t\t\tif(strstr(a->value, \"red/48000/2\"))\n\t\t\t\t\treturn pt;\n\t\t\t}\n\t\t\tma = ma->next;\n\t\t}\n\t\tif(index != -1)\n\t\t\tbreak;\n\t\tml = ml->next;\n\t}\n\treturn -1;\n}\n\nchar *janus_sdp_write(janus_sdp *imported) {\n\tif(!imported)\n\t\treturn NULL;\n\tjanus_refcount_increase(&imported->ref);\n\tchar *sdp = g_malloc(2560), mline[8192], buffer[2048];\n\t*sdp = '\\0';\n\tsize_t sdplen = 2560, mlen = sizeof(mline), offset = 0, moffset = 0;\n\t/* v= */\n\tg_snprintf(buffer, sizeof(buffer), \"v=%d\\r\\n\", imported->version);\n\tjanus_strlcat_fast(sdp, buffer, sdplen, &offset);\n\t/* o= */\n\tg_snprintf(buffer, sizeof(buffer), \"o=%s %\"SCNu64\" %\"SCNu64\" IN %s %s\\r\\n\",\n\t\timported->o_name, imported->o_sessid, imported->o_version,\n\t\timported->o_ipv4 ? \"IP4\" : \"IP6\", imported->o_addr);\n\tjanus_strlcat_fast(sdp, buffer, sdplen, &offset);\n\t/* s= */\n\tg_snprintf(buffer, sizeof(buffer), \"s=%s\\r\\n\", imported->s_name);\n\tjanus_strlcat_fast(sdp, buffer, sdplen, &offset);\n\t/* t= */\n\tg_snprintf(buffer, sizeof(buffer), \"t=%\"SCNu64\" %\"SCNu64\"\\r\\n\", imported->t_start, imported->t_stop);\n\tjanus_strlcat_fast(sdp, buffer, sdplen, &offset);\n\t/* c= */\n\tif(imported->c_addr != NULL) {\n\t\tif(imported->c_ipv4 && imported->c_addr && strstr(imported->c_addr, \":\"))\n\t\t\timported->c_ipv4 = FALSE;\n\t\tg_snprintf(buffer, sizeof(buffer), \"c=IN %s %s\\r\\n\",\n\t\t\timported->c_ipv4 ? \"IP4\" : \"IP6\", imported->c_addr);\n\t\tjanus_strlcat_fast(sdp, buffer, sdplen, &offset);\n\t}\n\t/* a= */\n\tGList *temp = imported->attributes;\n\twhile(temp) {\n\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)temp->data;\n\t\tif(a->value != NULL) {\n\t\t\tg_snprintf(buffer, sizeof(buffer), \"a=%s:%s\\r\\n\", a->name, a->value);\n\t\t} else {\n\t\t\tg_snprintf(buffer, sizeof(buffer), \"a=%s\\r\\n\", a->name);\n\t\t}\n\t\tjanus_strlcat_fast(sdp, buffer, sdplen, &offset);\n\t\ttemp = temp->next;\n\t}\n\t/* m= */\n\ttemp = imported->m_lines;\n\twhile(temp) {\n\t\tmline[0] = '\\0';\n\t\tmoffset = 0;\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tg_snprintf(buffer, sizeof(buffer), \"m=%s %d %s\", m->type_str, m->port, m->proto);\n\t\tjanus_strlcat_fast(mline, buffer, mlen, &moffset);\n\t\tif(m->port == 0 && m->type != JANUS_SDP_APPLICATION) {\n\t\t\t/* Remove all payload types/formats if we're rejecting the media */\n\t\t\tg_list_free_full(m->fmts, (GDestroyNotify)g_free);\n\t\t\tm->fmts = NULL;\n\t\t\tg_list_free(m->ptypes);\n\t\t\tm->ptypes = NULL;\n\t\t\tm->ptypes = g_list_append(m->ptypes, GINT_TO_POINTER(0));\n\t\t\tjanus_strlcat_fast(mline, \" 0\", mlen, &moffset);\n\t\t} else {\n\t\t\tif(m->proto != NULL && strstr(m->proto, \"RTP\") != NULL) {\n\t\t\t\t/* RTP profile, use payload types */\n\t\t\t\tGList *ptypes = m->ptypes;\n\t\t\t\twhile(ptypes) {\n\t\t\t\t\tg_snprintf(buffer, sizeof(buffer), \" %d\", GPOINTER_TO_INT(ptypes->data));\n\t\t\t\t\tjanus_strlcat_fast(mline, buffer, mlen, &moffset);\n\t\t\t\t\tptypes = ptypes->next;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Something else, use formats */\n\t\t\t\tGList *fmts = m->fmts;\n\t\t\t\twhile(fmts) {\n\t\t\t\t\tg_snprintf(buffer, sizeof(buffer), \" %s\", (char *)(fmts->data));\n\t\t\t\t\tjanus_strlcat_fast(mline, buffer, mlen, &moffset);\n\t\t\t\t\tfmts = fmts->next;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tjanus_strlcat_fast(mline, \"\\r\\n\", mlen, &moffset);\n\t\t/* c= */\n\t\tif(m->c_addr != NULL) {\n\t\t\tg_snprintf(buffer, sizeof(buffer), \"c=IN %s %s\\r\\n\",\n\t\t\t\tm->c_ipv4 ? \"IP4\" : \"IP6\", m->c_addr);\n\t\t\tjanus_strlcat_fast(mline, buffer, mlen, &moffset);\n\t\t}\n\t\tif(m->port > 0) {\n\t\t\t/* b= */\n\t\t\tif(m->b_name != NULL) {\n\t\t\t\tg_snprintf(buffer, sizeof(buffer), \"b=%s:%\"SCNu32\"\\r\\n\", m->b_name, m->b_value);\n\t\t\t\tjanus_strlcat_fast(mline, buffer, mlen, &moffset);\n\t\t\t}\n\t\t}\n\t\t/* a= (note that we don't format the direction if it's JANUS_SDP_DEFAULT) */\n\t\tconst char *direction = m->direction != JANUS_SDP_DEFAULT ? janus_sdp_mdirection_str(m->direction) : NULL;\n\t\tif(direction != NULL) {\n\t\t\tg_snprintf(buffer, sizeof(buffer), \"a=%s\\r\\n\", direction);\n\t\t\tjanus_strlcat_fast(mline, buffer, mlen, &moffset);\n\t\t}\n\t\tGList *temp2 = m->attributes;\n\t\twhile(temp2) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)temp2->data;\n\t\t\tif(m->port == 0 && strcasecmp(a->name, \"mid\")) {\n\t\t\t\t/* This media has been rejected or disabled: we only add the mid attribute, if available */\n\t\t\t\ttemp2 = temp2->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(a->value != NULL) {\n\t\t\t\tg_snprintf(buffer, sizeof(buffer), \"a=%s:%s\\r\\n\", a->name, a->value);\n\t\t\t} else {\n\t\t\t\tg_snprintf(buffer, sizeof(buffer), \"a=%s\\r\\n\", a->name);\n\t\t\t}\n\t\t\tjanus_strlcat_fast(mline, buffer, mlen, &moffset);\n\t\t\ttemp2 = temp2->next;\n\t\t}\n\t\t/* Append the generated m-line to the SDP */\n\t\tsize_t cur_sdplen = strlen(sdp);\n\t\tsize_t mlinelen = strlen(mline);\n\t\tif(cur_sdplen + mlinelen + 1 > sdplen) {\n\t\t\t/* Increase the SDP buffer first */\n\t\t\tif(sdplen < (mlinelen+1))\n\t\t\t\tsdplen = cur_sdplen + mlinelen + 1;\n\t\t\telse\n\t\t\t\tsdplen = sdplen*2;\n\t\t\tsdp = g_realloc(sdp, sdplen);\n\t\t}\n\t\tjanus_strlcat_fast(sdp, mline, sdplen, &offset);\n\t\t/* Move on */\n\t\ttemp = temp->next;\n\t}\n\tjanus_refcount_decrease(&imported->ref);\n\treturn sdp;\n}\n\nvoid janus_sdp_find_preferred_codec(janus_sdp *sdp, janus_sdp_mtype type, int index, const char **codec) {\n\tif(sdp == NULL)\n\t\treturn;\n\tjanus_refcount_increase(&sdp->ref);\n\tgboolean found = FALSE;\n\tGList *temp = sdp->m_lines;\n\twhile(temp) {\n\t\t/* Which media are available? */\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tif(index != -1 && index != m->index) {\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(m->type == type && m->port > 0 && m->direction != JANUS_SDP_INACTIVE) {\n\t\t\tuint i=0;\n\t\t\tfor(i=0; i<(type == JANUS_SDP_AUDIO ? janus_audio_codecs : janus_video_codecs); i++) {\n\t\t\t\tif(janus_sdp_get_codec_pt(sdp, m->index,\n\t\t\t\t\t\ttype == JANUS_SDP_AUDIO ? janus_preferred_audio_codecs[i] : janus_preferred_video_codecs[i]) > 0) {\n\t\t\t\t\tfound = TRUE;\n\t\t\t\t\tif(codec)\n\t\t\t\t\t\t*codec = (type == JANUS_SDP_AUDIO ? janus_preferred_audio_codecs[i] : janus_preferred_video_codecs[i]);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif(found || index != -1)\n\t\t\tbreak;\n\t\ttemp = temp->next;\n\t}\n\tjanus_refcount_decrease(&sdp->ref);\n}\n\nvoid janus_sdp_find_first_codec(janus_sdp *sdp, janus_sdp_mtype type, int index, const char **codec) {\n\tif(sdp == NULL)\n\t\treturn;\n\tjanus_refcount_increase(&sdp->ref);\n\tgboolean found = FALSE;\n\tGList *temp = sdp->m_lines;\n\twhile(temp) {\n\t\t/* Which media are available? */\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tif(index != -1 && index != m->index) {\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(m->type == type && m->port > 0 && m->direction != JANUS_SDP_INACTIVE && m->ptypes) {\n\t\t\tint pt = GPOINTER_TO_INT(m->ptypes->data);\n\t\t\tconst char *c = janus_sdp_get_codec_name(sdp, m->index, pt);\n\t\t\tif(c && !strcasecmp(c, \"red\")) {\n\t\t\t\t/* We're using RED, so check the second payload type for the actual codec */\n\t\t\t\tpt = m->ptypes->next ? GPOINTER_TO_INT(m->ptypes->next->data) : -1;\n\t\t\t\tc = janus_sdp_get_codec_name(sdp, m->index, pt);\n\t\t\t}\n\t\t\tc = janus_sdp_match_preferred_codec(m->type, (char *)c);\n\t\t\tif(c) {\n\t\t\t\tfound = TRUE;\n\t\t\t\tif(codec)\n\t\t\t\t\t*codec = c;\n\t\t\t}\n\t\t}\n\t\tif(found || index != -1)\n\t\t\tbreak;\n\t\ttemp = temp->next;\n\t}\n\tjanus_refcount_decrease(&sdp->ref);\n}\n\nconst char *janus_sdp_match_preferred_codec(janus_sdp_mtype type, char *codec) {\n\tif(codec == NULL)\n\t\treturn NULL;\n\tif(type != JANUS_SDP_AUDIO && type != JANUS_SDP_VIDEO)\n\t\treturn NULL;\n\tgboolean video = (type == JANUS_SDP_VIDEO);\n\tuint i=0;\n\tfor(i=0; i<(video ? janus_video_codecs : janus_audio_codecs); i++) {\n\t\tif(!strcasecmp(codec, (video ? janus_preferred_video_codecs[i] : janus_preferred_audio_codecs[i]))) {\n\t\t\t/* Found! */\n\t\t\treturn video ? janus_preferred_video_codecs[i] : janus_preferred_audio_codecs[i];\n\t\t}\n\t}\n\treturn NULL;\n}\n\njanus_sdp *janus_sdp_new(const char *name, const char *address) {\n\tjanus_sdp *sdp = g_malloc(sizeof(janus_sdp));\n\tg_atomic_int_set(&sdp->destroyed, 0);\n\tjanus_refcount_init(&sdp->ref, janus_sdp_free);\n\t/* Fill in some predefined stuff */\n\tsdp->version = 0;\n\tsdp->o_name = g_strdup(\"-\");\n\tsdp->o_sessid = janus_get_real_time();\n\tsdp->o_version = 1;\n\tsdp->o_ipv4 = TRUE;\n\tsdp->o_addr = g_strdup(address ? address : \"127.0.0.1\");\n\tsdp->s_name = g_strdup(name ? name : \"Janus session\");\n\tsdp->t_start = 0;\n\tsdp->t_stop = 0;\n\tsdp->c_ipv4 = TRUE;\n\tsdp->c_addr = g_strdup(address ? address : \"127.0.0.1\");\n\tsdp->attributes = NULL;\n\tsdp->m_lines = NULL;\n\t/* Done */\n\treturn sdp;\n}\n\nstatic int janus_sdp_id_compare(gconstpointer a, gconstpointer b) {\n\treturn GPOINTER_TO_INT(a) - GPOINTER_TO_INT(b);\n}\njanus_sdp *janus_sdp_generate_offer(const char *name, const char *address, ...) {\n\t/* This method has a variable list of arguments, telling us what we should offer */\n\tint property = -1;\n\tva_list args;\n\tva_start(args, address);\n\n\t/* Create a new janus_sdp object */\n\tjanus_sdp *offer = janus_sdp_new(name, address);\n\n\tgboolean new_mline = FALSE, mline_enabled = FALSE;\n\tjanus_sdp_mtype type = JANUS_SDP_OTHER;\n\tgboolean audio_dtmf = FALSE, video_rtcpfb = TRUE, data_legacy = FALSE;\n\tint pt = -1, opusred_pt = -1;\n\tconst char *codec = NULL, *mid = NULL, *msid = NULL, *mstid = NULL,\n\t\t*fmtp = NULL, *vp9_profile = NULL, *h264_profile = NULL;\n\tjanus_sdp_mdirection mdir = JANUS_SDP_DEFAULT;\n\tGHashTable *extmaps = NULL, *extids = NULL, *m_extids = NULL;\n\n\twhile(property != JANUS_SDP_OA_DONE) {\n\t\tproperty = va_arg(args, int);\n\t\tif(!new_mline && property != JANUS_SDP_OA_MLINE && property != JANUS_SDP_OA_DONE) {\n\t\t\t/* The first attribute MUST be JANUS_SDP_OA_MLINE or JANUS_SDP_OA_DONE */\n\t\t\tJANUS_LOG(LOG_ERR, \"First attribute is not JANUS_SDP_OA_MLINE or JANUS_SDP_OA_DONE\\n\");\n\t\t\tjanus_sdp_destroy(offer);\n\t\t\tif(extmaps != NULL)\n\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\tif(extids != NULL)\n\t\t\t\tg_hash_table_destroy(extids);\n\t\t\tif(m_extids != NULL)\n\t\t\t\tg_hash_table_destroy(m_extids);\n\t\t\tva_end(args);\n\t\t\treturn NULL;\n\t\t}\n\t\tif(property == JANUS_SDP_OA_MLINE || property == JANUS_SDP_OA_DONE) {\n\t\t\t/* A new m-line is starting or we're done, should we wrap the previous one? */\n\t\t\tnew_mline = TRUE;\n\t\t\tif(mline_enabled) {\n\t\t\t\t/* Create a new m-line with the data collected so far */\n\t\t\t\tif(type == JANUS_SDP_AUDIO) {\n\t\t\t\t\tif(janus_sdp_generate_offer_mline(offer,\n\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\t\t\t\tJANUS_SDP_OA_MID, mid,\n\t\t\t\t\t\tJANUS_SDP_OA_MSID, msid, mstid,\n\t\t\t\t\t\tJANUS_SDP_OA_OPUSRED_PT, opusred_pt,\n\t\t\t\t\t\tJANUS_SDP_OA_CODEC, codec,\n\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, mdir,\n\t\t\t\t\t\tJANUS_SDP_OA_FMTP, fmtp,\n\t\t\t\t\t\tJANUS_SDP_OA_EXTENSIONS, m_extids,\n\t\t\t\t\t\tJANUS_SDP_OA_AUDIO_DTMF, audio_dtmf,\n\t\t\t\t\t\tJANUS_SDP_OA_DONE\n\t\t\t\t\t) < 0) {\n\t\t\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\t\t\tif(extmaps != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\t\t\t\tif(extids != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t\t\t\tif(m_extids != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(m_extids);\n\t\t\t\t\t\tva_end(args);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t} else if(type == JANUS_SDP_VIDEO) {\n\t\t\t\t\tif(janus_sdp_generate_offer_mline(offer,\n\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_VIDEO,\n\t\t\t\t\t\tJANUS_SDP_OA_MID, mid,\n\t\t\t\t\t\tJANUS_SDP_OA_MSID, msid, mstid,\n\t\t\t\t\t\tJANUS_SDP_OA_PT, pt,\n\t\t\t\t\t\tJANUS_SDP_OA_CODEC, codec,\n\t\t\t\t\t\tJANUS_SDP_OA_DIRECTION, mdir,\n\t\t\t\t\t\tJANUS_SDP_OA_FMTP, fmtp,\n\t\t\t\t\t\tJANUS_SDP_OA_EXTENSIONS, m_extids,\n\t\t\t\t\t\tJANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS, video_rtcpfb,\n\t\t\t\t\t\tJANUS_SDP_OA_VP9_PROFILE, vp9_profile,\n\t\t\t\t\t\tJANUS_SDP_OA_H264_PROFILE, h264_profile,\n\t\t\t\t\t\tJANUS_SDP_OA_DONE\n\t\t\t\t\t) < 0) {\n\t\t\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\t\t\tif(extmaps != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\t\t\t\tif(extids != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t\t\t\tif(m_extids != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(m_extids);\n\t\t\t\t\t\tva_end(args);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t} else if(type == JANUS_SDP_APPLICATION) {\n\t\t\t\t\tif(janus_sdp_generate_offer_mline(offer,\n\t\t\t\t\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_APPLICATION,\n\t\t\t\t\t\tJANUS_SDP_OA_MID, mid,\n\t\t\t\t\t\tJANUS_SDP_OA_DATA_LEGACY, data_legacy,\n\t\t\t\t\t\tJANUS_SDP_OA_DONE\n\t\t\t\t\t) < 0) {\n\t\t\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\t\t\tif(extmaps != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\t\t\t\tif(extids != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t\t\t\tif(m_extids != NULL)\n\t\t\t\t\t\t\tg_hash_table_destroy(m_extids);\n\t\t\t\t\t\tva_end(args);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(property != JANUS_SDP_OA_MLINE)\n\t\t\t\tcontinue;\n\t\t\t/* Now reset the properties */\n\t\t\taudio_dtmf = FALSE;\n\t\t\tvideo_rtcpfb = TRUE;\n\t\t\tdata_legacy = FALSE;\n\t\t\tpt = -1;\n\t\t\topusred_pt = -1;\n\t\t\tmid = NULL;\n\t\t\tmsid = NULL;\n\t\t\tmstid = NULL;\n\t\t\tcodec = NULL;\n\t\t\tfmtp = NULL;\n\t\t\tvp9_profile = NULL;\n\t\t\th264_profile = NULL;\n\t\t\tif(m_extids != NULL)\n\t\t\t\tg_hash_table_destroy(m_extids);\n\t\t\tm_extids = NULL;\n\t\t\tmdir = JANUS_SDP_DEFAULT;\n\t\t\tmline_enabled = TRUE;\n\t\t\t/* The value of JANUS_SDP_OA_MLINE MUST be the media we want to add */\n\t\t\ttype = va_arg(args, int);\n\t\t\tif(type == JANUS_SDP_AUDIO) {\n\t\t\t\t/* Audio, let's set some defaults */\n\t\t\t\tpt = 111;\n\t\t\t\tcodec = \"opus\";\n\t\t\t} else if(type == JANUS_SDP_VIDEO) {\n\t\t\t\t/* Video, let's set some defaults */\n\t\t\t\tpt = 96;\n\t\t\t\tcodec = \"vp8\";\n\t\t\t} else if(type == JANUS_SDP_APPLICATION) {\n\t\t\t\t/* Data */\n\t\t\t} else {\n\t\t\t\t/* Unsupported m-line type */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid m-line type\\n\");\n\t\t\t\tjanus_sdp_destroy(offer);\n\t\t\t\tif(extmaps != NULL)\n\t\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\t\tif(extids != NULL)\n\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t\tif(m_extids != NULL)\n\t\t\t\t\tg_hash_table_destroy(m_extids);\n\t\t\t\tva_end(args);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\t/* Let's assume the m-line is enabled, by default */\n\t\t\tmline_enabled = TRUE;\n\t\t} else if(property == JANUS_SDP_OA_ENABLED) {\n\t\t\tmline_enabled = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_MID) {\n\t\t\tmid = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_MSID) {\n\t\t\tmsid = va_arg(args, char *);\n\t\t\tmstid = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_DIRECTION) {\n\t\t\tmdir = va_arg(args, janus_sdp_mdirection);\n\t\t} else if(property == JANUS_SDP_OA_CODEC) {\n\t\t\tcodec = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_PT) {\n\t\t\tpt = va_arg(args, int);\n\t\t} else if(property == JANUS_SDP_OA_OPUSRED_PT) {\n\t\t\topusred_pt = va_arg(args, int);\n\t\t} else if(property == JANUS_SDP_OA_FMTP) {\n\t\t\tfmtp = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_VP9_PROFILE) {\n\t\t\tvp9_profile = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_H264_PROFILE) {\n\t\t\th264_profile = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_AUDIO_DTMF) {\n\t\t\taudio_dtmf = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS) {\n\t\t\tvideo_rtcpfb = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_DATA_LEGACY) {\n\t\t\tdata_legacy = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_EXTENSION) {\n\t\t\tchar *extmap = va_arg(args, char *);\n\t\t\tint id = va_arg(args, int);\n\t\t\tif(extmap != NULL && id > 0 && id < 15) {\n\t\t\t\tif(extmaps == NULL)\n\t\t\t\t\textmaps = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\t\tif(extids == NULL)\n\t\t\t\t\textids = g_hash_table_new(NULL, NULL);\n\t\t\t\t/* Make sure the extmap and ID have not been added already */\n\t\t\t\tif(g_hash_table_lookup(extids, GINT_TO_POINTER(id)) == NULL &&\n\t\t\t\t\t\tg_hash_table_lookup(extmaps, extmap) == NULL) {\n\t\t\t\t\tg_hash_table_insert(extmaps, extmap, GINT_TO_POINTER(id));\n\t\t\t\t\tg_hash_table_insert(extids, GINT_TO_POINTER(id), extmap);\n\t\t\t\t}\n\t\t\t\tif(g_hash_table_lookup(extmaps, extmap) == GINT_TO_POINTER(id)) {\n\t\t\t\t\tif(m_extids == NULL)\n\t\t\t\t\t\tm_extids = g_hash_table_new(NULL, NULL);\n\t\t\t\t\tg_hash_table_insert(m_extids, GINT_TO_POINTER(id), extmap);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unknown property %d for preparing SDP offer, ignoring...\\n\", property);\n\t\t}\n\t}\n\tif(extmaps != NULL)\n\t\tg_hash_table_destroy(extmaps);\n\tif(extids != NULL)\n\t\tg_hash_table_destroy(extids);\n\n\t/* Done */\n\tva_end(args);\n\n\treturn offer;\n}\n\nint janus_sdp_generate_offer_mline(janus_sdp *offer, ...) {\n\tif(offer == NULL)\n\t\treturn -1;\n\n\t/* This method has a variable list of arguments, telling us what we should offer */\n\tva_list args;\n\tva_start(args, offer);\n\n\t/* First of all, let's see what we should add */\n\tjanus_sdp_mtype type = JANUS_SDP_OTHER;\n\tgboolean audio_dtmf = FALSE, video_rtcpfb = TRUE, data_legacy = FALSE;\n\tint pt = -1, opusred_pt = -1;\n\tconst char *codec = NULL, *mid = NULL, *msid = NULL, *mstid = NULL,\n\t\t*rtpmap = NULL, *fmtp = NULL, *vp9_profile = NULL, *h264_profile = NULL;\n\tjanus_sdp_mdirection mdir = JANUS_SDP_DEFAULT;\n\tGHashTable *extmaps = NULL, *extids = NULL;\n\tgboolean extids_allocated = FALSE;\n\tgboolean twcc = FALSE;\n\n\tint property = va_arg(args, int);\n\tif(property != JANUS_SDP_OA_MLINE) {\n\t\t/* The first attribute MUST be JANUS_SDP_OA_MLINE */\n\t\tJANUS_LOG(LOG_ERR, \"First attribute is not JANUS_SDP_OA_MLINE\\n\");\n\t\tva_end(args);\n\t\treturn -2;\n\t}\n\ttype = va_arg(args, int);\n\tif(type == JANUS_SDP_AUDIO) {\n\t\t/* Audio */\n\t\tpt = 111;\n\t\tcodec = \"opus\";\n\t} else if(type == JANUS_SDP_VIDEO) {\n\t\t/* Video */\n\t\tpt = 96;\n\t\tcodec = \"vp8\";\n\t} else if(type == JANUS_SDP_APPLICATION) {\n\t\t/* Data */\n#ifndef HAVE_SCTP\n\t\tva_end(args);\n\t\treturn -3;\n#endif\n\t} else {\n\t\t/* Unsupported m-line type */\n\t\tJANUS_LOG(LOG_ERR, \"Invalid m-line type\\n\");\n\t\tjanus_sdp_destroy(offer);\n\t\tva_end(args);\n\t\treturn -4;\n\t}\n\n\t/* Let's see what we should do with the media to add */\n\tproperty = va_arg(args, int);\n\twhile(property != JANUS_SDP_OA_DONE) {\n\t\tif(property == JANUS_SDP_OA_DIRECTION) {\n\t\t\tmdir = va_arg(args, janus_sdp_mdirection);\n\t\t} else if(property == JANUS_SDP_OA_CODEC) {\n\t\t\tcodec = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_MID) {\n\t\t\tmid = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_MSID) {\n\t\t\tmsid = va_arg(args, char *);\n\t\t\tmstid = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_PT) {\n\t\t\tpt = va_arg(args, int);\n\t\t} else if(property == JANUS_SDP_OA_OPUSRED_PT) {\n\t\t\topusred_pt = va_arg(args, int);\n\t\t} else if(property == JANUS_SDP_OA_FMTP) {\n\t\t\tfmtp = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_AUDIO_DTMF) {\n\t\t\taudio_dtmf = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS) {\n\t\t\tvideo_rtcpfb = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_VP9_PROFILE) {\n\t\t\tvp9_profile = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_H264_PROFILE) {\n\t\t\th264_profile = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_DATA_LEGACY) {\n\t\t\tdata_legacy = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_EXTENSION) {\n\t\t\tif((extmaps != NULL || extids != NULL) && !extids_allocated) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Conflicting extensions settings (can't use both JANUS_SDP_OA_EXTENSION and JANUS_SDP_OA_EXTENSIONS)\\n\");\n\t\t\t\tif(extmaps != NULL)\n\t\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\t\tif(extids_allocated) {\n\t\t\t\t\tif(extids != NULL)\n\t\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t\t}\n\t\t\t\tva_end(args);\n\t\t\t\treturn -5;\n\t\t\t}\n\t\t\tchar *extmap = va_arg(args, char *);\n\t\t\tint id = va_arg(args, int);\n\t\t\tif(extmap != NULL && id > 0 && id < 15) {\n\t\t\t\tif(extmaps == NULL)\n\t\t\t\t\textmaps = g_hash_table_new(g_str_hash, g_str_equal);\n\t\t\t\tif(extids == NULL)\n\t\t\t\t\textids = g_hash_table_new(NULL, NULL);\n\t\t\t\textids_allocated = TRUE;\n\t\t\t\t/* Make sure the extmap and ID have not been added already */\n\t\t\t\tchar *check_extmap = g_hash_table_lookup(extids, GINT_TO_POINTER(id));\n\t\t\t\tif(check_extmap != NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring duplicate extension %d (already added: %s)\\n\", id, check_extmap);\n\t\t\t\t} else {\n\t\t\t\t\tif(g_hash_table_lookup(extmaps, extmap) != NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Ignoring duplicate extension %s (already added: %d)\\n\",\n\t\t\t\t\t\t\textmap, GPOINTER_TO_INT(g_hash_table_lookup(extmaps, extmap)));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tg_hash_table_insert(extmaps, extmap, GINT_TO_POINTER(id));\n\t\t\t\t\t\tg_hash_table_insert(extids, GINT_TO_POINTER(id), extmap);\n\t\t\t\t\t\tif(!strcasecmp(extmap, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC)) {\n\t\t\t\t\t\t\t/* Take note of the fact we may negotiate TWCC for this m-line */\n\t\t\t\t\t\t\ttwcc = TRUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(property == JANUS_SDP_OA_EXTENSIONS) {\n\t\t\tif(extmaps != NULL || extids != NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Conflicting extensions settings (can't use both JANUS_SDP_OA_EXTENSION and JANUS_SDP_OA_EXTENSIONS)\\n\");\n\t\t\t\tif(extmaps != NULL)\n\t\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\t\tif(extids_allocated) {\n\t\t\t\t\tif(extids != NULL)\n\t\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t\t}\n\t\t\t\tva_end(args);\n\t\t\t\treturn -5;\n\t\t\t}\n\t\t\textids = va_arg(args, GHashTable *);\n\t\t\textids_allocated = FALSE;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unknown property %d for preparing SDP answer, ignoring...\\n\", property);\n\t\t}\n\t\tproperty = va_arg(args, int);\n\t}\n\t/* Configure some defaults, if values weren't specified */\n\tif(type == JANUS_SDP_AUDIO) {\n\t\tif(codec == NULL)\n\t\t\tcodec = \"opus\";\n\t\trtpmap = janus_sdp_get_codec_rtpmap(codec);\n\t\tif(rtpmap == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported audio codec '%s', can't prepare an offer\\n\", codec);\n\t\t\tif(extmaps != NULL)\n\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\tif(extids_allocated) {\n\t\t\t\tif(extids != NULL)\n\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t}\n\t\t\tva_end(args);\n\t\t\treturn -3;\n\t\t}\n\t} else if(type == JANUS_SDP_VIDEO) {\n\t\tif(codec == NULL)\n\t\t\tcodec = \"vp8\";\n\t\trtpmap = janus_sdp_get_codec_rtpmap(codec);\n\t\tif(rtpmap == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported video codec '%s', can't prepare an offer\\n\", codec);\n\t\t\tif(extmaps != NULL)\n\t\t\t\tg_hash_table_destroy(extmaps);\n\t\t\tif(extids_allocated) {\n\t\t\t\tif(extids != NULL)\n\t\t\t\t\tg_hash_table_destroy(extids);\n\t\t\t}\n\t\t\tva_end(args);\n\t\t\treturn -4;\n\t\t}\n\t}\n\n\t/* Create the m-line */\n\tconst char *transport = \"UDP/TLS/RTP/SAVPF\";\n\tif(type == JANUS_SDP_APPLICATION)\n\t\ttransport = (data_legacy ? \"DTLS/SCTP\" : \"UDP/DTLS/SCTP\");\n\tjanus_sdp_mline *m = janus_sdp_mline_create(type, 9, transport, mdir);\n\tm->index = g_list_length(offer->m_lines);\n\tm->c_ipv4 = TRUE;\n\tm->c_addr = g_strdup(offer->c_addr);\n\tjanus_sdp_attribute *a = NULL;\n\t/* Any mid we should set? */\n\tif(mid != NULL) {\n\t\ta = janus_sdp_attribute_create(\"mid\", \"%s\", mid);\n\t\tm->attributes = g_list_append(m->attributes, a);\n\t}\n\t/* Any msid we should set? */\n\tif(type != JANUS_SDP_APPLICATION && msid != NULL && mstid != NULL) {\n\t\ta = janus_sdp_attribute_create(\"msid\", \"%s %s\", msid, mstid);\n\t\tm->attributes = g_list_append(m->attributes, a);\n\t}\n\tif(type == JANUS_SDP_AUDIO || type == JANUS_SDP_VIDEO) {\n\t\t/* Add the selected codec */\n\t\tif(type == JANUS_SDP_AUDIO && opusred_pt > 0) {\n\t\t\t/* ... but add RED first */\n\t\t\tm->ptypes = g_list_append(m->ptypes, GINT_TO_POINTER(opusred_pt));\n\t\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d red/48000/2\", opusred_pt);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t}\n\t\tm->ptypes = g_list_append(m->ptypes, GINT_TO_POINTER(pt));\n\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d %s\", pt, rtpmap);\n\t\tm->attributes = g_list_append(m->attributes, a);\n\t\tif(type == JANUS_SDP_AUDIO) {\n\t\t\t/* Check if we need to add a payload type for DTMF tones (telephone-event/8000) */\n\t\t\tif(audio_dtmf) {\n\t\t\t\t/* We do */\n\t\t\t\tint dtmf_pt = 126;\n\t\t\t\tm->ptypes = g_list_append(m->ptypes, GINT_TO_POINTER(dtmf_pt));\n\t\t\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d %s\", dtmf_pt, janus_sdp_get_codec_rtpmap(\"dtmf\"));\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t}\n\t\t}\n\t\tif(type == JANUS_SDP_VIDEO && video_rtcpfb) {\n\t\t\t/* Add rtcp-fb attributes */\n\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d ccm fir\", pt);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d nack\", pt);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d nack pli\", pt);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d goog-remb\", pt);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t}\n\t\tif(twcc) {\n\t\t\t/* Add the transport-wide RTCP feedback message here, since the header extension was negotiated */\n\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d transport-cc\", pt);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t}\n\t\t/* Check if we need to add extensions to the SDP */\n\t\tif(extids != NULL) {\n\t\t\tGList *ids = g_list_sort(g_hash_table_get_keys(extids), janus_sdp_id_compare), *iter = ids;\n\t\t\twhile(iter) {\n\t\t\t\tchar *extmap = g_hash_table_lookup(extids, iter->data);\n\t\t\t\tif(extmap != NULL) {\n\t\t\t\t\ta = janus_sdp_attribute_create(\"extmap\",\n\t\t\t\t\t\t\"%d %s\", GPOINTER_TO_INT(iter->data), extmap);\n\t\t\t\t\tjanus_sdp_attribute_add_to_mline(m, a);\n\t\t\t\t}\n\t\t\t\titer = iter->next;\n\t\t\t}\n\t\t\tg_list_free(ids);\n\t\t}\n\t\t/* If RED is being offered, add an fmtp line for that */\n\t\tif(type == JANUS_SDP_AUDIO && opusred_pt > 0) {\n\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d %d/%d\", opusred_pt, pt, pt);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t}\n\t\t/* Check if there's a custom fmtp line to add */\n\t\tif(type == JANUS_SDP_AUDIO && fmtp != NULL) {\n\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d %s\", pt, fmtp);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t} else if(type == JANUS_SDP_VIDEO) {\n\t\t\t/* For video we can configure an fmtp in different ways */\n\t\t\tif(!strcasecmp(codec, \"vp9\") && vp9_profile) {\n\t\t\t\t/* Add a profile-id fmtp attribute */\n\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d profile-id=%s\", pt, vp9_profile);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t} else if(!strcasecmp(codec, \"h264\") && h264_profile) {\n\t\t\t\t/* Add a profile-level-id fmtp attribute */\n\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d profile-level-id=%s;packetization-mode=1\",\n\t\t\t\t\tpt, h264_profile);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t} else if(fmtp) {\n\t\t\t\t/* There's a custom fmtp line to add for video */\n\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d %s\", pt, fmtp);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t}\n\t\t}\n\t} else {\n\t\tm->fmts = g_list_append(m->fmts, g_strdup(data_legacy ? \"5000\" : \"webrtc-datachannel\"));\n\t\t/* Add an sctpmap attribute */\n\t\tif(data_legacy) {\n\t\t\ta = janus_sdp_attribute_create(\"sctpmap\", \"5000 webrtc-datachannel 16\");\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t} else {\n\t\t\ta = janus_sdp_attribute_create(\"sctp-port\", \"5000\");\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t}\n\t}\n\toffer->m_lines = g_list_append(offer->m_lines, m);\n\n\tif(extmaps != NULL)\n\t\tg_hash_table_destroy(extmaps);\n\tif(extids_allocated) {\n\t\tif(extids != NULL)\n\t\t\tg_hash_table_destroy(extids);\n\t}\n\n\t/* Done */\n\tva_end(args);\n\n\treturn 0;\n}\n\njanus_sdp *janus_sdp_generate_answer(janus_sdp *offer) {\n\tif(offer == NULL)\n\t\treturn NULL;\n\n\tjanus_refcount_increase(&offer->ref);\n\t/* Create an SDP answer, and start by copying some of the headers */\n\tjanus_sdp *answer = g_malloc(sizeof(janus_sdp));\n\tg_atomic_int_set(&answer->destroyed, 0);\n\tjanus_refcount_init(&answer->ref, janus_sdp_free);\n\tanswer->version = offer->version;\n\tanswer->o_name = g_strdup(offer->o_name ? offer->o_name : \"-\");\n\tanswer->o_sessid = offer->o_sessid;\n\tanswer->o_version = offer->o_version;\n\tanswer->o_ipv4 = offer->o_ipv4;\n\tanswer->o_addr = g_strdup(offer->o_addr ? offer->o_addr : \"127.0.0.1\");\n\tanswer->s_name = g_strdup(offer->s_name ? offer->s_name : \"Janus session\");\n\tanswer->t_start = 0;\n\tanswer->t_stop = 0;\n\tanswer->c_ipv4 = offer->c_ipv4;\n\tanswer->c_addr = g_strdup(offer->c_addr ? offer->c_addr : \"127.0.0.1\");\n\tanswer->attributes = NULL;\n\tanswer->m_lines = NULL;\n\n\t/* Iterate on all m-lines to add, if any */\n\tGList *temp = offer->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t/* For each m-line we parse, we'll need a corresponding one in the answer */\n\t\tjanus_sdp_mline *am = g_malloc0(sizeof(janus_sdp_mline));\n\t\tjanus_refcount_init(&am->ref, janus_sdp_mline_free);\n\t\tam->index = m->index;\n\t\tam->type = m->type;\n\t\tam->type_str = m->type_str ? g_strdup(m->type_str) : NULL;\n\t\tam->proto = g_strdup(m->proto ? m->proto : \"UDP/TLS/RTP/SAVPF\");\n\t\tam->c_ipv4 = m->c_ipv4;\n\t\tam->c_addr = g_strdup(am->c_addr ? am->c_addr : \"127.0.0.1\");\n\t\t/* We reject the media line by default, but this can be changed later */\n\t\tam->port = 0;\n\t\tam->direction = JANUS_SDP_INACTIVE;\n\t\tam->ptypes = g_list_append(am->ptypes, GINT_TO_POINTER(0));\n\t\tif(am->type == JANUS_SDP_APPLICATION) {\n\t\t\tGList *fmt = m->fmts;\n\t\t\twhile(fmt) {\n\t\t\t\tchar *fmt_str = (char *)fmt->data;\n\t\t\t\tif(fmt_str)\n\t\t\t\t\tam->fmts = g_list_append(am->fmts, g_strdup(fmt_str));\n\t\t\t\tfmt = fmt->next;\n\t\t\t}\n\t\t}\n\t\t/* Append to the list of m-lines in the answer */\n\t\tanswer->m_lines = g_list_append(answer->m_lines, am);\n\t\ttemp = temp->next;\n\t}\n\tjanus_refcount_decrease(&offer->ref);\n\n\t/* Done*/\n\treturn answer;\n}\n\nint janus_sdp_generate_answer_mline(janus_sdp *offer, janus_sdp *answer, janus_sdp_mline *offered, ...) {\n\tif(answer == NULL || offered == NULL)\n\t\treturn -1;\n\n\tjanus_refcount_increase(&offer->ref);\n\tjanus_refcount_increase(&answer->ref);\n\t/* This method has a variable list of arguments, telling us how we should respond */\n\tva_list args;\n\tva_start(args, offered);\n\n\t/* Let's see what we should do with the media */\n\tgboolean mline_enabled = TRUE;\n\tjanus_sdp_mtype type = JANUS_SDP_OTHER;\n\tgboolean audio_dtmf = FALSE, audio_opusred = FALSE, video_rtcpfb = TRUE;\n\tconst char *codec = NULL, *msid = NULL, *mstid = NULL,\n\t\t*fmtp = NULL, *vp9_profile = NULL, *h264_profile = NULL;\n\tchar *custom_audio_fmtp = NULL;\n\tGList *extmaps = NULL;\n\tgboolean twcc = FALSE;\n\tjanus_sdp_mdirection mdir = JANUS_SDP_DEFAULT;\n\tint property = va_arg(args, int);\n\tif(property != JANUS_SDP_OA_MLINE) {\n\t\t/* The first attribute MUST be JANUS_SDP_OA_MLINE */\n\t\tJANUS_LOG(LOG_ERR, \"First attribute is not JANUS_SDP_OA_MLINE\\n\");\n\t\tva_end(args);\n\t\tjanus_refcount_decrease(&offer->ref);\n\t\tjanus_refcount_decrease(&answer->ref);\n\t\treturn -2;\n\t}\n\ttype = va_arg(args, int);\n\tif(type != JANUS_SDP_AUDIO && type != JANUS_SDP_VIDEO && type != JANUS_SDP_APPLICATION) {\n\t\t/* Unsupported m-line type */\n\t\tJANUS_LOG(LOG_ERR, \"Invalid m-line type\\n\");\n\t\tva_end(args);\n\t\tjanus_refcount_decrease(&offer->ref);\n\t\tjanus_refcount_decrease(&answer->ref);\n\t\treturn -3;\n\t}\n\n\t/* Let's see what we should do with the media to add */\n\tproperty = va_arg(args, int);\n\twhile(property != JANUS_SDP_OA_DONE) {\n\t\tif(property == JANUS_SDP_OA_ENABLED) {\n\t\t\tmline_enabled = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_DIRECTION) {\n\t\t\tmdir = va_arg(args, janus_sdp_mdirection);\n\t\t} else if(property == JANUS_SDP_OA_CODEC) {\n\t\t\tcodec = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_MSID) {\n\t\t\tmsid = va_arg(args, char *);\n\t\t\tmstid = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_FMTP) {\n\t\t\tfmtp = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_VP9_PROFILE) {\n\t\t\tvp9_profile = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_H264_PROFILE) {\n\t\t\th264_profile = va_arg(args, char *);\n\t\t} else if(property == JANUS_SDP_OA_AUDIO_DTMF) {\n\t\t\taudio_dtmf = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS) {\n\t\t\tvideo_rtcpfb = va_arg(args, gboolean);\n\t\t} else if(property == JANUS_SDP_OA_ACCEPT_EXTMAP) {\n\t\t\tconst char *extension = va_arg(args, char *);\n\t\t\tif(extension != NULL) {\n\t\t\t\textmaps = g_list_append(extmaps, (char *)extension);\n\t\t\t\tif(!strcasecmp(extension, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC)) {\n\t\t\t\t\t/* Take note of the fact we may negotiate TWCC for this m-line */\n\t\t\t\t\ttwcc = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(property == JANUS_SDP_OA_ACCEPT_OPUSRED) {\n\t\t\taudio_opusred = va_arg(args, gboolean);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unknown property %d for preparing SDP answer, ignoring...\\n\", property);\n\t\t}\n\t\tproperty = va_arg(args, int);\n\t}\n\n\t/* Iterate on all m-lines to add, if any, to find the one with the same index */\n\tGList *temp = answer->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *am = (janus_sdp_mline *)temp->data;\n\t\tif(am->index != offered->index) {\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* When answering, m-lines are disabled by default */\n\t\tam->direction = JANUS_SDP_INACTIVE;\n\t\tg_list_free(am->ptypes);\n\t\tam->ptypes = NULL;\n\t\tg_list_free_full(am->fmts, (GDestroyNotify)g_free);\n\t\tam->fmts = NULL;\n\t\tg_list_free_full(am->attributes, (GDestroyNotify)janus_sdp_attribute_destroy);\n\t\tam->attributes = NULL;\n\t\tif(!mline_enabled) {\n\t\t\tam->ptypes = g_list_append(am->ptypes, GINT_TO_POINTER(0));\n\t\t\tif(am->type == JANUS_SDP_APPLICATION) {\n\t\t\t\tGList *fmt = offered->fmts;\n\t\t\t\twhile(fmt) {\n\t\t\t\t\tchar *fmt_str = (char *)fmt->data;\n\t\t\t\t\tif(fmt_str)\n\t\t\t\t\t\tam->fmts = g_list_append(am->fmts, g_strdup(fmt_str));\n\t\t\t\t\tfmt = fmt->next;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tam->port = 9;\n\t\tif(am->type == JANUS_SDP_AUDIO || am->type == JANUS_SDP_VIDEO) {\n\t\t\t/* What is the direction we were offered? And how were we asked to react?\n\t\t\t * Adapt the direction in our answer accordingly */\n\t\t\tswitch(offered->direction) {\n\t\t\t\tcase JANUS_SDP_RECVONLY:\n\t\t\t\t\tif(mdir == JANUS_SDP_SENDRECV || mdir == JANUS_SDP_DEFAULT || mdir == JANUS_SDP_SENDONLY) {\n\t\t\t\t\t\t/* Peer is recvonly, we'll only send */\n\t\t\t\t\t\tam->direction = JANUS_SDP_SENDONLY;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Peer is recvonly, but we're not ok to send, so reply with inactive */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"%s offered as '%s', but we need '%s' for us: using 'inactive'\\n\",\n\t\t\t\t\t\t\tam->type == JANUS_SDP_AUDIO ? \"Audio\" : \"Video\",\n\t\t\t\t\t\t\tjanus_sdp_mdirection_str(offered->direction), janus_sdp_mdirection_str(mdir));\n\t\t\t\t\t\tam->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase JANUS_SDP_SENDONLY:\n\t\t\t\t\tif(mdir == JANUS_SDP_SENDRECV || mdir == JANUS_SDP_DEFAULT || mdir == JANUS_SDP_RECVONLY) {\n\t\t\t\t\t\t/* Peer is sendonly, we'll only receive */\n\t\t\t\t\t\tam->direction = JANUS_SDP_RECVONLY;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Peer is sendonly, but we're not ok to receive, so reply with inactive */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"%s offered as '%s', but we need '%s' for us: using 'inactive'\\n\",\n\t\t\t\t\t\t\tam->type == JANUS_SDP_AUDIO ? \"Audio\" : \"Video\",\n\t\t\t\t\t\t\tjanus_sdp_mdirection_str(offered->direction), janus_sdp_mdirection_str(mdir));\n\t\t\t\t\t\tam->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase JANUS_SDP_INACTIVE:\n\t\t\t\t\t/* Peer inactive, set inactive in the answer to */\n\t\t\t\t\tam->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\tbreak;\n\t\t\t\tcase JANUS_SDP_SENDRECV:\n\t\t\t\tdefault:\n\t\t\t\t\t/* The peer is fine with everything, so use our constraint */\n\t\t\t\t\tam->direction = mdir;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\t/* Look for the right codec and stick to that */\n\t\t\tif(codec == NULL) {\n\t\t\t\t/* FIXME User didn't provide a codec to accept? Let's see if Opus (for audio)\n\t\t\t\t * of VP8 (for video) were negotiated: if so, use them, otherwise let's\n\t\t\t\t * pick some other codec we know about among the ones that were offered.\n\t\t\t\t * Notice that if it's not a codec we understand, we reject the medium,\n\t\t\t\t * as browsers would reject it anyway. If you need more flexibility you'll\n\t\t\t\t * have to generate an answer yourself, rather than automatically... */\n\t\t\t\tcodec = am->type == JANUS_SDP_AUDIO ? \"opus\" : \"vp8\";\n\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t/* We couldn't find our preferred codec, let's try something else */\n\t\t\t\t\tif(am->type == JANUS_SDP_AUDIO) {\n\t\t\t\t\t\t/* Opus not found, maybe mu-law? */\n\t\t\t\t\t\tcodec = \"pcmu\";\n\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t/* mu-law not found, maybe a-law? */\n\t\t\t\t\t\t\tcodec = \"pcma\";\n\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t/* a-law not found, maybe G.722? */\n\t\t\t\t\t\t\t\tcodec = \"g722\";\n\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t\t/* G.722 not found, maybe isac32? */\n\t\t\t\t\t\t\t\t\tcodec = \"isac32\";\n\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t\t\t/* isac32 not found, maybe isac16? */\n\t\t\t\t\t\t\t\t\t\tcodec = \"isac16\";\n\t\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t\t\t\t/* isac16 not found, maybe multiopus? */\n\t\t\t\t\t\t\t\t\t\t\tcodec = \"multiopus\";\n\t\t\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t/* multiopus not found, maybe L16/48000? */\n\t\t\t\t\t\t\t\t\t\t\t\tcodec = \"l16-48\";\n\t\t\t\t\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t/* L16/48000 not found, maybe L16/16000? */\n\t\t\t\t\t\t\t\t\t\t\t\t\tcodec = \"l16\";\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* VP8 not found, maybe VP9? */\n\t\t\t\t\t\tcodec = \"vp9\";\n\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t/* VP9 not found either, maybe H.264? */\n\t\t\t\t\t\t\tcodec = \"h264\";\n\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t/* H.264 not found either, maybe AV1? */\n\t\t\t\t\t\t\t\tcodec = \"av1\";\n\t\t\t\t\t\t\t\tif(janus_sdp_get_codec_pt(offer, offered->index, codec) < 0) {\n\t\t\t\t\t\t\t\t\t/* AV1 not found either, maybe H.265? */\n\t\t\t\t\t\t\t\t\tcodec = \"h265\";\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst char *video_profile = NULL;\n\t\t\tif(codec && !strcasecmp(codec, \"vp9\"))\n\t\t\t\tvideo_profile = vp9_profile;\n\t\t\telse if(codec && !strcasecmp(codec, \"h264\"))\n\t\t\t\tvideo_profile = h264_profile;\n\t\t\tint pt = janus_sdp_get_codec_pt_full(offer, offered->index, codec, video_profile);\n\t\t\tif(pt < 0) {\n\t\t\t\t/* Reject */\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Couldn't find codec we needed (%s) in the offer, rejecting %s\\n\",\n\t\t\t\t\tcodec, am->type == JANUS_SDP_AUDIO ? \"audio\" : \"video\");\n\t\t\t\tam->port = 0;\n\t\t\t\tam->direction = JANUS_SDP_INACTIVE;\n\t\t\t\tam->ptypes = g_list_append(am->ptypes, GINT_TO_POINTER(0));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(am->type == JANUS_SDP_AUDIO && !strcasecmp(codec, \"multiopus\") &&\n\t\t\t\t\t(fmtp == NULL || strstr(fmtp, \"channel_mapping\") == NULL)) {\n\t\t\t\t/* Missing channel mapping for the multiopus m-line, check the offer */\n\t\t\t\tGList *mo = offered->attributes;\n\t\t\t\twhile(mo) {\n\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)mo->data;\n\t\t\t\t\tif(a->name && strstr(a->name, \"fmtp\") && a->value) {\n\t\t\t\t\t\tchar *tmp = strchr(a->value, ' ');\n\t\t\t\t\t\tif(tmp && strlen(tmp) > 1 && custom_audio_fmtp == NULL) {\n\t\t\t\t\t\t\ttmp++;\n\t\t\t\t\t\t\tcustom_audio_fmtp = g_strdup(tmp);\n\t\t\t\t\t\t\t/* FIXME We should integrate the existing audio_fmtp */\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tmo = mo->next;\n\t\t\t\t}\n\t\t\t}\n\t\t\tam->ptypes = g_list_append(am->ptypes, GINT_TO_POINTER(pt));\n\t\t\t/* Any msid we should set? */\n\t\t\tif(msid != NULL && mstid != NULL) {\n\t\t\t\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"msid\", \"%s %s\", msid, mstid);\n\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t}\n\t\t\t/* Before moving to the attributes, check if the Trasport\n\t\t\t * Wide CC RTP extension was negotiated */\n\t\t\tif(extmaps != NULL && twcc) {\n\t\t\t\t/* We're trying to accept TWCC, but make sure it was offered */\n\t\t\t\ttwcc = FALSE;\n\t\t\t\tGList *ma = offered->attributes;\n\t\t\t\twhile(ma) {\n\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\t\tif(a->name && strstr(a->name, \"extmap\") && a->value &&\n\t\t\t\t\t\t\tstrstr(a->value, JANUS_RTP_EXTMAP_TRANSPORT_WIDE_CC) != NULL) {\n\t\t\t\t\t\t/* Take note of the fact we'll have to negotiate TWCC for this m-line */\n\t\t\t\t\t\ttwcc = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tma = ma->next;\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Add the related attributes */\n\t\t\tif(am->type == JANUS_SDP_AUDIO) {\n\t\t\t\t/* Add rtpmap attribute */\n\t\t\t\tint opusred_pt = -1;\n\t\t\t\tconst char *codec_rtpmap = janus_sdp_get_codec_rtpmap(codec);\n\t\t\t\tjanus_sdp_attribute *a = NULL;\n\t\t\t\tif(codec_rtpmap) {\n\t\t\t\t\t/* If we're supposed to negotiate opus/red as well, check if it's there */\n\t\t\t\t\tif(!strcasecmp(codec, \"opus\") && audio_opusred) {\n\t\t\t\t\t\topusred_pt = janus_sdp_get_opusred_pt(offer, am->index);\n\t\t\t\t\t\tif(opusred_pt > 0) {\n\t\t\t\t\t\t\t/* Add rtpmap attribute for opus/red too */\n\t\t\t\t\t\t\tam->ptypes = g_list_prepend(am->ptypes, GINT_TO_POINTER(opusred_pt));\n\t\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d red/48000/2\", opusred_pt);\n\t\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d %s\", pt, codec_rtpmap);\n\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t/* Check if we need to add a payload type for DTMF tones (telephone-event/8000) */\n\t\t\t\t\tif(audio_dtmf) {\n\t\t\t\t\t\tint dtmf_pt = janus_sdp_get_codec_pt(offer, am->index, \"dtmf\");\n\t\t\t\t\t\tif(dtmf_pt >= 0) {\n\t\t\t\t\t\t\t/* We do */\n\t\t\t\t\t\t\tam->ptypes = g_list_append(am->ptypes, GINT_TO_POINTER(dtmf_pt));\n\t\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d %s\", dtmf_pt, janus_sdp_get_codec_rtpmap(\"dtmf\"));\n\t\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* If we're negotiating opus/red, add an fmtp line for that */\n\t\t\t\t\tif(audio_opusred && opusred_pt > 0) {\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d %d/%d\", opusred_pt, pt, pt);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t}\n\t\t\t\t\t/* Check if there's a custom fmtp line to add for audio\n\t\t\t\t\t * FIXME We should actually check if it matches the offer */\n\t\t\t\t\tif(fmtp || custom_audio_fmtp) {\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d %s\",\n\t\t\t\t\t\t\tpt, custom_audio_fmtp ? custom_audio_fmtp : fmtp);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t}\n\t\t\t\t\tif(twcc) {\n\t\t\t\t\t\t/* Add the transport-wide RTCP feedback message here, since the header extension was negotiated */\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d transport-cc\", pt);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Add rtpmap attribute */\n\t\t\t\tconst char *codec_rtpmap = janus_sdp_get_codec_rtpmap(codec);\n\t\t\t\tjanus_sdp_attribute *a = NULL;\n\t\t\t\tif(codec_rtpmap) {\n\t\t\t\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d %s\", pt, codec_rtpmap);\n\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\tif(video_rtcpfb) {\n\t\t\t\t\t\t/* Add rtcp-fb attributes */\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d ccm fir\", pt);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d nack\", pt);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d nack pli\", pt);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d goog-remb\", pt);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t}\n\t\t\t\t\tif(twcc) {\n\t\t\t\t\t\t/* Add the transport-wide RTCP feedback message here, since the header extension was negotiated */\n\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtcp-fb\", \"%d transport-cc\", pt);\n\t\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif(!strcasecmp(codec, \"vp9\") && vp9_profile) {\n\t\t\t\t\t/* Add a profile-id fmtp attribute */\n\t\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d profile-id=%s\", pt, vp9_profile);\n\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t} else if(!strcasecmp(codec, \"h264\") && h264_profile) {\n\t\t\t\t\t/* Add a profile-level-id fmtp attribute */\n\t\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d profile-level-id=%s;packetization-mode=1\", pt, h264_profile);\n\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t} else if(fmtp) {\n\t\t\t\t\t/* There's a custom fmtp line to add for video\n\t\t\t\t\t * FIXME We should actually check if it matches the offer */\n\t\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d %s\", pt, fmtp);\n\t\t\t\t\tam->attributes = g_list_append(am->attributes, a);\n\t\t\t\t}\n\t\t\t}\n\t\t\t/* Add the extmap attributes, if needed */\n\t\t\tif(extmaps != NULL) {\n\t\t\t\tGList *ma = offered->attributes;\n\t\t\t\twhile(ma) {\n\t\t\t\t\t/* Iterate on all attributes, to see if there's an extension to accept */\n\t\t\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;\n\t\t\t\t\tif(a->name && strstr(a->name, \"extmap\") && a->value) {\n\t\t\t\t\t\tGList *emtemp = extmaps;\n\t\t\t\t\t\twhile(emtemp != NULL) {\n\t\t\t\t\t\t\tchar *extension = (char *)emtemp->data;\n\t\t\t\t\t\t\tif(strstr(a->value, extension)) {\n\t\t\t\t\t\t\t\t/* Accept the extension */\n\t\t\t\t\t\t\t\tint id = atoi(a->value);\n\t\t\t\t\t\t\t\tif(id < 0) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid extension ID (%d)\\n\", id);\n\t\t\t\t\t\t\t\t\temtemp = emtemp->next;\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif(strstr(a->value, JANUS_RTP_EXTMAP_DEPENDENCY_DESC) &&\n\t\t\t\t\t\t\t\t\t\tstrcasecmp(codec, \"av1\") && strcasecmp(codec, \"vp9\")) {\n\t\t\t\t\t\t\t\t\t/* Don't negotiate the Dependency Descriptor extension,\n\t\t\t\t\t\t\t\t\t * unless we're doing AV1 or VP9 for SVC. See for ref:\n\t\t\t\t\t\t\t\t\t * https://issues.webrtc.org/issues/42226269 */\n\t\t\t\t\t\t\t\t\temtemp = emtemp->next;\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst char *direction = NULL;\n\t\t\t\t\t\t\t\tswitch(a->direction) {\n\t\t\t\t\t\t\t\t\tcase JANUS_SDP_SENDONLY:\n\t\t\t\t\t\t\t\t\t\tdirection = \"/recvonly\";\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\tcase JANUS_SDP_RECVONLY:\n\t\t\t\t\t\t\t\t\t\tdirection = \"/sendonly\";\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\tcase JANUS_SDP_INACTIVE:\n\t\t\t\t\t\t\t\t\t\tdirection = \"/inactive\";\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\t\tdirection = \"\";\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ta = janus_sdp_attribute_create(\"extmap\",\n\t\t\t\t\t\t\t\t\t\"%d%s %s\", id, direction, extension);\n\t\t\t\t\t\t\t\tjanus_sdp_attribute_add_to_mline(am, a);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\temtemp = emtemp->next;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if(am->type == JANUS_SDP_VIDEO && a->name && strstr(a->name, \"fmtp\") &&\n\t\t\t\t\t\t\ta->value && atoi(a->value) == pt) {\n\t\t\t\t\t\t/* Check if we need to copy the fmtp attribute too */\n\t\t\t\t\t\tif(((!strcasecmp(codec, \"vp8\") && fmtp == NULL)) ||\n\t\t\t\t\t\t\t\t((!strcasecmp(codec, \"vp9\") && vp9_profile == NULL && fmtp == NULL)) ||\n\t\t\t\t\t\t\t\t((!strcasecmp(codec, \"h264\") && h264_profile == NULL && fmtp == NULL))) {\n\t\t\t\t\t\t\t/* FIXME Copy the fmtp attribute (we should check if we support it) */\n\t\t\t\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%s\", a->value);\n\t\t\t\t\t\t\tjanus_sdp_attribute_add_to_mline(am, a);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tma = ma->next;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t/* This is for data, add formats and an sctpmap attribute */\n\t\t\tam->direction = JANUS_SDP_DEFAULT;\n\t\t\tGList *fmt = offered->fmts;\n\t\t\twhile(fmt) {\n\t\t\t\tchar *fmt_str = (char *)fmt->data;\n\t\t\t\tif(fmt_str)\n\t\t\t\t\tam->fmts = g_list_append(am->fmts, g_strdup(fmt_str));\n\t\t\t\tfmt = fmt->next;\n\t\t\t}\n\t\t}\n\t\t/* Nothing else we need to do */\n\t\tbreak;\n\t}\n\tjanus_refcount_decrease(&offer->ref);\n\tjanus_refcount_decrease(&answer->ref);\n\n\t/* Done */\n\tg_list_free(extmaps);\n\tg_free(custom_audio_fmtp);\n\tva_end(args);\n\n\treturn 0;\n}\n"
  },
  {
    "path": "src/sdp-utils.h",
    "content": "/*! \\file    sdp-utils.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    SDP utilities (headers)\n * \\details  Implementation of an internal SDP representation. Allows\n * to parse SDP strings to an internal janus_sdp object, the manipulation\n * of such object by playing with its properties, and a serialization\n * to an SDP string that can be passed around. Since they don't have any\n * core dependencies, these utilities can be used by plugins as well.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_SDP_UTILS_H\n#define JANUS_SDP_UTILS_H\n\n\n#include <inttypes.h>\n#include <glib.h>\n\n#include \"rtp.h\"\n#include \"refcount.h\"\n\n/*! \\brief Janus SDP internal object representation */\ntypedef struct janus_sdp {\n\t/*! \\brief v= */\n\tint version;\n\t/*! \\brief o= name */\n\tchar *o_name;\n\t/*! \\brief o= session ID */\n\tguint64 o_sessid;\n\t/*! \\brief o= version */\n\tguint64 o_version;\n\t/*! \\brief o= protocol */\n\tgboolean o_ipv4;\n\t/*! \\brief o= address */\n\tchar *o_addr;\n\t/*! \\brief s= */\n\tchar *s_name;\n\t/*! \\brief t= start */\n\tguint64 t_start;\n\t/*! \\brief t= stop */\n\tguint64 t_stop;\n\t/*! \\brief c= protocol (not rendered for WebRTC usage) */\n\tgboolean c_ipv4;\n\t/*! \\brief c= address (not rendered for WebRTC usage) */\n\tchar *c_addr;\n\t/*! \\brief List of global a= attributes */\n\tGList *attributes;\n\t/*! \\brief List of m= m-lines */\n\tGList *m_lines;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_sdp;\n\n/*! \\brief Helper enumeration to quickly identify m-line media types */\ntypedef enum janus_sdp_mtype {\n\t/*! \\brief m=audio */\n\tJANUS_SDP_AUDIO,\n\t/*! \\brief m=video */\n\tJANUS_SDP_VIDEO,\n\t/*! \\brief m=application */\n\tJANUS_SDP_APPLICATION,\n\t/*! \\brief m=whatever (we don't care, unsupported) */\n\tJANUS_SDP_OTHER\n} janus_sdp_mtype;\n/*! \\brief Helper method to get a janus_sdp_mtype from a string\n * @param[in] type The type to parse as a string (e.g., \"audio\")\n * @returns The corresponding janus_sdp_mtype value */\njanus_sdp_mtype janus_sdp_parse_mtype(const char *type);\n/*! \\brief Helper method to get the string associated to a janus_sdp_mtype value\n * @param[in] type The type to stringify\n * @returns The type as a string, if valid, or NULL otherwise */\nconst char *janus_sdp_mtype_str(janus_sdp_mtype type);\n\n/*! \\brief Helper enumeration to quickly identify m-line directions */\ntypedef enum janus_sdp_mdirection {\n\t/*! \\brief default=sendrecv */\n\tJANUS_SDP_DEFAULT,\n\t/*! \\brief sendrecv */\n\tJANUS_SDP_SENDRECV,\n\t/*! \\brief sendonly */\n\tJANUS_SDP_SENDONLY,\n\t/*! \\brief recvonly */\n\tJANUS_SDP_RECVONLY,\n\t/*! \\brief inactive */\n\tJANUS_SDP_INACTIVE,\n\t/*! \\brief invalid direction (when parsing) */\n\tJANUS_SDP_INVALID\n} janus_sdp_mdirection;\n/*! \\brief Helper method to get a janus_sdp_mdirection from a string\n * @param[in] direction The direction to parse as a string (e.g., \"sendrecv\")\n * @returns The corresponding janus_sdp_mdirection value */\njanus_sdp_mdirection janus_sdp_parse_mdirection(const char *direction);\n/*! \\brief Helper method to get the string associated to a janus_sdp_mdirection value\n * @param[in] direction The direction to stringify\n * @returns The direction as a string, if valid, or NULL otherwise */\nconst char *janus_sdp_mdirection_str(janus_sdp_mdirection direction);\n\n/*! \\brief Helper method to return the preferred audio or video codec in an SDP offer or answer,\n * (where by preferred we mean the codecs we prefer ourselves, and not the m-line SDP order)\n * as long as the m-line direction is not disabled (port=0 or direction=inactive) in the SDP\n * \\note The codec argument is input/output, and it will be set to a static value\n * in janus_preferred_audio_codecs or janus_preferred_video_codecs, so don't free it.\n * @param[in] sdp The Janus SDP object to parse\n * @param[in] type Whether we're looking at an audio or video codec\n * @param[in] index The m-line to refer to (use -1 for the first m-line that matches)\n * @param[out] codec The audio or video codec that was found */\nvoid janus_sdp_find_preferred_codec(janus_sdp *sdp, janus_sdp_mtype type, int index, const char **codec);\n/*! \\brief Helper method to return the first audio or video codec in an SDP offer or answer,\n * (no matter whether we personally prefer them ourselves or not)\n * as long as the m-line direction is not disabled (port=0 or direction=inactive) in the SDP\n * \\note The codec argument is input/output, and it will be set to a static value\n * in janus_preferred_audio_codecs or janus_preferred_video_codecs, so don't free it.\n * @param[in] sdp The Janus SDP object to parse\n * @param[in] type Whether we're looking at an audio or video codec\n * @param[in] index The m-line to refer to (use -1 for the first m-line that matches)\n * @param[out] codec The audio or video codec that was found */\nvoid janus_sdp_find_first_codec(janus_sdp *sdp, janus_sdp_mtype type, int index, const char **codec);\n/*! \\brief Helper method to match a codec to one of the preferred codecs\n * \\note Don't free the returned value, as it's a constant value\n * @param[in] type The type of media to match\n * @param[in] codec The codec to match\n * @returns The codec, if found, or NULL otherwise */\nconst char *janus_sdp_match_preferred_codec(janus_sdp_mtype type, char *codec);\n\n/*! \\brief SDP m-line representation */\ntypedef struct janus_sdp_mline {\n\t/*! \\brief Media index in the SDP */\n\tint index;\n\t/*! \\brief Media type as a janus_sdp_mtype enumerator */\n\tjanus_sdp_mtype type;\n\t/*! \\brief Media type (string) */\n\tchar *type_str;\n\t/*! \\brief Media port */\n\tguint16 port;\n\t/*! \\brief Media protocol */\n\tchar *proto;\n\t/*! \\brief List of formats */\n\tGList *fmts;\n\t/*! \\brief List of payload types */\n\tGList *ptypes;\n\t/*! \\brief Media c= protocol */\n\tgboolean c_ipv4;\n\t/*! \\brief Media c= address */\n\tchar *c_addr;\n\t/*! \\brief Media b= type */\n\tchar *b_name;\n\t/*! \\brief Media b= value */\n\tuint32_t b_value;\n\t/*! \\brief Media direction */\n\tjanus_sdp_mdirection direction;\n\t/*! \\brief List of m-line attributes */\n\tGList *attributes;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_sdp_mline;\n/*! \\brief Helper method to quickly create a janus_sdp_mline instance\n * @note The \\c type_str property of the new m-line is created automatically\n * depending on the provided \\c type attribute. If \\c type is JANUS_SDP_OTHER,\n * though, \\c type_str will NOT we allocated, and will be up to the caller.\n * @param[in] type Type of the media (audio/video/application) as a janus_sdp_mtype\n * @param[in] port Port to advertise\n * @param[in] proto Profile to advertise\n * @param[in] direction Direction of the media as a janus_sdp_direction\n * @returns A pointer to a valid janus_sdp_mline instance, if successful, NULL otherwise */\njanus_sdp_mline *janus_sdp_mline_create(janus_sdp_mtype type, guint16 port, const char *proto, janus_sdp_mdirection direction);\n/*! \\brief Helper method to free a janus_sdp_mline instance\n * @note This method does not remove the m-line from the janus_sdp instance, that's up to the caller\n * @param[in] mline The janus_sdp_mline instance to free */\nvoid janus_sdp_mline_destroy(janus_sdp_mline *mline);\n/*! \\brief Helper method to get the janus_sdp_mline associated to a media type\n * @note This currently returns the first m-line of the specified type it finds: as\n * such, it's mostly here for making things easier for plugins not doing multistream.\n * @param[in] sdp The Janus SDP object to search\n * @param[in] type The type of media to search\n * @returns The janus_sdp_mline instance, if found, or NULL otherwise */\njanus_sdp_mline *janus_sdp_mline_find(janus_sdp *sdp, janus_sdp_mtype type);\n/*! \\brief Helper method to get the janus_sdp_mline by its index\n * @param[in] sdp The Janus SDP object to search\n * @param[in] index The index of the m-line in the SDP\n * @returns The janus_sdp_mline instance, if found, or NULL otherwise */\njanus_sdp_mline *janus_sdp_mline_find_by_index(janus_sdp *sdp, int index);\n/*! \\brief Helper method to remove the janus_sdp_mline associated to a media type from the SDP\n * @note This currently removes the first m-line of the specified type it finds: as\n * such, it's mostly here for making things easier for plugins not doing multistream.\n * @param[in] sdp The Janus SDP object to modify\n * @param[in] type The type of media to remove\n * @returns 0 if successful, a negative integer otherwise */\nint janus_sdp_mline_remove(janus_sdp *sdp, janus_sdp_mtype type);\n\n/*! \\brief SDP a= attribute representation */\ntypedef struct janus_sdp_attribute {\n\t/*! \\brief Attribute name */\n\tchar *name;\n\t/*! \\brief Attribute value */\n\tchar *value;\n\t/*! \\brief Attribute direction (e.g., for extmap) */\n\tjanus_sdp_mdirection direction;\n\t/*! \\brief Atomic flag to check if this instance has been destroyed */\n\tvolatile gint destroyed;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n} janus_sdp_attribute;\n/*! \\brief Helper method to quickly create a janus_sdp_attribute instance\n * @param[in] name Name of the attribute\n * @param[in] value Value of the attribute, as a printf compliant string (variable arguments)\n * @returns A pointer to a valid janus_sdp_attribute instance, if successful, NULL otherwise */\njanus_sdp_attribute *janus_sdp_attribute_create(const char *name, const char *value, ...) G_GNUC_PRINTF(2, 3);\n/*! \\brief Helper method to free a janus_sdp_attribute instance\n * @note This method does not remove the attribute from the global or m-line attributes, that's up to the caller\n * @param[in] attr The janus_sdp_attribute instance to free */\nvoid janus_sdp_attribute_destroy(janus_sdp_attribute *attr);\n/*! \\brief Helper method to add an attribute to a media line\n * @param[in] mline The m-line to add the attribute to\n * @param[in] attr The attribute to add\n * @returns 0 in case of success, -1 otherwise */\nint janus_sdp_attribute_add_to_mline(janus_sdp_mline *mline, janus_sdp_attribute *attr);\n\n/*! \\brief Method to parse an SDP string to a janus_sdp object\n * @param[in] sdp The SDP string to parse\n * @param[in,out] error Buffer to receive a reason for an error, if any\n * @param[in] errlen The length of the error buffer\n * @returns A pointer to a janus_sdp object, if successful, NULL otherwise; in case\n * of errors, if provided the error string is filled with a reason  */\njanus_sdp *janus_sdp_parse(const char *sdp, char *error, size_t errlen);\n\n/*! \\brief Helper method to quickly remove all traces (m-line, rtpmap, fmtp, etc.) of a payload type\n * @param[in] sdp The janus_sdp object to remove the payload type from\n * @param[in] index The m-line to remove the payload type from (use -1 for the first m-line that matches)\n * @param[in] pt The payload type to remove\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_sdp_remove_payload_type(janus_sdp *sdp, int index, int pt);\n\n/*! \\brief Method to serialize a janus_sdp object to an SDP string\n * @param[in] sdp The janus_sdp object to serialize\n * @returns A pointer to a string with the serialized SDP, if successful, NULL otherwise */\nchar *janus_sdp_write(janus_sdp *sdp);\n\n/*! \\brief Method to quickly generate a janus_sdp instance from a few selected fields\n * @note This allocates the \\c o_addr, \\c s_name and \\c c_addr properties: if you\n * want to replace them, don't forget to \\c g_free the original pointers first.\n * @param[in] name The session name (if NULL, a default value will be set)\n * @param[in] address The IP to set in o= and c= fields (if NULL, a default value will be set)\n * @returns A pointer to a janus_sdp object, if successful, NULL otherwise */\njanus_sdp *janus_sdp_new(const char *name, const char *address);\n\n/*! \\brief Method to destroy a Janus SDP object\n * @param[in] sdp The Janus SDP object to free */\nvoid janus_sdp_destroy(janus_sdp *sdp);\n\ntypedef enum janus_sdp_oa_type {\n/*! \\brief Add a new m-line of the specific kind (used as a separator for audio, video and data details passed to janus_sdp_generate_offer) */\nJANUS_SDP_OA_MLINE = 1,\n/*! \\brief Whether we should enable a specific m-line when offering/answering (depends on what follows, true by default) */\nJANUS_SDP_OA_ENABLED,\n/*! \\brief When generating an offer or answer automatically, use this direction for media (depends on value that follows, sendrecv by default) */\nJANUS_SDP_OA_DIRECTION,\n/*! \\brief When generating an offer automatically, use this mid (depends on value that follows, needs to be a string) */\nJANUS_SDP_OA_MID,\n/*! \\brief When generating an offer or answer automatically, use this msid (depends on the two strings that follow, stream and track ID respectively) */\nJANUS_SDP_OA_MSID,\n/*! \\brief When generating an offer or answer automatically, use this codec (depends on value that follows, opus/vp8 by default) */\nJANUS_SDP_OA_CODEC,\n/*! \\brief When generating an offer (this is ignored for answers), negotiate this extension: needs two arguments, extmap value and extension ID (can be used multiple times) */\nJANUS_SDP_OA_EXTENSION,\n/*! \\brief When generating an offer (this is ignored for answers), negotiate these extensions: needs a hashtable with the mappings to a specific extmap\n * @note This is only used internally, and will be ignored if provided; in plugins, you should stick to JANUS_SDP_OA_EXTENSION */\nJANUS_SDP_OA_EXTENSIONS,\n/*! \\brief When generating an answer (this is ignored for offers), accept this extension (by default, we reject them all; can be used multiple times) */\nJANUS_SDP_OA_ACCEPT_EXTMAP,\n/*! \\brief When generating an offer (this is ignored for answers), use this payload type (depends on value that follows) */\nJANUS_SDP_OA_PT,\n/*! \\brief When generating an offer or answer automatically, add this custom fmtp string\n * @note When dealing with video, this property is ignored if JANUS_SDP_OA_VP9_PROFILE or JANUS_SDP_OA_H264_PROFILE is used on a compliant codec. */\nJANUS_SDP_OA_FMTP,\n/*! \\brief When generating an offer or answer automatically, do or do not negotiate telephone events (FIXME telephone-event/8000 only, true by default) */\nJANUS_SDP_OA_AUDIO_DTMF,\n/*! \\brief When generating an offer (this is ignored for answers), use this payload type for RED/Opus (depends on value that follows) */\nJANUS_SDP_OA_OPUSRED_PT,\n/*! \\brief When generating an answer (this is ignored for offers), accept opus/red if offered */\nJANUS_SDP_OA_ACCEPT_OPUSRED,\n/*! \\brief When generating an offer or answer automatically, use this profile for VP9 (depends on value that follows) */\nJANUS_SDP_OA_VP9_PROFILE,\n/*! \\brief When generating an offer or answer automatically, use this profile for H.264 (depends on value that follows) */\nJANUS_SDP_OA_H264_PROFILE,\n/*! \\brief When generating an offer or answer automatically, do or do not add the rtcpfb attributes we typically negotiate (fir, nack, pli, remb; true by default) */\nJANUS_SDP_OA_VIDEO_RTCPFB_DEFAULTS,\n/*! \\brief When generating an offer (this is ignored for answers), use the old \"DTLS/SCTP\" instead of the new \"UDP/DTLS/SCTP (depends on what follows, false by default) */\nJANUS_SDP_OA_DATA_LEGACY,\n/*! \\brief MUST be used as the last argument in janus_sdp_generate_offer, janus_sdp_generate_offer_mline and janus_sdp_generate_answer */\nJANUS_SDP_OA_DONE = 0\n} janus_sdp_oa_type;\nconst char *janus_sdp_oa_type_str(janus_sdp_oa_type type);\n\n/*! \\brief Method to generate a janus_sdp offer, using variable arguments to dictate\n * what to negotiate (e.g., in terms of media to offer, directions, etc.). Variable\n * arguments are in the form of a sequence of name-value terminated by a JANUS_SDP_OA_DONE, e.g.:\n \\verbatim\n\tjanus_sdp *offer = janus_sdp_generate_offer(\"My session\", \"127.0.0.1\",\n\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\tJANUS_SDP_OA_PT, 100,\n\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_SENDONLY,\n\t\t\tJANUS_SDP_OA_CODEC, \"opus\",\n\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_VIDEO,\n\t\t\tJANUS_SDP_OA_PT, 101,\n\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_RECVONLY,\n\t\t\tJANUS_SDP_OA_CODEC, \"vp8\",\n \\endverbatim\n * to offer a \\c sendonly Opus audio stream being offered with 100 as\n * payload type, and a \\c recvonly VP8 video stream with 101 as payload type.\n * Refer to the property names in the header file for a complete\n * list of how you can drive the offer. Other media streams can be added,\n * as long as you prefix/specify them with JANUS_SDP_OA_MLINE as done here.\n * The default, if not specified, is to not offer anything, meaning it\n * will be up to you to add m-lines subsequently.\n * @param[in] name The session name (if NULL, a default value will be set)\n * @param[in] address The IP to set in o= and c= fields (if NULL, a default value will be set)\n * @returns A pointer to a janus_sdp object, if successful, NULL otherwise */\njanus_sdp *janus_sdp_generate_offer(const char *name, const char *address, ...);\n/*! \\brief Method to add a single m-line to a new offer, using the same\n * variable arguments janus_sdp_generate_offer supports. This is useful\n * whenever you need to create a new offer, but don't know in advance\n * how many m-lines you'll need, or it would be hard to do programmatically\n * in a single call to janus_sdp_generate_offer. The first argument\n * MUST be JANUS_SDP_OA_MLINE, specifying the type of the media.\n * \\note In case case you add audio and don't specify anything else, the\n * default is to use Opus and payload type 111. For video, the default\n * is VP8 and payload type 96. The default media direction is \\c sendrecv.\n * @param[in] offer The Janus SDP offer to add the new m-line to\n * @returns 0 if successful, a negative integer otherwise */\nint janus_sdp_generate_offer_mline(janus_sdp *offer, ...);\n/*! \\brief Method to generate a janus_sdp answer to a provided janus_sdp offer.\n * Notice that this doesn't address the individual m-lines: it will just\n * create an empty response, create the corresponding m-lines, but leave\n * them all \"rejected\". To answer each m-line you'll have to iterate on\n * the offer m-lines and call janus_sdp_generate_answer_mline instead, e.g.:\n \\verbatim\n\tjanus_sdp *answer = janus_sdp_generate_answer(offer);\n\tGList *temp = offer->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tjanus_sdp_generate_answer_mline(offer, answer, m,\n\t\t\t[..]\n\t\t\tJANUS_SDP_OA_DONE);\n\t\ttemp = temp->next;\n\t}\n \\endverbatim\n * to only accept the audio stream being offered, but as \\c recvonly, use Opus\n * and reject both video and datachannels. Refer to the property names in\n * the header file for a complete list of how you can drive the answer.\n * The default, if not specified, is to accept everything as \\c sendrecv.\n * @param[in] offer The Janus SDP offer to respond to\n * @returns A pointer to a janus_sdp object, if successful, NULL otherwise */\njanus_sdp *janus_sdp_generate_answer(janus_sdp *offer);\n/*! \\brief Method to respond to a single m-line in an offer, using the same\n * variable arguments janus_sdp_generate_offer_mline supports. The first\n * argument MUST be JANUS_SDP_OA_MLINE, specifying the type of the media, e.g.:\n \\verbatim\n\tjanus_sdp_generate_answer_mline(offer, answer, offer_mline,\n\t\tJANUS_SDP_OA_MLINE, JANUS_SDP_AUDIO,\n\t\t\tJANUS_SDP_OA_CODEC, \"opus\",\n\t\t\tJANUS_SDP_OA_DIRECTION, JANUS_SDP_RECVONLY,\n\t\tJANUS_SDP_OA_DONE);\n \\endverbatim\n * to respond to an offered m-line with recvonly audio and use Opus.\n * @param[in] offer The Janus SDP offer\n * @param[in] answer The Janus SDP answer to add the new m-line to\n * @param[in] offered The Janus SDP m-line from the offer to respond to\n * @returns 0 if successful, a negative integer otherwise */\nint janus_sdp_generate_answer_mline(janus_sdp *offer, janus_sdp *answer, janus_sdp_mline *offered, ...);\n\n/*! \\brief Helper to get the payload type associated to a specific codec in an m-line\n * @note This version doesn't involve profiles, which means that in case\n * of multiple payload types associated to the same codec because of\n * different profiles (e.g., VP9 and H.264), this will simply return the\n * first payload type associated with it the codec itself.\n * @param sdp The Janus SDP instance to process\n * @param index The m-line to refer to (use -1 for the first m-line that matches)\n * @param codec The codec to find, as a string\n * @returns The payload type, if found, or -1 otherwise */\nint janus_sdp_get_codec_pt(janus_sdp *sdp, int index, const char *codec);\n\n/*! \\brief Helper to get the payload type associated to a specific codec,\n * in an m-line, taking into account a codec profile as a hint as well\n * @note The profile will only be used if the codec supports it, and the\n * core is aware of it: right now, this is only VP9 and H.264. If the codec\n * is there but the profile is not found, then no payload type is returned.\n * @param sdp The Janus SDP instance to process\n * @param index The m-line to refer to (use -1 for the first m-line that matches)\n * @param codec The codec to find, as a string\n * @param profile The codec profile to use as a hint, as a string\n * @returns The payload type, if found, or -1 otherwise */\nint janus_sdp_get_codec_pt_full(janus_sdp *sdp, int index, const char *codec, const char *profile);\n\n/*! \\brief Helper to get the codec name associated to a specific payload type in an m-line\n * @param sdp The Janus SDP instance to process\n * @param index The m-line to refer to (use -1 for the first m-line that matches)\n * @param pt The payload type to find\n * @returns The codec name, if found, or NULL otherwise */\nconst char *janus_sdp_get_codec_name(janus_sdp *sdp, int index, int pt);\n\n/*! \\brief Helper to get the codec name associated to a specific rtpmap\n * @param rtpmap The rtpmap, as a string (e.g., \"VP8/90000\")\n * @returns The codec name, if found (e.g., \"vp8\"), or NULL otherwise */\nconst char *janus_sdp_get_rtpmap_codec(const char *rtpmap);\n\n/*! \\brief Helper to get the rtpmap associated to a specific codec\n * @param codec The codec name, as a string (e.g., \"opus\")\n * @returns The rtpmap value, if found (e.g., \"opus/48000/2\"), or NULL otherwise */\nconst char *janus_sdp_get_codec_rtpmap(const char *codec);\n\n/*! \\brief Helper to get the fmtp associated to a specific payload type\n * @param sdp The Janus SDP instance to process\n * @param index The m-line to refer to (use -1 for the first m-line that matches)\n * @param pt The payload type to find\n * @returns The fmtp content, if found, or NULL otherwise */\nconst char *janus_sdp_get_fmtp(janus_sdp *sdp, int index, int pt);\n\n/*! \\brief Helper to extract the H.264 or VP9 profile from a fmtp string\n * @note The returned profile string is allocated, so must be freed by the caller\n * @param codec The video codec to refer to\n * @param fmtp The fmtp value to parse\n * @returns The profile content, if found, or NULL otherwise */\nchar *janus_sdp_get_video_profile(janus_videocodec codec, const char *fmtp);\n\n/*! \\brief Helper to get the opus/red payload type from an SDP, if present\n * @param sdp The Janus SDP instance to process\n * @param index The m-line to refer to (use -1 for the first m-line that matches)\n * @returns The payload type, if found, or -1 otherwise */\nint janus_sdp_get_opusred_pt(janus_sdp *sdp, int index);\n\n#endif\n"
  },
  {
    "path": "src/sdp.c",
    "content": "/*! \\file    sdp.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    SDP processing\n * \\details  Implementation of an SDP\n * parser/merger/generator in the server. Each SDP coming from peers is\n * stripped/anonymized before it is passed to the plugins: all\n * DTLS/ICE/transport related information is removed, only leaving the\n * relevant information in place. SDP coming from plugins is stripped/anonymized\n * as well, and merged with the proper DTLS/ICE/transport information before\n * it is sent to the peers. The actual SDP processing (parsing SDP strings,\n * representation of SDP as an internal format, and so on) is done via\n * the tools provided in sdp-utils.h.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#include <netdb.h>\n\n#include <gio/gio.h>\n\n#include \"janus.h\"\n#include \"ice.h\"\n#include \"sdp.h\"\n#include \"utils.h\"\n#include \"ip-utils.h\"\n#include \"debug.h\"\n#include \"events.h\"\n\n\n/* Pre-parse SDP: is this SDP valid? how many audio/video lines? any features to take into account? */\njanus_sdp *janus_sdp_preparse(void *ice_handle, const char *jsep_sdp, char *error_str, size_t errlen,\n\t\tjanus_dtls_role *dtls_role, int *audio, int *video, int *data) {\n\tif(!ice_handle || !jsep_sdp) {\n\t\tJANUS_LOG(LOG_ERR, \"  Can't preparse, invalid arguments\\n\");\n\t\treturn NULL;\n\t}\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice_handle;\n\tjanus_sdp *parsed_sdp = janus_sdp_parse(jsep_sdp, error_str, errlen);\n\tif(!parsed_sdp) {\n\t\tJANUS_LOG(LOG_ERR, \"  Error parsing SDP? %s\\n\", error_str ? error_str : \"(unknown reason)\");\n\t\t/* Invalid SDP */\n\t\treturn NULL;\n\t}\n\tgboolean dtls_role_found = FALSE;\n\tif(dtls_role) {\n\t\t/* We're interested in checking which role was advertised, traverse global attributes too */\n\t\tGList *tempA = parsed_sdp->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name && !strcasecmp(a->name, \"setup\")) {\n\t\t\t\tif(a->value == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid setup attribute (no value)\\n\", handle->handle_id);\n\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t\tif(!strcasecmp(a->value, \"actpass\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer advertised 'actpass' DTLS role, we'll be a DTLS client\\n\", handle->handle_id);\n\t\t\t\t\t*dtls_role = JANUS_DTLS_ROLE_ACTPASS;\n\t\t\t\t} else if(!strcasecmp(a->value, \"passive\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer advertised 'passive' DTLS role, we'll be a DTLS client\\n\", handle->handle_id);\n\t\t\t\t\t*dtls_role = JANUS_DTLS_ROLE_SERVER;\n\t\t\t\t} else if(!strcasecmp(a->value, \"active\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer advertised 'active' DTLS role, we'll be a DTLS server\\n\", handle->handle_id);\n\t\t\t\t\t*dtls_role = JANUS_DTLS_ROLE_CLIENT;\n\t\t\t\t}\n\t\t\t\tdtls_role_found = TRUE;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t}\n\t/* Look for m-lines */\n\tGList *temp = parsed_sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tif(audio && m->type == JANUS_SDP_AUDIO) {\n\t\t\t*audio = *audio+1;\n\t\t} else if(video && m->type == JANUS_SDP_VIDEO) {\n\t\t\t*video = *video+1;\n\t\t} else if(data && m->type == JANUS_SDP_APPLICATION && strstr(m->proto, \"DTLS/SCTP\")) {\n\t\t\t*data = *data+1;\n\t\t}\n\t\tif(m->index == 0 && m->port == 0)\n\t\t\tm->port = 9;\n\t\t/* Preparse the mid as well, and check if bundle-only is used */\n\t\tGList *tempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name) {\n\t\t\t\tif(!strcasecmp(a->name, \"bundle-only\") && m->port == 0) {\n\t\t\t\t\t/* Port 0 but bundle-only is used, don't disable this m-line */\n\t\t\t\t\tm->port = 9;\n\t\t\t\t} else if(!strcasecmp(a->name, \"mid\")) {\n\t\t\t\t\t/* Found mid attribute */\n\t\t\t\t\tif(a->value == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid mid attribute (no value)\\n\", handle->handle_id);\n\t\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t\tif((m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) && m->port > 0) {\n\t\t\t\t\t\tif(strnlen(a->value, 16 + 1) > 16) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] mid on m-line #%d too large: (%zu > 16)\\n\",\n\t\t\t\t\t\t\t\thandle->handle_id, m->index, strlen(a->value));\n\t\t\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if(dtls_role && !dtls_role_found && !strcasecmp(a->name, \"setup\")) {\n\t\t\t\t\tif(a->value == NULL) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid setup attribute (no value)\\n\", handle->handle_id);\n\t\t\t\t\t\tjanus_sdp_destroy(parsed_sdp);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t\tif(!strcasecmp(a->value, \"actpass\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer advertised 'actpass' DTLS role, we'll be a DTLS client\\n\", handle->handle_id);\n\t\t\t\t\t\t*dtls_role = JANUS_DTLS_ROLE_ACTPASS;\n\t\t\t\t\t} else if(!strcasecmp(a->value, \"passive\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer advertised 'passive' DTLS role, we'll be a DTLS client\\n\", handle->handle_id);\n\t\t\t\t\t\t*dtls_role = JANUS_DTLS_ROLE_SERVER;\n\t\t\t\t\t} else if(!strcasecmp(a->value, \"active\")) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer advertised 'active' DTLS role, we'll be a DTLS server\\n\", handle->handle_id);\n\t\t\t\t\t\t*dtls_role = JANUS_DTLS_ROLE_CLIENT;\n\t\t\t\t\t}\n\t\t\t\t\tdtls_role_found = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* If the m-line is disabled don't actually increase the count */\n\t\tif(m->port == 0) {\n\t\t\tif(m->type == JANUS_SDP_AUDIO) {\n\t\t\t\t*audio = *audio - 1;\n\t\t\t} else if(m->type == JANUS_SDP_VIDEO) {\n\t\t\t\t*video = *video - 1;\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\n\treturn parsed_sdp;\n}\n\n/* Parse remote SDP */\nint janus_sdp_process_remote(void *ice_handle, janus_sdp *remote_sdp, gboolean rids_hml, gboolean update) {\n\tif(!ice_handle || !remote_sdp)\n\t\treturn -1;\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice_handle;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc)\n\t\treturn -1;\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tgchar *ruser = NULL, *rpass = NULL, *rhashing = NULL, *rfingerprint = NULL;\n\tgboolean rtx = FALSE;\n\tchar *simulcast_rids = NULL;\n\t/* Ok, let's start with global attributes */\n\tGList *temp = remote_sdp->attributes;\n\twhile(temp) {\n\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)temp->data;\n\t\tif(a && a->name && a->value) {\n\t\t\tif(!strcasecmp(a->name, \"fingerprint\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Fingerprint (global) : %s\\n\", handle->handle_id, a->value);\n\t\t\t\tif(!strncasecmp(a->value, \"sha-256 \", strlen(\"sha-256 \"))) {\n\t\t\t\t\trhashing = g_strdup(\"sha-256\");\n\t\t\t\t\trfingerprint = g_strdup(a->value + strlen(\"sha-256 \"));\n\t\t\t\t} else if(!strncasecmp(a->value, \"sha-1 \", strlen(\"sha-1 \"))) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]  Hashing algorithm not the one we expected (sha-1 instead of sha-256), but that's ok\\n\", handle->handle_id);\n\t\t\t\t\trhashing = g_strdup(\"sha-1\");\n\t\t\t\t\trfingerprint = g_strdup(a->value + strlen(\"sha-1 \"));\n\t\t\t\t} else {\n\t\t\t\t\t/* FIXME We should handle this somehow anyway... OpenSSL supports them all */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]  Hashing algorithm not the one we expected (sha-256/sha-1), *NOT* cool\\n\", handle->handle_id);\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(a->name, \"ice-ufrag\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] ICE ufrag (global):   %s\\n\", handle->handle_id, a->value);\n\t\t\t\truser = g_strdup(a->value);\n\t\t\t} else if(!strcasecmp(a->name, \"ice-pwd\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] ICE pwd (global):     %s\\n\", handle->handle_id, a->value);\n\t\t\t\trpass = g_strdup(a->value);\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\t/* Now go on with m-line and their attributes */\n\ttemp = remote_sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\t/* Audio/Video */\n\t\t\tif(handle->rtp_profile == NULL && m->proto != NULL)\n\t\t\t\thandle->rtp_profile = g_strdup(m->proto);\n\t\t\t/* Find the internal medium instance */\n\t\t\tmedium = g_hash_table_lookup(pc->media, GINT_TO_POINTER(m->index));\n\t\t\tif(!medium) {\n\t\t\t\t/* We don't have it, create one now */\n\t\t\t\tmedium = janus_ice_peerconnection_medium_create(handle,\n\t\t\t\t\tm->type == JANUS_SDP_VIDEO ? JANUS_MEDIA_VIDEO : JANUS_MEDIA_AUDIO);\n\t\t\t}\n\t\t\tif(m->port > 0) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Parsing m-line #%d...\\n\", handle->handle_id, m->index);\n\t\t\t\tgboolean receiving = (medium->recv == TRUE);\n\t\t\t\tswitch(m->direction) {\n\t\t\t\t\tcase JANUS_SDP_INACTIVE:\n\t\t\t\t\tcase JANUS_SDP_INVALID:\n\t\t\t\t\t\tmedium->send = FALSE;\n\t\t\t\t\t\tmedium->recv = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_SDP_SENDONLY:\n\t\t\t\t\t\t/* A sendonly peer means recvonly for Janus */\n\t\t\t\t\t\tmedium->send = FALSE;\n\t\t\t\t\t\tmedium->recv = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_SDP_RECVONLY:\n\t\t\t\t\t\t/* A recvonly peer means sendonly for Janus */\n\t\t\t\t\t\tmedium->send = TRUE;\n\t\t\t\t\t\tmedium->recv = FALSE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase JANUS_SDP_SENDRECV:\n\t\t\t\t\tcase JANUS_SDP_DEFAULT:\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tmedium->send = TRUE;\n\t\t\t\t\t\tmedium->recv = TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif(receiving != medium->recv)\n\t\t\t\t\tjanus_ice_notify_media_stopped(handle);\n\t\t\t\tif(m->ptypes != NULL) {\n\t\t\t\t\tg_list_free(medium->payload_types);\n\t\t\t\t\tmedium->payload_types = g_list_copy(m->ptypes);\n\t\t\t\t\tif(pc->payload_types == NULL)\n\t\t\t\t\t\tpc->payload_types = g_hash_table_new(NULL, NULL);\n\t\t\t\t\tGList *temp = medium->payload_types;\n\t\t\t\t\twhile(temp) {\n\t\t\t\t\t\tg_hash_table_insert(pc->payload_types, temp->data, temp->data);\n\t\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* Medium rejected? */\n\t\t\t\tmedium->send = FALSE;\n\t\t\t\tmedium->recv = FALSE;\n\t\t\t}\n\t\t} else if(m->type == JANUS_SDP_APPLICATION) {\n\t\t\t/* Find the internal medium instance */\n\t\t\tmedium = g_hash_table_lookup(pc->media, GINT_TO_POINTER(m->index));\n\t\t\tif(!medium) {\n\t\t\t\t/* We don't have it, create one now */\n\t\t\t\tmedium = janus_ice_peerconnection_medium_create(handle, JANUS_MEDIA_DATA);\n\t\t\t}\n\t\t\t/* Is this SCTP for DataChannels? */\n\t\t\tif(!strcasecmp(m->proto, \"DTLS/SCTP\") || !strcasecmp(m->proto, \"UDP/DTLS/SCTP\")) {\n#ifdef HAVE_SCTP\n\t\t\t\tif(m->port > 0) {\n\t\t\t\t\t/* Yep */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Parsing m-line #%d... (data channels)\\n\", handle->handle_id, m->index);\n\t\t\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)) {\n\t\t\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS);\n\t\t\t\t\t}\n\t\t\t\t\tif(!strcasecmp(m->proto, \"UDP/DTLS/SCTP\")) {\n\t\t\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\t\t\t\t\t}\n\t\t\t\t\tmedium->send = TRUE;\n\t\t\t\t\tmedium->recv = TRUE;\n\t\t\t\t} else {\n\t\t\t\t\t/* Data channels rejected? */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Data channels rejected by peer...\\n\", handle->handle_id);\n\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS);\n\t\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\t\t\t\t\tmedium->send = FALSE;\n\t\t\t\t\tmedium->recv = FALSE;\n\t\t\t\t}\n#else\n\t\t\t\t/* Data channels unsupported */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Data channels unsupported...\\n\", handle->handle_id);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\t\t\t\tmedium->send = FALSE;\n\t\t\t\tmedium->recv = FALSE;\n#endif\n\t\t\t} else {\n\t\t\t\t/* Unsupported data channels format. */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Data channels format %s unsupported, skipping\\n\", handle->handle_id, m->proto);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\t\t\t}\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping disabled/unsupported media line...\\n\", handle->handle_id);\n\t\t\tmedium = g_hash_table_lookup(pc->media, GINT_TO_POINTER(m->index));\n\t\t\tif(!medium) {\n\t\t\t\t/* We don't have it, create one now */\n\t\t\t\tmedium = janus_ice_peerconnection_medium_create(handle, JANUS_MEDIA_UNKNOWN);\n\t\t\t}\n\t\t}\n\t\tif(medium == NULL) {\n\t\t\t/* No medium? Should never happen */\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Look for mid, msid, ICE credentials and fingerprint first: check media attributes */\n\t\tGList *tempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name && a->value) {\n\t\t\t\tif(!strcasecmp(a->name, \"mid\")) {\n\t\t\t\t\t/* Found mid attribute */\n\t\t\t\t\tif(strnlen(a->value, 16 + 1) > 16) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] mid on m-line #%d too large: (%zu > 16)\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->index, strlen(a->value));\n\t\t\t\t\t\treturn -2;\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->mid != NULL && strcasecmp(medium->mid, a->value)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] mid on m-line #%d changed (%s --> %s), ignoring new value\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->index, medium->mid, a->value);\n\t\t\t\t\t} else if(medium->mid == NULL) {\n\t\t\t\t\t\tmedium->mid = g_strdup(a->value);\n\t\t\t\t\t\tif(!g_hash_table_lookup(pc->media_bymid, medium->mid)) {\n\t\t\t\t\t\t\tg_hash_table_insert(pc->media_bymid, g_strdup(medium->mid), medium);\n\t\t\t\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(handle->pc_mid == NULL)\n\t\t\t\t\t\thandle->pc_mid = g_strdup(a->value);\n\t\t\t\t} else if(!strcasecmp(a->name, \"msid\")) {\n\t\t\t\t\t/* Found msid attribute */\n\t\t\t\t\tchar msid[65], mstid[65];\n\t\t\t\t\tmsid[0] = '\\0';\n\t\t\t\t\tmstid[0] = '\\0';\n\t\t\t\t\tif(sscanf(a->value, \"%64s %64s\", msid, mstid) != 2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid msid on m-line #%d\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->index);\n\t\t\t\t\t\treturn -2;\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->remote_msid == NULL || strcasecmp(medium->remote_msid, msid)) {\n\t\t\t\t\t\tchar *old_msid = medium->remote_msid;\n\t\t\t\t\t\tmedium->remote_msid = g_strdup(msid);\n\t\t\t\t\t\tg_free(old_msid);\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->remote_mstid == NULL || strcasecmp(medium->remote_mstid, mstid)) {\n\t\t\t\t\t\tchar *old_mstid = medium->remote_mstid;\n\t\t\t\t\t\tmedium->remote_mstid = g_strdup(mstid);\n\t\t\t\t\t\tg_free(old_mstid);\n\t\t\t\t\t}\n\t\t\t\t} else if(!strcasecmp(a->name, \"fingerprint\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Fingerprint (local) : %s\\n\", handle->handle_id, a->value);\n\t\t\t\t\tif(!strncasecmp(a->value, \"sha-256 \", strlen(\"sha-256 \"))) {\n\t\t\t\t\t\tg_free(rhashing);\t/* FIXME We're overwriting the global one, if any */\n\t\t\t\t\t\trhashing = g_strdup(\"sha-256\");\n\t\t\t\t\t\tg_free(rfingerprint);\t/* FIXME We're overwriting the global one, if any */\n\t\t\t\t\t\trfingerprint = g_strdup(a->value + strlen(\"sha-256 \"));\n\t\t\t\t\t} else if(!strncasecmp(a->value, \"sha-1 \", strlen(\"sha-1 \"))) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]  Hashing algorithm not the one we expected (sha-1 instead of sha-256), but that's ok\\n\", handle->handle_id);\n\t\t\t\t\t\tg_free(rhashing);\t/* FIXME We're overwriting the global one, if any */\n\t\t\t\t\t\trhashing = g_strdup(\"sha-1\");\n\t\t\t\t\t\tg_free(rfingerprint);\t/* FIXME We're overwriting the global one, if any */\n\t\t\t\t\t\trfingerprint = g_strdup(a->value + strlen(\"sha-1 \"));\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* FIXME We should handle this somehow anyway... OpenSSL supports them all */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]  Hashing algorithm not the one we expected (sha-256), *NOT* cool\\n\", handle->handle_id);\n\t\t\t\t\t}\n\t\t\t\t} else if(!strcasecmp(a->name, \"setup\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] DTLS setup (local):  %s\\n\", handle->handle_id, a->value);\n\t\t\t\t\tif(!update) {\n\t\t\t\t\t\tif(!strcasecmp(a->value, \"actpass\") || !strcasecmp(a->value, \"passive\")) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Setting connect state (DTLS client)\\n\", handle->handle_id);\n\t\t\t\t\t\t\tpc->dtls_role = JANUS_DTLS_ROLE_CLIENT;\n\t\t\t\t\t\t} else if(!strcasecmp(a->value, \"active\")) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Setting accept state (DTLS server)\\n\", handle->handle_id);\n\t\t\t\t\t\t\tpc->dtls_role = JANUS_DTLS_ROLE_SERVER;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(pc->dtls)\n\t\t\t\t\t\t\tpc->dtls->dtls_role = pc->dtls_role;\n\t\t\t\t\t}\n\t\t\t\t\t/* TODO Handle holdconn... */\n\t\t\t\t} else if(!strcasecmp(a->name, \"ice-ufrag\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] ICE ufrag (local):   %s\\n\", handle->handle_id, a->value);\n\t\t\t\t\tg_free(ruser);\t/* FIXME We're overwriting the global one, if any */\n\t\t\t\t\truser = g_strdup(a->value);\n\t\t\t\t} else if(!strcasecmp(a->name, \"ice-pwd\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] ICE pwd (local):     %s\\n\", handle->handle_id, a->value);\n\t\t\t\t\tg_free(rpass);\t/* FIXME We're overwriting the global one, if any */\n\t\t\t\t\trpass = g_strdup(a->value);\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\tif(m->index == 0) {\n\t\t\tif(!ruser || !rpass || (janus_is_webrtc_encryption_enabled() && (!rfingerprint || !rhashing))) {\n\t\t\t\t/* Missing mandatory information, failure... */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] SDP missing mandatory information\\n\", handle->handle_id);\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] %p, %p, %p, %p\\n\", handle->handle_id, ruser, rpass, rfingerprint, rhashing);\n\t\t\t\tg_free(ruser);\n\t\t\t\tg_free(rpass);\n\t\t\t\tg_free(rhashing);\n\t\t\t\tg_free(rfingerprint);\n\t\t\t\treturn -2;\n\t\t\t}\n\t\t\t/* If we received the ICE credentials for the first time, enforce them */\n\t\t\tif(ruser && !pc->ruser && rpass && !pc->rpass) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Setting remote credentials...\\n\", handle->handle_id);\n\t\t\t\tif(!nice_agent_set_remote_credentials(handle->agent, handle->stream_id, ruser, rpass)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to set remote credentials!\\n\", handle->handle_id);\n\t\t\t\t}\n\t\t\t} else\n\t\t\t/* If this is a renegotiation, check if this is an ICE restart */\n\t\t\tif((ruser && pc->ruser && strcmp(ruser, pc->ruser)) ||\n\t\t\t\t\t(rpass && pc->rpass && strcmp(rpass, pc->rpass))) {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] ICE restart detected\\n\", handle->handle_id);\n\t\t\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES);\n\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ICE_RESTART);\n\t\t\t}\n\t\t\t/* Store fingerprint and hashing */\n\t\t\tif(janus_is_webrtc_encryption_enabled()) {\n\t\t\t\tg_free(pc->remote_hashing);\n\t\t\t\tpc->remote_hashing = g_strdup(rhashing);\n\t\t\t\tg_free(pc->remote_fingerprint);\n\t\t\t\tpc->remote_fingerprint = g_strdup(rfingerprint);\n\t\t\t}\n\t\t\t/* Store the ICE username and password for this stream */\n\t\t\tg_free(pc->ruser);\n\t\t\tpc->ruser = g_strdup(ruser);\n\t\t\tg_free(pc->rpass);\n\t\t\tpc->rpass = g_strdup(rpass);\n\t\t}\n\t\t/* Is simulcasting enabled, using rid? (we need to check this before parsing SSRCs) */\n\t\ttempA = m->attributes;\n\t\tmedium->rids_hml = rids_hml;\n\t\tg_free(medium->rid[0]);\n\t\tmedium->rid[0] = NULL;\n\t\tg_free(medium->rid[1]);\n\t\tmedium->rid[1] = NULL;\n\t\tg_free(medium->rid[2]);\n\t\tmedium->rid[2] = NULL;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name && !strcasecmp(a->name, \"rid\") && a->value) {\n\t\t\t\t/* This attribute is used for simulcasting */\n\t\t\t\tchar rid[16];\n\t\t\t\tif(sscanf(a->value, \"%15s send\", rid) != 1) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse rid attribute...\\n\", handle->handle_id);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Parsed rid: %s\\n\", handle->handle_id, rid);\n\t\t\t\t\tmedium->disabled_rid[rids_hml ? 2 : 0] = FALSE;\n\t\t\t\t\tif(medium->rid[rids_hml ? 2 : 0] == NULL) {\n\t\t\t\t\t\tmedium->rid[rids_hml ? 2 : 0] = g_strdup(rid);\n\t\t\t\t\t} else if(medium->rid[1] == NULL) {\n\t\t\t\t\t\tmedium->rid[1] = g_strdup(rid);\n\t\t\t\t\t} else if(medium->rid[rids_hml ? 0 : 2] == NULL) {\n\t\t\t\t\t\tmedium->rid[rids_hml ? 0 : 2] = g_strdup(rid);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Too many RTP Stream IDs, ignoring '%s'...\\n\", handle->handle_id, rid);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if(a->name && !strcasecmp(a->name, \"simulcast\") && a->value) {\n\t\t\t\t/* Firefox and Chrome signal simulcast support differently */\n\t\t\t\tmedium->legacy_rid = strstr(a->value, \"rid=\") ? TRUE : FALSE;\n\t\t\t\t/* If the attribute contains a tilde, some of the substreams\n\t\t\t\t * are currently disabled, so let's track it and use it later */\n\t\t\t\tif(!medium->legacy_rid && strstr(a->value, \"~\") != NULL) {\n\t\t\t\t\tchar *index = strstr(a->value, \"send \");\n\t\t\t\t\tif(index != NULL) {\n\t\t\t\t\t\tindex += strlen(\"send \");\n\t\t\t\t\t\tif(index != NULL && simulcast_rids == NULL)\n\t\t\t\t\t\t\tsimulcast_rids = g_strdup(index);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* If rid is involved, check how many of them we have (it may be less than 3) */\n\t\tif(medium->rid[0] == NULL && medium->rid[2] != NULL) {\n\t\t\tmedium->rid[0] = medium->rid[1];\n\t\t\tmedium->rid[1] = medium->rid[2];\n\t\t\tmedium->rid[2] = NULL;\n\t\t}\n\t\tif(medium->rid[0] == NULL && medium->rid[1] != NULL) {\n\t\t\tmedium->rid[0] = medium->rid[1];\n\t\t\tmedium->rid[1] = NULL;\n\t\t}\n\t\tif(simulcast_rids != NULL) {\n\t\t\t/* Some substreams are disabled, check which ones */\n\t\t\tgchar **list = g_strsplit(simulcast_rids, \";\", 3);\n\t\t\tgchar *index = list[0];\n\t\t\tchar rid[50];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0, j=0;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\tif(strlen(index) > 0 && strstr(index, \"~\") == index) {\n\t\t\t\t\t\tfor(j=0; j<3; j++) {\n\t\t\t\t\t\t\tif(medium->rid[j] != NULL) {\n\t\t\t\t\t\t\t\tg_snprintf(rid, sizeof(rid), \"~%s\", medium->rid[j]);\n\t\t\t\t\t\t\t\tif(!strcasecmp(index, rid)) {\n\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] rid %s is currently disabled\\n\",\n\t\t\t\t\t\t\t\t\t\thandle->handle_id, medium->rid[j]);\n\t\t\t\t\t\t\t\t\tmedium->disabled_rid[j] = TRUE;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_clear_pointer(&list, g_strfreev);\n\t\t\tg_free(simulcast_rids);\n\t\t\tsimulcast_rids = NULL;\n\t\t}\n\t\t/* Let's start figuring out the SSRCs, and any grouping that may be there */\n\t\tmedium->ssrc_peer_new[0] = 0;\n\t\tmedium->ssrc_peer_new[1] = 0;\n\t\tmedium->ssrc_peer_new[2] = 0;\n\t\tmedium->ssrc_peer_rtx_new[0] = 0;\n\t\tmedium->ssrc_peer_rtx_new[1] = 0;\n\t\tmedium->ssrc_peer_rtx_new[2] = 0;\n\t\t/* Any SSRC SIM group? */\n\t\ttempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name && a->value) {\n\t\t\t\tif(!strcasecmp(a->name, \"ssrc-group\") && strstr(a->value, \"SIM\")) {\n\t\t\t\t\tint res = janus_sdp_parse_ssrc_group(medium, (const char *)a->value, m->type == JANUS_SDP_VIDEO);\n\t\t\t\t\tif(res != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse SSRC SIM group attribute... (%d)\\n\", handle->handle_id, res);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* Any SSRC FID group? */\n\t\ttempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name && a->value) {\n\t\t\t\tif(!strcasecmp(a->name, \"ssrc-group\") && strstr(a->value, \"FID\")) {\n\t\t\t\t\tint res = janus_sdp_parse_ssrc_group(medium, (const char *)a->value, m->type == JANUS_SDP_VIDEO);\n\t\t\t\t\tif(res != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse SSRC FID group attribute... (%d)\\n\", handle->handle_id, res);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* Any SSRC in general? */\n\t\ttempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name && a->value) {\n\t\t\t\tif(!strcasecmp(a->name, \"ssrc\")) {\n\t\t\t\t\tint res = janus_sdp_parse_ssrc(medium, (const char *)a->value, m->type == JANUS_SDP_VIDEO);\n\t\t\t\t\tif(res != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse SSRC attribute... (%d)\\n\", handle->handle_id, res);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* Now look for candidates and other info */\n\t\ttempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name) {\n\t\t\t\tif(!strcasecmp(a->name, \"candidate\")) {\n\t\t\t\t\tif(m->index > 1) {\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] This is a %s candidate, but we're bundling on another stream, ignoring...\\n\",\n\t\t\t\t\t\t\thandle->handle_id, janus_sdp_mtype_str(m->type));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tint res = janus_sdp_parse_candidate(pc, (const char *)a->value, 0);\n\t\t\t\t\t\tif(res != 0) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse candidate... (%d)\\n\", handle->handle_id, res);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if(!strcasecmp(a->name, \"rtcp-fb\")) {\n\t\t\t\t\tif(a->value && strstr(a->value, \"nack\") && medium) {\n\t\t\t\t\t\t/* Enable NACKs */\n\t\t\t\t\t\tmedium->do_nacks = TRUE;\n\t\t\t\t\t}\n\t\t\t\t} else if(!strcasecmp(a->name, \"fmtp\")) {\n\t\t\t\t\tif(a->value && strstr(a->value, \"apt=\")) {\n\t\t\t\t\t\t/* RFC4588 rtx payload type mapping */\n\t\t\t\t\t\tint ptype = -1, rtx_ptype = -1;\n\t\t\t\t\t\tif(sscanf(a->value, \"%d apt=%d\", &rtx_ptype, &ptype) != 2) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse fmtp/apt attribute...\\n\", handle->handle_id);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trtx = TRUE;\n\t\t\t\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX);\n\t\t\t\t\t\t\tif(pc->rtx_payload_types == NULL)\n\t\t\t\t\t\t\t\tpc->rtx_payload_types = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\tif(pc->rtx_payload_types_rev == NULL)\n\t\t\t\t\t\t\t\tpc->rtx_payload_types_rev = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\tint map_pt = GPOINTER_TO_INT(g_hash_table_lookup(pc->rtx_payload_types_rev, GINT_TO_POINTER(rtx_ptype)));\n\t\t\t\t\t\t\tif(map_pt && map_pt != ptype) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] RTX payload type %d already mapped to %d, skipping fmtp/apt mapping with %d...\\n\",\n\t\t\t\t\t\t\t\t\thandle->handle_id, rtx_ptype, map_pt, ptype);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tg_hash_table_insert(pc->rtx_payload_types, GINT_TO_POINTER(ptype), GINT_TO_POINTER(rtx_ptype));\n\t\t\t\t\t\t\t\tg_hash_table_insert(pc->rtx_payload_types_rev, GINT_TO_POINTER(rtx_ptype), GINT_TO_POINTER(ptype));\n\t\t\t\t\t\t\t\tif(medium->rtx_payload_types == NULL)\n\t\t\t\t\t\t\t\t\tmedium->rtx_payload_types = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\t\tg_hash_table_insert(medium->rtx_payload_types, GINT_TO_POINTER(ptype), GINT_TO_POINTER(rtx_ptype));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if(!strcasecmp(a->name, \"rtpmap\")) {\n\t\t\t\t\tif(a->value && strstr(a->value, \"rtx\") == NULL) {\n\t\t\t\t\t\tint ptype = atoi(a->value);\n\t\t\t\t\t\tif(ptype > -1) {\n\t\t\t\t\t\t\tchar *cr = strchr(a->value, '/');\n\t\t\t\t\t\t\tif(cr != NULL) {\n\t\t\t\t\t\t\t\tcr++;\n\t\t\t\t\t\t\t\tuint32_t clock_rate = 0;\n\t\t\t\t\t\t\t\tif(janus_string_to_uint32(cr, &clock_rate) == 0) {\n\t\t\t\t\t\t\t\t\tif(pc->clock_rates == NULL)\n\t\t\t\t\t\t\t\t\t\tpc->clock_rates = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\t\t\tif(medium->clock_rates == NULL)\n\t\t\t\t\t\t\t\t\t\tmedium->clock_rates = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\t\t\tuint32_t map_cr = GPOINTER_TO_UINT(g_hash_table_lookup(pc->clock_rates, GINT_TO_POINTER(ptype)));\n\t\t\t\t\t\t\t\t\tif(map_cr && map_cr != clock_rate) {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Payload type %d already mapped to clock rate %d, skipping rtpmap mapping with %d...\\n\",\n\t\t\t\t\t\t\t\t\t\t\thandle->handle_id, ptype, map_cr, clock_rate);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tg_hash_table_insert(pc->clock_rates, GINT_TO_POINTER(ptype), GUINT_TO_POINTER(clock_rate));\n\t\t\t\t\t\t\t\t\t\tg_hash_table_insert(medium->clock_rates, GINT_TO_POINTER(ptype), GUINT_TO_POINTER(clock_rate));\n\t\t\t\t\t\t\t\t\t\t/* Check if opus/red is negotiated */\n\t\t\t\t\t\t\t\t\t\tif(strstr(a->value, \"red/48000/2\"))\n\t\t\t\t\t\t\t\t\t\t\tmedium->opusred_pt = ptype;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n#ifdef HAVE_SCTP\n\t\t\t\telse if(!strcasecmp(a->name, \"sctpmap\")) {\n\t\t\t\t\t/* We don't really care */\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Got a sctpmap attribute: %s\\n\", a->value);\n\t\t\t\t}\n#endif\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* Any change in SSRCs we should be aware of? */\n\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\tint vindex = 0;\n\t\t\tfor(vindex=0; vindex<(m->type == JANUS_SDP_VIDEO ? 3 :1); vindex++) {\n\t\t\t\tif(medium->ssrc_peer_new[vindex] > 0) {\n\t\t\t\t\tif(medium->ssrc_peer[vindex] > 0 && medium->ssrc_peer[vindex] != medium->ssrc_peer_new[vindex]) {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] %s SSRC (#%d) on mline #%d changed: %\"SCNu32\" --> %\"SCNu32\"\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->type == JANUS_SDP_VIDEO ? \"Video\" : \"Audio\",\n\t\t\t\t\t\t\tvindex, m->index, medium->ssrc_peer[vindex], medium->ssrc_peer_new[vindex]);\n\t\t\t\t\t\t/* Reset the RTCP context */\n\t\t\t\t\t\tjanus_mutex_lock(&medium->mutex);\n\t\t\t\t\t\tif(medium->rtcp_ctx[vindex]) {\n\t\t\t\t\t\t\tmemset(medium->rtcp_ctx[vindex], 0, sizeof(*medium->rtcp_ctx[vindex]));\n\t\t\t\t\t\t\tmedium->rtcp_ctx[vindex]->tb = (m->type == JANUS_SDP_VIDEO ? 90000 : 48000);\t/* May change later */;\n\t\t\t\t\t\t\tmedium->rtcp_ctx[vindex]->in_link_quality = 100;\n\t\t\t\t\t\t\tmedium->rtcp_ctx[vindex]->in_media_link_quality = 100;\n\t\t\t\t\t\t\tmedium->rtcp_ctx[vindex]->out_link_quality = 100;\n\t\t\t\t\t\t\tmedium->rtcp_ctx[vindex]->out_media_link_quality = 100;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif(medium->last_seqs[vindex])\n\t\t\t\t\t\t\tjanus_seq_list_free(&medium->last_seqs[vindex]);\n\t\t\t\t\t\tjanus_mutex_unlock(&medium->mutex);\n\t\t\t\t\t}\n\t\t\t\t\tmedium->ssrc_peer[vindex] = medium->ssrc_peer_new[vindex];\n\t\t\t\t\tmedium->ssrc_peer_new[vindex] = 0;\n\t\t\t\t}\n\t\t\t\tif(!g_hash_table_lookup(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_peer[vindex]))) {\n\t\t\t\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_peer[vindex]), medium);\n\t\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\t}\n\t\t\t\t/* Do the same with the related rtx SSRC, if any */\n\t\t\t\tif(medium->ssrc_peer_rtx_new[vindex] > 0) {\n\t\t\t\t\tif(medium->ssrc_peer_rtx[vindex] > 0 && medium->ssrc_peer_rtx[vindex] != medium->ssrc_peer_rtx_new[vindex]) {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"[%\"SCNu64\"] %s SSRC (#%d rtx) on mline #%d changed: %\"SCNu32\" --> %\"SCNu32\"\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->type == JANUS_SDP_VIDEO ? \"Video\" : \"Audio\",\n\t\t\t\t\t\t\tvindex, m->index, medium->ssrc_peer_rtx[vindex], medium->ssrc_peer_rtx_new[vindex]);\n\t\t\t\t\t}\n\t\t\t\t\tmedium->ssrc_peer_rtx[vindex] = medium->ssrc_peer_rtx_new[vindex];\n\t\t\t\t\tmedium->ssrc_peer_rtx_new[vindex] = 0;\n\t\t\t\t\tif(medium->ssrc_rtx == 0)\n\t\t\t\t\t\tmedium->ssrc_rtx = janus_random_uint32();\t/* FIXME Should we look for conflicts? */\n\t\t\t\t\tif(!g_hash_table_lookup(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_peer_rtx[vindex]))) {\n\t\t\t\t\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_peer_rtx[vindex]), medium);\n\t\t\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\t\t}\n\t\t\t\t\tif(!g_hash_table_lookup(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_rtx))) {\n\t\t\t\t\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_rtx), medium);\n\t\t\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(m->type == JANUS_SDP_VIDEO) {\n\t\t\t\tif((medium->ssrc_peer[1] || medium->rid[1] != NULL) && medium->rtcp_ctx[1] == NULL) {\n\t\t\t\t\tmedium->rtcp_ctx[1] = g_malloc0(sizeof(rtcp_context));\n\t\t\t\t\tmedium->rtcp_ctx[1]->tb = 90000;\n\t\t\t\t\tmedium->rtcp_ctx[1]->in_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[1]->in_media_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[1]->out_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[1]->out_media_link_quality = 100;\n\t\t\t\t}\n\t\t\t\tif((medium->ssrc_peer[2] || medium->rid[rids_hml ? 2 : 0] != NULL) && medium->rtcp_ctx[2] == NULL) {\n\t\t\t\t\tmedium->rtcp_ctx[2] = g_malloc0(sizeof(rtcp_context));\n\t\t\t\t\tmedium->rtcp_ctx[2]->tb = 90000;\n\t\t\t\t\tmedium->rtcp_ctx[2]->in_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[2]->in_media_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[2]->out_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[2]->out_media_link_quality = 100;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(m->type == JANUS_SDP_VIDEO && medium->rtx_payload_types && m->ptypes) {\n\t\t\t\t/* Check if there are new payload types that conflict with our rtx additions */\n\t\t\t\tGList *ptypes = g_list_copy(m->ptypes), *tempP = ptypes;\n\t\t\t\tGList *rtx_ptypes = g_hash_table_get_values(medium->rtx_payload_types);\n\t\t\t\twhile(tempP) {\n\t\t\t\t\tint ptype = GPOINTER_TO_INT(tempP->data);\n\t\t\t\t\tif(g_hash_table_lookup(medium->clock_rates, GINT_TO_POINTER(ptype)) &&\n\t\t\t\t\t\t\tg_list_find(rtx_ptypes, GINT_TO_POINTER(ptype))) {\n\t\t\t\t\t\t/* We have a payload type that is both a codec and rtx, get rid of it */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Removing duplicate payload type %d\\n\", handle->handle_id, ptype);\n\t\t\t\t\t\tjanus_sdp_remove_payload_type(remote_sdp, medium->mindex, ptype);\n\t\t\t\t\t\tg_hash_table_remove(pc->clock_rates, GINT_TO_POINTER(ptype));\n\t\t\t\t\t\tg_hash_table_remove(medium->clock_rates, GINT_TO_POINTER(ptype));\n\t\t\t\t\t}\n\t\t\t\t\ttempP = tempP->next;\n\t\t\t\t}\n\t\t\t\tg_list_free(ptypes);\n\t\t\t\tg_list_free(rtx_ptypes);\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\t/* Disable RFC4588 if the peer didn't negotiate it */\n\tif(!rtx) {\n\t\tjanus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX);\n\t\t/* Iterate on all media */\n\t\tuint mi=0;\n\t\tfor(mi=0; mi<g_hash_table_size(pc->media); mi++) {\n\t\t\tmedium = g_hash_table_lookup(pc->media, GUINT_TO_POINTER(mi));\n\t\t\tif(medium) {\n\t\t\t\tg_hash_table_remove(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_rtx));\n\t\t\t\tmedium->ssrc_rtx = 0;\n\t\t\t}\n\t\t}\n\t}\n\t/* Cleanup */\n\tg_free(ruser);\n\tg_free(rpass);\n\tg_free(rhashing);\n\tg_free(rfingerprint);\n\n\treturn 0;\t/* FIXME Handle errors better */\n}\n\n/* Parse local SDP */\nint janus_sdp_process_local(void *ice_handle, janus_sdp *remote_sdp, gboolean update) {\n\tif(!ice_handle || !remote_sdp)\n\t\treturn -1;\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice_handle;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(!pc)\n\t\treturn -1;\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\t/* We only go through m-lines to setup medium instances accordingly */\n\tGList *temp = remote_sdp->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t/* Find the internal medium instance */\n\t\tmedium = g_hash_table_lookup(pc->media, GINT_TO_POINTER(m->index));\n\t\tif(!medium) {\n\t\t\t/* We don't have it, create one now */\n\t\t\tif(m->type == JANUS_SDP_AUDIO)\n\t\t\t\tmedium = janus_ice_peerconnection_medium_create(handle, JANUS_MEDIA_AUDIO);\n\t\t\telse if(m->type == JANUS_SDP_VIDEO)\n\t\t\t\tmedium = janus_ice_peerconnection_medium_create(handle, JANUS_MEDIA_VIDEO);\n\t\t\telse if(m->type == JANUS_SDP_APPLICATION && strstr(m->proto, \"DTLS/SCTP\"))\n\t\t\t\tmedium = janus_ice_peerconnection_medium_create(handle, JANUS_MEDIA_DATA);\n\t\t\telse\n\t\t\t\tmedium = janus_ice_peerconnection_medium_create(handle, JANUS_MEDIA_UNKNOWN);\n\t\t}\n\t\t/* Check if the offer contributed a mid and/or msid */\n\t\tGList *tempA = m->attributes;\n\t\tgboolean have_msid = FALSE;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->name && a->value) {\n\t\t\t\tif(!strcasecmp(a->name, \"mid\")) {\n\t\t\t\t\t/* Found mid attribute */\n\t\t\t\t\tif(strnlen(a->value, 16 + 1) > 16) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] mid on m-line #%d too large: (%zu > 16)\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->index, strlen(a->value));\n\t\t\t\t\t\treturn -2;\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->mid != NULL && strcasecmp(medium->mid, a->value)) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] mid on m-line #%d changed (%s --> %s), ignoring new value\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->index, medium->mid, a->value);\n\t\t\t\t\t} else if(medium->mid == NULL) {\n\t\t\t\t\t\tmedium->mid = g_strdup(a->value);\n\t\t\t\t\t\tif(!g_hash_table_lookup(pc->media_bymid, medium->mid)) {\n\t\t\t\t\t\t\tg_hash_table_insert(pc->media_bymid, g_strdup(medium->mid), medium);\n\t\t\t\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif(handle->pc_mid == NULL)\n\t\t\t\t\t\thandle->pc_mid = g_strdup(a->value);\n\t\t\t\t\t/* Remove this mid attribute, the core will add it again later */\n\t\t\t\t\tGList *mid_attr = tempA;\n\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\tm->attributes = g_list_remove_link(m->attributes, mid_attr);\n\t\t\t\t\tg_list_free_full(mid_attr, (GDestroyNotify)janus_sdp_attribute_destroy);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(!strcasecmp(a->name, \"msid\")) {\n\t\t\t\t\t/* Found msid attribute */\n\t\t\t\t\thave_msid = TRUE;\n\t\t\t\t\tchar msid[65], mstid[65];\n\t\t\t\t\tmsid[0] = '\\0';\n\t\t\t\t\tmstid[0] = '\\0';\n\t\t\t\t\tif(sscanf(a->value, \"%64s %64s\", msid, mstid) != 2) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Invalid msid on m-line #%d\\n\",\n\t\t\t\t\t\t\thandle->handle_id, m->index);\n\t\t\t\t\t\treturn -2;\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->msid == NULL || strcasecmp(medium->msid, msid)) {\n\t\t\t\t\t\tchar *old_msid = medium->msid;\n\t\t\t\t\t\tmedium->msid = g_strdup(msid);\n\t\t\t\t\t\tg_free(old_msid);\n\t\t\t\t\t}\n\t\t\t\t\tif(medium->mstid == NULL || strcasecmp(medium->mstid, mstid)) {\n\t\t\t\t\t\tchar *old_mstid = medium->mstid;\n\t\t\t\t\t\tmedium->mstid = g_strdup(mstid);\n\t\t\t\t\t\tg_free(old_mstid);\n\t\t\t\t\t}\n\t\t\t\t\t/* Remove this msid attribute, the core will add it again later */\n\t\t\t\t\tGList *msid_attr = tempA;\n\t\t\t\t\ttempA = tempA->next;\n\t\t\t\t\tm->attributes = g_list_remove_link(m->attributes, msid_attr);\n\t\t\t\t\tg_list_free_full(msid_attr, (GDestroyNotify)janus_sdp_attribute_destroy);\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(!strcasecmp(a->name, \"rtpmap\")) {\n\t\t\t\t\tif(a->value && strstr(a->value, \"rtx\") == NULL) {\n\t\t\t\t\t\tint ptype = atoi(a->value);\n\t\t\t\t\t\tif(ptype > -1) {\n\t\t\t\t\t\t\tchar *cr = strchr(a->value, '/');\n\t\t\t\t\t\t\tif(cr != NULL) {\n\t\t\t\t\t\t\t\tcr++;\n\t\t\t\t\t\t\t\tuint32_t clock_rate = 0;\n\t\t\t\t\t\t\t\tif(janus_string_to_uint32(cr, &clock_rate) == 0) {\n\t\t\t\t\t\t\t\t\tif(medium->clock_rates == NULL)\n\t\t\t\t\t\t\t\t\t\tmedium->clock_rates = g_hash_table_new(NULL, NULL);\n\t\t\t\t\t\t\t\t\tg_hash_table_insert(medium->clock_rates, GINT_TO_POINTER(ptype), GUINT_TO_POINTER(clock_rate));\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\tif(medium->mid == NULL) {\n\t\t\t/* No mid provided, generate one now */\n\t\t\tchar mid[5];\n\t\t\tmemset(mid, 0, sizeof(mid));\n\t\t\tg_snprintf(mid, sizeof(mid), \"%d\", m->index);\n\t\t\tmedium->mid = g_strdup(mid);\n\t\t\tif(!g_hash_table_lookup(pc->media_bymid, medium->mid)) {\n\t\t\t\tg_hash_table_insert(pc->media_bymid, g_strdup(medium->mid), medium);\n\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t}\n\t\t}\n\t\tif(!have_msid) {\n\t\t\tg_free(medium->msid);\n\t\t\tmedium->msid = NULL;\n\t\t\tg_free(medium->mstid);\n\t\t\tmedium->mstid = NULL;\n\t\t}\n\t\tif(m->direction == JANUS_SDP_INACTIVE) {\n\t\t\t/* Reset the local SSRCs and RTCP context */\n\t\t\tif(medium->ssrc != 0)\n\t\t\t\tg_hash_table_remove(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc));\n\t\t\tmedium->ssrc = 0;\n\t\t\tif(medium->ssrc_rtx != 0)\n\t\t\t\tg_hash_table_remove(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_rtx));\n\t\t\tmedium->ssrc_rtx = 0;\n\t\t\tint vindex = 0;\n\t\t\tfor(vindex=0; vindex<3; vindex++) {\n\t\t\t\tif(medium->rtcp_ctx[vindex]) {\n\t\t\t\t\tint tb = medium->rtcp_ctx[vindex]->tb;\n\t\t\t\t\tmemset(medium->rtcp_ctx[vindex], 0, sizeof(janus_rtcp_context));\n\t\t\t\t\tmedium->rtcp_ctx[vindex]->tb = tb;\n\t\t\t\t\tmedium->rtcp_ctx[vindex]->in_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[vindex]->in_media_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[vindex]->out_link_quality = 100;\n\t\t\t\t\tmedium->rtcp_ctx[vindex]->out_media_link_quality = 100;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(m->type != JANUS_SDP_APPLICATION) {\n\t\t\tif(medium->ssrc == 0) {\n\t\t\t\tmedium->ssrc = janus_random_uint32();\t/* FIXME Should we look for conflicts? */\n\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)) {\n\t\t\t\t\t/* Create an SSRC for RFC4588 as well */\n\t\t\t\t\tmedium->ssrc_rtx = janus_random_uint32();\t/* FIXME Should we look for conflicts? */\n\t\t\t\t}\n\t\t\t\t/* Update the SSRC-indexed map */\n\t\t\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc), medium);\n\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\tif(medium->ssrc_rtx > 0) {\n\t\t\t\t\tg_hash_table_insert(pc->media_byssrc, GINT_TO_POINTER(medium->ssrc_rtx), medium);\n\t\t\t\t\tjanus_refcount_increase(&medium->ref);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\treturn 0;\t/* FIXME Handle errors better */\n}\n\ntypedef struct janus_sdp_mdns_candidate {\n\tjanus_ice_handle *handle;\n\tchar *candidate, *local;\n\tGCancellable *cancellable;\n} janus_sdp_mdns_candidate;\nstatic void janus_sdp_mdns_resolved(GObject *source_object, GAsyncResult *res, gpointer user_data) {\n\t/* This callback is invoked when the address is resolved */\n\tjanus_sdp_mdns_candidate *mc = (janus_sdp_mdns_candidate *)user_data;\n\tGResolver *resolver = g_resolver_get_default();\n\tGError *error = NULL;\n\tGList *list = g_resolver_lookup_by_name_finish(resolver, res, &error);\n\tif(mc == NULL) {\n\t\tg_resolver_free_addresses(list);\n\t\tg_object_unref(resolver);\n\t\treturn;\n\t}\n\tchar *resolved = NULL;\n\tif(error != NULL || list == NULL || list->data == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Error resolving mDNS address (%s): %s\\n\",\n\t\t\tmc->handle->handle_id, mc->local, error ? error->message : \"no results\");\n\t} else {\n\t\tresolved = g_inet_address_to_string((GInetAddress *)list->data);\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] mDNS address (%s) resolved: %s\\n\",\n\t\t\tmc->handle->handle_id, mc->local, resolved);\n\t}\n\tg_resolver_free_addresses(list);\n\tg_object_unref(resolver);\n\tif(resolved != NULL && mc->handle->pc && mc->handle->app_handle &&\n\t\t\t!g_atomic_int_get(&mc->handle->app_handle->stopped) &&\n\t\t\t!g_atomic_int_get(&mc->handle->destroyed)) {\n\t\t/* Replace the .local address with the resolved one in the candidate string */\n\t\tmc->candidate = janus_string_replace(mc->candidate, mc->local, resolved);\n\t\t/* Parse the candidate again */\n\t\tjanus_mutex_lock(&mc->handle->mutex);\n\t\t(void)janus_sdp_parse_candidate(mc->handle->pc, mc->candidate, 1);\n\t\tjanus_mutex_unlock(&mc->handle->mutex);\n\t}\n\tg_free(resolved);\n\t/* Get rid of the helper struct */\n\tjanus_refcount_decrease(&mc->handle->ref);\n\tg_free(mc->candidate);\n\tg_free(mc->local);\n\tg_free(mc);\n}\n\nint janus_sdp_parse_candidate(void *ice_pc, const char *candidate, int trickle) {\n\tif(ice_pc == NULL || candidate == NULL)\n\t\treturn -1;\n\tjanus_ice_peerconnection *pc = (janus_ice_peerconnection *)ice_pc;\n\tjanus_ice_handle *handle = pc->handle;\n\tif(handle == NULL)\n\t\treturn -2;\n\tif(strlen(candidate) == 0 || strstr(candidate, \"end-of-candidates\")) {\n\t\t/* FIXME Should we do something with this? */\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] end-of-candidates received\\n\", handle->handle_id);\n\t\treturn 0;\n\t}\n\tif(strstr(candidate, \"candidate:\") == candidate) {\n\t\t/* Skipping the 'candidate:' prefix Firefox puts in trickle candidates */\n\t\tcandidate += strlen(\"candidate:\");\n\t}\n\tchar rfoundation[33], rtransport[4], rip[50], rtype[6], rrelip[40];\n\tguint32 rcomponent, rpriority, rport, rrelport;\n\tint res = sscanf(candidate, \"%32s %30u %3s %30u %49s %30u typ %5s %*s %39s %*s %30u\",\n\t\trfoundation, &rcomponent, rtransport, &rpriority,\n\t\t\trip, &rport, rtype, rrelip, &rrelport);\n\tif(res < 7) {\n\t\t/* Failed to parse this address, can it be IPv6? */\n\t\tif(!janus_ice_is_ipv6_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Received IPv6 candidate, but IPv6 support is disabled...\\n\", handle->handle_id);\n\t\t\treturn res;\n\t\t}\n\t}\n\tif(res >= 7) {\n\t\tif(strstr(rip, \".local\")) {\n\t\t\t/* The IP is actually an mDNS address, try to resolve it\n\t\t\t * https://tools.ietf.org/html/draft-ietf-rtcweb-mdns-ice-candidates-04 */\n\t\t\tif(!janus_ice_is_mdns_enabled()) {\n\t\t\t\t/* ...unless mDNS resolution is disabled, in which case ignore this candidate */\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] mDNS candidate ignored\\n\", handle->handle_id);\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\t/* We'll resolve this address asynchronously, in order not to keep this thread busy */\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Resolving mDNS address (%s) asynchronously\\n\",\n\t\t\t\thandle->handle_id, rip);\n\t\t\tjanus_sdp_mdns_candidate *mc = g_malloc(sizeof(janus_sdp_mdns_candidate));\n\t\t\tjanus_refcount_increase(&handle->ref);\n\t\t\tmc->handle = handle;\n\t\t\tmc->candidate = g_strdup(candidate);\n\t\t\tmc->local = g_strdup(rip);\n\t\t\tmc->cancellable = NULL;\n\t\t\tGResolver *resolver = g_resolver_get_default();\n\t\t\tg_resolver_lookup_by_name_async(resolver, rip, NULL,\n\t\t\t\t(GAsyncReadyCallback)janus_sdp_mdns_resolved, mc);\n\t\t\tg_object_unref(resolver);\n\t\t\treturn 0;\n\t\t}\n\t\t/* Add remote candidate */\n\t\tif(rcomponent > 1) {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- Skipping component %d in stream %d (rtcp-muxing)\\n\", handle->handle_id, rcomponent, pc->stream_id);\n\t\t} else {\n\t\t\t//~ if(trickle) {\n\t\t\t\t//~ if(pc->dtls != NULL) {\n\t\t\t\t\t//~ /* This component is already ready, ignore this further candidate */\n\t\t\t\t\t//~ JANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]   -- Ignoring this candidate, the component is already ready\\n\", handle->handle_id);\n\t\t\t\t\t//~ return 0;\n\t\t\t\t//~ }\n\t\t\t//~ }\n\t\t\tpc->component_id = rcomponent;\n\t\t\tNiceCandidate *c = NULL;\n\t\t\tif(!strcasecmp(rtype, \"host\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  Adding remote candidate component:%d stream:%d type:host %s:%d\\n\",\n\t\t\t\t\thandle->handle_id, rcomponent, pc->stream_id, rip, rport);\n\t\t\t\t/* Unless this is libnice >= 0.1.8, we only support UDP... */\n\t\t\t\tif(!strcasecmp(rtransport, \"udp\")) {\n\t\t\t\t\tc = nice_candidate_new(NICE_CANDIDATE_TYPE_HOST);\n#ifdef HAVE_LIBNICE_TCP\n\t\t\t\t} else if(!strcasecmp(rtransport, \"tcp\") && janus_ice_is_ice_tcp_enabled()) {\n\t\t\t\t\tc = nice_candidate_new(NICE_CANDIDATE_TYPE_HOST);\n#endif\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]    Skipping unsupported transport '%s' for media\\n\", handle->handle_id, rtransport);\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(rtype, \"srflx\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  Adding remote candidate component:%d stream:%d type:srflx %s:%d --> %s:%d \\n\",\n\t\t\t\t\thandle->handle_id, rcomponent, pc->stream_id,  rrelip, rrelport, rip, rport);\n\t\t\t\t/* Unless this is libnice >= 0.1.8, we only support UDP... */\n\t\t\t\tif(!strcasecmp(rtransport, \"udp\")) {\n\t\t\t\t\tc = nice_candidate_new(NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE);\n#ifdef HAVE_LIBNICE_TCP\n\t\t\t\t} else if(!strcasecmp(rtransport, \"tcp\") && janus_ice_is_ice_tcp_enabled()) {\n\t\t\t\t\tc = nice_candidate_new(NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE);\n#endif\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]    Skipping unsupported transport '%s' for media\\n\", handle->handle_id, rtransport);\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(rtype, \"prflx\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  Adding remote candidate component:%d stream:%d type:prflx %s:%d --> %s:%d\\n\",\n\t\t\t\t\thandle->handle_id, rcomponent, pc->stream_id, rrelip, rrelport, rip, rport);\n\t\t\t\t/* Unless this is libnice >= 0.1.8, we only support UDP... */\n\t\t\t\tif(!strcasecmp(rtransport, \"udp\")) {\n\t\t\t\t\tc = nice_candidate_new(NICE_CANDIDATE_TYPE_PEER_REFLEXIVE);\n#ifdef HAVE_LIBNICE_TCP\n\t\t\t\t} else if(!strcasecmp(rtransport, \"tcp\") && janus_ice_is_ice_tcp_enabled()) {\n\t\t\t\t\tc = nice_candidate_new(NICE_CANDIDATE_TYPE_PEER_REFLEXIVE);\n#endif\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]    Skipping unsupported transport '%s' for media\\n\", handle->handle_id, rtransport);\n\t\t\t\t}\n\t\t\t} else if(!strcasecmp(rtype, \"relay\")) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  Adding remote candidate component:%d stream:%d type:relay %s:%d --> %s:%d\\n\",\n\t\t\t\t\thandle->handle_id, rcomponent, pc->stream_id, rrelip, rrelport, rip, rport);\n\t\t\t\t/* We only support UDP/TCP/TLS... */\n\t\t\t\tif(strcasecmp(rtransport, \"udp\") && strcasecmp(rtransport, \"tcp\") && strcasecmp(rtransport, \"tls\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]    Skipping unsupported transport '%s' for media\\n\", handle->handle_id, rtransport);\n\t\t\t\t} else {\n\t\t\t\t\tc = nice_candidate_new(NICE_CANDIDATE_TYPE_RELAYED);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t/* FIXME What now? */\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"]  Unknown remote candidate type:%s for component:%d stream:%d!\\n\",\n\t\t\t\t\thandle->handle_id, rtype, rcomponent, pc->stream_id);\n\t\t\t}\n\t\t\tif(c != NULL) {\n\t\t\t\tc->component_id = rcomponent;\n\t\t\t\tc->stream_id = pc->stream_id;\n#ifndef HAVE_LIBNICE_TCP\n\t\t\t\tc->transport = NICE_CANDIDATE_TRANSPORT_UDP;\n#else\n\t\t\t\tif(!strcasecmp(rtransport, \"udp\")) {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  Transport: UDP\\n\", handle->handle_id);\n\t\t\t\t\tc->transport = NICE_CANDIDATE_TRANSPORT_UDP;\n\t\t\t\t} else {\n\t\t\t\t\t/* Check the type (https://tools.ietf.org/html/rfc6544#section-4.5) */\n\t\t\t\t\tconst char *type = NULL;\n\t\t\t\t\tint ctype = 0;\n\t\t\t\t\tif(strstr(candidate, \"tcptype active\")) {\n\t\t\t\t\t\ttype = \"active\";\n\t\t\t\t\t\tctype = NICE_CANDIDATE_TRANSPORT_TCP_ACTIVE;\n\t\t\t\t\t} else if(strstr(candidate, \"tcptype passive\")) {\n\t\t\t\t\t\ttype = \"passive\";\n\t\t\t\t\t\tctype = NICE_CANDIDATE_TRANSPORT_TCP_PASSIVE;\n\t\t\t\t\t} else if(strstr(candidate, \"tcptype so\")) {\n\t\t\t\t\t\ttype = \"so\";\n\t\t\t\t\t\tctype = NICE_CANDIDATE_TRANSPORT_TCP_SO;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* TODO: We should actually stop here... */\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Missing tcptype info for the TCP candidate!\\n\", handle->handle_id);\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"]  Transport: TCP (%s)\\n\", handle->handle_id, type);\n\t\t\t\t\tc->transport = ctype;\n\t\t\t\t}\n#endif\n\t\t\t\tg_strlcpy(c->foundation, rfoundation, NICE_CANDIDATE_MAX_FOUNDATION);\n\t\t\t\tc->priority = rpriority;\n\t\t\t\tgboolean added = nice_address_set_from_string(&c->addr, rip);\n\t\t\t\tif(!added) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]    Invalid address '%s', skipping %s candidate (%s)\\n\",\n\t\t\t\t\t\thandle->handle_id, rip, rtype, candidate);\n\t\t\t\t\tnice_candidate_free(c);\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\tnice_address_set_port(&c->addr, rport);\n\t\t\t\tif(c->type == NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE || c->type == NICE_CANDIDATE_TYPE_PEER_REFLEXIVE) {\n\t\t\t\t\tadded = nice_address_set_from_string(&c->base_addr, rrelip);\n\t\t\t\t\tif(added)\n\t\t\t\t\t\tnice_address_set_port(&c->base_addr, rrelport);\n\t\t\t\t} else if(c->type == NICE_CANDIDATE_TYPE_RELAYED) {\n\t\t\t\t\t/* FIXME Do we really need the base address for TURN? */\n\t\t\t\t\tadded = nice_address_set_from_string(&c->base_addr, rrelip);\n\t\t\t\t\tif(added)\n\t\t\t\t\t\tnice_address_set_port(&c->base_addr, rrelport);\n\t\t\t\t}\n\t\t\t\tif(!added) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"]    Invalid base address '%s', skipping %s candidate (%s)\\n\",\n\t\t\t\t\t\thandle->handle_id, rrelip, rtype, candidate);\n\t\t\t\t\tnice_candidate_free(c);\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\tpc->candidates = g_slist_append(pc->candidates, c);\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"]    Candidate added to the list! (%u elements for %d/%d)\\n\", handle->handle_id,\n\t\t\t\t\tg_slist_length(pc->candidates), pc->stream_id, pc->component_id);\n\t\t\t\t/* Save for the summary, in case we need it */\n\t\t\t\tpc->remote_candidates = g_slist_append(pc->remote_candidates, g_strdup(candidate));\n\t\t\t\t/* Notify event handlers */\n\t\t\t\tif(janus_events_is_enabled()) {\n\t\t\t\t\tjanus_session *session = (janus_session *)handle->session;\n\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\tjson_object_set_new(info, \"remote-candidate\", json_string(candidate));\n\t\t\t\t\tjson_object_set_new(info, \"stream_id\", json_integer(pc->stream_id));\n\t\t\t\t\tjson_object_set_new(info, \"component_id\", json_integer(pc->component_id));\n\t\t\t\t\tjanus_events_notify_handlers(JANUS_EVENT_TYPE_WEBRTC, JANUS_EVENT_SUBTYPE_WEBRTC_RCAND,\n\t\t\t\t\t\tsession->session_id, handle->handle_id, handle->opaque_id, info);\n\t\t\t\t}\n\t\t\t\t/* See if we need to process this */\n\t\t\t\tif(trickle) {\n\t\t\t\t\tif(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_START)) {\n\t\t\t\t\t\t/* This is a trickle candidate and ICE has started, we should process it right away */\n\t\t\t\t\t\tif(!pc->process_started) {\n\t\t\t\t\t\t\t/* Actually, ICE has JUST started for this component, take care of the candidates we've added so far */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] ICE already started for this component, setting candidates we have up to now\\n\", handle->handle_id);\n\t\t\t\t\t\t\tjanus_ice_setup_remote_candidates(handle, pc->stream_id, pc->component_id);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Queue the candidate, we'll process it in the loop */\n\t\t\t\t\t\t\tjanus_ice_add_remote_candidate(handle, c);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* ICE hasn't started yet: to make sure we're not stuck, also check if we stopped processing the SDP */\n\t\t\t\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PROCESSING_OFFER)) {\n\t\t\t\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_START);\n\t\t\t\t\t\t\t/* This is a trickle candidate and ICE has started, we should process it right away */\n\t\t\t\t\t\t\tif(!pc->process_started) {\n\t\t\t\t\t\t\t\t/* Actually, ICE has JUST started for this component, take care of the candidates we've added so far */\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] SDP processed but ICE not started yet for this component, setting candidates we have up to now\\n\", handle->handle_id);\n\t\t\t\t\t\t\t\tjanus_ice_setup_remote_candidates(handle, pc->stream_id, pc->component_id);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t/* Queue the candidate, we'll process it in the loop */\n\t\t\t\t\t\t\t\tjanus_ice_add_remote_candidate(handle, c);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t/* Still processing the offer/answer: queue the trickle candidate for now, we'll process it later */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Queueing trickle candidate, status is not START yet\\n\", handle->handle_id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"[%\"SCNu64\"] Failed to parse candidate (res=%d)...\\n\", handle->handle_id, res);\n\t\treturn res;\n\t}\n\treturn 0;\n}\n\nint janus_sdp_parse_ssrc_group(void *m, const char *group_attr, int video) {\n\tif(m == NULL || group_attr == NULL)\n\t\treturn -1;\n\tjanus_ice_peerconnection_medium *medium = (janus_ice_peerconnection_medium *)m;\n\tjanus_ice_peerconnection *pc = medium->pc;\n\tjanus_ice_handle *handle = pc->handle;\n\tif(handle == NULL)\n\t\treturn -2;\n\tif(!video)\t/* We only do rtx for video, return */\n\t\treturn 0;\n\tif(medium->rid[0] != NULL) {\n\t\t/* Simulcasting is rid-based, don't parse SSRCs for now */\n\t\treturn 0;\n\t}\n\tgboolean fid = strstr(group_attr, \"FID\") != NULL;\n\tgboolean sim = strstr(group_attr, \"SIM\") != NULL;\n\tguint64 ssrc = 0;\n\tguint32 first_ssrc = 0;\n\tgchar **list = g_strsplit(group_attr, \" \", -1);\n\tgchar *index = list[0];\n\tif(index != NULL) {\n\t\tint i=0;\n\t\twhile(index != NULL) {\n\t\t\tif(i > 0 && strlen(index) > 0) {\n\t\t\t\tssrc = g_ascii_strtoull(index, NULL, 0);\n\t\t\t\tswitch(i) {\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\tfirst_ssrc = ssrc;\n\t\t\t\t\t\tif(medium->ssrc_peer_new[0] == ssrc || medium->ssrc_peer_new[1] == ssrc\n\t\t\t\t\t\t\t\t|| medium->ssrc_peer_new[2] == ssrc) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%\"SCNu64\"] Already parsed this SSRC: %\"SCNu64\" (%s group)\\n\",\n\t\t\t\t\t\t\t\thandle->handle_id, ssrc, (fid ? \"FID\" : (sim ? \"SIM\" : \"??\")));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif(medium->ssrc_peer_new[0] == 0) {\n\t\t\t\t\t\t\t\tmedium->ssrc_peer_new[0] = ssrc;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC: %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_new[0]);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t/* We already have a video SSRC: check if rid is involved, and we'll keep track of this for simulcasting */\n\t\t\t\t\t\t\t\tif(medium->rid[0]) {\n\t\t\t\t\t\t\t\t\tif(medium->ssrc_peer_new[1] == 0) {\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer_new[1] = ssrc;\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC (sim-1): %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_new[1]);\n\t\t\t\t\t\t\t\t\t} else if(medium->ssrc_peer_new[2] == 0) {\n\t\t\t\t\t\t\t\t\t\tmedium->ssrc_peer_new[2] = ssrc;\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC (sim-2): %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_new[2]);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Don't know what to do with video SSRC: %\"SCNu64\"\\n\", handle->handle_id, ssrc);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 2:\n\t\t\t\t\t\tif(fid) {\n\t\t\t\t\t\t\tif(medium->ssrc_peer_new[0] == first_ssrc && medium->ssrc_peer_rtx_new[0] == 0) {\n\t\t\t\t\t\t\t\tmedium->ssrc_peer_rtx_new[0] = ssrc;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC (rtx): %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_rtx_new[0]);\n\t\t\t\t\t\t\t} else if(medium->ssrc_peer_new[1] == first_ssrc && medium->ssrc_peer_rtx_new[1] == 0) {\n\t\t\t\t\t\t\t\tmedium->ssrc_peer_rtx_new[1] = ssrc;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC (sim-1 rtx): %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_rtx_new[1]);\n\t\t\t\t\t\t\t} else if(medium->ssrc_peer_new[2] == first_ssrc && medium->ssrc_peer_rtx_new[2] == 0) {\n\t\t\t\t\t\t\t\tmedium->ssrc_peer_rtx_new[2] = ssrc;\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC (sim-2 rtx): %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_rtx_new[2]);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Don't know what to do with rtx SSRC: %\"SCNu64\"\\n\", handle->handle_id, ssrc);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if(sim) {\n\t\t\t\t\t\t\tmedium->ssrc_peer_new[1] = ssrc;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC (sim-1): %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_new[1]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Don't know what to do with SSRC: %\"SCNu64\"\\n\", handle->handle_id, ssrc);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 3:\n\t\t\t\t\t\tif(fid) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Found one too many retransmission SSRC (rtx): %\"SCNu64\"\\n\", handle->handle_id, ssrc);\n\t\t\t\t\t\t} else if(sim) {\n\t\t\t\t\t\t\tmedium->ssrc_peer_new[2] = ssrc;\n\t\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer video SSRC (sim-2): %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_new[2]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Don't know what to do with SSRC: %\"SCNu64\"\\n\", handle->handle_id, ssrc);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Don't know what to do with video SSRC: %\"SCNu64\"\\n\", handle->handle_id, ssrc);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\ti++;\n\t\t\tindex = list[i];\n\t\t}\n\t}\n\tg_clear_pointer(&list, g_strfreev);\n\treturn 0;\n}\n\nint janus_sdp_parse_ssrc(void *m, const char *ssrc_attr, int video) {\n\tif(m == NULL || ssrc_attr == NULL)\n\t\treturn -1;\n\tjanus_ice_peerconnection_medium *medium = (janus_ice_peerconnection_medium *)m;\n\tjanus_ice_peerconnection *pc = medium->pc;\n\tjanus_ice_handle *handle = pc->handle;\n\tif(handle == NULL)\n\t\treturn -2;\n\tguint64 ssrc = g_ascii_strtoull(ssrc_attr, NULL, 0);\n\tif(ssrc == 0 || ssrc > G_MAXUINT32)\n\t\treturn -3;\n\tif(medium->rid[0] != NULL) {\n\t\t/* Simulcasting is rid-based, only keep track of a single SSRC for fallback */\n\t\tif(medium->ssrc_peer_temp == 0) {\n\t\t\tmedium->ssrc_peer_temp = ssrc;\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Peer video fallback SSRC: %\"SCNu32\"\\n\", handle->handle_id, medium->ssrc_peer_temp);\n\t\t}\n\t\treturn 0;\n\t}\n\tif(medium->ssrc_peer_new[0] == 0) {\n\t\tmedium->ssrc_peer_new[0] = ssrc;\n\t\tJANUS_LOG(LOG_VERB, \"[%\"SCNu64\"] Peer %s SSRC: %\"SCNu32\"\\n\",\n\t\t\thandle->handle_id, video ? \"video\" : \"audio\", medium->ssrc_peer_new[0]);\n\t}\n\treturn 0;\n}\n\nint janus_sdp_anonymize(janus_sdp *anon) {\n\tif(anon == NULL)\n\t\treturn -1;\n\tint data = 0;\n\t\t/* o= */\n\tif(anon->o_addr != NULL) {\n\t\tg_free(anon->o_addr);\n\t\tanon->o_ipv4 = TRUE;\n\t\tanon->o_addr = g_strdup(\"1.1.1.1\");\n\t}\n\t\t/* a= */\n\tGList *temp = anon->attributes;\n\twhile(temp) {\n\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)temp->data;\n\t\t/* These are attributes we handle ourselves, the plugins don't need them */\n\t\tif(!strcasecmp(a->name, \"ice-ufrag\")\n\t\t\t\t|| !strcasecmp(a->name, \"ice-pwd\")\n\t\t\t\t|| !strcasecmp(a->name, \"ice-options\")\n\t\t\t\t|| !strcasecmp(a->name, \"fingerprint\")\n\t\t\t\t|| !strcasecmp(a->name, \"group\")\n\t\t\t\t|| !strcasecmp(a->name, \"msid-semantic\")\n\t\t\t\t|| !strcasecmp(a->name, \"extmap-allow-mixed\")\n\t\t\t\t|| !strcasecmp(a->name, \"rtcp-rsize\")) {\n\t\t\tanon->attributes = g_list_remove(anon->attributes, a);\n\t\t\ttemp = anon->attributes;\n\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\tcontinue;\n\t\t}\n\t\ttemp = temp->next;\n\t\tcontinue;\n\t}\n\t\t/* m= */\n\ttemp = anon->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tif(m->type == JANUS_SDP_AUDIO && m->port > 0) {\n\t\t\tm->port = 9;\n\t\t} else if(m->type == JANUS_SDP_VIDEO && m->port > 0) {\n\t\t\tm->port = 9;\n\t\t} else if(m->type == JANUS_SDP_APPLICATION && m->port > 0) {\n\t\t\tif(m->proto != NULL && (!strcasecmp(m->proto, \"DTLS/SCTP\") || !strcasecmp(m->proto, \"UDP/DTLS/SCTP\"))) {\n\t\t\t\tdata++;\n\t\t\t\tm->port = data == 1 ? 9 : 0;\n\t\t\t} else {\n\t\t\t\tm->port = 0;\n\t\t\t}\n\t\t} else {\n\t\t\tm->port = 0;\n\t\t}\n\t\t\t/* c= */\n\t\tif(m->c_addr != NULL) {\n\t\t\tg_free(m->c_addr);\n\t\t\tm->c_ipv4 = TRUE;\n\t\t\tm->c_addr = g_strdup(\"1.1.1.1\");\n\t\t}\n\t\t\t/* a= */\n\t\tGList *tempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(!a->name) {\n\t\t\t\ttempA = tempA->next;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t/* These are attributes we handle ourselves, the plugins don't need them */\n\t\t\tif(!strcasecmp(a->name, \"ice-ufrag\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"ice-pwd\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"ice-options\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"crypto\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"fingerprint\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"setup\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"connection\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"group\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"msid-semantic\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"rid\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"simulcast\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"rtcp\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"rtcp-mux\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"rtcp-rsize\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"candidate\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"end-of-candidates\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"ssrc\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"ssrc-group\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"sctpmap\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"sctp-port\")\n\t\t\t\t\t|| !strcasecmp(a->name, \"max-message-size\")) {\n\t\t\t\tm->attributes = g_list_remove(m->attributes, a);\n\t\t\t\ttempA = m->attributes;\n\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* We don't support encrypted RTP extensions yet, so get rid of them */\n\t\ttempA = m->attributes;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->value && strstr(a->value, JANUS_RTP_EXTMAP_ENCRYPTED)) {\n\t\t\t\tm->attributes = g_list_remove(m->attributes, a);\n\t\t\t\ttempA = m->attributes;\n\t\t\t\tjanus_sdp_attribute_destroy(a);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\t/* Also remove attributes/formats we know we don't support (or don't want to support) now */\n\t\ttempA = m->attributes;\n\t\tGList *purged_ptypes = NULL;\n\t\twhile(tempA) {\n\t\t\tjanus_sdp_attribute *a = (janus_sdp_attribute *)tempA->data;\n\t\t\tif(a->value && (strstr(a->value, \"red/90000\") || strstr(a->value, \"ulpfec/90000\") ||\n\t\t\t\t\tstrstr(a->value, \"flexfec-03/90000\") || strstr(a->value, \"rtx/90000\"))) {\n\t\t\t\tint ptype = atoi(a->value);\n\t\t\t\tif(ptype < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid payload type (%d)\\n\", ptype);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Will remove payload type %d (%s)\\n\", ptype, a->value);\n\t\t\t\t\tpurged_ptypes = g_list_append(purged_ptypes, GINT_TO_POINTER(ptype));\n\t\t\t\t}\n\t\t\t}\n\t\t\ttempA = tempA->next;\n\t\t}\n\t\tif(purged_ptypes) {\n\t\t\ttempA = purged_ptypes;\n\t\t\twhile(tempA) {\n\t\t\t\tint ptype = GPOINTER_TO_INT(tempA->data);\n\t\t\t\tjanus_sdp_remove_payload_type(anon, m->index, ptype);\n\t\t\t\ttempA = tempA->next;\n\t\t\t}\n\t\t\tg_list_free(purged_ptypes);\n\t\t\tpurged_ptypes = NULL;\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\n\tJANUS_LOG(LOG_VERB, \" -------------------------------------------\\n\");\n\tJANUS_LOG(LOG_VERB, \"  >> Anonymized\\n\");\n\tJANUS_LOG(LOG_VERB, \" -------------------------------------------\\n\");\n\n\treturn 0;\n}\n\nchar *janus_sdp_merge(void *ice_handle, janus_sdp *anon, gboolean offer) {\n\tif(ice_handle == NULL || anon == NULL)\n\t\treturn NULL;\n\tjanus_ice_handle *handle = (janus_ice_handle *)ice_handle;\n\tjanus_ice_peerconnection *pc = handle->pc;\n\tif(pc == NULL)\n\t\treturn NULL;\n\tjanus_ice_peerconnection_medium *medium = NULL;\n\tchar *rtp_profile = handle->rtp_profile ? handle->rtp_profile : (char *)\"UDP/TLS/RTP/SAVPF\";\n\tif(!janus_is_webrtc_encryption_enabled())\n\t\trtp_profile = (char *)\"RTP/AVPF\";\n\tgboolean ipv4 = !strstr(janus_get_public_ip(0), \":\");\n\t/* Origin o= */\n\tgint64 sessid = janus_get_real_time();\n\tif(anon->o_name == NULL)\n\t\tanon->o_name = g_strdup(\"-\");\n\tif(anon->o_sessid == 0 || anon->o_version == 0) {\n\t\tanon->o_sessid = sessid;\n\t\tanon->o_version = 1;\n\t}\n\tanon->o_ipv4 = ipv4;\n\tg_free(anon->o_addr);\n\tanon->o_addr = g_strdup(janus_get_public_ip(0));\n\t/* Session name s= */\n\tif(anon->s_name == NULL)\n\t\tanon->s_name = g_strdup(\"Meetecho Janus\");\n\t/* Chrome doesn't like global c= lines, remove it */\n\tg_free(anon->c_addr);\n\tanon->c_addr = NULL;\n\t/* bundle: add new global attribute */\n\tchar buffer[8192], buffer_part[2048];\n\tbuffer[0] = '\\0';\n\tbuffer_part[0] = '\\0';\n\tg_snprintf(buffer, sizeof(buffer), \"BUNDLE\");\n\t/* Iterate on available media */\n#ifdef HAVE_SCTP\n\tint data = 0;\n#endif\n\tGList *temp = anon->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\t/* Find the internal medium instance */\n\t\tmedium = g_hash_table_lookup(pc->media, GINT_TO_POINTER(m->index));\n\t\tif(medium && m->port > 0) {\n\t\t\tg_snprintf(buffer_part, sizeof(buffer_part), \" %s\", medium->mid);\n\t\t\tjanus_strlcat(buffer, buffer_part, sizeof(buffer));\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\t/* Global attributes: start with group */\n\tGList *first = anon->attributes;\n\tjanus_sdp_attribute *a = janus_sdp_attribute_create(\"group\", \"%s\", buffer);\n\tanon->attributes = g_list_insert_before(anon->attributes, first, a);\n\t/* Advertise trickle support */\n\ta = janus_sdp_attribute_create(\"ice-options\", \"trickle\");\n\tanon->attributes = g_list_insert_before(anon->attributes, first, a);\n\tif(janus_is_webrtc_encryption_enabled()) {\n\t\t/* We put the fingerprint in the global attributes */\n\t\ta = janus_sdp_attribute_create(\"fingerprint\", \"sha-256 %s\", janus_dtls_get_local_fingerprint());\n\t\tanon->attributes = g_list_insert_before(anon->attributes, first, a);\n\t}\n\t/* Notify we support 1-byte and 2-byte extensions\n\t * FIXME We should actually negotiate this, in the future */\n\ta = janus_sdp_attribute_create(\"extmap-allow-mixed\", NULL);\n\tanon->attributes = g_list_insert_before(anon->attributes, first, a);\n\t/* msid-semantic: add new global attribute */\n\ta = janus_sdp_attribute_create(\"msid-semantic\", \" WMS *\");\n\tanon->attributes = g_list_insert_before(anon->attributes, first, a);\n\t/* ICE Full or Lite? */\n\tif(janus_ice_is_ice_lite_enabled()) {\n\t\t/* Janus is acting in ICE Lite mode, advertise this */\n\t\ta = janus_sdp_attribute_create(\"ice-lite\", NULL);\n\t\tanon->attributes = g_list_insert_before(anon->attributes, first, a);\n\t}\n\t/* Media lines now */\n#ifdef HAVE_SCTP\n\tdata = 0;\n#endif\n\tgboolean media_stopped = FALSE;\n\ttemp = anon->m_lines;\n\twhile(temp) {\n\t\tjanus_sdp_mline *m = (janus_sdp_mline *)temp->data;\n\t\tfirst = m->attributes;\n\t\t/* Find the internal medium instance */\n\t\tmedium = g_hash_table_lookup(pc->media, GINT_TO_POINTER(m->index));\n\t\tif(!medium) {\n\t\t\t/* TODO We don't have it, which should never happen! */\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] No medium? Expect trouble!\\n\", handle->handle_id);\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\t/* Overwrite RTP profile for audio and video */\n\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\tg_free(m->proto);\n\t\t\tm->proto = g_strdup(rtp_profile);\n\t\t}\n\t\t/* Media connection c= */\n\t\tg_free(m->c_addr);\n\t\tm->c_ipv4 = ipv4;\n\t\tm->c_addr = g_strdup(janus_get_public_ip(0));\n\t\t/* a=mid */\n\t\tif(medium->mid) {\n\t\t\ta = janus_sdp_attribute_create(\"mid\", \"%s\", medium->mid);\n\t\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\t}\n\t\t/* Check if we need to refuse the media or not */\n\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\t/* Audio/Video */\n\t\t\tif(m->port == 0) {\n\t\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\t\tmedium->ssrc = 0;\n\t\t\t}\n\t\t\tgboolean receiving = (medium->recv == TRUE);\n\t\t\tswitch(m->direction) {\n\t\t\t\tcase JANUS_SDP_INACTIVE:\n\t\t\t\t\tmedium->send = FALSE;\n\t\t\t\t\tmedium->recv = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\tcase JANUS_SDP_SENDONLY:\n\t\t\t\t\tmedium->send = TRUE;\n\t\t\t\t\tmedium->recv = FALSE;\n\t\t\t\t\tbreak;\n\t\t\t\tcase JANUS_SDP_RECVONLY:\n\t\t\t\t\tmedium->send = FALSE;\n\t\t\t\t\tmedium->recv = TRUE;\n\t\t\t\t\tbreak;\n\t\t\t\tcase JANUS_SDP_SENDRECV:\n\t\t\t\tcase JANUS_SDP_DEFAULT:\n\t\t\t\tdefault:\n\t\t\t\t\tmedium->send = TRUE;\n\t\t\t\t\tmedium->recv = TRUE;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(receiving != medium->recv)\n\t\t\t\tmedia_stopped = TRUE;\n\t\t\tif(medium->do_nacks && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX)) {\n\t\t\t\t/* Add RFC4588 stuff */\n\t\t\t\tif(medium->rtx_payload_types && g_hash_table_size(medium->rtx_payload_types) > 0) {\n\t\t\t\t\tjanus_sdp_attribute *a = NULL;\n\t\t\t\t\tGList *ptypes = g_list_copy(m->ptypes), *tempP = ptypes;\n\t\t\t\t\twhile(tempP) {\n\t\t\t\t\t\tint ptype = GPOINTER_TO_INT(tempP->data);\n\t\t\t\t\t\tint rtx_ptype = GPOINTER_TO_INT(g_hash_table_lookup(medium->rtx_payload_types, GINT_TO_POINTER(ptype)));\n\t\t\t\t\t\tif(rtx_ptype > 0) {\n\t\t\t\t\t\t\tm->ptypes = g_list_append(m->ptypes, GINT_TO_POINTER(rtx_ptype));\n\t\t\t\t\t\t\ta = janus_sdp_attribute_create(\"rtpmap\", \"%d rtx/90000\", rtx_ptype);\n\t\t\t\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t\t\t\t\ta = janus_sdp_attribute_create(\"fmtp\", \"%d apt=%d\", rtx_ptype, ptype);\n\t\t\t\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttempP = tempP->next;\n\t\t\t\t\t}\n\t\t\t\t\tg_list_free(ptypes);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if(m->type == JANUS_SDP_APPLICATION) {\n#ifdef HAVE_SCTP\n\t\t\t/* Is this SCTP for DataChannels? */\n\t\t\tif(m->port > 0 && (!strcasecmp(m->proto, \"DTLS/SCTP\") || !strcasecmp(m->proto, \"UDP/DTLS/SCTP\"))) {\n\t\t\t\t/* Yep */\n\t\t\t\tdata++;\n\t\t\t\tif(data > 1) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping SCTP line (we have one already)\\n\", handle->handle_id);\n\t\t\t\t\tm->port = 0;\n\t\t\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\t\t\ttemp = temp->next;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping unsupported application media line...\\n\", handle->handle_id);\n\t\t\t\tm->port = 0;\n\t\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\t\ttemp = temp->next;\n\t\t\t\tcontinue;\n\t\t\t}\n#else\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping unsupported application media line...\\n\", handle->handle_id);\n\t\t\tm->port = 0;\n\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n#endif\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"[%\"SCNu64\"] Skipping disabled/unsupported media line...\\n\", handle->handle_id);\n\t\t\tm->port = 0;\n\t\t\tm->direction = JANUS_SDP_INACTIVE;\n\t\t\ttemp = temp->next;\n\t\t\tcontinue;\n\t\t}\n\t\tif(m->type == JANUS_SDP_APPLICATION) {\n\t\t\tif(!strcasecmp(m->proto, \"UDP/DTLS/SCTP\"))\n\t\t\t\tjanus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP);\n\t\t\tif(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP)) {\n\t\t\t\ta = janus_sdp_attribute_create(\"sctpmap\", \"5000 webrtc-datachannel 16\");\n\t\t\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\t\t} else {\n\t\t\t\ta = janus_sdp_attribute_create(\"sctp-port\", \"5000\");\n\t\t\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\t\t}\n\t\t} else if(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\ta = janus_sdp_attribute_create(\"rtcp-mux\", NULL);\n\t\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\t}\n\t\t/* ICE ufrag and pwd, DTLS fingerprint setup and connection a= */\n\t\tgchar *ufrag = NULL;\n\t\tgchar *password = NULL;\n\t\tnice_agent_get_local_credentials(handle->agent, pc->stream_id, &ufrag, &password);\n\t\ta = janus_sdp_attribute_create(\"ice-ufrag\", \"%s\", ufrag);\n\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\ta = janus_sdp_attribute_create(\"ice-pwd\", \"%s\", password);\n\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\tg_free(ufrag);\n\t\tg_free(password);\n\t\ta = janus_sdp_attribute_create(\"ice-options\", \"trickle\");\n\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\tif(janus_is_webrtc_encryption_enabled()) {\n\t\t\ta = janus_sdp_attribute_create(\"setup\", \"%s\", janus_get_dtls_srtp_role(offer ? JANUS_DTLS_ROLE_ACTPASS : pc->dtls_role));\n\t\t\tm->attributes = g_list_insert_before(m->attributes, first, a);\n\t\t}\n\t\t/* Add last attributes, rtcp and ssrc (msid) */\n\t\tif(medium->ssrc_rtx > 0 && m->type == JANUS_SDP_VIDEO && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX) &&\n\t\t\t\t(m->direction == JANUS_SDP_DEFAULT || m->direction == JANUS_SDP_SENDRECV || m->direction == JANUS_SDP_SENDONLY)) {\n\t\t\t/* Add FID group to negotiate the RFC4588 stuff */\n\t\t\ta = janus_sdp_attribute_create(\"ssrc-group\", \"FID %\"SCNu32\" %\"SCNu32, medium->ssrc, medium->ssrc_rtx);\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t}\n\t\tif(m->type == JANUS_SDP_AUDIO || m->type == JANUS_SDP_VIDEO) {\n\t\t\tif(m->direction != JANUS_SDP_INACTIVE) {\n\t\t\t\tif(medium->msid && medium->mstid) {\n\t\t\t\t\ta = janus_sdp_attribute_create(\"msid\", \"%s %s\", medium->msid, medium->mstid);\n\t\t\t\t} else {\n\t\t\t\t\ta = janus_sdp_attribute_create(\"msid\", \"janus janus%s\", medium->mid);\n\t\t\t\t}\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t}\n\t\t\tif(medium->ssrc > 0) {\n\t\t\t\ta = janus_sdp_attribute_create(\"ssrc\", \"%\"SCNu32\" cname:janus\", medium->ssrc);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t\tif(medium->ssrc_rtx > 0 && m->type == JANUS_SDP_VIDEO &&\n\t\t\t\t\t\tjanus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RFC4588_RTX) &&\n\t\t\t\t\t\t(m->direction == JANUS_SDP_DEFAULT || m->direction == JANUS_SDP_SENDRECV || m->direction == JANUS_SDP_SENDONLY)) {\n\t\t\t\t\t/* Add rtx SSRC group to negotiate the RFC4588 stuff */\n\t\t\t\t\ta = janus_sdp_attribute_create(\"ssrc\", \"%\"SCNu32\" cname:janus\", medium->ssrc_rtx);\n\t\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* FIXME If the peer is Firefox and is negotiating simulcasting, add the rid attributes */\n\t\tif(m->type == JANUS_SDP_VIDEO && medium->rid[0] != NULL) {\n\t\t\tchar rids[50];\n\t\t\trids[0] = '\\0';\n\t\t\tint i=0, index=0;\n\t\t\tfor(i=2; i>=0; i--) {\n\t\t\t\tindex = (medium->rids_hml ? i : (2-i));\n\t\t\t\tif(medium->rid[index] == NULL)\n\t\t\t\t\tcontinue;\n\t\t\t\ta = janus_sdp_attribute_create(\"rid\", \"%s recv\", medium->rid[index]);\n\t\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t\t\tif(strlen(rids) == 0) {\n\t\t\t\t\tif(medium->disabled_rid[index])\n\t\t\t\t\t\tjanus_strlcat(rids, \"~\", sizeof(rids));\n\t\t\t\t\tjanus_strlcat(rids, medium->rid[index], sizeof(rids));\n\t\t\t\t} else {\n\t\t\t\t\tjanus_strlcat(rids, \";\", sizeof(rids));\n\t\t\t\t\tif(medium->disabled_rid[index])\n\t\t\t\t\t\tjanus_strlcat(rids, \"~\", sizeof(rids));\n\t\t\t\t\tjanus_strlcat(rids, medium->rid[index], sizeof(rids));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(medium->legacy_rid) {\n\t\t\t\ta = janus_sdp_attribute_create(\"simulcast\", \" recv rid=%s\", rids);\n\t\t\t} else {\n\t\t\t\ta = janus_sdp_attribute_create(\"simulcast\", \"recv %s\", rids);\n\t\t\t}\n\t\t\tm->attributes = g_list_append(m->attributes, a);\n\t\t}\n\t\tif(!janus_ice_is_full_trickle_enabled()) {\n\t\t\t/* And now the candidates (but only if we're half-trickling) */\n\t\t\tjanus_ice_candidates_to_sdp(handle, m, pc->stream_id, 1);\n\t\t\t/* Since we're half-trickling, we need to notify the peer that these are all the\n\t\t\t * candidates we have for this media stream, via an end-of-candidates attribute:\n\t\t\t * https://tools.ietf.org/html/draft-ietf-mmusic-trickle-ice-02#section-4.1 */\n\t\t\tjanus_sdp_attribute *end = janus_sdp_attribute_create(\"end-of-candidates\", NULL);\n\t\t\tm->attributes = g_list_append(m->attributes, end);\n\t\t}\n\t\t/* Next */\n\t\ttemp = temp->next;\n\t}\n\tif(media_stopped)\n\t\tjanus_ice_notify_media_stopped(handle);\n\n\tchar *sdp = janus_sdp_write(anon);\n\n\tJANUS_LOG(LOG_VERB, \" -------------------------------------------\\n\");\n\tJANUS_LOG(LOG_VERB, \"  >> Merged (%zu bytes)\\n\", strlen(sdp));\n\tJANUS_LOG(LOG_VERB, \" -------------------------------------------\\n\");\n\tJANUS_LOG(LOG_VERB, \"%s\\n\", sdp);\n\n\treturn sdp;\n}\n"
  },
  {
    "path": "src/sdp.h",
    "content": "/*! \\file    sdp.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    SDP processing (headers)\n * \\details  Implementation of an SDP\n * parser/merger/generator in the server. Each SDP coming from peers is\n * stripped/anonymized before it is passed to the plugins: all\n * DTLS/ICE/transport related information is removed, only leaving the\n * relevant information in place. SDP coming from plugins is stripped/anonymized\n * as well, and merged with the proper DTLS/ICE/transport information before\n * it is sent to the peers. The actual SDP processing (parsing SDP strings,\n * representation of SDP as an internal format, and so on) is done via\n * the tools provided in sdp-utils.h.\n *\n * \\todo Right now, we only support sessions with up to a single audio\n * and/or a single video stream (as in, a single audio and/or video\n * m-line) plus an optional DataChannel. Later versions of the server\n * will add support for more media streams of the same type in a session.\n *\n * \\ingroup protocols\n * \\ref protocols\n */\n\n#ifndef JANUS_SDP_H\n#define JANUS_SDP_H\n\n\n#include <inttypes.h>\n\n#include \"sdp-utils.h\"\n#include \"dtls.h\"\n\n\n/** @name Janus SDP helper methods\n */\n///@{\n/*! \\brief Method to pre-parse a session description\n * \\details This method is only used to quickly check how many audio and video lines are in an SDP, and to generate a Janus SDP instance\n * @param[in] handle Opaque pointer to the ICE handle this session description will modify\n * @param[in] jsep_sdp The SDP that the browser peer originated\n * @param[in,out] error_str Buffer to receive a reason for an error, if any\n * @param[in] errlen The length of the error buffer\n * @param[out] dtls_role The advertised DTLS role\n * @param[out] audio The number of audio m-lines\n * @param[out] video The number of video m-lines\n * @param[out] data The number of SCTP m-lines\n * @returns The Janus SDP object in case of success, NULL in case the SDP is invalid */\njanus_sdp *janus_sdp_preparse(void *handle, const char *jsep_sdp, char *error_str, size_t errlen,\n\tjanus_dtls_role *dtls_role, int *audio, int *video, int *data);\n\n/*! \\brief Method to process a remote parsed session description\n * \\details This method will process a session description coming from a peer, and set up the ICE candidates accordingly\n * @param[in] handle Opaque pointer to the ICE handle this session description will modify\n * @param[in] sdp The Janus SDP object to process\n * @param[in] rids_hml Whether the order of rids in the SDP, if present, will be h-m-l (TRUE) or l-m-h (FALSE)\n * @param[in] update Whether this SDP is an update to an existing session or not\n * @returns 0 in case of success, -1 in case of an error */\nint janus_sdp_process_remote(void *handle, janus_sdp *sdp, gboolean rids_hml, gboolean update);\n\n/*! \\brief Method to process a local parsed session description\n * \\details This method will process a session description coming from a plugin, and set up the ICE candidates accordingly\n * @param[in] handle Opaque pointer to the ICE handle this session description will modify\n * @param[in] sdp The Janus SDP object to process\n * @param[in] update Whether this SDP is an update to an existing session or not\n * @returns 0 in case of success, -1 in case of an error */\nint janus_sdp_process_local(void *handle, janus_sdp *sdp, gboolean update);\n\n/*! \\brief Method to parse a single candidate\n * \\details This method will parse a single remote candidate provided by a peer, whether it is trickling or not\n * @param[in] pc Opaque pointer to the WebRTC PeerConnection this candidate refers to\n * @param[in] candidate The remote candidate to process\n * @param[in] trickle Whether this is a trickle candidate, or coming from the SDP\n * @returns 0 in case of success, a non-zero integer in case of an error */\nint janus_sdp_parse_candidate(void *pc, const char *candidate, int trickle);\n\n/*! \\brief Method to parse a SSRC group attribute\n * \\details This method will parse a SSRC group attribute, and set the parsed values for the peer\n * @param[in] medium Opaque pointer to the medium this candidate refers to\n * @param[in] group_attr The SSRC group attribute value to parse\n * @param[in] video Whether this is video-related or not\n * @returns 0 in case of success, a non-zero integer in case of an error */\nint janus_sdp_parse_ssrc_group(void *medium, const char *group_attr, int video);\n\n/*! \\brief Method to parse a SSRC attribute\n * \\details This method will parse a SSRC attribute, and set it for the peer\n * @param[in] medium Opaque pointer to the medium this candidate refers to\n * @param[in] ssrc_attr The SSRC attribute value to parse\n * @param[in] video Whether this is a video SSRC or not\n * @returns 0 in case of success, a non-zero integer in case of an error */\nint janus_sdp_parse_ssrc(void *medium, const char *ssrc_attr, int video);\n\n/*! \\brief Method to strip/anonymize a session description\n * @param[in,out] sdp The Janus SDP description object to strip/anonymize\n * @returns 0 in case of success, a non-zero integer in case of an error */\nint janus_sdp_anonymize(janus_sdp *sdp);\n\n/*! \\brief Method to merge a stripped session description and the right transport information\n * @param[in] handle Opaque pointer to the ICE handle this session description is related to\n * @param[in] sdp The Janus SDP description object to merge/enrich\n * @param[in] offer Whether the SDP is an offer or an answer\n * @returns A string containing the full session description in case of success, NULL if the SDP is invalid */\nchar *janus_sdp_merge(void *handle, janus_sdp *sdp, gboolean offer);\n///@}\n\n#endif\n"
  },
  {
    "path": "src/text2pcap.c",
    "content": "/*! \\file    text2pcap.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Dumping of RTP/RTCP packets to text2pcap or pcap format\n * \\details  Implementation of a simple helper utility that can be used\n * to dump incoming and outgoing RTP/RTCP packets to pcap or text2pcap format.\n * Saving to pcap natively can be more efficient but will lack some features,\n * as the target will be a legacy (v2.4) \\c .pcap file and not a \\c .pcapng one.\n * When saving to a text file, instead, the resulting file can be passed to\n * the \\c text2pcap application in order to get a \\c .pcap or \\c .pcapng file\n * that can be analyzed via Wireshark or similar applications, e.g.:\n *\n\\verbatim\n/usr/sbin/text2pcap -D -n -l 1 -i 17 -u 1000,2000 -t '%H:%M:%S.' dump.txt dump.pcapng\n/usr/sbin/wireshark dump.pcapng\n\\endverbatim\n *\n * While plugins are free to take advantage of this functionality, it's been\n * specifically added to make debugging from the core easier. Enabling and\n * disabling the dump of RTP/RTCP packets for the media traffic of a\n * specific handle is done via the \\ref admin so check the documentation\n * of that section for more details. Notice that starting a new dump on\n * an existing filename will result in the new packets to be appended.\n *\n * \\note Motivation and inspiration for this work came from a\n * <a href=\"https://blog.mozilla.org/webrtc/debugging-encrypted-rtp-is-more-fun-than-it-used-to-be/\">similar effort</a>\n * recently done in Firefox, and from a discussion related to a\n * <a href=\"https://webrtchacks.com/video_replay/\">blog post</a> on\n * WebRTC hacks, where guidelines are provided with respect to debugging\n * based on pcap files.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <errno.h>\n#include <sys/time.h>\n#include <arpa/inet.h>\n#ifdef __MACH__\n#include <machine/endian.h>\n#define __BYTE_ORDER BYTE_ORDER\n#define __BIG_ENDIAN BIG_ENDIAN\n#define __LITTLE_ENDIAN LITTLE_ENDIAN\n#elif defined(__FreeBSD__)\n#include <sys/endian.h>\n#define __BYTE_ORDER BYTE_ORDER\n#define __BIG_ENDIAN BIG_ENDIAN\n#define __LITTLE_ENDIAN LITTLE_ENDIAN\n#else\n#include <endian.h>\n#endif\n\n#include \"text2pcap.h\"\n#include \"debug.h\"\n#include \"utils.h\"\n\n#define CASE_STR(name) case name: return #name\nconst char *janus_text2pcap_packet_string(janus_text2pcap_packet type) {\n\tswitch(type) {\n\t\tCASE_STR(JANUS_TEXT2PCAP_RTP);\n\t\tCASE_STR(JANUS_TEXT2PCAP_RTCP);\n\t\tCASE_STR(JANUS_TEXT2PCAP_DATA);\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\n/* Helper struct to define a libpcap global header\n * https://wiki.wireshark.org/Development/LibpcapFileFormat */\ntypedef struct janus_text2pcap_global_header {\n\tguint32 magic_number;\t/* Magic number */\n\tguint16 version_major;\t/* Major version number */\n\tguint16 version_minor;\t/* Minor version number */\n\tgint32  thiszone;\t\t/* GMT to local correction */\n\tguint32 sigfigs;\t\t/* Accuracy of timestamps */\n\tguint32 snaplen;\t\t/* Max length of captured packets, in octets */\n\tguint32 network;\t\t/* Data link type */\n} janus_text2pcap_global_header;\n\n/* Helper struct to define a libpcap packet header\n * https://wiki.wireshark.org/Development/LibpcapFileFormat */\ntypedef struct janus_text2pcap_packet_header {\n\tguint32 ts_sec;\t\t\t/* Timestamp seconds */\n\tguint32 ts_usec;\t\t/* Timestamp microseconds */\n\tguint32 incl_len;\t\t/* Number of octets of packet saved in file */\n\tguint32 orig_len;\t\t/* Actual length of packet */\n} janus_text2pcap_packet_header;\n\n/* Ethernet header */\ntypedef struct janus_text2pcap_ethernet_header {\n\tuint8_t dst[6];\n\tuint8_t src[6];\n\tuint16_t type;\n} janus_text2pcap_ethernet_header;\nstatic void janus_text2pcap_ethernet_header_init(janus_text2pcap_ethernet_header *eth) {\n\tmemset(eth, 0, sizeof(*eth));\n\teth->type = htons(0x0800);\n}\n\n/* IP header */\ntypedef struct janus_text2pcap_ip_header {\n#if __BYTE_ORDER == __BIG_ENDIAN\n\tuint8_t version:4;\n\tuint8_t hlen:4;\n#elif __BYTE_ORDER == __LITTLE_ENDIAN\n\tuint8_t hlen:4;\n\tuint8_t version:4;\n#endif\n\tuint8_t tos;\n\tuint16_t tlen;\n\tuint16_t id;\n\tuint16_t flags;\n\tuint8_t ttl;\n\tuint8_t protocol;\n\tuint16_t csum;\n\tuint8_t src[4];\n\tuint8_t dst[4];\n} janus_text2pcap_ip_header;\nstatic void janus_text2pcap_ip_header_init(janus_text2pcap_ip_header *ip, gboolean incoming, int psize) {\n\tip->version = 4;\n\tip->hlen = 5;\n\tip->tos = 0;\n\tip->tlen = htons(28+psize);\n\tip->id = htons(0);\n\tip->flags = htons(0x4000);\n\tip->ttl = 64;\n\tip->protocol = 17;\n\tip->csum = 0;\n\tip->src[0] = 10;\n\tip->src[1] = incoming ? 1 : 2;\n\tip->src[2] = incoming ? 1 : 2;\n\tip->src[3] = incoming ? 1 : 2;\n\tip->dst[0] = 10;\n\tip->dst[1] = incoming ? 2 : 1;\n\tip->dst[2] = incoming ? 2 : 1;\n\tip->dst[3] = incoming ? 2 : 1;\n}\n\n/* UDP header */\ntypedef struct janus_text2pcap_udp_header {\n\tuint16_t srcport;\n\tuint16_t dstport;\n\tuint16_t len;\n\tuint16_t csum;\n} janus_text2pcap_udp_header;\nstatic void janus_text2pcap_udp_header_init(janus_text2pcap_udp_header *udp, gboolean incoming, int psize) {\n\tudp->srcport = htons(incoming ? 1000 : 2000);\n\tudp->dstport = htons(incoming ? 2000 : 1000);\n\tudp->len = htons(8+psize);\n\tudp->csum = 0;\n}\n\n\njanus_text2pcap *janus_text2pcap_create(const char *dir, const char *filename, int truncate, gboolean text) {\n\tjanus_text2pcap *tp;\n\tchar newname[1024];\n\tchar *fname;\n\tFILE *f;\n\n\tif(truncate < 0)\n\t\treturn NULL;\n\n\t/* Copy given filename or generate a random one */\n\tif(filename == NULL) {\n\t\tg_snprintf(newname, sizeof(newname),\n\t\t    \"janus-text2pcap-%\"SCNu32\".%s\", janus_random_uint32(), text ? \"txt\" : \"pcap\");\n\t} else {\n\t\tg_strlcpy(newname, filename, sizeof(newname));\n\t}\n\n\t/* Create the directory, if needed */\n\tif(dir != NULL && janus_mkdir(dir, 0755) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"mkdir error: %d\\n\", errno);\n\t\treturn NULL;\n\t}\n\n\t/* Make sure we can write to the target folder */\n\tfname = (dir ? g_strdup_printf(\"%s/%s\", dir, newname) : g_strdup(newname));\n\tif(janus_is_folder_protected(fname)) {\n\t\tJANUS_LOG(LOG_ERR, \"Target capture path '%s' is in protected folder...\\n\", fname);\n\t\tg_free(fname);\n\t\treturn NULL;\n\t}\n\n\t/* Try opening the file now */\n\tf = fopen(fname, \"ab\");\n\tif (f == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"fopen(%s) error: %d\\n\", fname, errno);\n\t\tg_free(fname);\n\t\treturn NULL;\n\t}\n\n\t/* Create the text2pcap instance */\n\ttp = g_malloc(sizeof(janus_text2pcap));\n\ttp->filename = fname;\n\ttp->file = f;\n\ttp->truncate = truncate;\n\ttp->text = text;\n\tg_atomic_int_set(&tp->writable, 1);\n\tjanus_mutex_init(&tp->mutex);\n\n\t/* If we're saving to .pcap directly, generate a global header */\n\tif(!text) {\n\t\tjanus_text2pcap_global_header header = {\n\t\t\t0xa1b2c3d4, 2, 4, 0, 0, 65535, 1\n\t\t};\n\t\tfwrite(&header, sizeof(char), sizeof(header), f);\n\t}\n\n\treturn tp;\n}\n\nint janus_text2pcap_dump(janus_text2pcap *instance,\n\t\tjanus_text2pcap_packet type, gboolean incoming, char *buf, int len, const char *format, ...) {\n\tif(instance == NULL || buf == NULL || len < 1)\n\t\treturn -1;\n\tjanus_mutex_lock_nodebug(&instance->mutex);\n\tif(instance->file == NULL || !g_atomic_int_get(&instance->writable)) {\n\t\tjanus_mutex_unlock_nodebug(&instance->mutex);\n\t\treturn -1;\n\t}\n\t/* If we're saving to .pcap directly, generate a packet header and save the payload */\n\tif(!instance->text) {\n\t\t/* Are we truncating? */\n\t\tint size = instance->truncate ? (len > instance->truncate ? instance->truncate : len) : len;\n\t\tint hsize = sizeof(janus_text2pcap_ethernet_header) + sizeof(janus_text2pcap_ip_header) +\n\t\t\tsizeof(janus_text2pcap_udp_header);\n\t\tint hsize_cut = hsize + size;\n\t\tint hsize_tot = hsize + len;\n\t\t/* We need a fake Ethernet/IP/UDP encapsulation for this packet */\n\t\tjanus_text2pcap_ethernet_header eth;\n\t\tjanus_text2pcap_ethernet_header_init(&eth);\n\t\tjanus_text2pcap_ip_header ip;\n\t\tjanus_text2pcap_ip_header_init(&ip, incoming, len);\n\t\tjanus_text2pcap_udp_header udp;\n\t\tjanus_text2pcap_udp_header_init(&udp, incoming, len);\n\t\t/* Now prepare the packet header */\n\t\tstruct timeval tv;\n\t\tgettimeofday(&tv, NULL);\n\t\tjanus_text2pcap_packet_header header = {\n\t\t\ttv.tv_sec, tv.tv_usec, hsize_cut, hsize_tot\n\t\t};\n\t\tfwrite(&header, sizeof(char), sizeof(header), instance->file);\n\t\tfwrite(&eth, sizeof(char), sizeof(eth), instance->file);\n\t\tfwrite(&ip, sizeof(char), sizeof(ip), instance->file);\n\t\tfwrite(&udp, sizeof(char), sizeof(udp), instance->file);\n\t\t/* The write the packet itself (or part of it) */\n\t\tint temp = 0, tot = size;\n\t\twhile(tot > 0) {\n\t\t\ttemp = fwrite(buf+size-tot, sizeof(char), tot, instance->file);\n\t\t\tif(temp <= 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error dumping packet...\\n\");\n\t\t\t\tjanus_mutex_unlock_nodebug(&instance->mutex);\n\t\t\t\treturn -2;\n\t\t\t}\n\t\t\ttot -= temp;\n\t\t}\n\t\t/* Done */\n\t\tjanus_mutex_unlock_nodebug(&instance->mutex);\n\t\treturn 0;\n\t}\n\t/* If we got here, we need to prepare a text representation of the packet */\n\tchar buffer[5000], timestamp[20], usec[10], byte[10];\n\tmemset(timestamp, 0, sizeof(timestamp));\n\tmemset(usec, 0, sizeof(usec));\n\ttime_t t = time(NULL);\n\tstruct tm *tm = localtime(&t);\n\tstruct timeval tv;\n\tgettimeofday(&tv, NULL);\n\tstrftime(timestamp, sizeof(timestamp), \"%H:%M:%S\", tm);\n\tg_snprintf(usec, sizeof(usec), \".%06ld\", tv.tv_usec);\n\tjanus_strlcat(timestamp, usec, sizeof(timestamp));\n\tmemset(buffer, 0, sizeof(buffer));\n\tg_snprintf(buffer, sizeof(buffer), \"%s %s 000000 \", incoming ? \"I\" : \"O\", timestamp);\n\tint i=0;\n\tint stop = instance->truncate ? (len > instance->truncate ? instance->truncate : len) : len;\n\tfor(i=0; i<stop; i++) {\n\t\tmemset(byte, 0, sizeof(byte));\n\t\tg_snprintf(byte, sizeof(byte), \" %02x\", (unsigned char)buf[i]);\n\t\tjanus_strlcat(buffer, byte, sizeof(buffer));\n\t}\n\tjanus_strlcat(buffer, \" \", sizeof(buffer));\n\tjanus_strlcat(buffer, janus_text2pcap_packet_string(type), sizeof(buffer));\n\tif(format) {\n\t\t/* This callback has variable arguments (error string) */\n\t\tchar custom[512];\n\t\tva_list ap;\n\t\tva_start(ap, format);\n\t\tg_vsnprintf(custom, sizeof(custom), format, ap);\n\t\tva_end(ap);\n\t\tjanus_strlcat(buffer, \" \", sizeof(buffer));\n\t\tjanus_strlcat(buffer, custom, sizeof(buffer));\n\t}\n\tjanus_strlcat(buffer, \"\\r\\n\", sizeof(buffer));\n\t/* Save textified packet on file */\n\tint temp = 0, buflen = strlen(buffer), tot = buflen;\n\twhile(tot > 0) {\n\t\ttemp = fwrite(buffer+buflen-tot, sizeof(char), tot, instance->file);\n\t\tif(temp <= 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error dumping packet...\\n\");\n\t\t\tjanus_mutex_unlock_nodebug(&instance->mutex);\n\t\t\treturn -2;\n\t\t}\n\t\ttot -= temp;\n\t}\n\t/* Done */\n\tjanus_mutex_unlock_nodebug(&instance->mutex);\n\treturn 0;\n}\n\nint janus_text2pcap_close(janus_text2pcap *instance) {\n\tif(instance == NULL)\n\t\treturn -1;\n\tjanus_mutex_lock_nodebug(&instance->mutex);\n\tif(!g_atomic_int_compare_and_exchange(&instance->writable, 1, 0)) {\n\t\tjanus_mutex_unlock_nodebug(&instance->mutex);\n\t\treturn 0;\n\t}\n\tfclose(instance->file);\n\tinstance->file = NULL;\n\tjanus_mutex_unlock_nodebug(&instance->mutex);\n\treturn 0;\n}\n\nvoid janus_text2pcap_free(janus_text2pcap *instance) {\n\tif(instance == NULL)\n\t\treturn;\n\tjanus_text2pcap_close(instance);\n\tg_free(instance->filename);\n\tjanus_mutex_destroy(&instance->mutex);\n\tg_free(instance);\n}\n"
  },
  {
    "path": "src/text2pcap.h",
    "content": "/*! \\file    text2pcap.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Dumping of RTP/RTCP packets to text2pcap or pcap format (headers)\n * \\details  Implementation of a simple helper utility that can be used\n * to dump incoming and outgoing RTP/RTCP packets to pcap or text2pcap format.\n * Saving to pcap natively can be more efficient but will lack some features,\n * as the target will be a legacy (v2.4) \\c .pcap file and not a \\c .pcapng one.\n * When saving to a text file, instead, the resulting file can be passed to\n * the \\c text2pcap application in order to get a \\c .pcap or \\c .pcapng file\n * that can be analyzed via Wireshark or similar applications, e.g.:\n *\n\\verbatim\n/usr/sbin/text2pcap -D -n -l 1 -i 17 -u 1000,2000 -t '%H:%M:%S.' dump.txt dump.pcapng\n/usr/sbin/wireshark dump.pcapng\n\\endverbatim\n *\n * While plugins are free to take advantage of this functionality, it's been\n * specifically added to make debugging from the core easier. Enabling and\n * disabling the dump of RTP/RTCP packets for the media traffic of a\n * specific handle is done via the \\ref admin so check the documentation\n * of that section for more details. Notice that starting a new dump on\n * an existing filename will result in the new packets to be appended.\n *\n * \\note Motivation and inspiration for this work came from a\n * <a href=\"https://blog.mozilla.org/webrtc/debugging-encrypted-rtp-is-more-fun-than-it-used-to-be/\">similar effort</a>\n * recently done in Firefox, and from a discussion related to a\n * <a href=\"https://webrtchacks.com/video_replay/\">blog post</a> on\n * WebRTC hacks, where guidelines are provided with respect to debugging\n * based on pcap files.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_TEXT2PCAP_H\n#define JANUS_TEXT2PCAP_H\n\n#include <glib.h>\n\n#include <inttypes.h>\n#include <string.h>\n#include <stdio.h>\n#include <stdlib.h>\n\n#include \"mutex.h\"\n\n/*! \\brief Instance of a text2pcap recorder */\ntypedef struct janus_text2pcap {\n\t/*! \\brief Absolute path to where the text2pcap file is stored */\n\tchar *filename;\n\t/*! \\brief Pointer to the file handle */\n\tFILE *file;\n\t/*! \\brief Number of bytes to truncate at */\n\tint truncate;\n\t/*! \\brief Whether we'll save as text, or directly to pcap */\n\tgboolean text;\n\t/*! \\brief Whether we can write to this file or not */\n\tvolatile int writable;\n\t/*! \\brief Mutex to lock/unlock this recorder instance */\n\tjanus_mutex mutex;\n} janus_text2pcap;\n\n/*! \\brief Packet types we can dump */\ntypedef enum janus_text2pcap_packet {\n\tJANUS_TEXT2PCAP_RTP,\n\tJANUS_TEXT2PCAP_RTCP,\n\tJANUS_TEXT2PCAP_DATA\n} janus_text2pcap_packet;\nconst char *janus_text2pcap_packet_string(janus_text2pcap_packet type);\n\n/*! \\brief Create a text2pcap recorder\n * \\note If no target directory is provided, the current directory will be used. If no filename\n * is passed, a random filename will be used.\n * @param[in] dir Path of the directory to save the recording into (will try to create it if it doesn't exist)\n * @param[in] filename Filename to use for the recording\n * @param[in] truncate Number of bytes to truncate each packet at (0 to not truncate at all)\n * @param[in] text Whether we'll save as text, or directly to pcap\n * @returns A valid janus_text2pcap instance in case of success, NULL otherwise */\njanus_text2pcap *janus_text2pcap_create(const char *dir, const char *filename, int truncate, gboolean text);\n\n/*! \\brief Dump an RTP or RTCP packet\n * @param[in] instance Instance of the janus_text2pcap recorder to dump the packet to\n * @param[in] type Type of the packet we're going to dump\n * @param[in] incoming Whether this is an incoming or outgoing packet\n * @param[in] buf Packet data to dump\n * @param[in] len Size of the packet data to dump\n * @param[in] format Format for the optional string to append to the line, if any\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_text2pcap_dump(janus_text2pcap *instance,\n\tjanus_text2pcap_packet type, gboolean incoming, char *buf, int len, const char *format, ...) G_GNUC_PRINTF(6, 7);\n\n/*! \\brief Close a text2pcap recorder\n * @param[in] instance Instance of the janus_text2pcap recorder to close\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_text2pcap_close(janus_text2pcap *instance);\n\n/*! \\brief Free a text2pcap instance\n * @param[in] instance Instance of the janus_text2pcap recorder to free */\nvoid janus_text2pcap_free(janus_text2pcap *instance);\n\n#endif\n"
  },
  {
    "path": "src/transports/janus_http.c",
    "content": "/*! \\file   janus_http.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus RESTs transport plugin\n * \\details  This is an implementation of a RESTs transport for the\n * Janus API, using the libmicrohttpd library (http://www.gnu.org/software/libmicrohttpd/).\n * This module allows browsers to make use of HTTP to talk to the Janus core.\n * Since a Janus instance may be deployed on a different domain than the web\n * server hosting the web applications using it, the plugin automatically\n * handles OPTIONS request to comply with the CORS specification.\n * POST requests can be used to ask for the management of a session with\n * the server, to attach to a plugin, to send messages to the plugin\n * itself and so on. GET requests instead are used for getting events\n * associated to a Janus session (and as such to all its plugin handles\n * and the events plugins push in the session itself), using a long poll\n * approach. A JavaScript library (janus.js) implements all of this on\n * the client side automatically.\n * \\note There's a well known bug in libmicrohttpd that may cause it to\n * spike to 100% of the CPU when using HTTPS on some distributions. In\n * case you're interested in HTTPS support, it's better to just rely on\n * HTTP in Janus, and put a frontend like Apache HTTPD or nginx to take\n * care of securing the traffic. More details are available in \\ref deploy.\n *\n * \\ingroup transports\n * \\ref transports\n */\n\n#include \"transport.h\"\n\n#include <arpa/inet.h>\n#include <ifaddrs.h>\n#include <net/if.h>\n#include <sys/socket.h>\n#include <sys/types.h>\n#include <netdb.h>\n\n#include <microhttpd.h>\n#if defined(MHD_VERSION) && MHD_VERSION >= 0x00097002\n/* enum MHD_Result introduced in libmicrohttpd v0.9.71 */\ntypedef enum MHD_Result janus_MHD_Result;\n#else\ntypedef int janus_MHD_Result;\n#endif\n\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../ip-utils.h\"\n#include \"../utils.h\"\n\n\n/* Transport plugin information */\n#define JANUS_HTTP_VERSION\t\t\t2\n#define JANUS_HTTP_VERSION_STRING\t\"0.0.2\"\n#define JANUS_HTTP_DESCRIPTION\t\t\"This transport plugin adds REST (HTTP/HTTPS) support to the Janus API via libmicrohttpd.\"\n#define JANUS_HTTP_NAME\t\t\t\t\"JANUS REST (HTTP/HTTPS) transport plugin\"\n#define JANUS_HTTP_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_HTTP_PACKAGE\t\t\t\"janus.transport.http\"\n\n/* Transport methods */\njanus_transport *create(void);\nint janus_http_init(janus_transport_callbacks *callback, const char *config_path);\nvoid janus_http_destroy(void);\nint janus_http_get_api_compatibility(void);\nint janus_http_get_version(void);\nconst char *janus_http_get_version_string(void);\nconst char *janus_http_get_description(void);\nconst char *janus_http_get_name(void);\nconst char *janus_http_get_author(void);\nconst char *janus_http_get_package(void);\ngboolean janus_http_is_janus_api_enabled(void);\ngboolean janus_http_is_admin_api_enabled(void);\nint janus_http_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message);\nvoid janus_http_session_created(janus_transport_session *transport, guint64 session_id);\nvoid janus_http_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed);\nvoid janus_http_session_claimed(janus_transport_session *transport, guint64 session_id);\njson_t *janus_http_query_transport(json_t *request);\n\n\n/* Transport setup */\nstatic janus_transport janus_http_transport =\n\tJANUS_TRANSPORT_INIT (\n\t\t.init = janus_http_init,\n\t\t.destroy = janus_http_destroy,\n\n\t\t.get_api_compatibility = janus_http_get_api_compatibility,\n\t\t.get_version = janus_http_get_version,\n\t\t.get_version_string = janus_http_get_version_string,\n\t\t.get_description = janus_http_get_description,\n\t\t.get_name = janus_http_get_name,\n\t\t.get_author = janus_http_get_author,\n\t\t.get_package = janus_http_get_package,\n\n\t\t.is_janus_api_enabled = janus_http_is_janus_api_enabled,\n\t\t.is_admin_api_enabled = janus_http_is_admin_api_enabled,\n\n\t\t.send_message = janus_http_send_message,\n\t\t.session_created = janus_http_session_created,\n\t\t.session_over = janus_http_session_over,\n\t\t.session_claimed = janus_http_session_claimed,\n\n\t\t.query_transport = janus_http_query_transport,\n\t);\n\n/* Transport creator */\njanus_transport *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_HTTP_NAME);\n\treturn &janus_http_transport;\n}\n\n/* MHD uses this value as default */\n#define DEFAULT_CONNECTION_LIMIT (FD_SETSIZE-4)\nstatic unsigned int connection_limit = DEFAULT_CONNECTION_LIMIT;\n\n/* Useful stuff */\nstatic gint initialized = 0, stopping = 0;\nstatic janus_transport_callbacks *gateway = NULL;\nstatic gboolean http_janus_api_enabled = FALSE;\nstatic gboolean http_admin_api_enabled = FALSE;\nstatic gboolean notify_events = TRUE;\nstatic enum MHD_FLAG mhd_debug_flag = MHD_NO_FLAG;\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Parameter validation (for tweaking and queries via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"events\", JANUS_JSON_BOOL, 0},\n\t{\"json\", JSON_STRING, 0},\n};\n/* Error codes (for the tweaking and queries via Admin API) */\n#define JANUS_HTTP_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_HTTP_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_HTTP_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_HTTP_ERROR_UNKNOWN_ERROR\t\t\t499\n\n\n/* Incoming HTTP message */\ntypedef struct janus_http_msg {\n\tstruct MHD_Connection *connection;\t/* The MHD connection this message came from */\n\tvolatile int suspended;\t\t\t\t/* Whether this connection is currently suspended */\n\tvolatile void *longpoll;\t\t\t/* Whether this is a long poll connection for a session */\n\tint max_events;\t\t\t\t\t\t/* In case this is a long poll, how many events we should send back */\n\tchar *acro;\t\t\t\t\t\t\t/* Value of the Origin HTTP header, if any (needed for CORS) */\n\tchar *acrh;\t\t\t\t\t\t\t/* Value of the Access-Control-Request-Headers HTTP header, if any (needed for CORS) */\n\tchar *acrm;\t\t\t\t\t\t\t/* Value of the Access-Control-Request-Method HTTP header, if any (needed for CORS) */\n\tchar *xff;\t\t\t\t\t\t\t/* Value of the X-Forwarded-For HTTP header, if any  */\n\tchar *contenttype;\t\t\t\t\t/* Content-Type of the payload */\n\tchar *payload;\t\t\t\t\t\t/* Payload of the message */\n\tsize_t len;\t\t\t\t\t\t\t/* Length of the message in octets */\n\tgint64 session_id;\t\t\t\t\t/* Janus-Client session identifier this message belongs to */\n\tchar *response;\t\t\t\t\t\t/* The response from the core as a string */\n\tsize_t resplen;\t\t\t\t\t\t/* Length of the response in octets */\n\tGSource *timeout;\t\t\t\t\t/* Timeout monitor, if any */\n\tvolatile gint timeout_flag;\t\t\t/* Whether a timeout hasn't fired yet */\n\tvolatile gint destroyed;\t\t\t/* Whether this session has been destroyed */\n\tjanus_refcount ref;\t\t\t\t\t/* Reference counter for this message */\n} janus_http_msg;\nstatic GHashTable *messages = NULL;\nstatic janus_mutex messages_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_http_msg_free(const janus_refcount *msg_ref) {\n\tjanus_http_msg *request = janus_refcount_containerof(msg_ref, janus_http_msg, ref);\n\t/* This message can be destroyed, free all the resources */\n\tif(!request)\n\t\treturn;\n\tg_free(request->payload);\n\tg_free(request->contenttype);\n\tg_free(request->acro);\n\tg_free(request->acrh);\n\tg_free(request->acrm);\n\tg_free(request->xff);\n\tg_free(request->response);\n\tg_free(request);\n}\n\nstatic void janus_http_msg_destroy(void *msg) {\n\tjanus_http_msg *request = (janus_http_msg *)msg;\n\tif(request && g_atomic_int_compare_and_exchange(&request->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&request->ref);\n}\n\n\n/* Helper for long poll: HTTP events to push per session */\ntypedef struct janus_http_session {\n\tguint64 session_id;\t\t\t/* Core session identifier */\n\tGAsyncQueue *events;\t\t/* Events to notify for this session */\n\tGList *longpolls;\t\t\t/* Long poll connection */\n\tjanus_mutex mutex;\t\t\t/* Mutex to lock this instance */\n\tvolatile gint destroyed;\t/* Whether this session has been destroyed */\n\tjanus_refcount ref;\t\t\t/* Reference counter for this session */\n} janus_http_session;\n/* We keep track of created sessions as we handle long polls */\nstatic const char *keepalive_id = \"keepalive\";\nstatic GHashTable *sessions = NULL;\nstatic janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_http_session_destroy(janus_http_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n\nstatic void janus_http_session_free(const janus_refcount *session_ref) {\n\tjanus_http_session *session = janus_refcount_containerof(session_ref, janus_http_session, ref);\n\t/* This session can be destroyed, free all the resources */\n\tif(session->events) {\n\t\tjson_t *event = NULL;\n\t\twhile((event = g_async_queue_try_pop(session->events)) != NULL)\n\t\t\tjson_decref(event);\n\t\tg_async_queue_unref(session->events);\n\t}\n\tjanus_mutex_destroy(&session->mutex);\n\tg_free(session);\n}\n\n\n/* Custom GSource for tracking request timeouts (including long polls) */\ntypedef struct janus_http_request_timeout {\n\tGSource source;\n\tjanus_transport_session *ts;\n\tjanus_http_session *session;\n} janus_http_request_timeout;\n/* Helper to handle timeouts */\nstatic void janus_http_timeout(janus_transport_session *ts, janus_http_session *session, gboolean lock_session);\n/* GSource Functions */\nstatic gboolean janus_http_request_timeout_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) {\n\tJANUS_LOG(LOG_DBG, \"[%p] dispatch\\n\", source);\n\tjanus_http_request_timeout *t = (janus_http_request_timeout *)source;\n\t/* Timeout fired, invoke the function */\n\tjanus_http_timeout(t->ts, t->session, TRUE);\n\t/* We're done */\n\tg_source_destroy(source);\n\tg_source_unref(source);\n\treturn G_SOURCE_REMOVE;\n}\nstatic void janus_http_request_timeout_finalize(GSource *source) {\n\tJANUS_LOG(LOG_DBG, \"[%p] finalize\\n\", source);\n\tjanus_http_request_timeout *timeout = (janus_http_request_timeout *)source;\n\tif(timeout) {\n\t\tif(timeout->session)\n\t\t\tjanus_refcount_decrease(&timeout->session->ref);\n\t\tif(timeout->ts)\n\t\t\tjanus_refcount_decrease(&timeout->ts->ref);\n\t}\n}\nstatic GSourceFuncs janus_http_request_timeout_funcs = {\n\tNULL, NULL,\n\tjanus_http_request_timeout_dispatch,\n\tjanus_http_request_timeout_finalize,\n\tNULL, NULL\n};\nstatic GSource *janus_http_request_timeout_create(janus_transport_session *ts, janus_http_session *session, gint timeout) {\n\tGSource *source = g_source_new(&janus_http_request_timeout_funcs, sizeof(janus_http_request_timeout));\n\tjanus_http_request_timeout *t = (janus_http_request_timeout *)source;\n\tt->ts = ts;\n\tt->session = session;\n\tg_source_set_ready_time(source, janus_get_monotonic_time_internal() + timeout*G_USEC_PER_SEC);\n\tJANUS_LOG(LOG_DBG, \"[%p] create (%d)\\n\", source, timeout);\n\treturn source;\n}\n\n\n/* Callback (libmicrohttpd) invoked when a new connection is attempted on the REST API */\nstatic janus_MHD_Result janus_http_client_connect(void *cls, const struct sockaddr *addr, socklen_t addrlen);\n/* Callback (libmicrohttpd) invoked when a new connection is attempted on the admin/monitor webserver */\nstatic janus_MHD_Result janus_http_admin_client_connect(void *cls, const struct sockaddr *addr, socklen_t addrlen);\n/* Callback (libmicrohttpd) invoked when an HTTP message (GET, POST, OPTIONS, etc.) is available */\nstatic janus_MHD_Result janus_http_handler(void *cls, struct MHD_Connection *connection,\n\tconst char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **ptr);\n/* Callback (libmicrohttpd) invoked when an admin/monitor HTTP message (GET, POST, OPTIONS, etc.) is available */\nstatic janus_MHD_Result janus_http_admin_handler(void *cls, struct MHD_Connection *connection,\n\tconst char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **ptr);\n/* Callback (libmicrohttpd) invoked when headers of an incoming HTTP message have been parsed */\nstatic janus_MHD_Result janus_http_headers(void *cls, enum MHD_ValueKind kind, const char *key, const char *value);\n/* Callback (libmicrohttpd) invoked when a request has been processed and can be freed */\nstatic void janus_http_request_completed(void *cls, struct MHD_Connection *connection,\n\tvoid **con_cls, enum MHD_RequestTerminationCode toe);\n/* Callback to send data back after resuming a connection */\nstatic ssize_t janus_http_response_callback(void *cls, uint64_t pos, char *buf, size_t max);\n/* Worker to handle requests that are actually long polls */\nstatic int janus_http_notifier(janus_http_msg *msg);\n/* Helper to quickly send a success response */\nstatic janus_MHD_Result janus_http_return_success(janus_transport_session *ts, char *payload);\n/* Helper to quickly send an error response */\nstatic janus_MHD_Result janus_http_return_error(janus_transport_session *ts, uint64_t session_id,\n\tconst char *transaction, gint error, const char *format, ...) G_GNUC_PRINTF(5, 6);\n\n\n/* MHD Web Server */\nstatic struct MHD_Daemon *ws = NULL, *sws = NULL;\nstatic char *ws_path = NULL;\nstatic char *cert_pem_bytes = NULL, *cert_key_bytes = NULL;\n\n/* Admin/Monitor MHD Web Server */\nstatic struct MHD_Daemon *admin_ws = NULL, *admin_sws = NULL;\nstatic char *admin_ws_path = NULL;\n\n/* Custom Access-Control-Allow-Origin value, if specified */\nstatic char *allow_origin = NULL;\nstatic gboolean enforce_cors = FALSE;\n\n/* REST and Admin/Monitor ACL list */\nstatic GList *janus_http_access_list = NULL, *janus_http_admin_access_list = NULL;\nstatic gboolean janus_http_check_xff = FALSE, janus_http_admin_check_xff = FALSE;\nstatic janus_mutex access_list_mutex = JANUS_MUTEX_INITIALIZER;\nstatic void janus_http_allow_address(const char *ip, gboolean admin) {\n\tif(ip == NULL)\n\t\treturn;\n\t/* Is this an IP or an interface? */\n\tjanus_mutex_lock(&access_list_mutex);\n\tif(!admin)\n\t\tjanus_http_access_list = g_list_append(janus_http_access_list, (gpointer)ip);\n\telse\n\t\tjanus_http_admin_access_list = g_list_append(janus_http_admin_access_list, (gpointer)ip);\n\tjanus_mutex_unlock(&access_list_mutex);\n}\nstatic gboolean janus_http_is_allowed(const char *ip, gboolean admin) {\n\tif(ip == NULL)\n\t\treturn FALSE;\n\tjanus_mutex_lock(&access_list_mutex);\n\tif(!admin && janus_http_access_list == NULL) {\n\t\tjanus_mutex_unlock(&access_list_mutex);\n\t\treturn TRUE;\n\t}\n\tif(admin && janus_http_admin_access_list == NULL) {\n\t\tjanus_mutex_unlock(&access_list_mutex);\n\t\treturn TRUE;\n\t}\n\tGList *temp = admin ? janus_http_admin_access_list : janus_http_access_list;\n\twhile(temp) {\n\t\tconst char *allowed = (const char *)temp->data;\n\t\tif(allowed != NULL && strstr(ip, allowed)) {\n\t\t\tjanus_mutex_unlock(&access_list_mutex);\n\t\t\treturn TRUE;\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tjanus_mutex_unlock(&access_list_mutex);\n\treturn FALSE;\n}\n\n/* Helper method to get the port from a struct sockaddr */\nstatic uint16_t janus_http_sockaddr_to_port(struct sockaddr *address) {\n\tif(address == NULL)\n\t\treturn 0;\n\tstruct sockaddr_in *sin = NULL;\n\tstruct sockaddr_in6 *sin6 = NULL;\n\n\tswitch(address->sa_family) {\n\t\tcase AF_INET:\n\t\t\tsin = (struct sockaddr_in *)address;\n\t\t\treturn ntohs(sin->sin_port);\n\t\tcase AF_INET6:\n\t\t\tsin6 = (struct sockaddr_in6 *)address;\n\t\t\treturn ntohs(sin6->sin6_port);\n\t\tdefault:\n\t\t\t/* Unknown family */\n\t\t\tbreak;\n\t}\n\treturn 0;\n}\n\n/* Random string helper (for transactions) */\nstatic char charset[] = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\nstatic void janus_http_random_string(int length, char *buffer) {\n\tif(length > 0 && buffer) {\n\t\tint l = (int)(sizeof(charset)-1);\n\t\tint i=0;\n\t\tfor(i=0; i<length; i++) {\n\t\t\tint key = janus_random_uint32() % l;\n\t\t\tbuffer[i] = charset[key];\n\t\t}\n\t\tbuffer[length-1] = '\\0';\n\t}\n}\n\n/* Event loop for timeout monitoring purposes */\nstatic GThread *httptimer = NULL;\nstatic GMainLoop *httploop = NULL;\nstatic GMainContext *httpctx = NULL;\nstatic gpointer janus_http_timer(gpointer user_data) {\n\tJANUS_LOG(LOG_INFO, \"HTTP transport timer started\\n\");\n\tg_main_loop_run(httploop);\n\treturn NULL;\n}\n\n\n/* Helper to create a MHD daemon */\nstatic struct MHD_Daemon *janus_http_create_daemon(gboolean admin, char *path,\n\t\tconst char *interface, const char *ip, int port,\n\t\tconst char *server_pem, const char *server_key, const char *password, const char *ciphers) {\n\tstruct MHD_Daemon *daemon = NULL;\n\tgboolean secure = server_pem && server_key;\n\t/* Any interface or IP address we need to limit ourselves to?\n\t * NOTE WELL: specifying an interface does NOT bind to all IPs associated\n\t * with that interface, but only to the first one that's detected */\n\tstatic struct sockaddr_in addr = { 0 };\n\tstruct sockaddr_in6 addr6 = { 0 };\n\tgboolean ipv6 = FALSE;\n\tif(ip && strstr(ip, \":\"))\n\t\tipv6 = TRUE;\n\tgboolean found = FALSE;\n\tif(ip) {\n\t\t/* Do a quick check to see if we need to bind on all addresses of a specific family */\n\t\tif(!strcasecmp(ip, \"0.0.0.0\")) {\n\t\t\t/* Bind on all IPv4 addresses */\n\t\t\tfound = TRUE;\n\t\t\tmemset(&addr, 0, sizeof (struct sockaddr_in));\n\t\t\taddr.sin_family = AF_INET;\n\t\t\taddr.sin_port = htons(port);\n\t\t\taddr.sin_addr.s_addr = INADDR_ANY;\n\t\t} else if(!strcasecmp(ip, \"::\")) {\n\t\t\t/* Bind on all IPv6 addresses */\n\t\t\tfound = TRUE;\n\t\t\tmemset(&addr6, 0, sizeof (struct sockaddr_in6));\n\t\t\taddr6.sin6_family = AF_INET6;\n\t\t\taddr6.sin6_port = htons(port);\n\t\t\taddr6.sin6_addr = in6addr_any;\n\t\t}\n\t}\n\tif(!found && (ip || interface)) {\n\t\tstruct ifaddrs *ifaddr = NULL, *ifa = NULL;\n\t\tint family = 0, s = 0, n = 0;\n\t\tchar host[NI_MAXHOST];\n\t\tif(getifaddrs(&ifaddr) == -1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error getting list of interfaces to bind %s API %s webserver...\\n\",\n\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\");\n\t\t\treturn NULL;\n\t\t} else {\n\t\t\tfor(ifa = ifaddr, n = 0; ifa != NULL; ifa = ifa->ifa_next, n++) {\n\t\t\t\tif(ifa->ifa_addr == NULL)\n\t\t\t\t\tcontinue;\n\t\t\t\tfamily = ifa->ifa_addr->sa_family;\n\t\t\t\tif(interface && strcasecmp(ifa->ifa_name, interface))\n\t\t\t\t\tcontinue;\n\t\t\t\tif(ifa->ifa_addr == NULL)\n\t\t\t\t\tcontinue;\n\t\t\t\t/* Skip interfaces which are not up and running */\n\t\t\t\tif(!((ifa->ifa_flags & IFF_UP) && (ifa->ifa_flags & IFF_RUNNING)))\n\t\t\t\t\tcontinue;\n\t\t\t\t/* FIXME When being explicit about the interface only, we only bind IPv4 for now:\n\t\t\t\t * specifying or adding a precise IPv6 address gets you an IPv6 binding instead */\n\t\t\t\tif(!ipv6 && family == AF_INET) {\n\t\t\t\t\ts = getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);\n\t\t\t\t\tif(s != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error doing a getnameinfo() to bind %s API %s webserver to '%s'...\\n\",\n\t\t\t\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\", ip ? ip : interface);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t\tif(ip && strcmp(host, ip))\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tfound = TRUE;\n\t\t\t\t\tbreak;\n\t\t\t\t} else if(ipv6 && family == AF_INET6) {\n\t\t\t\t\ts = getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);\n\t\t\t\t\tif(s != 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error doing a getnameinfo() to bind %s API %s webserver to '%s'...\\n\",\n\t\t\t\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\", ip ? ip : interface);\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\t}\n\t\t\t\t\tif(ip && strcmp(host, ip))\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tfound = TRUE;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfreeifaddrs(ifaddr);\n\t\t}\n\t\tif(!found) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error binding to %s '%s' for %s API %s webserver...\\n\",\n\t\t\t\tip ? \"IP\" : \"interface\", ip ? ip : interface,\n\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\");\n\t\t\treturn NULL;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Going to bind the %s API %s webserver to %s (asked for %s)\\n\",\n\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\", host, ip ? ip : interface);\n\t\tif(!ipv6) {\n\t\t\tmemset(&addr, 0, sizeof(struct sockaddr_in));\n\t\t\taddr.sin_family = AF_INET;\n\t\t\taddr.sin_port = htons(port);\n\t\t\tint res = inet_pton(AF_INET, host, &addr.sin_addr);\n\t\t\tif(res != 1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to convert address '%s' (%d)\\n\", host, res);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t} else {\n\t\t\tmemset(&addr6, 0, sizeof(struct sockaddr_in6));\n\t\t\taddr6.sin6_family = AF_INET6;\n\t\t\taddr6.sin6_port = htons(port);\n\t\t\tint res = inet_pton(AF_INET6, host, &addr6.sin6_addr);\n\t\t\tif(res != 1) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to convert address '%s' (%d)\\n\", host, res);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t}\n\t}\n\n\tif(!secure) {\n\t\t/* HTTP web server */\n\t\tif(!interface && !ip) {\n\t\t\t/* Bind to all interfaces */\n\t\t\tJANUS_LOG(LOG_VERB, \"Binding to all interfaces for the %s API %s webserver\\n\",\n\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\");\n\t\t\tdaemon = MHD_start_daemon(\n\t\t\t\tMHD_USE_AUTO_INTERNAL_THREAD | MHD_USE_AUTO | MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK | mhd_debug_flag,\n\t\t\t\tport,\n\t\t\t\tadmin ? &janus_http_admin_client_connect : &janus_http_client_connect,\n\t\t\t\tNULL,\n\t\t\t\tadmin ? &janus_http_admin_handler : &janus_http_handler,\n\t\t\t\tpath,\n\t\t\t\tMHD_OPTION_NOTIFY_COMPLETED, &janus_http_request_completed, NULL,\n\t\t\t\tMHD_OPTION_CONNECTION_TIMEOUT, 120,\n\t\t\t\tMHD_OPTION_CONNECTION_LIMIT, connection_limit,\n\t\t\t\tMHD_OPTION_END);\n\t\t} else {\n\t\t\t/* Bind to the interface that was specified */\n\t\t\tJANUS_LOG(LOG_VERB, \"Binding to %s '%s' for the %s API %s webserver\\n\",\n\t\t\t\tip ? \"IP\" : \"interface\", ip ? ip : interface,\n\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\");\n\t\t\tdaemon = MHD_start_daemon(\n\t\t\t\tMHD_USE_AUTO_INTERNAL_THREAD | MHD_USE_AUTO | MHD_USE_SUSPEND_RESUME | (ipv6 ? MHD_USE_IPv6 : 0) | mhd_debug_flag,\n\t\t\t\tport,\n\t\t\t\tadmin ? &janus_http_admin_client_connect : &janus_http_client_connect,\n\t\t\t\tNULL,\n\t\t\t\tadmin ? &janus_http_admin_handler : &janus_http_handler,\n\t\t\t\tpath,\n\t\t\t\tMHD_OPTION_NOTIFY_COMPLETED, &janus_http_request_completed, NULL,\n\t\t\t\tMHD_OPTION_SOCK_ADDR, ipv6 ? (struct sockaddr *)&addr6 : (struct sockaddr *)&addr,\n\t\t\t\tMHD_OPTION_CONNECTION_TIMEOUT, 120,\n\t\t\t\tMHD_OPTION_CONNECTION_LIMIT, connection_limit,\n\t\t\t\tMHD_OPTION_END);\n\t\t}\n\t} else {\n\t\t/* HTTPS web server, read certificate and key */\n\t\tg_file_get_contents(server_pem, &cert_pem_bytes, NULL, NULL);\n\t\tg_file_get_contents(server_key, &cert_key_bytes, NULL, NULL);\n\n\t\t/* Start webserver */\n\t\tif(!interface && !ip) {\n\t\t\t/* Bind to all interfaces */\n\t\t\tJANUS_LOG(LOG_VERB, \"Binding to all interfaces for the %s API %s webserver\\n\",\n\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\");\n\t\t\tdaemon = MHD_start_daemon(\n\t\t\t\tMHD_USE_SSL | MHD_USE_AUTO_INTERNAL_THREAD | MHD_USE_AUTO | MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK | mhd_debug_flag,\n\t\t\t\tport,\n\t\t\t\tadmin ? &janus_http_admin_client_connect : &janus_http_client_connect,\n\t\t\t\tNULL,\n\t\t\t\tadmin ? &janus_http_admin_handler : &janus_http_handler,\n\t\t\t\tpath,\n\t\t\t\tMHD_OPTION_NOTIFY_COMPLETED, &janus_http_request_completed, NULL,\n\t\t\t\tMHD_OPTION_HTTPS_PRIORITIES, ciphers,\n\t\t\t\tMHD_OPTION_HTTPS_MEM_CERT, cert_pem_bytes,\n\t\t\t\tMHD_OPTION_HTTPS_MEM_KEY, cert_key_bytes,\n\t\t\t\tMHD_OPTION_HTTPS_KEY_PASSWORD, password,\n\t\t\t\tMHD_OPTION_CONNECTION_TIMEOUT, 120,\n\t\t\t\tMHD_OPTION_CONNECTION_LIMIT, connection_limit,\n\t\t\t\tMHD_OPTION_END);\n\t\t} else {\n\t\t\t/* Bind to the interface that was specified */\n\t\t\tJANUS_LOG(LOG_VERB, \"Binding to %s '%s' for the %s API %s webserver\\n\",\n\t\t\t\tip ? \"IP\" : \"interface\", ip ? ip : interface,\n\t\t\t\tadmin ? \"Admin\" : \"Janus\", secure ? \"HTTPS\" : \"HTTP\");\n\t\t\tdaemon = MHD_start_daemon(\n\t\t\t\tMHD_USE_SSL | MHD_USE_AUTO_INTERNAL_THREAD | MHD_USE_AUTO | MHD_USE_SUSPEND_RESUME | (ipv6 ? MHD_USE_IPv6 : 0) | mhd_debug_flag,\n\t\t\t\tport,\n\t\t\t\tadmin ? &janus_http_admin_client_connect : &janus_http_client_connect,\n\t\t\t\tNULL,\n\t\t\t\tadmin ? &janus_http_admin_handler : &janus_http_handler,\n\t\t\t\tpath,\n\t\t\t\tMHD_OPTION_NOTIFY_COMPLETED, &janus_http_request_completed, NULL,\n\t\t\t\tMHD_OPTION_HTTPS_PRIORITIES, ciphers,\n\t\t\t\tMHD_OPTION_HTTPS_MEM_CERT, cert_pem_bytes,\n\t\t\t\tMHD_OPTION_HTTPS_MEM_KEY, cert_key_bytes,\n\t\t\t\tMHD_OPTION_HTTPS_KEY_PASSWORD, password,\n\t\t\t\tMHD_OPTION_SOCK_ADDR, ipv6 ? (struct sockaddr *)&addr6 : (struct sockaddr *)&addr,\n\t\t\t\tMHD_OPTION_CONNECTION_TIMEOUT, 120,\n\t\t\t\tMHD_OPTION_CONNECTION_LIMIT, connection_limit,\n\t\t\t\tMHD_OPTION_END);\n\t\t}\n\t}\n\treturn daemon;\n}\n\n/* Static helper method to fill in the CORS headers */\nstatic void janus_http_add_cors_headers(janus_http_msg *msg, struct MHD_Response *response) {\n\tif(msg == NULL || g_atomic_int_get(&msg->destroyed) || response == NULL)\n\t\treturn;\n\tjanus_refcount_increase(&msg->ref);\n\tMHD_add_response_header(response, \"Access-Control-Allow-Origin\", allow_origin ? allow_origin : \"*\");\n\tif(allow_origin) {\n\t\t/* We need these two headers as well, in case Access-Control-Allow-Origin is custom */\n\t\tMHD_add_response_header(response, \"Access-Control-Allow-Credentials\", \"true\");\n\t\tMHD_add_response_header(response, \"Vary\", \"Origin\");\n\t}\n\tMHD_add_response_header(response, \"Access-Control-Max-Age\", \"86400\");\n\tif(msg->acrm)\n\t\tMHD_add_response_header(response, \"Access-Control-Allow-Methods\", msg->acrm);\n\tif(msg->acrh)\n\t\tMHD_add_response_header(response, \"Access-Control-Allow-Headers\", msg->acrh);\n\tjanus_refcount_decrease(&msg->ref);\n}\n\n/* Static callback that we register to */\nstatic void janus_http_mhd_panic(void *cls, const char *file, unsigned int line, const char *reason) {\n\tJANUS_LOG(LOG_WARN, \"[%s]: Error in GNU libmicrohttpd %s:%u: %s\\n\",\n\t\tJANUS_HTTP_PACKAGE, file, line, reason);\n}\n\n/* Transport implementation */\nint janus_http_init(janus_transport_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Register a function to detect problems for which MHD would typically abort */\n\tMHD_set_panic_func(&janus_http_mhd_panic, NULL);\n\n\t/* Start the timer */\n\thttpctx = g_main_context_new();\n\thttploop = g_main_loop_new(httpctx, FALSE);\n\tGError *error = NULL;\n\thttptimer = g_thread_try_new(\"http timer\", &janus_http_timer, NULL, &error);\n\tif(error != NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to start HTTP timer...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_HTTP_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_HTTP_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_HTTP_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_category *config_admin = janus_config_get_create(config, NULL, janus_config_type_category, \"admin\");\n\t\tjanus_config_category *config_cors = janus_config_get_create(config, NULL, janus_config_type_category, \"cors\");\n\t\tjanus_config_category *config_certs = janus_config_get_create(config, NULL, janus_config_type_category, \"certificates\");\n\n\t\t/* Handle configuration */\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\t\tif(item && item->value) {\n\t\t\t/* Check how we need to format/serialize the JSON output */\n\t\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t}\n\t\t}\n\n\t\t/* Check if we need to send events to handlers */\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_HTTP_NAME);\n\t\t}\n\n\t\t/* Check the base paths */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"base_path\");\n\t\tif(item && item->value) {\n\t\t\tif(item->value[0] != '/') {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Invalid base path %s (it should start with a /, e.g., /janus\\n\", item->value);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tws_path = g_strdup(item->value);\n\t\t\tif(strnlen(ws_path, 1 + 1) > 1 && ws_path[strlen(ws_path)-1] == '/') {\n\t\t\t\t/* Remove the trailing slash, it makes things harder when we parse requests later */\n\t\t\t\tws_path[strlen(ws_path)-1] = '\\0';\n\t\t\t}\n\t\t} else {\n\t\t\tws_path = g_strdup(\"/janus\");\n\t\t}\n\t\t/* Do the same for the admin/monitor interface */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_base_path\");\n\t\tif(item && item->value) {\n\t\t\tif(item->value[0] != '/') {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Invalid admin/monitor base path %s (it should start with a /, e.g., /admin\\n\", item->value);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tadmin_ws_path = g_strdup(item->value);\n\t\t\tif(strnlen(admin_ws_path, 1 + 1) > 1 && admin_ws_path[strlen(admin_ws_path)-1] == '/') {\n\t\t\t\t/* Remove the trailing slash, it makes things harder when we parse requests later */\n\t\t\t\tadmin_ws_path[strlen(admin_ws_path)-1] = '\\0';\n\t\t\t}\n\t\t} else {\n\t\t\tadmin_ws_path = g_strdup(\"/admin\");\n\t\t}\n\t\t/* Check the open connections limit for mhd */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"mhd_connection_limit\");\n\t\tif(item && item->value && janus_string_to_uint32(item->value, &connection_limit) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid mhd_connection_limit (%s), falling back to default\\n\", item->value);\n\t\t\tconnection_limit = DEFAULT_CONNECTION_LIMIT;\n\t\t}\n\t\t/* Should we set the debug flag in libmicrohttpd? */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"mhd_debug\");\n\t\tif(item && item->value && janus_is_true(item->value))\n\t\t\tmhd_debug_flag = MHD_USE_DEBUG;\n\n\t\t/* Any ACL for either the Janus or Admin API? */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"acl\");\n\t\tif(item && item->value) {\n\t\t\tchar **list = g_strsplit(item->value, \",\", -1);\n\t\t\tchar *index = list[0];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\tif(strlen(index) > 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Adding '%s' to the Janus API allowed list...\\n\", index);\n\t\t\t\t\t\tjanus_http_allow_address(g_strdup(index), FALSE);\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_strfreev(list);\n\t\t\tlist = NULL;\n\t\t\t/* Check if we should use the value of X-Forwarded-For for checks too */\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"acl_forwarded\");\n\t\t\tif(item && item->value)\n\t\t\t\tjanus_http_check_xff = janus_is_true(item->value);\n\t\t}\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_acl\");\n\t\tif(item && item->value) {\n\t\t\tchar **list = g_strsplit(item->value, \",\", -1);\n\t\t\tchar *index = list[0];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\tif(strlen(index) > 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Adding '%s' to the Admin/monitor allowed list...\\n\", index);\n\t\t\t\t\t\tjanus_http_allow_address(g_strdup(index), TRUE);\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_strfreev(list);\n\t\t\tlist = NULL;\n\t\t\t/* Check if we should use the value of X-Forwarded-For for checks too */\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"admin_acl_forwarded\");\n\t\t\tif(item && item->value)\n\t\t\t\tjanus_http_admin_check_xff = janus_is_true(item->value);\n\t\t}\n\n\t\t/* Any custom value for the Access-Control-Allow-Origin header? */\n\t\titem = janus_config_get(config, config_cors, janus_config_type_item, \"allow_origin\");\n\t\tif(item && item->value) {\n\t\t\tallow_origin = g_strdup(item->value);\n\t\t\tJANUS_LOG(LOG_INFO, \"Restricting Access-Control-Allow-Origin to '%s'\\n\", allow_origin);\n\t\t}\n\t\tif(allow_origin != NULL) {\n\t\t\titem = janus_config_get(config, config_cors, janus_config_type_item, \"enforce_cors\");\n\t\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\t\tenforce_cors = TRUE;\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Going to enforce CORS by 403 errors\\n\");\n\t\t\t}\n\t\t}\n\n\t\t/* Start with the Janus API web server now */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"http\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"HTTP webserver disabled\\n\");\n\t\t} else {\n\t\t\tuint16_t wsport = 8088;\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"port\");\n\t\t\tif(item && item->value && janus_string_to_uint16(item->value, &wsport) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s), falling back to default\\n\", item->value);\n\t\t\t\twsport = 8088;\n\t\t\t}\n\t\t\tconst char *interface = NULL;\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"interface\");\n\t\t\tif(item && item->value)\n\t\t\t\tinterface = item->value;\n\t\t\tconst char *ip = NULL;\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ip\");\n\t\t\tif(item && item->value)\n\t\t\t\tip = item->value;\n\t\t\tws = janus_http_create_daemon(FALSE, ws_path, interface, ip, wsport,\n\t\t\t\tNULL, NULL, NULL, NULL);\n\t\t\tif(ws == NULL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Couldn't start webserver on port %d...\\n\", wsport);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"HTTP webserver started (port %d, %s path listener)...\\n\", wsport, ws_path);\n\t\t\t}\n\t\t}\n\t\t/* Do we also have to provide an HTTPS one? */\n\t\tchar *server_pem = NULL;\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_pem\");\n\t\tif(item && item->value)\n\t\t\tserver_pem = (char *)item->value;\n\t\tchar *server_key = NULL;\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_key\");\n\t\tif(item && item->value)\n\t\t\tserver_key = (char *)item->value;\n\t\tchar *password = NULL;\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_pwd\");\n\t\tif(item && item->value)\n\t\t\tpassword = (char *)item->value;\n\t\tconst char *ciphers = \"NORMAL\";\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"ciphers\");\n\t\tif(item && item->value)\n\t\t\tciphers = item->value;\n\t\tif(server_key)\n\t\t\tJANUS_LOG(LOG_VERB, \"Using certificates:\\n\\t%s\\n\\t%s\\n\", server_pem, server_key);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"https\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"HTTPS webserver disabled\\n\");\n\t\t} else {\n\t\t\tif(!server_key || !server_pem) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Missing certificate/key path\\n\");\n\t\t\t} else {\n\t\t\t\tuint16_t swsport = 8089;\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"secure_port\");\n\t\t\t\tif(item && item->value && janus_string_to_uint16(item->value, &swsport) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s), falling back to default\\n\", item->value);\n\t\t\t\t\tswsport = 8089;\n\t\t\t\t}\n\t\t\t\tconst char *interface = NULL;\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"secure_interface\");\n\t\t\t\tif(item && item->value)\n\t\t\t\t\tinterface = item->value;\n\t\t\t\tconst char *ip = NULL;\n\t\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"secure_ip\");\n\t\t\t\tif(item && item->value)\n\t\t\t\t\tip = item->value;\n\t\t\t\tsws = janus_http_create_daemon(FALSE, ws_path, interface, ip, swsport,\n\t\t\t\t\tserver_pem, server_key, password, ciphers);\n\t\t\t\tif(sws == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_FATAL, \"Couldn't start secure webserver on port %d...\\n\", swsport);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"HTTPS webserver started (port %d, %s path listener)...\\n\", swsport, ws_path);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Admin/monitor time: start web server, if enabled */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_http\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Admin/monitor HTTP webserver disabled\\n\");\n\t\t} else {\n\t\t\tuint16_t wsport = 7088;\n\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_port\");\n\t\t\tif(item && item->value && janus_string_to_uint16(item->value, &wsport) < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s), falling back to default\\n\", item->value);\n\t\t\t\twsport = 7088;\n\t\t\t}\n\t\t\tconst char *interface = NULL;\n\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_interface\");\n\t\t\tif(item && item->value)\n\t\t\t\tinterface = item->value;\n\t\t\tconst char *ip = NULL;\n\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_ip\");\n\t\t\tif(item && item->value)\n\t\t\t\tip = item->value;\n\t\t\tadmin_ws = janus_http_create_daemon(TRUE, admin_ws_path, interface, ip, wsport,\n\t\t\t\tNULL, NULL, NULL, NULL);\n\t\t\tif(admin_ws == NULL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Couldn't start admin/monitor webserver on port %d...\\n\", wsport);\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Admin/monitor HTTP webserver started (port %d, %s path listener)...\\n\", wsport, admin_ws_path);\n\t\t\t}\n\t\t}\n\t\t/* Do we also have to provide an HTTPS one? */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_https\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Admin/monitor HTTPS webserver disabled\\n\");\n\t\t} else {\n\t\t\tif(!server_key) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Missing certificate/key path\\n\");\n\t\t\t} else {\n\t\t\t\tuint16_t swsport = 7889;\n\t\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_secure_port\");\n\t\t\t\tif(item && item->value && janus_string_to_uint16(item->value, &swsport) < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s), falling back to default\\n\", item->value);\n\t\t\t\t\tswsport = 7889;\n\t\t\t\t}\n\t\t\t\tconst char *interface = NULL;\n\t\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_secure_interface\");\n\t\t\t\tif(item && item->value)\n\t\t\t\t\tinterface = item->value;\n\t\t\t\tconst char *ip = NULL;\n\t\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_secure_ip\");\n\t\t\t\tif(item && item->value)\n\t\t\t\t\tip = item->value;\n\t\t\t\tadmin_sws = janus_http_create_daemon(TRUE, admin_ws_path, interface, ip, swsport,\n\t\t\t\t\tserver_pem, server_key, password, ciphers);\n\t\t\t\tif(admin_sws == NULL) {\n\t\t\t\t\tJANUS_LOG(LOG_FATAL, \"Couldn't start secure admin/monitor webserver on port %d...\\n\", swsport);\n\t\t\t\t} else {\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Admin/monitor HTTPS webserver started (port %d, %s path listener)...\\n\",\n\t\t\t\t\t\tswsport, admin_ws_path);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\tif(!ws && !sws && !admin_ws && !admin_sws) {\n\t\tJANUS_LOG(LOG_WARN, \"No HTTP/HTTPS server started, giving up...\\n\");\n\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t}\n\thttp_janus_api_enabled = ws || sws;\n\thttp_admin_api_enabled = admin_ws || admin_sws;\n\n\tmessages = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_transport_session_destroy);\n\tsessions = g_hash_table_new_full(g_int64_hash, g_int64_equal, (GDestroyNotify)g_free, (GDestroyNotify)janus_http_session_destroy);\n\n\t/* Done */\n\tg_atomic_int_set(&initialized, 1);\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_HTTP_NAME);\n\treturn 0;\n}\n\nvoid janus_http_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tg_main_loop_quit(httploop);\n\tg_thread_join(httptimer);\n\tg_main_context_unref(httpctx);\n\n\t/* Resume all pending connections, before stopping the webservers */\n\tjanus_mutex_lock(&messages_mutex);\n\tGHashTableIter iter;\n\tgpointer value;\n\tg_hash_table_iter_init(&iter, messages);\n\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_transport_session *transport = value;\n\t\tjanus_http_msg *msg = (janus_http_msg *)transport->transport_p;\n\t\tif(msg != NULL && !g_atomic_int_get(&msg->destroyed)) {\n\t\t\tMHD_resume_connection(msg->connection);\n\t\t}\n\t}\n\tjanus_mutex_unlock(&messages_mutex);\n\n\tJANUS_LOG(LOG_INFO, \"Stopping webserver(s)...\\n\");\n\tif(ws)\n\t\tMHD_stop_daemon(ws);\n\tws = NULL;\n\tif(sws)\n\t\tMHD_stop_daemon(sws);\n\tsws = NULL;\n\tif(admin_ws)\n\t\tMHD_stop_daemon(admin_ws);\n\tadmin_ws = NULL;\n\tif(admin_sws)\n\t\tMHD_stop_daemon(admin_sws);\n\tadmin_sws = NULL;\n\tg_free((gpointer)cert_pem_bytes);\n\tcert_pem_bytes = NULL;\n\tg_free((gpointer)cert_key_bytes);\n\tcert_key_bytes = NULL;\n\tg_free(allow_origin);\n\tallow_origin = NULL;\n\n\tjanus_mutex_lock(&messages_mutex);\n\tg_hash_table_destroy(messages);\n\tmessages = NULL;\n\tjanus_mutex_unlock(&messages_mutex);\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_destroy(sessions);\n\tsessions = NULL;\n\tjanus_mutex_unlock(&sessions_mutex);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_HTTP_NAME);\n}\n\nint janus_http_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_TRANSPORT_API_VERSION;\n}\n\nint janus_http_get_version(void) {\n\treturn JANUS_HTTP_VERSION;\n}\n\nconst char *janus_http_get_version_string(void) {\n\treturn JANUS_HTTP_VERSION_STRING;\n}\n\nconst char *janus_http_get_description(void) {\n\treturn JANUS_HTTP_DESCRIPTION;\n}\n\nconst char *janus_http_get_name(void) {\n\treturn JANUS_HTTP_NAME;\n}\n\nconst char *janus_http_get_author(void) {\n\treturn JANUS_HTTP_AUTHOR;\n}\n\nconst char *janus_http_get_package(void) {\n\treturn JANUS_HTTP_PACKAGE;\n}\n\ngboolean janus_http_is_janus_api_enabled(void) {\n\treturn http_janus_api_enabled;\n}\n\ngboolean janus_http_is_admin_api_enabled(void) {\n\treturn http_admin_api_enabled;\n}\n\nint janus_http_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message) {\n\tJANUS_LOG(LOG_HUGE, \"Got a %s API %s to send (%p)\\n\", admin ? \"admin\" : \"Janus\", request_id ? \"response\" : \"event\", transport);\n\tif(message == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No message...\\n\");\n\t\treturn -1;\n\t}\n\tif(request_id == NULL) {\n\t\t/* This is an event, add to the session queue */\n\t\tjson_t *s = json_object_get(message, \"session_id\");\n\t\tif(!s || !json_is_integer(s)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't notify event, no session_id...\\n\");\n\t\t\tjson_decref(message);\n\t\t\treturn -1;\n\t\t}\n\t\tguint64 session_id = json_integer_value(s);\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_http_session *session = g_hash_table_lookup(sessions, &session_id);\n\t\tif(session == NULL || g_atomic_int_get(&session->destroyed)) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't notify event, no session object...\\n\");\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tjson_decref(message);\n\t\t\treturn -1;\n\t\t}\n\t\tjanus_refcount_increase(&session->ref);\n\t\tg_async_queue_push(session->events, message);\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* Are there long polls waiting? */\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tjanus_http_msg *msg = NULL;\n\t\twhile(session->longpolls) {\n\t\t\ttransport = (janus_transport_session *)session->longpolls->data;\n\t\t\tmsg = (janus_http_msg *)(transport ? transport->transport_p : NULL);\n\t\t\t/* Is this connection ready to send a response back? */\n\t\t\tif(msg && g_atomic_pointer_compare_and_exchange(&msg->longpoll, (volatile void *)session, NULL)) {\n\t\t\t\tjanus_refcount_increase(&msg->ref);\n\t\t\t\t/* Send the events back */\n\t\t\t\tif(g_atomic_int_compare_and_exchange(&msg->timeout_flag, 1, 0)) {\n\t\t\t\t\tg_source_destroy(msg->timeout);\n\t\t\t\t\tg_source_unref(msg->timeout);\n\t\t\t\t}\n\t\t\t\tmsg->timeout = NULL;\n\t\t\t\tjanus_http_notifier(msg);\n\t\t\t\tjanus_refcount_decrease(&msg->ref);\n\t\t\t}\n\t\t\tsession->longpolls = g_list_remove(session->longpolls, transport);\n\t\t}\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\tjanus_refcount_decrease(&session->ref);\n\t} else {\n\t\tif(request_id == keepalive_id) {\n\t\t\t/* It's a response from our fake long-poll related keepalive, ignore */\n\t\t\tjson_decref(message);\n\t\t\treturn 0;\n\t\t}\n\t\t/* This is a response, we need a valid transport instance */\n\t\tif(transport == NULL || transport->transport_p == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid HTTP instance...\\n\");\n\t\t\tjson_decref(message);\n\t\t\treturn -1;\n\t\t}\n\t\t/* We have a response */\n\t\tjanus_transport_session *session = (janus_transport_session *)transport;\n\t\tjanus_mutex_lock(&messages_mutex);\n\t\tif(g_hash_table_lookup(messages, session) == NULL) {\n\t\t\tjanus_mutex_unlock(&messages_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid HTTP connection...\\n\");\n\t\t\tjson_decref(message);\n\t\t\treturn -1;\n\t\t}\n\t\tjanus_http_msg *msg = (janus_http_msg *)transport->transport_p;\n\t\tjanus_refcount_increase(&msg->ref);\n\t\tjanus_mutex_unlock(&messages_mutex);\n\t\tif(!msg->connection) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid HTTP connection...\\n\");\n\t\t\tjson_decref(message);\n\t\t\tjanus_refcount_decrease(&msg->ref);\n\t\t\treturn -1;\n\t\t}\n\t\tif(g_atomic_int_compare_and_exchange(&msg->timeout_flag, 1, 0)) {\n\t\t\tg_source_destroy(msg->timeout);\n\t\t\tg_source_unref(msg->timeout);\n\t\t}\n\t\tmsg->timeout = NULL;\n\t\tchar *response_text = json_dumps(message, json_format);\n\t\tjson_decref(message);\n\t\tif(response_text == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\tjanus_refcount_decrease(&msg->ref);\n\t\t\treturn -1;\n\t\t}\n\t\tmsg->response = response_text;\n\t\tmsg->resplen = strlen(response_text);\n\t\tMHD_resume_connection(msg->connection);\n\t\tjanus_refcount_decrease(&msg->ref);\n\t}\n\treturn 0;\n}\n\nvoid janus_http_session_created(janus_transport_session *transport, guint64 session_id) {\n\tif(transport == NULL || transport->transport_p == NULL)\n\t\treturn;\n\tJANUS_LOG(LOG_VERB, \"Session created (%\"SCNu64\"), create a queue for the long poll\\n\", session_id);\n\t/* Create a queue of events for this session */\n\tjanus_mutex_lock(&sessions_mutex);\n\tif(g_hash_table_lookup(sessions, &session_id) != NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Ignoring created session, apparently we're already handling it?\\n\");\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\treturn;\n\t}\n\tjanus_http_session *session = g_malloc(sizeof(janus_http_session));\n\tsession->session_id = session_id;\n\tsession->events = g_async_queue_new();\n\tsession->longpolls = NULL;\n\tjanus_mutex_init(&session->mutex);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tjanus_refcount_init(&session->ref, janus_http_session_free);\n\tg_hash_table_insert(sessions, janus_uint64_dup(session_id), session);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nvoid janus_http_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed) {\n\tJANUS_LOG(LOG_VERB, \"Session %s %s (%\"SCNu64\"), getting rid of the queue for the long poll\\n\",\n\t\ttimeout ? \"has timed out\" : \"is over\",\n\t\tclaimed ? \"but has been claimed\" : \"and has not been claimed\", session_id);\n\t/* Get rid of the session's queue of events */\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_remove(sessions, &session_id);\n\tjanus_mutex_unlock(&sessions_mutex);\n}\n\nvoid janus_http_session_claimed(janus_transport_session *transport, guint64 session_id) {\n\t/* Session has been claimed -- is there anything to do here? */\n\tJANUS_LOG(LOG_VERB, \"Session has been claimed: (%\"SCNu64\"), adding to hash table\\n\", session_id);\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_http_session *old_session = g_hash_table_lookup(sessions, &session_id);\n\tif(old_session != NULL)\n\t\tjanus_refcount_increase(&old_session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tjanus_http_session *session = g_malloc(sizeof(janus_http_session));\n\tsession->session_id = session_id;\n\tsession->events = g_async_queue_new();\n\tsession->longpolls = NULL;\n\tjanus_mutex_init(&session->mutex);\n\tg_atomic_int_set(&session->destroyed, 0);\n\tjanus_refcount_init(&session->ref, janus_http_session_free);\n\tjanus_mutex_lock(&sessions_mutex);\n\tg_hash_table_insert(sessions, janus_uint64_dup(session_id), session);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tif(old_session == NULL)\n\t\treturn;\n\t/* Were there long polls waiting? */\n\tjanus_mutex_lock(&old_session->mutex);\n\tjanus_http_msg *msg = NULL;\n\twhile(old_session->longpolls) {\n\t\ttransport = (janus_transport_session *)old_session->longpolls->data;\n\t\tmsg = (janus_http_msg *)(transport ? transport->transport_p : NULL);\n\t\tif(msg != NULL) {\n\t\t\tjanus_refcount_increase(&msg->ref);\n\t\t\tif(g_atomic_int_compare_and_exchange(&msg->timeout_flag, 1, 0)) {\n\t\t\t\tg_source_destroy(msg->timeout);\n\t\t\t\tg_source_unref(msg->timeout);\n\t\t\t}\n\t\t\tmsg->timeout = NULL;\n\t\t\tif(g_atomic_pointer_compare_and_exchange(&msg->longpoll, (volatile void *)session, NULL)) {\n\t\t\t\t/* Return an error on the long poll right away */\n\t\t\t\tjanus_http_timeout(transport, old_session, FALSE);\n\t\t\t}\n\t\t\tjanus_refcount_decrease(&msg->ref);\n\t\t}\n\t\tsession->longpolls = g_list_remove(old_session->longpolls, transport);\n\t}\n\tjanus_mutex_unlock(&old_session->mutex);\n\tjanus_refcount_decrease(&old_session->ref);\n}\n\njson_t *janus_http_query_transport(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this request to dynamically change the behaviour of\n\t * the transport plugin, and/or query for some specific information */\n\tjson_t *response = json_object();\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_HTTP_ERROR_MISSING_ELEMENT, JANUS_HTTP_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"configure\")) {\n\t\t/* We only allow for the configuration of some basic properties:\n\t\t * changing more complex things (e.g., port to bind to, etc.)\n\t\t * would likely require restarting backends, so just too much */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, configure_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_HTTP_ERROR_MISSING_ELEMENT, JANUS_HTTP_ERROR_INVALID_ELEMENT);\n\t\t/* Check if we now need to send events to handlers */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjson_t *notes = NULL;\n\t\tgboolean events = json_is_true(json_object_get(request, \"events\"));\n\t\tif(events && !gateway->events_is_enabled()) {\n\t\t\t/* Notify that this will be ignored */\n\t\t\tnotes = json_array();\n\t\t\tjson_array_append_new(notes, json_string(\"Event handlers disabled at the core level\"));\n\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t}\n\t\tif(events != notify_events) {\n\t\t\tnotify_events = events;\n\t\t\tif(!notify_events && gateway->events_is_enabled()) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_HTTP_NAME);\n\t\t\t}\n\t\t}\n\t\tconst char *indentation = json_string_value(json_object_get(request, \"json\"));\n\t\tif(indentation != NULL) {\n\t\t\tif(!strcasecmp(indentation, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', ignoring tweak\\n\", indentation);\n\t\t\t\t/* Notify that this will be ignored */\n\t\t\t\tif(notes == NULL) {\n\t\t\t\t\tnotes = json_array();\n\t\t\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t\t\t}\n\t\t\t\tjson_array_append_new(notes, json_string(\"Ignored unsupported indentation format\"));\n\t\t\t}\n\t\t}\n\t} else if(!strcasecmp(request_text, \"connections\")) {\n\t\t/* Return the number of active connections currently handled by the plugin */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjson_t *connections = json_object();\n\t\tjson_object_set_new(response, \"connections\", connections);\n\t\tif(ws != NULL) {\n\t\t\tconst union MHD_DaemonInfo *info = MHD_get_daemon_info(ws,\n\t\t\t\tMHD_DAEMON_INFO_CURRENT_CONNECTIONS, NULL);\n\t\t\tif(info != NULL)\n\t\t\t\tjson_object_set_new(connections, \"http\", json_integer(info->num_connections));\n\t\t}\n\t\tif(sws != NULL) {\n\t\t\tconst union MHD_DaemonInfo *info = MHD_get_daemon_info(sws,\n\t\t\t\tMHD_DAEMON_INFO_CURRENT_CONNECTIONS, NULL);\n\t\t\tif(info != NULL)\n\t\t\t\tjson_object_set_new(connections, \"https\", json_integer(info->num_connections));\n\t\t}\n\t\tif(admin_ws != NULL) {\n\t\t\tconst union MHD_DaemonInfo *info = MHD_get_daemon_info(admin_ws,\n\t\t\t\tMHD_DAEMON_INFO_CURRENT_CONNECTIONS, NULL);\n\t\t\tif(info != NULL)\n\t\t\t\tjson_object_set_new(connections, \"admin_http\", json_integer(info->num_connections));\n\t\t}\n\t\tif(admin_sws != NULL) {\n\t\t\tconst union MHD_DaemonInfo *info = MHD_get_daemon_info(admin_sws,\n\t\t\t\tMHD_DAEMON_INFO_CURRENT_CONNECTIONS, NULL);\n\t\t\tif(info != NULL)\n\t\t\t\tjson_object_set_new(connections, \"admin_https\", json_integer(info->num_connections));\n\t\t}\n\t\t/* Also add the global number of messages we're serving */\n\t\tjanus_mutex_lock(&messages_mutex);\n\t\tguint count = g_hash_table_size(messages);\n\t\tjanus_mutex_unlock(&messages_mutex);\n\t\tjson_object_set_new(response, \"messages\", json_integer(count));\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_HTTP_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n/* Connection notifiers */\nstatic janus_MHD_Result janus_http_client_connect(void *cls, const struct sockaddr *addr, socklen_t addrlen) {\n\tjanus_network_address naddr;\n\tjanus_network_address_string_buffer naddr_buf;\n\tif(janus_network_address_from_sockaddr((struct sockaddr *)addr, &naddr) != 0 ||\n\t\t\tjanus_network_address_to_string_buffer(&naddr, &naddr_buf) != 0) {\n\t\tJANUS_LOG(LOG_WARN, \"Error trying to resolve connection address...\\n\");\n\t\t/* Should this be MHD_NO instead? */\n\t\treturn MHD_YES;\n\t}\n\tconst char *ip = janus_network_address_string_from_buffer(&naddr_buf);\n\tJANUS_LOG(LOG_HUGE, \"New connection on REST API: %s\\n\", ip);\n\t/* Any access limitation based on this IP address? */\n\tif(!janus_http_is_allowed(ip, FALSE)) {\n\t\tJANUS_LOG(LOG_ERR, \"IP %s is unauthorized to connect to the Janus API interface\\n\", ip);\n\t\treturn MHD_NO;\n\t}\n\treturn MHD_YES;\n}\n\nstatic janus_MHD_Result janus_http_admin_client_connect(void *cls, const struct sockaddr *addr, socklen_t addrlen) {\n\tjanus_network_address naddr;\n\tjanus_network_address_string_buffer naddr_buf;\n\tif(janus_network_address_from_sockaddr((struct sockaddr *)addr, &naddr) != 0 ||\n\t\t\tjanus_network_address_to_string_buffer(&naddr, &naddr_buf) != 0) {\n\t\tJANUS_LOG(LOG_WARN, \"Error trying to resolve Admin connection address...\\n\");\n\t\t/* Should this be MHD_NO instead? */\n\t\treturn MHD_YES;\n\t}\n\tconst char *ip = janus_network_address_string_from_buffer(&naddr_buf);\n\tJANUS_LOG(LOG_HUGE, \"New connection on admin/monitor: %s\\n\", ip);\n\t/* Any access limitation based on this IP address? */\n\tif(!janus_http_is_allowed(ip, TRUE)) {\n\t\tJANUS_LOG(LOG_ERR, \"IP %s is unauthorized to connect to the admin/monitor interface\\n\", ip);\n\t\treturn MHD_NO;\n\t}\n\treturn MHD_YES;\n}\n\n\n/* WebServer requests handler */\nstatic janus_MHD_Result janus_http_handler(void *cls, struct MHD_Connection *connection,\n\t\tconst char *url, const char *method, const char *version,\n\t\tconst char *upload_data, size_t *upload_data_size, void **ptr) {\n\tif(!g_atomic_int_get(&initialized) || g_atomic_int_get(&stopping))\n\t\treturn MHD_NO;\n\tchar *payload = NULL;\n\tjson_t *root = NULL;\n\tstruct MHD_Response *response = NULL;\n\tint ret = MHD_NO;\n\tchar *session_path = NULL, *handle_path = NULL;\n\tchar **basepath = NULL, **path = NULL;\n\tguint64 session_id = 0, handle_id = 0;\n\n\t/* Is this the first round? */\n\tint firstround = 0;\n\tjanus_transport_session *ts = (janus_transport_session *)*ptr;\n\tjanus_http_msg *msg = NULL;\n\tif(ts == NULL) {\n\t\tfirstround = 1;\n\t\tJANUS_LOG(LOG_DBG, \"Got a HTTP %s request on %s...\\n\", method, url);\n\t\tJANUS_LOG(LOG_DBG, \" ... Just parsing headers for now...\\n\");\n\t\tmsg = g_malloc0(sizeof(janus_http_msg));\n\t\tmsg->connection = connection;\n\t\tjanus_refcount_init(&msg->ref, janus_http_msg_free);\n\t\tts = janus_transport_session_create(msg, janus_http_msg_destroy);\n\t\tjanus_mutex_lock(&messages_mutex);\n\t\tg_hash_table_insert(messages, ts, ts);\n\t\tjanus_mutex_unlock(&messages_mutex);\n\t\t*ptr = ts;\n\t\tMHD_get_connection_values(connection, MHD_HEADER_KIND, &janus_http_headers, msg);\n\t\tif(janus_http_check_xff && msg->xff) {\n\t\t\t/* Any access limitation based on this IP address? */\n\t\t\tif(!janus_http_is_allowed(msg->xff, FALSE)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"IP %s is unauthorized to connect to the Janus API interface\\n\", msg->xff);\n\t\t\t\treturn MHD_NO;\n\t\t\t}\n\t\t}\n\t\tret = MHD_YES;\n\t\t/* Notify handlers about this new transport instance */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"request\"));\n\t\t\tjson_object_set_new(info, \"admin_api\", json_false());\n\t\t\tconst union MHD_ConnectionInfo *conninfo = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS);\n\t\t\tif(conninfo != NULL) {\n\t\t\t\tjanus_network_address addr;\n\t\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\t\tif(janus_network_address_from_sockaddr((struct sockaddr *)conninfo->client_addr, &addr) == 0 &&\n\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\tconst char *ip = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\tjson_object_set_new(info, \"ip\", json_string(ip));\n\t\t\t\t}\n\t\t\t\tuint16_t port = janus_http_sockaddr_to_port((struct sockaddr *)conninfo->client_addr);\n\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t}\n\t\t\tgateway->notify_event(&janus_http_transport, ts, info);\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_DBG, \"Processing HTTP %s request on %s...\\n\", method, url);\n\t\tjanus_refcount_increase(&ts->ref);\n\t\tmsg = (janus_http_msg *)ts->transport_p;\n\t}\n\t/* Parse request */\n\tif(strcasecmp(method, \"GET\") && strcasecmp(method, \"POST\") && strcasecmp(method, \"OPTIONS\")) {\n\t\tif(firstround)\n\t\t\treturn ret;\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_TRANSPORT_SPECIFIC, \"Unsupported method %s\", method);\n\t\tgoto done;\n\t}\n\tif(firstround && enforce_cors && (msg->acro == NULL || strstr(msg->acro, allow_origin) != msg->acro)) {\n\t\t/* Got a request from the wrong origin and we're enforcing CORS with 403 errors */\n\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, response);\n\t\tMHD_destroy_response(response);\n\t\treturn ret;\n\t}\n\tif(!strcasecmp(method, \"OPTIONS\")) {\n\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(connection, MHD_HTTP_OK, response);\n\t\tMHD_destroy_response(response);\n\t}\n\t/* Get path components */\n\tif(strcasecmp(url, ws_path)) {\n\t\tif(strnlen(ws_path, 1 + 1) > 1) {\n\t\t\tbasepath = g_strsplit(url, ws_path, -1);\n\t\t} else {\n\t\t\t/* The base path is the web server too itself, we process the url itself */\n\t\t\tbasepath = g_malloc_n(3, sizeof(char *));\n\t\t\tbasepath[0] = g_strdup(\"/\");\n\t\t\tbasepath[1] = g_strdup(url);\n\t\t\tbasepath[2] = NULL;\n\t\t}\n\t\tif(basepath[0] == NULL || basepath[1] == NULL || basepath[1][0] != '/') {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid url %s\\n\", url);\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\t\tMHD_destroy_response(response);\n\t\t}\n\t\tif(firstround) {\n\t\t\tg_strfreev(basepath);\n\t\t\treturn ret;\n\t\t}\n\t\tpath = g_strsplit(basepath[1], \"/\", -1);\n\t\tif(path == NULL || path[1] == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid path %s (%s)\\n\", basepath[1], path ? path[1] : \"\");\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tg_strfreev(basepath);\n\t\t\tg_strfreev(path);\n\t\t\treturn ret;\n\t\t}\n\t}\n\tif(firstround) {\n\t\tg_strfreev(basepath);\n\t\tg_strfreev(path);\n\t\treturn ret;\n\t}\n\tJANUS_LOG(LOG_DBG, \" ... parsing request...\\n\");\n\tif(path != NULL && path[1] != NULL && strlen(path[1]) > 0) {\n\t\tsession_path = g_strdup(path[1]);\n\t\tJANUS_LOG(LOG_HUGE, \"Session: %s\\n\", session_path);\n\t}\n\tif(session_path != NULL && path[2] != NULL && strlen(path[2]) > 0) {\n\t\thandle_path = g_strdup(path[2]);\n\t\tJANUS_LOG(LOG_HUGE, \"Handle: %s\\n\", handle_path);\n\t}\n\tif(session_path != NULL && handle_path != NULL && path[3] != NULL && strlen(path[3]) > 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Too many components...\\n\");\n\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\tMHD_destroy_response(response);\n\t\tgoto done;\n\t}\n\t/* Get payload, if any */\n\tif(!strcasecmp(method, \"POST\")) {\n\t\tJANUS_LOG(LOG_HUGE, \"Processing POST data (%s) (%zu bytes)...\\n\", msg->contenttype, *upload_data_size);\n\t\tif(*upload_data_size != 0) {\n\t\t\tmsg->payload = g_realloc(msg->payload, msg->len+*upload_data_size+1);\n\t\t\tmemcpy(msg->payload+msg->len, upload_data, *upload_data_size);\n\t\t\tmsg->len += *upload_data_size;\n\t\t\tmemset(msg->payload + msg->len, '\\0', 1);\n\t\t\tJANUS_LOG(LOG_DBG, \"  -- Data we have now (%zu bytes)\\n\", msg->len);\n\t\t\t*upload_data_size = 0;\t/* Go on */\n\t\t\tret = MHD_YES;\n\t\t\tgoto done;\n\t\t}\n\t\tJANUS_LOG(LOG_DBG, \"Done getting payload, we can answer\\n\");\n\t\tif(msg->payload == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No payload :-(\\n\");\n\t\t\tret = MHD_NO;\n\t\t\tgoto done;\n\t\t}\n\t\tpayload = msg->payload;\n\t\tJANUS_LOG(LOG_HUGE, \"%s\\n\", payload);\n\t}\n\n\t/* Is this a generic request for info? */\n\tif(session_path != NULL && !strcmp(session_path, \"info\")) {\n\t\t/* The info REST endpoint, if contacted through a GET, provides information on the Janus core */\n\t\tif(strcasecmp(method, \"GET\")) {\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tgoto done;\n\t\t}\n\t\t/* Turn this into a fake \"info\" request */\n\t\tmethod = \"POST\";\n\t\tchar tr[12];\n\t\tjanus_http_random_string(12, (char *)&tr);\n\t\troot = json_object();\n\t\tjson_object_set_new(root, \"janus\", json_string(\"info\"));\n\t\tjson_object_set_new(root, \"transaction\", json_string(tr));\n\t\tgoto parsingdone;\n\t}\n\t/* Or maybe a ping */\n\tif(session_path != NULL && !strcmp(session_path, \"ping\")) {\n\t\t/* The ping REST endpoint, if contacted through a GET, pings the Janus core */\n\t\tif(strcasecmp(method, \"GET\")) {\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tgoto done;\n\t\t}\n\t\t/* Turn this into a fake \"ping\" request */\n\t\tmethod = \"POST\";\n\t\tchar tr[12];\n\t\tjanus_http_random_string(12, (char *)&tr);\n\t\troot = json_object();\n\t\tjson_object_set_new(root, \"janus\", json_string(\"ping\"));\n\t\tjson_object_set_new(root, \"transaction\", json_string(tr));\n\t\tgoto parsingdone;\n\t}\n\n\t/* Or maybe a long poll */\n\tif(!strcasecmp(method, \"GET\") || !payload) {\n\t\tsession_id = session_path ? g_ascii_strtoull(session_path, NULL, 10) : 0;\n\t\tif(session_id < 1) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid session %s\\n\", session_path);\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tgoto done;\n\t\t}\n\t\tmsg->session_id = session_id;\n\n\t\t/* Since we handle long polls ourselves, the core isn't involved (if not for providing us with events)\n\t\t * A long poll, though, can act as a keepalive, so we pass a fake one to the core to avoid undesirable timeouts */\n\n\t\t/* First of all, though, API secret and token based authentication may be enabled in the core, so since\n\t\t * we're bypassing it for notifications we'll have to check those ourselves */\n\t\tconst char *secret = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, \"apisecret\");\n\t\tconst char *token = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, \"token\");\n\t\tgboolean secret_authorized = FALSE, token_authorized = FALSE;\n\t\tgboolean is_api_secret_needed = gateway->is_api_secret_needed(&janus_http_transport);\n\t\tgboolean is_auth_token_needed = gateway->is_auth_token_needed(&janus_http_transport);\n\t\tif(!is_api_secret_needed && !is_auth_token_needed) {\n\t\t\t/* Nothing to check */\n\t\t\tsecret_authorized = TRUE;\n\t\t\ttoken_authorized = TRUE;\n\t\t} else {\n\t\t\tif(gateway->is_api_secret_valid(&janus_http_transport, secret)) {\n\t\t\t\t/* API secret is valid or disabled */\n\t\t\t\tsecret_authorized = TRUE;\n\t\t\t}\n\t\t\tif(gateway->is_auth_token_valid(&janus_http_transport, token)) {\n\t\t\t\t/* Token is valid or disabled */\n\t\t\t\ttoken_authorized = TRUE;\n\t\t\t}\n\t\t\t/* We consider a request authorized if either the proper API secret or a valid token has been provided */\n\t\t\tif(!(is_api_secret_needed && secret_authorized) && !(is_auth_token_needed && token_authorized)) {\n\t\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\t\tret = MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, response);\n\t\t\t\tMHD_destroy_response(response);\n\t\t\t\tgoto done;\n\t\t\t}\n\t\t}\n\t\t/* Ok, go on with the keepalive */\n\t\tchar tr[12];\n\t\tjanus_http_random_string(12, (char *)&tr);\n\t\troot = json_object();\n\t\tjson_object_set_new(root, \"janus\", json_string(\"keepalive\"));\n\t\tjson_object_set_new(root, \"session_id\", json_integer(session_id));\n\t\tjson_object_set_new(root, \"transaction\", json_string(tr));\n\t\tif(secret)\n\t\t\tjson_object_set_new(root, \"apisecret\", json_string(secret));\n\t\tif(token)\n\t\t\tjson_object_set_new(root, \"token\", json_string(token));\n\t\tgateway->incoming_request(&janus_http_transport, ts, (void *)keepalive_id, FALSE, root, NULL);\n\t\t/* Ok, go on */\n\t\tif(handle_path) {\n\t\t\tchar *location = g_malloc(strlen(ws_path) + strlen(session_path) + 2);\n\t\t\tg_sprintf(location, \"%s/%s\", ws_path, session_path);\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid GET to %s, redirecting to %s\\n\", url, location);\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tMHD_add_response_header(response, \"Location\", location);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, 302, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tg_free(location);\n\t\t\tgoto done;\n\t\t}\n\t\tjanus_mutex_lock(&sessions_mutex);\n\t\tjanus_http_session *session = g_hash_table_lookup(sessions, &session_id);\n\t\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t\tJANUS_LOG(LOG_ERR, \"Couldn't find any session %\"SCNu64\"...\\n\", session_id);\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tgoto done;\n\t\t}\n\t\tjanus_refcount_increase(&session->ref);\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\t/* How many messages can we send back in a single response? (just one by default) */\n\t\tint max_events = 1;\n\t\tconst char *maxev = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, \"maxev\");\n\t\tif(maxev != NULL) {\n\t\t\tmax_events = atoi(maxev);\n\t\t\tif(max_events < 1) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid maxev parameter passed (%d), defaulting to 1\\n\", max_events);\n\t\t\t\tmax_events = 1;\n\t\t\t}\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Session %\"SCNu64\" found... returning up to %d messages\\n\", session_id, max_events);\n\t\t/* Handle GET, taking the first message from the list */\n\t\tjanus_mutex_lock(&session->mutex);\n\t\tjson_t *event = g_async_queue_try_pop(session->events);\n\t\tif(event != NULL) {\n\t\t\tif(max_events == 1) {\n\t\t\t\t/* Return just this message and leave */\n\t\t\t\tchar *event_text = json_dumps(event, json_format);\n\t\t\t\tjson_decref(event);\n\t\t\t\tret = janus_http_return_success(ts, event_text);\n\t\t\t} else {\n\t\t\t\t/* The application is willing to receive more events at the same time, anything to report? */\n\t\t\t\tjson_t *list = json_array();\n\t\t\t\tjson_array_append_new(list, event);\n\t\t\t\tint events = 1;\n\t\t\t\twhile(events < max_events) {\n\t\t\t\t\tevent = g_async_queue_try_pop(session->events);\n\t\t\t\t\tif(event == NULL)\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tjson_array_append_new(list, event);\n\t\t\t\t\tevents++;\n\t\t\t\t}\n\t\t\t\t/* Return the array of messages and leave */\n\t\t\t\tchar *list_text = json_dumps(list, json_format);\n\t\t\t\tjson_decref(list);\n\t\t\t\tret = janus_http_return_success(ts, list_text);\n\t\t\t}\n\t\t} else {\n\t\t\t/* Still no message, wait */\n\t\t\tresponse = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN,\n\t\t\t\t500, &janus_http_response_callback, msg, NULL);\n\t\t\tif(response == NULL) {\n\t\t\t\tret = MHD_NO;\n\t\t\t} else {\n\t\t\t\tg_atomic_int_set(&msg->suspended, 1);\n\t\t\t\tMHD_suspend_connection(msg->connection);\n\t\t\t\tMHD_add_response_header(response, \"Content-Type\", \"application/json\");\n\t\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\t\tret = MHD_queue_response(msg->connection, MHD_HTTP_OK, response);\n\t\t\t\tMHD_destroy_response(response);\n\t\t\t\t/* We won't wait forever for an answer (about 30 seconds) */\n\t\t\t\tjanus_refcount_increase(&ts->ref);\n\t\t\t\tjanus_refcount_increase(&session->ref);\n\t\t\t\tg_atomic_int_set(&msg->timeout_flag, 1);\n\t\t\t\tmsg->timeout = janus_http_request_timeout_create(ts, session, 30);\n\t\t\t\tg_source_attach(msg->timeout, httpctx);\n\t\t\t\t/* Mark this connection as the long poll for this session */\n\t\t\t\tmsg->max_events = max_events;\n\t\t\t\tsession->longpolls = g_list_append(session->longpolls, ts);\n\t\t\t\tg_atomic_pointer_set(&msg->longpoll, session);\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&session->mutex);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\tgoto done;\n\t}\n\n\tjson_error_t error;\n\t/* Parse the JSON payload */\n\troot = json_loads(payload, 0, &error);\n\tif(!root) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_JSON, \"JSON error: on line %d: %s\",\n\t\t\terror.line, error.text);\n\t\tgoto done;\n\t}\n\tif(!json_is_object(root)) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_JSON_OBJECT, \"JSON error: not an object\");\n\t\tjson_decref(root);\n\t\tgoto done;\n\t}\n\nparsingdone:\n\t/* Check if we have session and handle identifiers, and how they were provided */\n\tsession_id = json_integer_value(json_object_get(root, \"session_id\"));\n\tif(session_id && session_path && g_ascii_strtoull(session_path, NULL, 10) != session_id) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_REQUEST_PATH, \"Conflicting session ID (payload and path)\");\n\t\tjson_decref(root);\n\t\tgoto done;\n\t}\n\tif(!session_id) {\n\t\t/* No session ID in the JSON object, maybe in the path? */\n\t\tsession_id = session_path ? g_ascii_strtoull(session_path, NULL, 10) : 0;\n\t\tif(session_id > 0)\n\t\t\tjson_object_set_new(root, \"session_id\", json_integer(session_id));\n\t}\n\thandle_id = json_integer_value(json_object_get(root, \"handle_id\"));\n\tif(handle_id && handle_path && g_ascii_strtoull(handle_path, NULL, 10) != handle_id) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_REQUEST_PATH, \"Conflicting handle ID (payload and path)\");\n\t\tjson_decref(root);\n\t\tgoto done;\n\t}\n\tif(!handle_id) {\n\t\t/* No session ID in the JSON object, maybe in the path? */\n\t\thandle_id = handle_path ? g_ascii_strtoull(handle_path, NULL, 10) : 0;\n\t\tif(handle_id > 0)\n\t\t\tjson_object_set_new(root, \"handle_id\", json_integer(handle_id));\n\t}\n\n\tresponse = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN,\n\t\t500, &janus_http_response_callback, msg, NULL);\n\tif(response == NULL) {\n\t\tret = MHD_NO;\n\t} else {\n\t\tg_atomic_int_set(&msg->suspended, 1);\n\t\t/* Suspend the connection */\n\t\tMHD_suspend_connection(msg->connection);\n\t\tMHD_add_response_header(response, \"Content-Type\", \"application/json\");\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(msg->connection, MHD_HTTP_OK, response);\n\t\tMHD_destroy_response(response);\n\t\t/* We won't wait forever for an answer (about 10 seconds) */\n\t\tjanus_refcount_increase(&ts->ref);\n\t\tg_atomic_int_set(&msg->timeout_flag, 1);\n\t\tmsg->timeout = janus_http_request_timeout_create(ts, NULL, 10);\n\t\tg_source_attach(msg->timeout, httpctx);\n\t\t/* Pass the ball to the core */\n\t\tJANUS_LOG(LOG_HUGE, \"Forwarding request to the core (%p)\\n\", msg);\n\t\tgateway->incoming_request(&janus_http_transport, ts, ts, FALSE, root, &error);\n\t}\n\ndone:\n\tjanus_refcount_decrease(&ts->ref);\n\tg_strfreev(basepath);\n\tg_strfreev(path);\n\tg_free(session_path);\n\tg_free(handle_path);\n\treturn ret;\n}\n\n/* Admin/monitor WebServer requests handler */\nstatic janus_MHD_Result janus_http_admin_handler(void *cls, struct MHD_Connection *connection,\n\t\tconst char *url, const char *method, const char *version,\n\t\tconst char *upload_data, size_t *upload_data_size, void **ptr) {\n\tif(!g_atomic_int_get(&initialized) || g_atomic_int_get(&stopping))\n\t\treturn MHD_NO;\n\tchar *payload = NULL;\n\tjson_t *root = NULL;\n\tstruct MHD_Response *response = NULL;\n\tint ret = MHD_NO;\n\tchar *session_path = NULL, *handle_path = NULL;\n\tchar **basepath = NULL, **path = NULL;\n\tguint64 session_id = 0, handle_id = 0;\n\n\t/* Is this the first round? */\n\tint firstround = 0;\n\tjanus_transport_session *ts = (janus_transport_session *)*ptr;\n\tjanus_http_msg *msg = NULL;\n\tif(ts == NULL) {\n\t\tfirstround = 1;\n\t\tJANUS_LOG(LOG_VERB, \"Got an admin/monitor HTTP %s request on %s...\\n\", method, url);\n\t\tJANUS_LOG(LOG_DBG, \" ... Just parsing headers for now...\\n\");\n\t\tmsg = g_malloc0(sizeof(janus_http_msg));\n\t\tmsg->connection = connection;\n\t\tjanus_refcount_init(&msg->ref, janus_http_msg_free);\n\t\tts = janus_transport_session_create(msg, janus_http_msg_destroy);\n\t\tjanus_mutex_lock(&messages_mutex);\n\t\tg_hash_table_insert(messages, ts, ts);\n\t\tjanus_mutex_unlock(&messages_mutex);\n\t\t*ptr = ts;\n\t\tMHD_get_connection_values(connection, MHD_HEADER_KIND, &janus_http_headers, msg);\n\t\tif(janus_http_admin_check_xff && msg->xff) {\n\t\t\t/* Any access limitation based on this IP address? */\n\t\t\tif(!janus_http_is_allowed(msg->xff, TRUE)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"IP %s is unauthorized to connect to the Janus API interface\\n\", msg->xff);\n\t\t\t\treturn MHD_NO;\n\t\t\t}\n\t\t}\n\t\tret = MHD_YES;\n\t\t/* Notify handlers about this new transport instance */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"request\"));\n\t\t\tjson_object_set_new(info, \"admin_api\", json_true());\n\t\t\tconst union MHD_ConnectionInfo *conninfo = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS);\n\t\t\tif(conninfo != NULL) {\n\t\t\t\tjanus_network_address addr;\n\t\t\t\tjanus_network_address_string_buffer addr_buf;\n\t\t\t\tif(janus_network_address_from_sockaddr((struct sockaddr *)conninfo->client_addr, &addr) == 0 &&\n\t\t\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) == 0) {\n\t\t\t\t\tconst char *ip = janus_network_address_string_from_buffer(&addr_buf);\n\t\t\t\t\tjson_object_set_new(info, \"ip\", json_string(ip));\n\t\t\t\t}\n\t\t\t\tuint16_t port = janus_http_sockaddr_to_port((struct sockaddr *)conninfo->client_addr);\n\t\t\t\tjson_object_set_new(info, \"port\", json_integer(port));\n\t\t\t}\n\t\t\tgateway->notify_event(&janus_http_transport, ts, info);\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_DBG, \"Processing HTTP %s request on %s...\\n\", method, url);\n\t\tjanus_refcount_increase(&ts->ref);\n\t\tmsg = (janus_http_msg *)ts->transport_p;\n\t}\n\t/* Parse request */\n\tif(strcasecmp(method, \"GET\") && strcasecmp(method, \"POST\") && strcasecmp(method, \"OPTIONS\")) {\n\t\tif(firstround)\n\t\t\treturn ret;\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_TRANSPORT_SPECIFIC, \"Unsupported method %s\", method);\n\t\tgoto done;\n\t}\n\tif(firstround && enforce_cors && (msg->acro == NULL || strstr(msg->acro, allow_origin) != msg->acro)) {\n\t\t/* Got a request from the wrong origin and we're enforcing CORS with 403 errors */\n\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, response);\n\t\tMHD_destroy_response(response);\n\t\treturn ret;\n\t}\n\tif(!strcasecmp(method, \"OPTIONS\")) {\n\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(connection, MHD_HTTP_OK, response);\n\t\tMHD_destroy_response(response);\n\t}\n\t/* Get path components */\n\tif(strcasecmp(url, admin_ws_path)) {\n\t\tif(strnlen(admin_ws_path, 1 + 1) > 1) {\n\t\t\tbasepath = g_strsplit(url, admin_ws_path, -1);\n\t\t} else {\n\t\t\t/* The base path is the web server too itself, we process the url itself */\n\t\t\tbasepath = g_malloc_n(3, sizeof(char *));\n\t\t\tbasepath[0] = g_strdup(\"/\");\n\t\t\tbasepath[1] = g_strdup(url);\n\t\t\tbasepath[2] = NULL;\n\t\t}\n\t\tif(basepath[0] == NULL || basepath[1] == NULL || basepath[1][0] != '/') {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid url %s\\n\", url);\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\t\tMHD_destroy_response(response);\n\t\t}\n\t\tif(firstround) {\n\t\t\tg_strfreev(basepath);\n\t\t\treturn ret;\n\t\t}\n\t\tpath = g_strsplit(basepath[1], \"/\", -1);\n\t\tif(path == NULL || path[1] == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid path %s (%s)\\n\", basepath[1], path ? path[1] : \"\");\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tg_strfreev(basepath);\n\t\t\tg_strfreev(path);\n\t\t\treturn ret;\n\t\t}\n\t}\n\tif(firstround) {\n\t\tg_strfreev(basepath);\n\t\tg_strfreev(path);\n\t\treturn ret;\n\t}\n\tJANUS_LOG(LOG_DBG, \" ... parsing request...\\n\");\n\tif(path != NULL && path[1] != NULL && strlen(path[1]) > 0) {\n\t\tsession_path = g_strdup(path[1]);\n\t\tJANUS_LOG(LOG_HUGE, \"Session: %s\\n\", session_path);\n\t}\n\tif(session_path != NULL && path[2] != NULL && strlen(path[2]) > 0) {\n\t\thandle_path = g_strdup(path[2]);\n\t\tJANUS_LOG(LOG_HUGE, \"Handle: %s\\n\", handle_path);\n\t}\n\tif(session_path != NULL && handle_path != NULL && path[3] != NULL && strlen(path[3]) > 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Too many components...\\n\");\n\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response);\n\t\tMHD_destroy_response(response);\n\t\tgoto done;\n\t}\n\t/* Get payload, if any */\n\tif(!strcasecmp(method, \"POST\")) {\n\t\tJANUS_LOG(LOG_HUGE, \"Processing POST data (%s) (%zu bytes)...\\n\", msg->contenttype, *upload_data_size);\n\t\tif(*upload_data_size != 0) {\n\t\t\tmsg->payload = g_realloc(msg->payload, msg->len+*upload_data_size+1);\n\t\t\tmemcpy(msg->payload+msg->len, upload_data, *upload_data_size);\n\t\t\tmsg->len += *upload_data_size;\n\t\t\tmemset(msg->payload + msg->len, '\\0', 1);\n\t\t\tJANUS_LOG(LOG_DBG, \"  -- Data we have now (%zu bytes)\\n\", msg->len);\n\t\t\t*upload_data_size = 0;\t/* Go on */\n\t\t\tret = MHD_YES;\n\t\t\tgoto done;\n\t\t}\n\t\tJANUS_LOG(LOG_DBG, \"Done getting payload, we can answer\\n\");\n\t\tif(msg->payload == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"No payload :-(\\n\");\n\t\t\tret = MHD_NO;\n\t\t\tgoto done;\n\t\t}\n\t\tpayload = msg->payload;\n\t\tJANUS_LOG(LOG_HUGE, \"%s\\n\", payload);\n\t}\n\n\t/* Is this a generic request for info? */\n\tif(session_path != NULL && !strcmp(session_path, \"info\")) {\n\t\t/* The info REST endpoint, if contacted through a GET, provides information on the Janus core */\n\t\tif(strcasecmp(method, \"GET\")) {\n\t\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_TRANSPORT_SPECIFIC, \"Use GET for the info endpoint\");\n\t\t\tgoto done;\n\t\t}\n\t\t/* Turn this into a fake \"info\" request */\n\t\tmethod = \"POST\";\n\t\tchar tr[12];\n\t\tjanus_http_random_string(12, (char *)&tr);\n\t\troot = json_object();\n\t\tjson_object_set_new(root, \"janus\", json_string(\"info\"));\n\t\tjson_object_set_new(root, \"transaction\", json_string(tr));\n\t\tgoto parsingdone;\n\t}\n\t/* Or maybe a ping */\n\tif(session_path != NULL && !strcmp(session_path, \"ping\")) {\n\t\t/* The ping REST endpoint, if contacted through a GET, pings the Janus core */\n\t\tif(strcasecmp(method, \"GET\")) {\n\t\t\tresponse = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);\n\t\t\tjanus_http_add_cors_headers(msg, response);\n\t\t\tret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, response);\n\t\t\tMHD_destroy_response(response);\n\t\t\tgoto done;\n\t\t}\n\t\t/* Turn this into a fake \"ping\" request */\n\t\tmethod = \"POST\";\n\t\tchar tr[12];\n\t\tjanus_http_random_string(12, (char *)&tr);\n\t\troot = json_object();\n\t\tjson_object_set_new(root, \"janus\", json_string(\"ping\"));\n\t\tjson_object_set_new(root, \"transaction\", json_string(tr));\n\t\tgoto parsingdone;\n\t}\n\n\t/* Without a payload we don't know what to do */\n\tif(!payload) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_JSON, \"Request payload missing\");\n\t\tgoto done;\n\t}\n\tjson_error_t error;\n\t/* Parse the JSON payload */\n\troot = json_loads(payload, 0, &error);\n\tif(!root) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_JSON, \"JSON error: on line %d: %s\",\n\t\t\terror.line, error.text);\n\t\tgoto done;\n\t}\n\tif(!json_is_object(root)) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_JSON_OBJECT, \"JSON error: not an object\");\n\t\tjson_decref(root);\n\t\tgoto done;\n\t}\n\nparsingdone:\n\t/* Check if we have session and handle identifiers, and how they were provided */\n\tsession_id = json_integer_value(json_object_get(root, \"session_id\"));\n\tif(session_id && session_path && g_ascii_strtoull(session_path, NULL, 10) != session_id) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_REQUEST_PATH, \"Conflicting session ID (payload and path)\");\n\t\tjson_decref(root);\n\t\tgoto done;\n\t}\n\tif(!session_id) {\n\t\t/* No session ID in the JSON object, maybe in the path? */\n\t\tsession_id = session_path ? g_ascii_strtoull(session_path, NULL, 10) : 0;\n\t\tif(session_id > 0)\n\t\t\tjson_object_set_new(root, \"session_id\", json_integer(session_id));\n\t}\n\thandle_id = json_integer_value(json_object_get(root, \"handle_id\"));\n\tif(handle_id && handle_path && g_ascii_strtoull(handle_path, NULL, 10) != handle_id) {\n\t\tret = janus_http_return_error(ts, 0, NULL, JANUS_ERROR_INVALID_REQUEST_PATH, \"Conflicting handle ID (payload and path)\");\n\t\tjson_decref(root);\n\t\tgoto done;\n\t}\n\tif(!handle_id) {\n\t\t/* No session ID in the JSON object, maybe in the path? */\n\t\thandle_id = handle_path ? g_ascii_strtoull(handle_path, NULL, 10) : 0;\n\t\tif(handle_id > 0)\n\t\t\tjson_object_set_new(root, \"handle_id\", json_integer(handle_id));\n\t}\n\n\tresponse = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN,\n\t\t500, &janus_http_response_callback, msg, NULL);\n\tif(response == NULL) {\n\t\tret = MHD_NO;\n\t} else {\n\t\tg_atomic_int_set(&msg->suspended, 1);\n\t\t/* Suspend the connection */\n\t\tMHD_suspend_connection(msg->connection);\n\t\tMHD_add_response_header(response, \"Content-Type\", \"application/json\");\n\t\tjanus_http_add_cors_headers(msg, response);\n\t\tret = MHD_queue_response(msg->connection, MHD_HTTP_OK, response);\n\t\tMHD_destroy_response(response);\n\t\t/* We won't wait forever for an answer (about 10 seconds) */\n\t\tjanus_refcount_increase(&ts->ref);\n\t\tg_atomic_int_set(&msg->timeout_flag, 1);\n\t\tmsg->timeout = janus_http_request_timeout_create(ts, NULL, 10);\n\t\tg_source_attach(msg->timeout, httpctx);\n\t\t/* Pass the ball to the core */\n\t\tJANUS_LOG(LOG_HUGE, \"Forwarding admin request to the core (%p)\\n\", msg);\n\t\tgateway->incoming_request(&janus_http_transport, ts, ts, TRUE, root, &error);\n\t}\n\ndone:\n\tjanus_refcount_decrease(&ts->ref);\n\tg_strfreev(basepath);\n\tg_strfreev(path);\n\tg_free(session_path);\n\tg_free(handle_path);\n\treturn ret;\n}\n\nstatic janus_MHD_Result janus_http_headers(void *cls, enum MHD_ValueKind kind, const char *key, const char *value) {\n\tjanus_http_msg *request = (janus_http_msg *)cls;\n\tJANUS_LOG(LOG_DBG, \"%s: %s\\n\", key, value);\n\tif(!request)\n\t\treturn MHD_YES;\n\tjanus_refcount_increase(&request->ref);\n\tif(!strcasecmp(key, MHD_HTTP_HEADER_CONTENT_TYPE)) {\n\t\trequest->contenttype = g_strdup(value);\n\t} else if(!strcasecmp(key, \"Referer\")) {\n\t\t/* We only use this as a backup in case Origin is missing (e.g., in GET) */\n\t\tif(request->acro == NULL)\n\t\t\trequest->acro = g_strdup(value);\n\t} else if(!strcasecmp(key, \"Origin\")) {\n\t\tg_free(request->acro);\n\t\trequest->acro = g_strdup(value);\n\t} else if(!strcasecmp(key, \"Access-Control-Request-Method\")) {\n\t\trequest->acrm = g_strdup(value);\n\t} else if(!strcasecmp(key, \"Access-Control-Request-Headers\")) {\n\t\trequest->acrh = g_strdup(value);\n\t} else if(!strcasecmp(key, \"X-Forwarded-For\")) {\n\t\trequest->xff = g_strdup(value);\n\t}\n\tjanus_refcount_decrease(&request->ref);\n\treturn MHD_YES;\n}\n\nstatic void janus_http_request_completed(void *cls, struct MHD_Connection *connection,\n\t\tvoid **con_cls, enum MHD_RequestTerminationCode toe) {\n\tJANUS_LOG(LOG_DBG, \"Request completed, freeing data\\n\");\n\tjanus_transport_session *ts = (janus_transport_session *)*con_cls;\n\tif(!ts)\n\t\treturn;\n\tjanus_http_msg *request = (janus_http_msg *)ts->transport_p;\n\tif(request != NULL) {\n\t\tjanus_refcount_increase(&request->ref);\n\t\tjanus_http_session *session = (janus_http_session *)g_atomic_pointer_get(&request->longpoll);\n\t\tif(session != NULL)\n\t\t\tjanus_refcount_increase(&session->ref);\n\t\tif(g_atomic_int_compare_and_exchange(&request->timeout_flag, 1, 0)) {\n\t\t\tg_source_destroy(request->timeout);\n\t\t\tg_source_unref(request->timeout);\n\t\t}\n\t\trequest->timeout = NULL;\n\t\tif(session) {\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\t\tsession->longpolls = g_list_remove(session->longpolls, ts);\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t}\n\t\tjanus_refcount_decrease(&request->ref);\n\t}\n\tjanus_mutex_lock(&messages_mutex);\n\tg_hash_table_remove(messages, ts);\n\tjanus_mutex_unlock(&messages_mutex);\n\t*con_cls = NULL;\n}\n\nstatic ssize_t janus_http_response_callback(void *cls, uint64_t pos, char *buf, size_t max) {\n\tjanus_http_msg *request = (janus_http_msg *)cls;\n\tif(request == NULL || request->response == NULL)\n\t\treturn MHD_CONTENT_READER_END_WITH_ERROR;\n\tif(pos >= request->resplen)\n\t\treturn MHD_CONTENT_READER_END_OF_STREAM;\n\tjanus_refcount_increase(&request->ref);\n\tsize_t bytes = request->resplen - pos;\n\tif(bytes > max)\n\t\tbytes = max;\n\tmemcpy(buf, request->response + pos, bytes);\n\tjanus_refcount_decrease(&request->ref);\n\treturn bytes;\n}\n\n/* Worker to handle notifications */\nstatic int janus_http_notifier(janus_http_msg *msg) {\n\tif(!msg || !msg->connection)\n\t\treturn MHD_NO;\n\tint max_events = msg->max_events;\n\tif(max_events < 1)\n\t\tmax_events = 1;\n\tJANUS_LOG(LOG_DBG, \"... handling long poll...\\n\");\n\tguint64 session_id = msg->session_id;\n\tjanus_mutex_lock(&sessions_mutex);\n\tjanus_http_session *session = g_hash_table_lookup(sessions, &session_id);\n\tif(!session || g_atomic_int_get(&session->destroyed)) {\n\t\tjanus_mutex_unlock(&sessions_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't find any session %\"SCNu64\"...\\n\", session_id);\n\t\t/* TODO return error */\n\t\tMHD_resume_connection(msg->connection);\n\t\treturn -1;\n\t}\n\tjanus_refcount_increase(&session->ref);\n\tjanus_mutex_unlock(&sessions_mutex);\n\tjson_t *event = NULL, *list = NULL;\n\tint events = 0;\n\twhile((event = g_async_queue_try_pop(session->events)) != NULL) {\n\t\tif(session->destroyed || g_atomic_int_get(&stopping)) {\n\t\t\t/* TODO return error */\n\t\t\tJANUS_LOG(LOG_ERR, \"Session %\"SCNu64\" destroyed...\\n\", session_id);\n\t\t\tMHD_resume_connection(msg->connection);\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\treturn -1;\n\t\t}\n\t\tif(max_events == 1) {\n\t\t\tbreak;\n\t\t} else {\n\t\t\t/* The application is willing to receive more events at the same time, anything to report? */\n\t\t\tif(list == NULL)\n\t\t\t\tlist = json_array();\n\t\t\tjson_array_append_new(list, event);\n\t\t\tevents++;\n\t\t\tif(events == max_events)\n\t\t\t\tbreak;\n\t\t}\n\t}\n\tif(event == NULL && list == NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"No events available for session %\"SCNu64\"...\\n\", session_id);\n\t\t/* Turn this into a \"keepalive\" response */\n\t\tchar tr[12];\n\t\tjanus_http_random_string(12, (char *)&tr);\n\t\tif(max_events == 1) {\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"janus\", json_string(\"keepalive\"));\n\t\t} else {\n\t\t\tlist = json_array();\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"janus\", json_string(\"keepalive\"));\n\t\t\tjson_array_append_new(list, event);\n\t\t}\n\t\t/* FIXME Improve the Janus protocol keep-alive mechanism in JavaScript */\n\t}\n\tchar *payload_text = json_dumps(max_events == 1 ? event : list, json_format);\n\tjson_decref(max_events == 1 ? event : list);\n\tif(payload_text == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\tMHD_resume_connection(msg->connection);\n\t\tjanus_refcount_decrease(&session->ref);\n\t\treturn -1;\n\t}\n\t/* Finish the request by sending the response */\n\tJANUS_LOG(LOG_HUGE, \"We have a message to serve...\\n\\t%s\\n\", payload_text);\n\t/* Send event back */\n\tmsg->response = payload_text;\n\tmsg->resplen = strlen(payload_text);\n\tMHD_resume_connection(msg->connection);\n\tjanus_refcount_decrease(&session->ref);\n\treturn 0;\n}\n\n/* Helper to quickly send a success response */\nstatic janus_MHD_Result janus_http_return_success(janus_transport_session *ts, char *payload) {\n\tif(!payload) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid payload...\\n\");\n\t\treturn MHD_NO;\n\t}\n\tif(!ts) {\n\t\tfree(payload);\n\t\treturn MHD_NO;\n\t}\n\tjanus_http_msg *msg = (janus_http_msg *)ts->transport_p;\n\tif(!msg || !msg->connection) {\n\t\tfree(payload);\n\t\treturn MHD_NO;\n\t}\n\tjanus_refcount_increase(&msg->ref);\n\tstruct MHD_Response *response = MHD_create_response_from_buffer(\n\t\tstrlen(payload),\n\t\t(void*)payload,\n\t\tMHD_RESPMEM_MUST_FREE);\n\tMHD_add_response_header(response, \"Content-Type\", \"application/json\");\n\tjanus_http_add_cors_headers(msg, response);\n\tint ret = MHD_queue_response(msg->connection, MHD_HTTP_OK, response);\n\tMHD_destroy_response(response);\n\tjanus_refcount_decrease(&msg->ref);\n\treturn ret;\n}\n\n/* Helper to quickly send an error response */\nstatic janus_MHD_Result janus_http_return_error(janus_transport_session *ts, uint64_t session_id,\n\t\tconst char *transaction, gint error, const char *format, ...) {\n\tgchar *error_string = NULL;\n\tgchar error_buf[512];\n\tif(format == NULL) {\n\t\t/* No error string provided, use the default one */\n\t\terror_string = (char *)janus_get_api_error(error);\n\t} else {\n\t\t/* This callback has variable arguments (error string) */\n\t\tva_list ap;\n\t\tva_start(ap, format);\n\t\tg_vsnprintf(error_buf, 512, format, ap);\n\t\tva_end(ap);\n\t\terror_string = error_buf;\n\t}\n\t/* Done preparing error */\n\tJANUS_LOG(LOG_VERB, \"[%s] Returning error %d (%s)\\n\", transaction, error, error_string ? error_string : \"no text\");\n\t/* Prepare JSON error */\n\tjson_t *reply = json_object();\n\tjson_object_set_new(reply, \"janus\", json_string(\"error\"));\n\tif(session_id > 0)\n\t\tjson_object_set_new(reply, \"session_id\", json_integer(session_id));\n\tif(transaction != NULL)\n\t\tjson_object_set_new(reply, \"transaction\", json_string(transaction));\n\tjson_t *error_data = json_object();\n\tjson_object_set_new(error_data, \"code\", json_integer(error));\n\tjson_object_set_new(error_data, \"reason\", json_string(error_string));\n\tjson_object_set_new(reply, \"error\", error_data);\n\tchar *reply_text = json_dumps(reply, json_format);\n\tjson_decref(reply);\n\t/* Use janus_http_return_error to send the error response */\n\treturn janus_http_return_success(ts, reply_text);\n}\n\n/* Helper to handle timeouts */\nvoid janus_http_timeout(janus_transport_session *ts, janus_http_session *session, gboolean lock_session) {\n\tif(g_atomic_int_get(&ts->destroyed))\n\t\treturn;\n\tjanus_refcount_increase(&ts->ref);\n\tjanus_http_msg *request = (janus_http_msg *)ts->transport_p;\n\tif(!g_atomic_int_compare_and_exchange(&request->timeout_flag, 1, 0)) {\n\t\trequest->timeout = NULL;\n\t\tjanus_refcount_decrease(&ts->ref);\n\t\treturn;\n\t}\n\trequest->timeout = NULL;\n\t/* Is this a long poll timeout, simply meaning we had nothing to send so far? */\n\tif(session != NULL) {\n\t\tjanus_refcount_increase(&session->ref);\n\t\tg_atomic_pointer_set(&request->longpoll, NULL);\n\t\t/* Long poll, turn this into a \"keepalive\" response */\n\t\tjson_t *event = NULL;\n\t\tif(request->max_events == 1) {\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"janus\", json_string(\"keepalive\"));\n\t\t} else {\n\t\t\tjson_t *list = json_array();\n\t\t\tevent = json_object();\n\t\t\tjson_object_set_new(event, \"janus\", json_string(\"keepalive\"));\n\t\t\tjson_array_append_new(list, event);\n\t\t\tevent = list;\n\t\t}\n\t\tchar *payload_text = json_dumps(event, json_format);\n\t\tjson_decref(event);\n\t\tif(payload_text == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\t\tjanus_refcount_decrease(&session->ref);\n\t\t\tMHD_resume_connection(request->connection);\n\t\t\tjanus_refcount_decrease(&ts->ref);\n\t\t\treturn;\n\t\t}\n\t\t/* Finish the request by sending the response */\n\t\tJANUS_LOG(LOG_HUGE, \"We have a message to serve...\\n\\t%s\\n\", payload_text);\n\t\t/* Send event back */\n\t\trequest->response = payload_text;\n\t\trequest->resplen = strlen(payload_text);\n\t\tMHD_resume_connection(request->connection);\n\t\tif(lock_session)\n\t\t\tjanus_mutex_lock(&session->mutex);\n\t\tsession->longpolls = g_list_remove(session->longpolls, ts);\n\t\tif(lock_session)\n\t\t\tjanus_mutex_unlock(&session->mutex);\n\t\tjanus_refcount_decrease(&session->ref);\n\t} else {\n\t\t/* Not a long poll, a request is taking way too much time */\n\t\tg_free(request->response);\n\t\trequest->response = NULL;\n\t\trequest->resplen = 0;\n\t\tMHD_resume_connection(request->connection);\n\t}\n\tjanus_refcount_decrease(&ts->ref);\n}\n"
  },
  {
    "path": "src/transports/janus_mqtt.c",
    "content": "/*! \\file   janus_mqtt.c\n * \\author Andrei Nesterov <ae.nesterov@gmail.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus MQTT transport plugin\n * \\details  This is an implementation of a MQTT transport for the Janus API,\n * using the Eclipse Paho C Client library (https://eclipse.org/paho/clients/c).\n * This means that this module adds support for MQTT based messaging as\n * an alternative \"transport\" for API requests, responses and notifications.\n * This is only useful when you're handling the communication with\n * clients your own way. Right now, you can only configure\n * the address of the MQTT broker to use, and the queues to\n * make use of to receive (to-janus) and send (from-janus) messages\n * from/to an external application. As with WebSockets, considering that\n * requests wouldn't include a path to address some mandatory information,\n * these requests addressed to Janus should include as part of their payload,\n * when needed, additional pieces of information like \\c session_id and\n * \\c handle_id. That is, where you'd send a Janus request related to a\n * specific session to the \\c /janus/\\<session> path, with MQTT\n * you'd have to send the same request with an additional \\c session_id\n * field in the JSON payload.\n * \\note When you create a session using MQTT, a subscription to the\n * events related to it is done automatically through the outgoing queue,\n * so no need for an explicit request as the GET in the plain HTTP API.\n *\n * \\ingroup transports\n * \\ref transports\n */\n\n#include \"transport.h\"\n\n#include <MQTTAsync.h>\n\n#include \"../debug.h\"\n#include \"../config.h\"\n#include \"../utils.h\"\n\n/* Transport plugin information */\n#define JANUS_MQTT_VERSION\t\t1\n#define JANUS_MQTT_VERSION_STRING \"0.0.1\"\n#define JANUS_MQTT_DESCRIPTION\t\"This transport plugin adds MQTT support to the Janus API via Paho client library.\"\n#define JANUS_MQTT_NAME\t\t   \"JANUS MQTT transport plugin\"\n#define JANUS_MQTT_AUTHOR\t\t \"Andrei Nesterov <ae.nesterov@gmail.com>\"\n#define JANUS_MQTT_PACKAGE\t\t\"janus.transport.mqtt\"\n\n/* Transport methods */\njanus_transport *create(void);\nint janus_mqtt_init(janus_transport_callbacks *callback, const char *config_path);\nvoid janus_mqtt_destroy(void);\nint janus_mqtt_get_api_compatibility(void);\nint janus_mqtt_get_version(void);\nconst char *janus_mqtt_get_version_string(void);\nconst char *janus_mqtt_get_description(void);\nconst char *janus_mqtt_get_name(void);\nconst char *janus_mqtt_get_author(void);\nconst char *janus_mqtt_get_package(void);\ngboolean janus_mqtt_is_janus_api_enabled(void);\ngboolean janus_mqtt_is_admin_api_enabled(void);\nint janus_mqtt_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message);\nvoid janus_mqtt_session_created(janus_transport_session *transport, guint64 session_id);\nvoid janus_mqtt_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed);\nvoid janus_mqtt_session_claimed(janus_transport_session *transport, guint64 session_id);\njson_t *janus_mqtt_query_transport(json_t *request);\n\n#define JANUS_MQTT_VERSION_3_1\t\t  \"3.1\"\n#define JANUS_MQTT_VERSION_3_1_1\t\t\"3.1.1\"\n#define JANUS_MQTT_VERSION_5\t\t\t\"5\"\n#define JANUS_MQTT_VERSION_DEFAULT\t  JANUS_MQTT_VERSION_3_1_1\n#define JANUS_MQTT_DEFAULT_STATUS_TOPIC\t\"status\"\n#define JANUS_MQTT_DEFAULT_STATUS_QOS   1\n\n/* Transport setup */\nstatic janus_transport janus_mqtt_transport_ =\n\tJANUS_TRANSPORT_INIT (\n\t\t.init = janus_mqtt_init,\n\t\t.destroy = janus_mqtt_destroy,\n\n\t\t.get_api_compatibility = janus_mqtt_get_api_compatibility,\n\t\t.get_version = janus_mqtt_get_version,\n\t\t.get_version_string = janus_mqtt_get_version_string,\n\t\t.get_description = janus_mqtt_get_description,\n\t\t.get_name = janus_mqtt_get_name,\n\t\t.get_author = janus_mqtt_get_author,\n\t\t.get_package = janus_mqtt_get_package,\n\n\t\t.is_janus_api_enabled = janus_mqtt_is_janus_api_enabled,\n\t\t.is_admin_api_enabled = janus_mqtt_is_admin_api_enabled,\n\n\t\t.send_message = janus_mqtt_send_message,\n\t\t.session_created = janus_mqtt_session_created,\n\t\t.session_over = janus_mqtt_session_over,\n\t\t.session_claimed = janus_mqtt_session_claimed,\n\n\t\t.query_transport = janus_mqtt_query_transport,\n\t);\n\n/* Transport creator */\njanus_transport *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_MQTT_NAME);\n\treturn &janus_mqtt_transport_;\n}\n\n/* API flags */\nstatic gboolean janus_mqtt_api_enabled_ = FALSE;\nstatic gboolean janus_mqtt_admin_api_enabled_ = FALSE;\n\n/* Event handlers */\nstatic gboolean notify_events = TRUE;\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Parameter validation (for tweaking and queries via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"events\", JANUS_JSON_BOOL, 0},\n\t{\"json\", JSON_STRING, 0},\n};\n/* Error codes (for the tweaking and queries via Admin API) */\n#define JANUS_MQTT_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_MQTT_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_MQTT_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_MQTT_ERROR_UNKNOWN_ERROR\t\t\t499\n\n\n/* MQTT client context */\ntypedef struct janus_mqtt_context {\n\tjanus_transport_callbacks *gateway;\n\tMQTTAsync client;\n\tstruct {\n\t\tint mqtt_version;\n\t\tint keep_alive_interval;\n\t\tint cleansession;\n\t\tchar *username;\n\t\tchar *password;\n\t\tint max_inflight;\n\t\tint max_buffered;\n\t} connect;\n\tstruct {\n\t\tint timeout;\n\t\tjanus_mutex mutex;\n\t\tjanus_condition cond;\n\t} disconnect;\n\t/* If we loose connection, the will is our last publish */\n\tstruct {\n\t\tgboolean enabled;\n\t\tchar *connect_message;\n\t\tchar *disconnect_message;\n\t\tchar *topic;\n\t\tint qos;\n\t\tgboolean retain;\n\t} status;\n\tstruct {\n\t\tchar *topic;\n\t\tint qos;\n\t} subscribe;\n\tstruct {\n\t\tchar *topic;\n\t\tint qos;\n\t\tgboolean retain;\n#ifdef MQTTVERSION_5\n\t\tGArray *proxy_transaction_user_properties;\n\t\tGArray *add_transaction_user_properties;\n#endif\n\t} publish;\n\tstruct {\n\t\tstruct {\n\t\t\tchar *topic;\n\t\t\tint qos;\n\t\t} subscribe;\n\t\tstruct {\n\t\t\tchar *topic;\n\t\t\tint qos;\n\t\t} publish;\n\t} admin;\n\t/* SSL config, if needed */\n\tgboolean ssl_enabled;\n\tchar *cacert_file;\n\tchar *cert_file;\n\tchar *key_file;\n\tgboolean verify_peer;\n#ifdef MQTTVERSION_5\n\tgint64 vacuum_interval;\n#endif\n} janus_mqtt_context;\n\n#ifdef MQTTVERSION_5\n/* MQTT 5 specific types */\ntypedef struct janus_mqtt_transaction_state {\n\tMQTTProperties *properties;\n\tgint64 created_at;\n} janus_mqtt_transaction_state;\n\ntypedef struct janus_mqtt_set_add_transaction_user_property_user_data {\n\tGArray *acc;\n\tjanus_config *config;\n} janus_mqtt_set_add_transaction_user_property_user_data;\n#endif\n\n/* Transport client methods */\nvoid janus_mqtt_client_connected(void *context, char *cause);\nvoid janus_mqtt_client_connection_lost(void *context, char *cause);\nint janus_mqtt_client_message_arrived(void *context, char *topicName, int topicLen, MQTTAsync_message *message);\nint janus_mqtt_client_connect(janus_mqtt_context *ctx);\nint janus_mqtt_client_reconnect(janus_mqtt_context *ctx);\nint janus_mqtt_client_disconnect(janus_mqtt_context *ctx);\nint janus_mqtt_client_subscribe(janus_mqtt_context *ctx, gboolean admin);\nint janus_mqtt_client_publish_status_message(janus_mqtt_context *ctx, char *payload);\nvoid janus_mqtt_client_destroy_context(janus_mqtt_context **ctx);\n/* MQTT v3.x interface callbacks */\nvoid janus_mqtt_client_connect_failure(void *context, MQTTAsync_failureData *response);\nvoid janus_mqtt_client_reconnect_success(void *context, MQTTAsync_successData *response);\nvoid janus_mqtt_client_reconnect_failure(void *context, MQTTAsync_failureData *response);\nvoid janus_mqtt_client_disconnect_success(void *context, MQTTAsync_successData *response);\nvoid janus_mqtt_client_disconnect_failure(void *context, MQTTAsync_failureData *response);\nvoid janus_mqtt_client_subscribe_success(void *context, MQTTAsync_successData *response);\nvoid janus_mqtt_client_subscribe_failure(void *context, MQTTAsync_failureData *response);\nvoid janus_mqtt_client_admin_subscribe_success(void *context, MQTTAsync_successData *response);\nvoid janus_mqtt_client_admin_subscribe_failure(void *context, MQTTAsync_failureData *response);\nvoid janus_mqtt_client_publish_janus_success(void *context, MQTTAsync_successData *response);\nvoid janus_mqtt_client_publish_janus_failure(void *context, MQTTAsync_failureData *response);\nvoid janus_mqtt_client_publish_admin_success(void *context, MQTTAsync_successData *response);\nvoid janus_mqtt_client_publish_admin_failure(void *context, MQTTAsync_failureData *response);\nvoid janus_mqtt_client_publish_status_success(void *context, MQTTAsync_successData *response);\nvoid janus_mqtt_client_publish_status_failure(void *context, MQTTAsync_failureData *response);\nint janus_mqtt_client_publish_message(janus_mqtt_context *ctx, char *payload, gboolean admin);\nint janus_mqtt_client_get_response_code(MQTTAsync_failureData *response);\n#ifdef MQTTVERSION_5\n/* MQTT v5 interface callbacks */\nvoid janus_mqtt_client_disconnected5(void *context, MQTTProperties *properties, enum MQTTReasonCodes reasonCode);\nvoid janus_mqtt_client_connect_failure5(void *context, MQTTAsync_failureData5 *response);\nvoid janus_mqtt_client_reconnect_success5(void *context, MQTTAsync_successData5 *response);\nvoid janus_mqtt_client_reconnect_failure5(void *context, MQTTAsync_failureData5 *response);\nvoid janus_mqtt_client_disconnect_success5(void *context, MQTTAsync_successData5 *response);\nvoid janus_mqtt_client_disconnect_failure5(void *context, MQTTAsync_failureData5 *response);\nvoid janus_mqtt_client_subscribe_success5(void *context, MQTTAsync_successData5 *response);\nvoid janus_mqtt_client_subscribe_failure5(void *context, MQTTAsync_failureData5 *response);\nvoid janus_mqtt_client_admin_subscribe_success5(void *context, MQTTAsync_successData5 *response);\nvoid janus_mqtt_client_admin_subscribe_failure5(void *context, MQTTAsync_failureData5 *response);\nvoid janus_mqtt_client_publish_janus_success5(void *context, MQTTAsync_successData5 *response);\nvoid janus_mqtt_client_publish_janus_failure5(void *context, MQTTAsync_failureData5 *response);\nvoid janus_mqtt_client_publish_admin_success5(void *context, MQTTAsync_successData5 *response);\nvoid janus_mqtt_client_publish_admin_failure5(void *context, MQTTAsync_failureData5 *response);\nvoid janus_mqtt_client_publish_status_success5(void *context, MQTTAsync_successData5 *response);\nvoid janus_mqtt_client_publish_status_failure5(void *context, MQTTAsync_failureData5 *response);\nint janus_mqtt_client_publish_message5(janus_mqtt_context *ctx, char *payload, gboolean admin, MQTTProperties *properties, char *custom_topic);\nint janus_mqtt_client_get_response_code5(MQTTAsync_failureData5 *response);\n#endif\n/* MQTT version independent callback implementations */\nvoid janus_mqtt_client_reconnect_success_impl(void *context);\nvoid janus_mqtt_client_connect_failure_impl(void *context, int rc);\nvoid janus_mqtt_client_reconnect_failure_impl(void *context, int rc);\nvoid janus_mqtt_client_disconnect_success_impl(void *context);\nvoid janus_mqtt_client_disconnect_failure_impl(void *context, int rc);\nvoid janus_mqtt_client_subscribe_success_impl(void *context);\nvoid janus_mqtt_client_subscribe_failure_impl(void *context, int rc);\nvoid janus_mqtt_client_admin_subscribe_success_impl(void *context);\nvoid janus_mqtt_client_admin_subscribe_failure_impl(void *context, int rc);\nvoid janus_mqtt_client_publish_janus_success_impl(char *topic);\nvoid janus_mqtt_client_publish_janus_failure_impl(int rc);\nvoid janus_mqtt_client_publish_admin_success_impl(char *topic);\nvoid janus_mqtt_client_publish_admin_failure_impl(int rc);\nvoid janus_mqtt_client_publish_status_success_impl(char *topic);\nvoid janus_mqtt_client_publish_status_failure_impl(int rc);\n\n/* We only handle a single client */\nstatic janus_mqtt_context *context_ = NULL;\nstatic janus_transport_session *mqtt_session = NULL;\n\n#ifdef MQTTVERSION_5\n/* MQTT 5 specific statics and functions */\nstatic GHashTable *janus_mqtt_transaction_states;\nstatic GRWLock janus_mqtt_transaction_states_lock;\nstatic GMainContext *vacuum_context = NULL;\nstatic GMainLoop *vacuum_loop = NULL;\nstatic GThread *vacuum_thread = NULL;\nstatic gpointer janus_mqtt_vacuum_thread(gpointer context);\nstatic gboolean janus_mqtt_vacuum(gpointer context);\nvoid janus_mqtt_transaction_state_free(gpointer ptr);\nchar *janus_mqtt_get_response_topic(janus_mqtt_transaction_state *state);\nvoid janus_mqtt_proxy_properties(janus_mqtt_transaction_state *state, GArray *user_property_names, MQTTProperties *properties);\nvoid janus_mqtt_add_properties(janus_mqtt_transaction_state *state, GArray *user_properties, MQTTProperties *properties);\nvoid janus_mqtt_set_proxy_transaction_user_property(gpointer item_ptr, gpointer acc_ptr);\nvoid janus_mqtt_set_add_transaction_user_property(gpointer item_ptr, gpointer user_data_ptr);\n#endif\n\nint janus_mqtt_init(janus_transport_callbacks *callback, const char *config_path) {\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* Initializing context */\n\tjanus_mqtt_context *ctx = g_malloc0(sizeof(struct janus_mqtt_context));\n\tctx->gateway = callback;\n\tcontext_ = ctx;\n\n\t/* Set default values */\n\t/* Strings are set to default values later */\n\tctx->status.enabled = FALSE;\n\tctx->status.qos = JANUS_MQTT_DEFAULT_STATUS_QOS;\n\tctx->status.retain = FALSE;\n\n\t/* Prepare the transport session (again, just one) */\n\tmqtt_session = janus_transport_session_create(context_, NULL);\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_MQTT_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_MQTT_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_MQTT_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\tjanus_config_print(config);\n\t}\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\tjanus_config_category *config_admin = janus_config_get_create(config, NULL, janus_config_type_category, \"admin\");\n\tjanus_config_category *config_status = janus_config_get_create(config, NULL, janus_config_type_category, \"status\");\n\n\t/* Handle configuration */\n\tjanus_config_item *url_item = janus_config_get(config, config_general, janus_config_type_item, \"url\");\n\tconst char *url = g_strdup((url_item && url_item->value) ? url_item->value : \"tcp://localhost:1883\");\n\n\tjanus_config_item *client_id_item = janus_config_get(config, config_general, janus_config_type_item, \"client_id\");\n\tconst char *client_id = g_strdup((client_id_item && client_id_item->value) ? client_id_item->value : \"guest\");\n\n\tjanus_config_item *username_item = janus_config_get(config, config_general, janus_config_type_item, \"username\");\n\tctx->connect.username = g_strdup((username_item && username_item->value) ? username_item->value : \"guest\");\n\n\tjanus_config_item *password_item = janus_config_get(config, config_general, janus_config_type_item, \"password\");\n\tctx->connect.password = g_strdup((password_item && password_item->value) ? password_item->value : \"guest\");\n\n\tjanus_config_item *json_item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\tif(json_item && json_item->value) {\n\t\t/* Check how we need to format/serialize the JSON output */\n\t\tif(!strcasecmp(json_item->value, \"indented\")) {\n\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(json_item->value, \"plain\")) {\n\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(json_item->value, \"compact\")) {\n\t\t\t/* Compact, so no spaces between separators */\n\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", json_item->value);\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t}\n\t}\n\n\t/* Check if we need to send events to handlers */\n\tjanus_config_item *events_item = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\tif(events_item && events_item->value)\n\t\tnotify_events = janus_is_true(events_item->value);\n\tif(!notify_events && callback->events_is_enabled()) {\n\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_MQTT_NAME);\n\t}\n\n\t/* Check if we need to enable SSL support */\n\tjanus_config_item *ssl_item = janus_config_get(config, config_general, janus_config_type_item, \"ssl_enabled\");\n\tif(ssl_item == NULL) {\n\t\t/* Try legacy property */\n\t\tssl_item = janus_config_get(config, config_general, janus_config_type_item, \"ssl_enable\");\n\t\tif (ssl_item && ssl_item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Found deprecated 'ssl_enable' property, please update it to 'ssl_enabled' instead\\n\");\n\t\t}\n\t}\n\tif(ssl_item && ssl_item->value && janus_is_true(ssl_item->value)) {\n\t\tif(strstr(url, \"ssl://\") != url)\n\t\t\tJANUS_LOG(LOG_WARN, \"SSL enabled, but MQTT url doesn't start with ssl://...\\n\");\n\n\t\tctx->ssl_enabled = TRUE;\n\n\t\tjanus_config_item *cacertfile = janus_config_get(config, config_general, janus_config_type_item, \"cacertfile\");\n\t\tif(!cacertfile || !cacertfile->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"No CA certificate for MQTT integration, using OpenSSL defaults\\n\");\n\t\t}\n\t\tctx->cacert_file = (cacertfile && cacertfile->value) ? g_strdup(cacertfile->value) : NULL;\n\n\n\t\tjanus_config_item *certfile = janus_config_get(config, config_general, janus_config_type_item, \"certfile\");\n\t\tctx->cert_file = (certfile && certfile->value) ? g_strdup(certfile->value) : NULL;\n\n\t\tjanus_config_item *keyfile = janus_config_get(config, config_general, janus_config_type_item, \"keyfile\");\n\t\tctx->key_file = (keyfile && keyfile->value) ? g_strdup(keyfile->value) : NULL;\n\n\t\tif(ctx->cert_file && !ctx->key_file) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Certificate is set but key isn't for MQTT integration...\\n\");\n\t\t\tgoto error;\n\t\t}\n\t\tif(!ctx->cert_file && ctx->key_file) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Key is set but certificate isn't for MQTT integration...\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\tjanus_config_item *verify = janus_config_get(config, config_general, janus_config_type_item, \"verify_peer\");\n\t\tctx->verify_peer = (verify && verify->value && janus_is_true(verify->value)) ? TRUE : FALSE;\n\t} else {\n\t\tJANUS_LOG(LOG_INFO, \"MQTT SSL support disabled\\n\");\n\t\tif(strstr(url, \"ssl://\") == url)\n\t\t\tJANUS_LOG(LOG_WARN, \"SSL disabled, but MQTT url starts with ssl:// instead of tcp://...\\n\");\n\t}\n\n\t/* Connect configuration */\n\tjanus_config_item *mqtt_version = janus_config_get(config, config_general, janus_config_type_item, \"mqtt_version\");\n\tconst char *mqtt_version_str = (mqtt_version && mqtt_version->value) ? mqtt_version->value : JANUS_MQTT_VERSION_DEFAULT;\n\n\tif(strcmp(mqtt_version_str, JANUS_MQTT_VERSION_3_1) == 0) {\n\t\tctx->connect.mqtt_version = MQTTVERSION_3_1;\n\t} else if(strcmp(mqtt_version_str, JANUS_MQTT_VERSION_3_1_1) == 0) {\n\t\tctx->connect.mqtt_version = MQTTVERSION_3_1_1;\n\t} else if(strcmp(mqtt_version_str, JANUS_MQTT_VERSION_5) == 0) {\n#ifdef MQTTVERSION_5\n\t\tctx->connect.mqtt_version = MQTTVERSION_5;\n#else\n\t\tJANUS_LOG(LOG_FATAL, \"Using MQTT v5 requires compilation with Paho >= 1.3.0\\n\");\n\t\tgoto error;\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_FATAL, \"Unknown MQTT version\\n\");\n\t\tgoto error;\n\t}\n\n\tjanus_config_item *keep_alive_interval_item = janus_config_get(config, config_general, janus_config_type_item, \"keep_alive_interval\");\n\tkeep_alive_interval_item = janus_config_get(config, config_general, janus_config_type_item, \"keep_alive_interval\");\n\tctx->connect.keep_alive_interval = (keep_alive_interval_item && keep_alive_interval_item->value) ?\n\t\tatoi(keep_alive_interval_item->value) : 20;\n\tif(ctx->connect.keep_alive_interval < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid keep-alive value: %s (falling back to default)\\n\", keep_alive_interval_item->value);\n\t\tctx->connect.keep_alive_interval = 20;\n\t}\n\n\tjanus_config_item *cleansession_item = janus_config_get(config, config_general, janus_config_type_item, \"cleansession\");\n\tctx->connect.cleansession = (cleansession_item && cleansession_item->value) ?\n\t\tatoi(cleansession_item->value) : 0;\n\tif(ctx->connect.cleansession < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid clean-session value: %s (falling back to default)\\n\", cleansession_item->value);\n\t\tctx->connect.cleansession = 0;\n\t}\n\n\tjanus_config_item *max_inflight_item = janus_config_get(config, config_general, janus_config_type_item, \"max_inflight\");\n\tmax_inflight_item = janus_config_get(config, config_general, janus_config_type_item, \"max_inflight\");\n\tctx->connect.max_inflight = (max_inflight_item && max_inflight_item->value) ?\n\t\tatoi(max_inflight_item->value) : 10;\n\tif(ctx->connect.max_inflight < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid max-inflight value: %s (falling back to default)\\n\", max_inflight_item->value);\n\t\tctx->connect.max_inflight = 10;\n\t}\n\n\tjanus_config_item *max_buffered_item = janus_config_get(config, config_general, janus_config_type_item, \"max_buffered\");\n\tmax_buffered_item = janus_config_get(config, config_general, janus_config_type_item, \"max_buffered\");\n\tctx->connect.max_buffered = (max_buffered_item && max_buffered_item->value) ?\n\t\tatoi(max_buffered_item->value) : 100;\n\tif(ctx->connect.max_buffered < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid max-buffered value: %s (falling back to default)\\n\", max_buffered_item->value);\n\t\tctx->connect.max_buffered = 100;\n\t}\n\n\tjanus_config_item *enabled_item = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\tif(enabled_item == NULL) {\n\t\t/* Try legacy property */\n\t\tenabled_item = janus_config_get(config, config_general, janus_config_type_item, \"enable\");\n\t\tif (enabled_item && enabled_item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Found deprecated 'enable' property, please update it to 'enabled' instead\\n\");\n\t\t}\n\t}\n\tif(enabled_item && enabled_item->value && janus_is_true(enabled_item->value)) {\n\t\tjanus_mqtt_api_enabled_ = TRUE;\n\n\t\t/* Subscribe configuration */\n\t\t{\n\t\t\tjanus_config_item *topic_item = janus_config_get(config, config_general, janus_config_type_item, \"subscribe_topic\");\n\t\t\tif(!topic_item || !topic_item->value) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Missing topic for incoming messages for MQTT integration...\\n\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tctx->subscribe.topic = g_strdup(topic_item->value);\n\n\t\t\tjanus_config_item *qos_item = janus_config_get(config, config_general, janus_config_type_item, \"subscribe_qos\");\n\t\t\tctx->subscribe.qos = (qos_item && qos_item->value) ? atoi(qos_item->value) : 1;\n\t\t\tif(ctx->subscribe.qos < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid subscribe-qos value: %s (falling back to default)\\n\", qos_item->value);\n\t\t\t\tctx->subscribe.qos = 1;\n\t\t\t}\n\t\t}\n\n\t\t/* Publish configuration */\n\t\t{\n\t\t\tjanus_config_item *topic_item = janus_config_get(config, config_general, janus_config_type_item, \"publish_topic\");\n\t\t\tif(!topic_item || !topic_item->value) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Missing topic for outgoing messages for MQTT integration...\\n\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tctx->publish.topic = g_strdup(topic_item->value);\n\n\t\t\tjanus_config_item *qos_item = janus_config_get(config, config_general, janus_config_type_item, \"publish_qos\");\n\t\t\tctx->publish.qos = (qos_item && qos_item->value) ? atoi(qos_item->value) : 1;\n\t\t\tif(ctx->publish.qos < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid publish-qos value: %s (falling back to default)\\n\", qos_item->value);\n\t\t\t\tctx->publish.qos = 1;\n\t\t\t}\n\n#ifdef MQTTVERSION_5\n\t\t\tif (ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\t\t\t/* MQTT 5 specific configuration */\n\t\t\t\tjanus_config_array *proxy_transaction_user_properties_array = janus_config_get(config, config_general, janus_config_type_array, \"proxy_transaction_user_properties\");\n\t\t\t\tif(proxy_transaction_user_properties_array) {\n\t\t\t\t\tGList *proxy_transaction_user_properties_array_items = janus_config_get_items(config, proxy_transaction_user_properties_array);\n\t\t\t\t\tif(proxy_transaction_user_properties_array_items != NULL) {\n\t\t\t\t\t\tint proxy_transaction_user_properties_array_len = g_list_length(proxy_transaction_user_properties_array_items);\n\t\t\t\t\t\tif(proxy_transaction_user_properties_array_len > 0) {\n\t\t\t\t\t\t\tctx->publish.proxy_transaction_user_properties = g_array_sized_new(FALSE, FALSE, sizeof(char*), proxy_transaction_user_properties_array_len);\n\t\t\t\t\t\t\tg_list_foreach(\n\t\t\t\t\t\t\t\tproxy_transaction_user_properties_array_items,\n\t\t\t\t\t\t\t\t(GFunc)janus_mqtt_set_proxy_transaction_user_property,\n\t\t\t\t\t\t\t\t(gpointer)ctx->publish.proxy_transaction_user_properties\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tjanus_config_array *add_transaction_user_properties_array = janus_config_get(config, config_general, janus_config_type_array, \"add_transaction_user_properties\");\n\t\t\t\tif(add_transaction_user_properties_array) {\n\t\t\t\t\tGList *add_transaction_user_properties_array_items = janus_config_get_arrays(config, add_transaction_user_properties_array);\n\t\t\t\t\tif(add_transaction_user_properties_array_items != NULL) {\n\t\t\t\t\t\tint add_transaction_user_properties_array_len = g_list_length(add_transaction_user_properties_array_items);\n\t\t\t\t\t\tif(add_transaction_user_properties_array_len > 0) {\n\t\t\t\t\t\t\tctx->publish.add_transaction_user_properties = g_array_sized_new(FALSE, FALSE, sizeof(MQTTProperty), add_transaction_user_properties_array_len);\n\t\t\t\t\t\t\tjanus_mqtt_set_add_transaction_user_property_user_data user_data = {\n\t\t\t\t\t\t\t\tctx->publish.add_transaction_user_properties,\n\t\t\t\t\t\t\t\tconfig\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tg_list_foreach(\n\t\t\t\t\t\t\t\tadd_transaction_user_properties_array_items,\n\t\t\t\t\t\t\t\t(GFunc)janus_mqtt_set_add_transaction_user_property,\n\t\t\t\t\t\t\t\t(gpointer)&user_data\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n#endif\n\t\t}\n\t} else {\n\t\tjanus_mqtt_api_enabled_ = FALSE;\n\t\tctx->subscribe.topic = NULL;\n\t\tctx->publish.topic = NULL;\n\t}\n\n\t/* Disconnect configuration */\n\tjanus_config_item *disconnect_timeout_item = janus_config_get(config, config_general, janus_config_type_item, \"disconnect_timeout\");\n\tctx->disconnect.timeout = (disconnect_timeout_item && disconnect_timeout_item->value) ?\n\t\tatoi(disconnect_timeout_item->value) : 100;\n\tif(ctx->disconnect.timeout < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid disconnect-timeout value: %s (falling back to default)\\n\", disconnect_timeout_item->value);\n\t\tctx->disconnect.timeout = 100;\n\t}\n\tjanus_mutex_init(&ctx->disconnect.mutex);\n\tjanus_condition_init(&ctx->disconnect.cond);\n\n\t/* Admin configuration */\n\tjanus_config_item *admin_enabled_item = janus_config_get(config, config_admin, janus_config_type_item, \"admin_enabled\");\n\tif(admin_enabled_item == NULL) {\n\t\t/* Try legacy property */\n\t\tadmin_enabled_item = janus_config_get(config, config_general, janus_config_type_item, \"admin_enable\");\n\t\tif (admin_enabled_item && admin_enabled_item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Found deprecated 'admin_enable' property, please update it to 'admin_enabled' instead\\n\");\n\t\t}\n\t}\n\tif(admin_enabled_item && admin_enabled_item->value && janus_is_true(admin_enabled_item->value)) {\n\t\tjanus_mqtt_admin_api_enabled_ = TRUE;\n\n\t\t/* Admin subscribe configuration */\n\t\t{\n\t\t\tjanus_config_item *topic_item = janus_config_get(config, config_admin, janus_config_type_item, \"subscribe_topic\");\n\t\t\tif(!topic_item || !topic_item->value) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Missing topic for incoming admin messages for MQTT integration...\\n\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tctx->admin.subscribe.topic = g_strdup(topic_item->value);\n\n\t\t\tjanus_config_item *qos_item = janus_config_get(config, config_admin, janus_config_type_item, \"subscribe_qos\");\n\t\t\tctx->admin.subscribe.qos = (qos_item && qos_item->value) ? atoi(qos_item->value) : 1;\n\t\t\tif(ctx->admin.subscribe.qos < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid subscribe-qos value: %s (falling back to default)\\n\", qos_item->value);\n\t\t\t\tctx->admin.subscribe.qos = 1;\n\t\t\t}\n\t\t}\n\n\t\t/* Admin publish configuration */\n\t\t{\n\t\t\tjanus_config_item *topic_item = janus_config_get(config, config_admin, janus_config_type_item, \"publish_topic\");\n\t\t\tif(!topic_item || !topic_item->value) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Missing topic for outgoing admin messages for MQTT integration...\\n\");\n\t\t\t\tgoto error;\n\t\t\t}\n\t\t\tctx->admin.publish.topic = g_strdup(topic_item->value);\n\n\t\t\tjanus_config_item *qos_item = janus_config_get(config, config_admin, janus_config_type_item, \"publish_qos\");\n\t\t\tctx->admin.publish.qos = (qos_item && qos_item->value) ? atoi(qos_item->value) : 1;\n\t\t\tif(ctx->admin.publish.qos < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid publish-qos value: %s (falling back to default)\\n\", qos_item->value);\n\t\t\t\tctx->admin.publish.qos = 1;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tjanus_mqtt_admin_api_enabled_ = FALSE;\n\t\tctx->admin.subscribe.topic = NULL;\n\t\tctx->admin.publish.topic = NULL;\n\t}\n\n\t/* Status messages configuration */\n\tjanus_config_item *status_enabled_item = janus_config_get(config, config_status, janus_config_type_item, \"enabled\");\n\tif(status_enabled_item && status_enabled_item->value && janus_is_true(status_enabled_item->value)) {\n\t\tctx->status.enabled = TRUE;\n\n\t\tjanus_config_item *status_connect_message_item = janus_config_get(config, config_status, janus_config_type_item, \"connect_message\");\n\t\tif(status_connect_message_item && status_connect_message_item->value) {\n\t\t\tctx->status.connect_message = g_strdup(status_connect_message_item->value);\n\t\t}\n\n\t\tjanus_config_item *status_disconnect_message_item = janus_config_get(config, config_status, janus_config_type_item, \"disconnect_message\");\n\t\tif(status_disconnect_message_item && status_disconnect_message_item->value) {\n\t\t\tctx->status.disconnect_message = g_strdup(status_disconnect_message_item->value);\n\t\t}\n\n\t\tjanus_config_item *status_topic_item = janus_config_get(config, config_status, janus_config_type_item, \"topic\");\n\t\tif(status_topic_item && status_topic_item->value) {\n\t\t\tctx->status.topic = g_strdup(status_topic_item->value);\n\t\t} else {\n\t\t\tctx->status.topic = g_strdup(JANUS_MQTT_DEFAULT_STATUS_TOPIC);\n\t\t}\n\n\t\tjanus_config_item *status_qos_item = janus_config_get(config, config_status, janus_config_type_item, \"qos\");\n\t\tif(status_qos_item && status_qos_item->value) {\n\t\t\tctx->status.qos = atoi(status_qos_item->value);\n\t\t\tif(ctx->status.qos < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Invalid status-qos value: %s (disabling)\\n\", status_qos_item->value);\n\t\t\t\tctx->status.qos = 0;\n\t\t\t}\n\t\t}\n\n\t\tjanus_config_item *status_retain_item = janus_config_get(config, config_status, janus_config_type_item, \"retain\");\n\t\tif(status_retain_item && status_retain_item->value && janus_is_true(status_retain_item->value)) {\n\t\t\tctx->status.retain = TRUE;\n\t\t}\n\t}\n\n\tif(!janus_mqtt_api_enabled_ && !janus_mqtt_admin_api_enabled_) {\n\t\tJANUS_LOG(LOG_WARN, \"MQTT support disabled for both Janus and Admin API, giving up\\n\");\n\t\tgoto error;\n\t}\n\n#ifdef MQTTVERSION_5\n\tif (ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\t/* Initialize transaction states hash table and its lock */\n\t\tjanus_mqtt_transaction_states = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, janus_mqtt_transaction_state_free);\n\t\tg_rw_lock_init(&janus_mqtt_transaction_states_lock);\n\n\t\t/* Getting vacuum interval from config */\n\t\tjanus_config_item *vacuum_interval_item = janus_config_get(config, config_general, janus_config_type_item, \"vacuum_interval\");\n\t\tctx->vacuum_interval = (vacuum_interval_item && vacuum_interval_item->value) ? (gint64)atoi(vacuum_interval_item->value) : 60;\n\t\tif(ctx->vacuum_interval <= 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid vacuum interval value: %s (falling back to default)\\n\", vacuum_interval_item->value);\n\t\t\tctx->vacuum_interval = 60;\n\t\t}\n\n\t\t/* Start vacuum thread */\n\t\tvacuum_context = g_main_context_new();\n\t\tvacuum_loop = g_main_loop_new(vacuum_context, FALSE);\n\t\tGError *terror = NULL;\n\t\tvacuum_thread = g_thread_try_new(\"mqtt vacuum\", &janus_mqtt_vacuum_thread, ctx, &terror);\n\t\tif(terror != NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to spawn MQTT transport vacuum thread (%d): %s\\n\", terror->code, terror->message ? terror->message : \"??\");\n\t\t\tgoto error;\n\t\t}\n\t}\n#endif\n\n\t/* Creating a client */\n\tMQTTAsync_createOptions create_options = MQTTAsync_createOptions_initializer;\n\n#ifdef MQTTVERSION_5\n\tif (ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\tcreate_options.MQTTVersion = MQTTVERSION_5;\n\t}\n#endif\n\n\tcreate_options.maxBufferedMessages = ctx->connect.max_buffered;\n\n\tif(MQTTAsync_createWithOptions(\n\t\t\t&ctx->client,\n\t\t\turl,\n\t\t\tclient_id,\n\t\t\tMQTTCLIENT_PERSISTENCE_NONE,\n\t\t\tNULL,\n\t\t\t&create_options) != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker: error creating client...\\n\");\n\t\tgoto error;\n\t}\n\n\tif(MQTTAsync_setConnected(ctx->client, ctx, janus_mqtt_client_connected) != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker: error setting up connected callback...\\n\");\n\t\tgoto error;\n\t}\n\n#ifdef MQTTVERSION_5\n\tif(MQTTAsync_setDisconnected(ctx->client, ctx, janus_mqtt_client_disconnected5) != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker: error setting up disconnected callback...\\n\");\n\t\tgoto error;\n\t}\n\n\tif(MQTTAsync_setConnectionLostCallback(ctx->client, ctx, janus_mqtt_client_connection_lost) != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker: error setting up connection lost callback...\\n\");\n\t\tgoto error;\n\t}\n\n\tif(MQTTAsync_setMessageArrivedCallback(ctx->client, ctx, janus_mqtt_client_message_arrived) != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker: error setting up message arrived callback...\\n\");\n\t\tgoto error;\n\t}\n#else\n\tif(MQTTAsync_setCallbacks(ctx->client, ctx, janus_mqtt_client_connection_lost, janus_mqtt_client_message_arrived, NULL) != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker: error callbacks...\\n\");\n\t\tgoto error;\n\t}\n#endif\n\n\t/* Connecting to the broker */\n\tint rc = janus_mqtt_client_connect(ctx);\n\tif(rc != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker, return code: %d\\n\", rc);\n\t\tgoto error;\n\t}\n\n\tg_free((char *)url);\n\tg_free((char *)client_id);\n\tjanus_config_destroy(config);\n\treturn 0;\n\nerror:\n\t/* If we got here, something went wrong */\n#ifdef MQTTVERSION_5\n\tif(vacuum_loop != NULL)\n\t\tg_main_loop_unref(vacuum_loop);\n\tif(vacuum_context != NULL)\n\t\tg_main_context_unref(vacuum_context);\n#endif\n\tjanus_transport_session_destroy(mqtt_session);\n\tjanus_mqtt_client_destroy_context(&ctx);\n\tg_free((char *)url);\n\tg_free((char *)client_id);\n\tjanus_config_destroy(config);\n\n\treturn -1;\n}\n\nvoid janus_mqtt_destroy(void) {\n\tJANUS_LOG(LOG_INFO, \"Disconnecting MQTT client...\\n\");\n\n\tjanus_transport_session_destroy(mqtt_session);\n\tjanus_mqtt_client_disconnect(context_);\n\n#ifdef MQTTVERSION_5\n\tif(vacuum_thread != NULL) {\n\t\tif(g_main_loop_is_running(vacuum_loop)) {\n\t\t\tg_main_loop_quit(vacuum_loop);\n\t\t\tg_main_context_wakeup(vacuum_context);\n\t\t}\n\t\tg_thread_join(vacuum_thread);\n\t\tvacuum_thread = NULL;\n\t}\n#endif\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_set_proxy_transaction_user_property(gpointer item_ptr, gpointer acc_ptr) {\n\tjanus_config_item *item = (janus_config_item*)item_ptr;\n\tif(item->value == NULL) return;\n\n\tgchar* name = g_strdup(item->value);\n\tg_array_append_val((GArray *)acc_ptr, name);\n}\n\nvoid janus_mqtt_set_add_transaction_user_property(gpointer item_ptr, gpointer user_data_ptr) {\n\tjanus_config_item *item = (janus_config_item*)item_ptr;\n\tif(item->value != NULL) return;\n\n\tjanus_mqtt_set_add_transaction_user_property_user_data *user_data = (janus_mqtt_set_add_transaction_user_property_user_data*)user_data_ptr;\n\tGList *key_value = janus_config_get_items(user_data->config, item);\n\tif(key_value == NULL || g_list_length(key_value) != 2) {\n\t\tJANUS_LOG(LOG_ERR, \"Expected a key-value pair\\n\");\n\t\treturn;\n\t}\n\n\tjanus_config_item *key_item = (janus_config_item*)g_list_first(key_value)->data;\n\tjanus_config_item *value_item = (janus_config_item*)g_list_last(key_value)->data;\n\n\tMQTTProperty property;\n\tproperty.identifier = MQTTPROPERTY_CODE_USER_PROPERTY;\n\tproperty.value.data.data = g_strdup(key_item->value);\n\tproperty.value.data.len = strlen(key_item->value);\n\tproperty.value.value.data = g_strdup(value_item->value);\n\tproperty.value.value.len = strlen(value_item->value);\n\tg_array_append_val(user_data->acc, property);\n}\n#endif\n\nint janus_mqtt_get_api_compatibility(void) {\n\treturn JANUS_TRANSPORT_API_VERSION;\n}\n\nint janus_mqtt_get_version(void) {\n\treturn JANUS_MQTT_VERSION;\n}\n\nconst char *janus_mqtt_get_version_string(void) {\n\treturn JANUS_MQTT_VERSION_STRING;\n}\n\nconst char *janus_mqtt_get_description(void) {\n\treturn JANUS_MQTT_DESCRIPTION;\n}\n\nconst char *janus_mqtt_get_name(void) {\n\treturn JANUS_MQTT_NAME;\n}\n\nconst char *janus_mqtt_get_author(void) {\n\treturn JANUS_MQTT_AUTHOR;\n}\n\nconst char *janus_mqtt_get_package(void) {\n\treturn JANUS_MQTT_PACKAGE;\n}\n\ngboolean janus_mqtt_is_janus_api_enabled(void) {\n\treturn janus_mqtt_api_enabled_;\n}\n\ngboolean janus_mqtt_is_admin_api_enabled(void) {\n\treturn janus_mqtt_admin_api_enabled_;\n}\n\nint janus_mqtt_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message) {\n\tif(message == NULL || transport == NULL) return -1;\n\n\t/* Not really needed as we always only have a single context, but that's fine */\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)transport->transport_p;\n\tif(ctx == NULL) {\n\t\tjson_decref(message);\n\t\treturn -1;\n\t}\n\n\tchar *payload = json_dumps(message, json_format);\n\tif(payload == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_HUGE, \"Sending %s API message via MQTT: %s\\n\", admin ? \"admin\" : \"Janus\", payload);\n\n\tint rc;\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\tchar *response_topic = NULL;\n\t\tMQTTProperties properties = MQTTProperties_initializer;\n\t\tchar *transaction = g_strdup(json_string_value(json_object_get(message, \"transaction\")));\n\t\tjanus_mqtt_transaction_state *state = NULL;\n\n\t\tif(transaction != NULL) {\n\t\t\tg_rw_lock_reader_lock(&janus_mqtt_transaction_states_lock);\n\t\t\tstate = g_hash_table_lookup(janus_mqtt_transaction_states, transaction);\n\n\t\t\tif(state != NULL) {\n\t\t\t\tresponse_topic = janus_mqtt_get_response_topic(state);\n\t\t\t\tjanus_mqtt_proxy_properties(state, ctx->publish.proxy_transaction_user_properties, &properties);\n\t\t\t\tjanus_mqtt_add_properties(state, ctx->publish.add_transaction_user_properties, &properties);\n\t\t\t}\n\n\t\t\tg_rw_lock_reader_unlock(&janus_mqtt_transaction_states_lock);\n\t\t}\n\n\t\trc = janus_mqtt_client_publish_message5(ctx, payload, admin, &properties, response_topic);\n\t\tif(response_topic != NULL) g_free(response_topic);\n\t\tMQTTProperties_free(&properties);\n\t} else {\n\t\trc = janus_mqtt_client_publish_message(ctx, payload, admin);\n\t}\n#else\n\trc = janus_mqtt_client_publish_message(ctx, payload, admin);\n#endif\n\n\tif(rc != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_ERR, \"Can't publish to MQTT topic: %s, return code: %d\\n\", admin ? ctx->admin.publish.topic : ctx->publish.topic, rc);\n\t}\n\n\tjson_decref(message);\n\tfree(payload);\n\treturn 0;\n}\n\n#ifdef MQTTVERSION_5\nchar *janus_mqtt_get_response_topic(janus_mqtt_transaction_state *state) {\n\tMQTTProperty *property = MQTTProperties_getProperty(state->properties, MQTTPROPERTY_CODE_RESPONSE_TOPIC);\n\tif(property == NULL) return NULL;\n\treturn g_strndup(property->value.data.data, property->value.data.len);\n}\n\nvoid janus_mqtt_proxy_properties(janus_mqtt_transaction_state *state, GArray *user_property_names, MQTTProperties *properties) {\n\t/* Proxy correlation data standard property unconditionally */\n\tMQTTProperty *corr_data_req_prop = MQTTProperties_getProperty(state->properties, MQTTPROPERTY_CODE_CORRELATION_DATA);\n\tif(corr_data_req_prop != NULL) {\n\t\tMQTTProperty corr_data_resp_prop;\n\t\tcorr_data_resp_prop.identifier = MQTTPROPERTY_CODE_CORRELATION_DATA;\n\t\tcorr_data_resp_prop.value.data.data = g_strndup(corr_data_req_prop->value.data.data, corr_data_req_prop->value.data.len);\n\t\tcorr_data_resp_prop.value.data.len = corr_data_req_prop->value.data.len;\n\n\t\tint rc = MQTTProperties_add(properties, &corr_data_resp_prop);\n\t\tif(rc != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to add correlation_data property to MQTT response\\n\");\n\t\t}\n\t}\n\n\t/* Proxy additional user properties from config */\n\tif(user_property_names == NULL || user_property_names->len == 0) return;\n\n\tint i = 0;\n\tfor(i = 0; i < state->properties->count; i++) {\n\t\tMQTTProperty request_prop = state->properties->array[i];\n\t\tif(request_prop.identifier != MQTTPROPERTY_CODE_USER_PROPERTY) continue;\n\n\t\tuint j = 0;\n\t\tfor(j = 0; j < user_property_names->len; j++) {\n\t\t\tchar *key = (char*)g_array_index(user_property_names, char*, j);\n\t\t\tint key_len = strlen(key);\n\n\t\t\tif(strncmp(request_prop.value.data.data, key, key_len) == 0) {\n\t\t\t\tMQTTProperty response_prop;\n\t\t\t\tresponse_prop.identifier = MQTTPROPERTY_CODE_USER_PROPERTY;\n\t\t\t\tresponse_prop.value.data.data = key;\n\t\t\t\tresponse_prop.value.data.len = key_len;\n\t\t\t\tresponse_prop.value.value.data = g_strndup(request_prop.value.value.data, request_prop.value.value.len);\n\t\t\t\tresponse_prop.value.value.len = request_prop.value.value.len;\n\n\t\t\t\tint rc = MQTTProperties_add(properties, &response_prop);\n\t\t\t\tif(rc == -1) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Failed to proxy `%s` user property to MQTT response\\n\", key);\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid janus_mqtt_add_properties(janus_mqtt_transaction_state *state, GArray *user_properties, MQTTProperties *properties) {\n\tif(user_properties == NULL || user_properties->len == 0) return;\n\n\tuint i = 0;\n\tfor(i = 0; i < user_properties->len; i++) {\n\t\tMQTTProperty *property = &g_array_index(user_properties, MQTTProperty, i);\n\t\tint rc = MQTTProperties_add(properties, property);\n\t\tif(rc != 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to user properties to MQTT response\\n\");\n\t\t}\n\t}\n}\n#endif\n\nvoid janus_mqtt_session_created(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care */\n}\n\nvoid janus_mqtt_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed) {\n\t/* We don't care, not even if it's a timeout (should we?), our client is always up */\n}\n\nvoid janus_mqtt_session_claimed(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care about this. We should start receiving messages from the core about this session: no action necessary */\n\t/* FIXME Is the above statement accurate? Should we care? Unlike the HTTP transport, there is no hashtable to update */\n}\n\njson_t *janus_mqtt_query_transport(json_t *request) {\n\tif(context_ == NULL) {\n\t\treturn NULL;\n\t}\n\t/* We can use this request to dynamically change the behaviour of\n\t * the transport plugin, and/or query for some specific information */\n\tjson_t *response = json_object();\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_MQTT_ERROR_MISSING_ELEMENT, JANUS_MQTT_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"configure\")) {\n\t\t/* We only allow for the configuration of some basic properties:\n\t\t * changing more complex things (e.g., port to bind to, etc.)\n\t\t * would likely require restarting backends, so just too much */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, configure_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_MQTT_ERROR_MISSING_ELEMENT, JANUS_MQTT_ERROR_INVALID_ELEMENT);\n\t\t/* Check if we now need to send events to handlers */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjson_t *notes = NULL;\n\t\tgboolean events = json_is_true(json_object_get(request, \"events\"));\n\t\tif(events && !context_->gateway->events_is_enabled()) {\n\t\t\t/* Notify that this will be ignored */\n\t\t\tnotes = json_array();\n\t\t\tjson_array_append_new(notes, json_string(\"Event handlers disabled at the core level\"));\n\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t}\n\t\tif(events != notify_events) {\n\t\t\tnotify_events = events;\n\t\t\tif(!notify_events && context_->gateway->events_is_enabled()) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_MQTT_NAME);\n\t\t\t}\n\t\t}\n\t\tconst char *indentation = json_string_value(json_object_get(request, \"json\"));\n\t\tif(indentation != NULL) {\n\t\t\tif(!strcasecmp(indentation, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', ignoring tweak\\n\", indentation);\n\t\t\t\t/* Notify that this will be ignored */\n\t\t\t\tif(notes == NULL) {\n\t\t\t\t\tnotes = json_array();\n\t\t\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t\t\t}\n\t\t\t\tjson_array_append_new(notes, json_string(\"Ignored unsupported indentation format\"));\n\t\t\t}\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_MQTT_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\nvoid janus_mqtt_client_connected(void *context, char *cause) {\n\tJANUS_LOG(LOG_INFO, \"Connected to MQTT broker: %s\\n\", cause);\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\n\t/* Subscribe to one (janus or admin) topic at the time */\n\tif(janus_mqtt_api_enabled_) {\n\t\tJANUS_LOG(LOG_INFO, \"Subscribing to MQTT topic %s\\n\", ctx->subscribe.topic);\n\t\tint rc = janus_mqtt_client_subscribe(context, FALSE);\n\t\tif(rc != MQTTASYNC_SUCCESS) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't subscribe to MQTT topic: %s, return code: %d\\n\", ctx->subscribe.topic, rc);\n\t\t}\n\t} else if(janus_mqtt_admin_api_enabled_) {\n\t\tJANUS_LOG(LOG_INFO, \"Subscribing to MQTT admin topic %s\\n\", ctx->admin.subscribe.topic);\n\t\tint rc = janus_mqtt_client_subscribe(context, TRUE);\n\t\tif(rc != MQTTASYNC_SUCCESS) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't subscribe to MQTT admin topic: %s, return code: %d\\n\", ctx->admin.subscribe.topic, rc);\n\t\t}\n\t}\n\n\t/* Notify handlers about this new transport */\n\tif(notify_events && ctx->gateway && ctx->gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(\"connected\"));\n\t\tctx->gateway->notify_event(&janus_mqtt_transport_, mqtt_session, info);\n\t}\n\n\t/* Send online status message */\n\tif (ctx->status.enabled && ctx->status.connect_message != NULL) {\n\t\tint rc = janus_mqtt_client_publish_status_message(ctx, ctx->status.connect_message);\n\t\tif (rc != MQTTASYNC_SUCCESS) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to publish connect status MQTT message, topic: %s, message: %s, return code: %d\\n\", ctx->status.topic, ctx->status.connect_message, rc);\n\t\t}\n\t}\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_disconnected5(void *context, MQTTProperties *properties, enum MQTTReasonCodes reasonCode) {\n\tconst char *reasonCodeStr = MQTTReasonCode_toString(reasonCode);\n\tJANUS_LOG(LOG_INFO, \"Disconnected from MQTT broker: %s\\n\", reasonCodeStr);\n\n\t/* Notify handlers about this transport being gone */\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tif(notify_events && ctx && ctx->gateway && ctx->gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(\"disconnected\"));\n\t\tctx->gateway->notify_event(&janus_mqtt_transport_, mqtt_session, info);\n\t}\n}\n#endif\n\nvoid janus_mqtt_client_connection_lost(void *context, char *cause) {\n\tJANUS_LOG(LOG_INFO, \"MQTT connection lost cause of %s. Reconnecting...\\n\", cause);\n\t/* Automatic reconnect */\n\n\t/* Notify handlers about this transport being gone */\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tif(notify_events && ctx && ctx->gateway && ctx->gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(\"reconnecting\"));\n\t\tctx->gateway->notify_event(&janus_mqtt_transport_, mqtt_session, info);\n\t}\n}\n\nint janus_mqtt_client_message_arrived(void *context, char *topicName, int topicLen, MQTTAsync_message *message) {\n\tint ret = FALSE;\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tgchar *topic = g_strndup(topicName, topicLen);\n\tconst gboolean janus = janus_mqtt_api_enabled_ && !strcasecmp(topic, ctx->subscribe.topic);\n\tconst gboolean admin = janus_mqtt_admin_api_enabled_ && !strcasecmp(topic, ctx->admin.subscribe.topic);\n\tg_free(topic);\n\n\tif((janus || admin) && message->payloadlen) {\n\t\tJANUS_LOG(LOG_HUGE, \"Receiving %s API message over MQTT: %.*s\\n\", admin ? \"admin\" : \"Janus\", message->payloadlen, (char*)message->payload);\n\n\t\tjson_error_t error;\n\t\tjson_t *root = json_loadb(message->payload, message->payloadlen, 0, &error);\n\n#ifdef MQTTVERSION_5\n\t\tif(ctx->connect.mqtt_version == MQTTVERSION_5 && !admin) {\n\t\t\t/* Save MQTT 5 properties copy to the state */\n\t\t\tconst gchar *transaction = g_strdup(json_string_value(json_object_get(root, \"transaction\")));\n\t\t\tif(transaction == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"`transaction` is missing or not a string\\n\");\n\t\t\t\tgoto done;\n\t\t\t}\n\n\t\t\tMQTTProperties *properties = g_malloc(sizeof(MQTTProperties));\n\t\t\t*properties = MQTTProperties_copy(&message->properties);\n\n\t\t\tjanus_mqtt_transaction_state *state = g_malloc(sizeof(janus_mqtt_transaction_state));\n\t\t\tstate->properties = properties;\n\t\t\tstate->created_at = janus_get_monotonic_time();\n\n\t\t\tg_rw_lock_writer_lock(&janus_mqtt_transaction_states_lock);\n\t\t\tg_hash_table_insert(janus_mqtt_transaction_states, (gpointer) transaction, (gpointer) state);\n\t\t\tg_rw_lock_writer_unlock(&janus_mqtt_transaction_states_lock);\n\t\t}\n#endif\n\n\t\tctx->gateway->incoming_request(&janus_mqtt_transport_, mqtt_session, NULL, admin, root, &error);\n\t}\n\n\tret = TRUE;\n\ndone:\n\tMQTTAsync_freeMessage(&message);\n\tMQTTAsync_free(topicName);\n\treturn ret;\n}\n\nint janus_mqtt_client_connect(janus_mqtt_context *ctx) {\n\tMQTTAsync_connectOptions options = MQTTAsync_connectOptions_initializer;\n\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\tMQTTAsync_connectOptions options5 = MQTTAsync_connectOptions_initializer5;\n\t\toptions = options5;\n\t\toptions.cleanstart = ctx->connect.cleansession;\n\t\toptions.onFailure5 = janus_mqtt_client_connect_failure5;\n\t} else {\n\t\toptions.cleansession = ctx->connect.cleansession;\n\t\toptions.onFailure = janus_mqtt_client_connect_failure;\n\t}\n#else\n\toptions.cleansession = ctx->connect.cleansession;\n\toptions.onFailure = janus_mqtt_client_connect_failure;\n#endif\n\n\toptions.MQTTVersion = ctx->connect.mqtt_version;\n\toptions.username = ctx->connect.username;\n\toptions.password = ctx->connect.password;\n\toptions.automaticReconnect = TRUE;\n\toptions.keepAliveInterval = ctx->connect.keep_alive_interval;\n\toptions.maxInflight = ctx->connect.max_inflight;\n\n\tMQTTAsync_SSLOptions ssl_opts = MQTTAsync_SSLOptions_initializer;\n\tif(ctx->ssl_enabled) {\n\t\tssl_opts.trustStore = ctx->cacert_file;\n\t\tssl_opts.keyStore = ctx->cert_file;\n\t\tssl_opts.privateKey = ctx->key_file;\n\t\tssl_opts.enableServerCertAuth = ctx->verify_peer;\n\t\toptions.ssl = &ssl_opts;\n\t}\n\n\tMQTTAsync_willOptions willOptions = MQTTAsync_willOptions_initializer;\n\tif(ctx->status.enabled && ctx->status.disconnect_message != NULL) {\n\t\twillOptions.topicName = ctx->status.topic;\n\t\twillOptions.message = ctx->status.disconnect_message;\n\t\twillOptions.retained = ctx->status.retain;\n\t\twillOptions.qos = ctx->status.qos;\n\t\toptions.will = &willOptions;\n\t}\n\n\toptions.context = ctx;\n\treturn MQTTAsync_connect(ctx->client, &options);\n}\n\nvoid janus_mqtt_client_connect_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_connect_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_connect_failure5(void *context, MQTTAsync_failureData5 *response) {\n\t\tint rc = janus_mqtt_client_get_response_code5(response);\n\t\tjanus_mqtt_client_connect_failure_impl(context, rc);\n}\n#endif\n\nvoid janus_mqtt_client_connect_failure_impl(void *context, int rc) {\n\tJANUS_LOG(LOG_ERR, \"MQTT client has failed connecting to the broker, return code: %d. Reconnecting...\\n\", rc);\n\t/* Automatic reconnect */\n\n\t/* Notify handlers about this transport failure */\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tif(notify_events && ctx && ctx->gateway && ctx->gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(\"failed\"));\n\t\tjson_object_set_new(info, \"code\", json_integer(rc));\n\t\tctx->gateway->notify_event(&janus_mqtt_transport_, mqtt_session, info);\n\t}\n}\n\nint janus_mqtt_client_reconnect(janus_mqtt_context *ctx) {\n\tMQTTAsync_disconnectOptions options = MQTTAsync_disconnectOptions_initializer;\n\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\toptions.onSuccess5 = janus_mqtt_client_reconnect_success5;\n\t\toptions.onFailure5 = janus_mqtt_client_reconnect_failure5;\n\t} else {\n\t\toptions.onSuccess = janus_mqtt_client_reconnect_success;\n\t\toptions.onFailure = janus_mqtt_client_reconnect_failure;\n\t}\n#else\n\toptions.onSuccess = janus_mqtt_client_reconnect_success;\n\toptions.onFailure = janus_mqtt_client_reconnect_failure;\n#endif\n\n\toptions.context = ctx;\n\toptions.timeout = ctx->disconnect.timeout;\n\treturn MQTTAsync_disconnect(ctx->client, &options);\n}\n\nvoid janus_mqtt_client_reconnect_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqtt_client_reconnect_success_impl(context);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_reconnect_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqtt_client_reconnect_success_impl(context);\n}\n#endif\n\nvoid janus_mqtt_client_reconnect_success_impl(void *context) {\n\tJANUS_LOG(LOG_INFO, \"MQTT client has been successfully disconnected. Reconnecting...\\n\");\n\n\tint rc = janus_mqtt_client_connect(context);\n\tif(rc != MQTTASYNC_SUCCESS) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to MQTT broker, return code: %d\\n\", rc);\n\t}\n}\n\nvoid janus_mqtt_client_reconnect_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_reconnect_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_reconnect_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqtt_client_get_response_code5(response);\n\tjanus_mqtt_client_reconnect_failure_impl(context, rc);\n}\n#endif\n\nvoid janus_mqtt_client_reconnect_failure_impl(void *context, int rc) {\n\tJANUS_LOG(LOG_ERR, \"MQTT client has failed reconnecting from MQTT broker, return code: %d\\n\", rc);\n}\n\nint janus_mqtt_client_disconnect(janus_mqtt_context *ctx) {\n\tif (ctx->status.enabled && ctx->status.disconnect_message != NULL) {\n\t\tint rc = janus_mqtt_client_publish_status_message(ctx, ctx->status.disconnect_message);\n\t\tif (rc != MQTTASYNC_SUCCESS) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Failed to publish disconnect status MQTT message, topic: %s, message: %s, return code: %d\\n\", ctx->status.topic, ctx->status.disconnect_message, rc);\n\t\t}\n\t}\n\n\tMQTTAsync_disconnectOptions options = MQTTAsync_disconnectOptions_initializer;\n\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\toptions.onSuccess5 = janus_mqtt_client_disconnect_success5;\n\t\toptions.onFailure5 = janus_mqtt_client_disconnect_failure5;\n\t} else {\n\t\toptions.onSuccess = janus_mqtt_client_disconnect_success;\n\t\toptions.onFailure = janus_mqtt_client_disconnect_failure;\n\t}\n#else\n\toptions.onSuccess = janus_mqtt_client_disconnect_success;\n\toptions.onFailure = janus_mqtt_client_disconnect_failure;\n#endif\n\n\toptions.context = ctx;\n\toptions.timeout = ctx->disconnect.timeout;\n\tint rc = MQTTAsync_disconnect(ctx->client, &options);\n\tif (rc == MQTTASYNC_SUCCESS) {\n\t\tjanus_mutex_lock(&ctx->disconnect.mutex);\n\t\t/* Block the thread awaiting asynchronous graceful disconnect to finish. */\n\t\tgint64 deadline = janus_get_monotonic_time() + ctx->disconnect.timeout * G_TIME_SPAN_MILLISECOND;\n\t\tjanus_condition_wait_until(&ctx->disconnect.cond, &ctx->disconnect.mutex, deadline);\n\t\tjanus_mutex_unlock(&ctx->disconnect.mutex);\n\t\tjanus_mqtt_client_destroy_context(&ctx);\n\t}\n\treturn rc;\n}\n\nvoid janus_mqtt_client_disconnect_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqtt_client_disconnect_success_impl(context);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_disconnect_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqtt_client_disconnect_success_impl(context);\n}\n#endif\n\nvoid janus_mqtt_client_disconnect_success_impl(void *context) {\n\tJANUS_LOG(LOG_INFO, \"MQTT client has been successfully disconnected.\\n\");\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tjanus_mutex_lock(&ctx->disconnect.mutex);\n\tjanus_condition_signal(&ctx->disconnect.cond);\n\tjanus_mutex_unlock(&ctx->disconnect.mutex);\n}\n\nvoid janus_mqtt_client_disconnect_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_disconnect_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_disconnect_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqtt_client_get_response_code5(response);\n\tjanus_mqtt_client_disconnect_failure_impl(context, rc);\n}\n#endif\n\nvoid janus_mqtt_client_disconnect_failure_impl(void *context, int rc) {\n\tJANUS_LOG(LOG_ERR, \"Can't disconnect from MQTT broker, return code: %d\\n\", rc);\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tjanus_mutex_lock(&ctx->disconnect.mutex);\n\tjanus_condition_signal(&ctx->disconnect.cond);\n\tjanus_mutex_unlock(&ctx->disconnect.mutex);\n}\n\nint janus_mqtt_client_subscribe(janus_mqtt_context *ctx, gboolean admin) {\n\tMQTTAsync_responseOptions options = MQTTAsync_responseOptions_initializer;\n\toptions.context = ctx;\n\tif(admin) {\n#ifdef MQTTVERSION_5\n\t\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\t\toptions.onSuccess5 = janus_mqtt_client_admin_subscribe_success5;\n\t\t\toptions.onFailure5 = janus_mqtt_client_admin_subscribe_failure5;\n\t\t} else {\n\t\t\toptions.onSuccess = janus_mqtt_client_admin_subscribe_success;\n\t\t\toptions.onFailure = janus_mqtt_client_admin_subscribe_failure;\n\t\t}\n#else\n\t\toptions.onSuccess = janus_mqtt_client_admin_subscribe_success;\n\t\toptions.onFailure = janus_mqtt_client_admin_subscribe_failure;\n#endif\n\t\treturn MQTTAsync_subscribe(ctx->client, ctx->admin.subscribe.topic, ctx->admin.subscribe.qos, &options);\n\t} else {\n#ifdef MQTTVERSION_5\n\t\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\t\toptions.onSuccess5 = janus_mqtt_client_subscribe_success5;\n\t\t\toptions.onFailure5 = janus_mqtt_client_subscribe_failure5;\n\t\t} else {\n\t\t\toptions.onSuccess = janus_mqtt_client_subscribe_success;\n\t\t\toptions.onFailure = janus_mqtt_client_subscribe_failure;\n\t\t}\n#else\n\t\toptions.onSuccess = janus_mqtt_client_subscribe_success;\n\t\toptions.onFailure = janus_mqtt_client_subscribe_failure;\n#endif\n\t\treturn MQTTAsync_subscribe(ctx->client, ctx->subscribe.topic, ctx->subscribe.qos, &options);\n\t}\n}\n\nvoid janus_mqtt_client_subscribe_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqtt_client_subscribe_success_impl(context);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_subscribe_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqtt_client_subscribe_success_impl(context);\n}\n#endif\n\nvoid janus_mqtt_client_subscribe_success_impl(void *context) {\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tJANUS_LOG(LOG_INFO, \"MQTT client has been successfully subscribed to MQTT topic: %s\\n\", ctx->subscribe.topic);\n\n\t/* Subscribe to admin topic if we haven't done it yet */\n\tif(janus_mqtt_admin_api_enabled_ && (!janus_mqtt_api_enabled_ || strcasecmp(ctx->subscribe.topic, ctx->admin.subscribe.topic))) {\n\t\tint rc = janus_mqtt_client_subscribe(context, TRUE);\n\t\tif(rc != MQTTASYNC_SUCCESS) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Can't subscribe to MQTT topic: %s, return code: %d\\n\", ctx->subscribe.topic, rc);\n\t\t}\n\t}\n}\n\nvoid janus_mqtt_client_subscribe_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_subscribe_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_subscribe_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqtt_client_get_response_code5(response);\n\tjanus_mqtt_client_subscribe_failure_impl(context, rc);\n}\n#endif\n\nvoid janus_mqtt_client_subscribe_failure_impl(void *context, int rc) {\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tJANUS_LOG(LOG_ERR, \"MQTT client has failed subscribing to MQTT topic: %s, return code: %d. Reconnecting...\\n\", ctx->subscribe.topic, rc);\n\n\t/* Reconnect */\n\t{\n\t\tint rc = janus_mqtt_client_reconnect(context);\n\t\tif(rc != MQTTASYNC_SUCCESS) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Can't reconnect to MQTT broker, return code: %d\\n\", rc);\n\t\t}\n\t}\n}\n\nvoid janus_mqtt_client_admin_subscribe_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqtt_client_admin_subscribe_success_impl(context);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_admin_subscribe_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqtt_client_admin_subscribe_success_impl(context);\n}\n#endif\n\nvoid janus_mqtt_client_admin_subscribe_success_impl(void *context) {\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tJANUS_LOG(LOG_INFO, \"MQTT client has been successfully subscribed to MQTT topic: %s\\n\", ctx->admin.subscribe.topic);\n}\n\nvoid janus_mqtt_client_admin_subscribe_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_admin_subscribe_failure_impl(context, rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_admin_subscribe_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqtt_client_get_response_code5(response);\n\tjanus_mqtt_client_admin_subscribe_failure_impl(context, rc);\n}\n#endif\n\nvoid janus_mqtt_client_admin_subscribe_failure_impl(void *context, int rc) {\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)context;\n\tJANUS_LOG(LOG_ERR, \"MQTT client has failed subscribing to MQTT topic: %s, return code: %d. Reconnecting...\\n\", ctx->admin.subscribe.topic, rc);\n\n\t/* Reconnect */\n\t{\n\t\tint rc = janus_mqtt_client_reconnect(context);\n\t\tif(rc != MQTTASYNC_SUCCESS) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Can't reconnect to MQTT broker, return code: %d\\n\", rc);\n\t\t}\n\t}\n}\n\nint janus_mqtt_client_publish_message(janus_mqtt_context *ctx, char *payload, gboolean admin) {\n\tMQTTAsync_message msg = MQTTAsync_message_initializer;\n\tmsg.payload = payload;\n\tmsg.payloadlen = strlen(payload);\n\tmsg.qos = ctx->publish.qos;\n\tmsg.retained = FALSE;\n\n\tchar *topic = admin ? ctx->admin.publish.topic : ctx->publish.topic;\n\n\tMQTTAsync_responseOptions options = MQTTAsync_responseOptions_initializer;\n\toptions.context = ctx;\n\n\tif(admin) {\n\t\toptions.onSuccess = janus_mqtt_client_publish_admin_success;\n\t\toptions.onFailure = janus_mqtt_client_publish_admin_failure;\n\t} else {\n\t\toptions.onSuccess = janus_mqtt_client_publish_janus_success;\n\t\toptions.onFailure = janus_mqtt_client_publish_janus_failure;\n\t}\n\n\treturn MQTTAsync_sendMessage(ctx->client, topic, &msg, &options);\n}\n\n#ifdef MQTTVERSION_5\nint janus_mqtt_client_publish_message5(janus_mqtt_context *ctx, char *payload, gboolean admin, MQTTProperties *properties, char *custom_topic) {\n\tMQTTAsync_message msg = MQTTAsync_message_initializer;\n\tmsg.payload = payload;\n\tmsg.payloadlen = strlen(payload);\n\tmsg.qos = ctx->publish.qos;\n\tmsg.retained = FALSE;\n\tmsg.properties = MQTTProperties_copy(properties);\n\n\tchar *topic;\n\tif(custom_topic) {\n\t\ttopic = custom_topic;\n\t} else if(admin) {\n\t\ttopic = ctx->admin.publish.topic;\n\t} else {\n\t\ttopic = ctx->publish.topic;\n\t}\n\n\tMQTTAsync_responseOptions options = MQTTAsync_responseOptions_initializer;\n\toptions.context = ctx;\n\n\tif(admin) {\n\t\toptions.onSuccess5 = janus_mqtt_client_publish_admin_success5;\n\t\toptions.onFailure5 = janus_mqtt_client_publish_admin_failure5;\n\t} else {\n\t\toptions.onSuccess5 = janus_mqtt_client_publish_janus_success5;\n\t\toptions.onFailure5 = janus_mqtt_client_publish_janus_failure5;\n\t}\n\n\treturn MQTTAsync_sendMessage(ctx->client, topic, &msg, &options);\n}\n#endif\n\nvoid janus_mqtt_client_publish_janus_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqtt_client_publish_janus_success_impl(response->alt.pub.destinationName);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_publish_janus_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqtt_client_publish_janus_success_impl(response->alt.pub.destinationName);\n}\n#endif\n\nvoid janus_mqtt_client_publish_janus_success_impl(char *topic) {\n\tJANUS_LOG(LOG_HUGE, \"MQTT client has been successfully published to MQTT topic: %s\\n\", topic);\n}\n\nvoid janus_mqtt_client_publish_janus_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_publish_janus_failure_impl(rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_publish_janus_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqtt_client_get_response_code5(response);\n\tjanus_mqtt_client_publish_janus_failure_impl(rc);\n}\n#endif\n\nvoid janus_mqtt_client_publish_janus_failure_impl(int rc) {\n\tJANUS_LOG(LOG_ERR, \"MQTT client has failed publishing, return code: %d\\n\", rc);\n}\n\nvoid janus_mqtt_client_publish_admin_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqtt_client_publish_admin_success_impl(response->alt.pub.destinationName);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_publish_admin_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqtt_client_publish_admin_success_impl(response->alt.pub.destinationName);\n}\n#endif\n\nvoid janus_mqtt_client_publish_admin_success_impl(char *topic) {\n\tJANUS_LOG(LOG_HUGE, \"MQTT client has been successfully published to MQTT topic: %s\\n\", topic);\n}\n\nvoid janus_mqtt_client_publish_admin_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_publish_admin_failure_impl(rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_publish_admin_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqtt_client_get_response_code5(response);\n\tjanus_mqtt_client_publish_admin_failure_impl(rc);\n}\n#endif\n\nvoid janus_mqtt_client_publish_admin_failure_impl(int rc) {\n\tJANUS_LOG(LOG_ERR, \"MQTT client has failed publishing to admin topic, return code: %d\\n\", rc);\n}\n\nint janus_mqtt_client_publish_status_message(janus_mqtt_context *ctx, char *payload) {\n\tMQTTAsync_message msg = MQTTAsync_message_initializer;\n\tmsg.payload = payload;\n\tmsg.payloadlen = strlen(payload);\n\tmsg.qos = ctx->status.qos;\n\tmsg.retained = ctx->status.retain;\n\n\tMQTTAsync_responseOptions options = MQTTAsync_responseOptions_initializer;\n\toptions.context = ctx;\n\n#ifdef MQTTVERSION_5\n\tif(ctx->connect.mqtt_version == MQTTVERSION_5) {\n\t\toptions.onSuccess5 = janus_mqtt_client_publish_status_success5;\n\t\toptions.onFailure5 = janus_mqtt_client_publish_status_failure5;\n\t} else {\n\t\toptions.onSuccess = janus_mqtt_client_publish_status_success;\n\t\toptions.onFailure = janus_mqtt_client_publish_status_failure;\n\t}\n#else\n\toptions.onSuccess = janus_mqtt_client_publish_status_success;\n\toptions.onFailure = janus_mqtt_client_publish_status_failure;\n#endif\n\n\treturn MQTTAsync_sendMessage(ctx->client, ctx->status.topic, &msg, &options);\n}\n\nvoid janus_mqtt_client_publish_status_success(void *context, MQTTAsync_successData *response) {\n\tjanus_mqtt_client_publish_status_success_impl(response->alt.pub.destinationName);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_publish_status_success5(void *context, MQTTAsync_successData5 *response) {\n\tjanus_mqtt_client_publish_status_success_impl(response->alt.pub.destinationName);\n}\n#endif\n\nvoid janus_mqtt_client_publish_status_success_impl(char *topic) {\n\tJANUS_LOG(LOG_HUGE, \"MQTT client has been successfully published to status MQTT topic: %s\\n\", topic);\n}\n\nvoid janus_mqtt_client_publish_status_failure(void *context, MQTTAsync_failureData *response) {\n\tint rc = janus_mqtt_client_get_response_code(response);\n\tjanus_mqtt_client_publish_status_failure_impl(rc);\n}\n\n#ifdef MQTTVERSION_5\nvoid janus_mqtt_client_publish_status_failure5(void *context, MQTTAsync_failureData5 *response) {\n\tint rc = janus_mqtt_client_get_response_code5(response);\n\tjanus_mqtt_client_publish_status_failure_impl(rc);\n}\n#endif\n\nvoid janus_mqtt_client_publish_status_failure_impl(int rc) {\n\tJANUS_LOG(LOG_ERR, \"MQTT client has failed publishing to status topic, return code: %d\\n\", rc);\n}\n\nvoid janus_mqtt_client_destroy_context(janus_mqtt_context **ptr) {\n\tjanus_mqtt_context *ctx = (janus_mqtt_context *)*ptr;\n\tif(ctx) {\n\t\tMQTTAsync_destroy(&ctx->client);\n\t\tg_free(ctx->subscribe.topic);\n\t\tg_free(ctx->publish.topic);\n\t\tg_free(ctx->connect.username);\n\t\tg_free(ctx->connect.password);\n\t\tjanus_mutex_destroy(&ctx->disconnect.mutex);\n\t\tjanus_condition_destroy(&ctx->disconnect.cond);\n\t\tg_free(ctx->admin.subscribe.topic);\n\t\tg_free(ctx->admin.publish.topic);\n\t#ifdef MQTTVERSION_5\n\t\tg_rw_lock_clear(&janus_mqtt_transaction_states_lock);\n\t#endif\n\t\tg_free(ctx);\n\t\t*ptr = NULL;\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_MQTT_NAME);\n}\n\nint janus_mqtt_client_get_response_code(MQTTAsync_failureData *response) {\n\treturn response ? response->code : 0;\n}\n\n#ifdef MQTTVERSION_5\nint janus_mqtt_client_get_response_code5(MQTTAsync_failureData5 *response) {\n\treturn response ? response->code : 0;\n}\n#endif\n\n#ifdef MQTTVERSION_5\nstatic gpointer janus_mqtt_vacuum_thread(gpointer context) {\n\tjanus_mqtt_context *ctx = (janus_mqtt_context*)context;\n\n\tGSource *timeout_source;\n\ttimeout_source = g_timeout_source_new_seconds(ctx->vacuum_interval);\n\tg_source_set_callback(timeout_source, janus_mqtt_vacuum, context, NULL);\n\tg_source_attach(timeout_source, vacuum_context);\n\tg_source_unref(timeout_source);\n\n\tJANUS_LOG(LOG_VERB, \"Starting MQTT transport vacuum thread\\n\");\n\tg_main_loop_run(vacuum_loop);\n\tJANUS_LOG(LOG_VERB, \"MQTT transport vacuum thread finished\\n\");\n\treturn NULL;\n}\n\nstatic gboolean janus_mqtt_vacuum(gpointer context) {\n\tjanus_mqtt_context *ctx = (janus_mqtt_context*)context;\n\tGHashTableIter iter;\n\tgpointer value;\n\n\tg_rw_lock_writer_lock(&janus_mqtt_transaction_states_lock);\n\tg_hash_table_iter_init(&iter, janus_mqtt_transaction_states);\n\n\twhile (g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\tjanus_mqtt_transaction_state* state = value;\n\t\tgint64 diff = (janus_get_monotonic_time() - state->created_at) / G_USEC_PER_SEC;\n\t\tif(diff > ctx->vacuum_interval) {\n\t\t\tg_hash_table_iter_remove(&iter);\n\t\t}\n\t}\n\n\tg_rw_lock_writer_unlock(&janus_mqtt_transaction_states_lock);\n\treturn G_SOURCE_CONTINUE;\n}\n\nvoid janus_mqtt_transaction_state_free(gpointer state_ptr) {\n\tjanus_mqtt_transaction_state *state = (janus_mqtt_transaction_state*)state_ptr;\n\tMQTTProperties_free(state->properties);\n\tg_free(state->properties);\n\tg_free(state);\n}\n#endif\n"
  },
  {
    "path": "src/transports/janus_nanomsg.c",
    "content": "/*! \\file   janus_nanomsg.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Nanomsg transport plugin\n * \\details  This is an implementation of a Nanomsg transport for the\n * Janus API. This means that, with the help of this module, local and\n * remote applications can use Nanomsg to make requests to Janus.\n * Note that not all the protocols Nanomsg implements are made available\n * in this plugin: specifically, you'll only be able to use the \\c NN_PAIR\n * transport mechanism. Future versions may implement more, but for the\n * time being these should be enough to cover most development requirements.\n *\n * \\ingroup transports\n * \\ref transports\n */\n\n#include \"transport.h\"\n\n#include <nanomsg/nn.h>\n#include <nanomsg/pair.h>\n#include <nanomsg/inproc.h>\n#include <nanomsg/ipc.h>\n#include <nanomsg/pipeline.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../utils.h\"\n\n\n/* Transport plugin information */\n#define JANUS_NANOMSG_VERSION\t\t\t1\n#define JANUS_NANOMSG_VERSION_STRING\t\"0.0.1\"\n#define JANUS_NANOMSG_DESCRIPTION\t\t\"This transport plugin adds Nanomsg support to the Janus API.\"\n#define JANUS_NANOMSG_NAME\t\t\t\t\"JANUS Nanomsg transport plugin\"\n#define JANUS_NANOMSG_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_NANOMSG_PACKAGE\t\t\t\"janus.transport.nanomsg\"\n\n/* Transport methods */\njanus_transport *create(void);\nint janus_nanomsg_init(janus_transport_callbacks *callback, const char *config_path);\nvoid janus_nanomsg_destroy(void);\nint janus_nanomsg_get_api_compatibility(void);\nint janus_nanomsg_get_version(void);\nconst char *janus_nanomsg_get_version_string(void);\nconst char *janus_nanomsg_get_description(void);\nconst char *janus_nanomsg_get_name(void);\nconst char *janus_nanomsg_get_author(void);\nconst char *janus_nanomsg_get_package(void);\ngboolean janus_nanomsg_is_janus_api_enabled(void);\ngboolean janus_nanomsg_is_admin_api_enabled(void);\nint janus_nanomsg_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message);\nvoid janus_nanomsg_session_created(janus_transport_session *transport, guint64 session_id);\nvoid janus_nanomsg_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed);\nvoid janus_nanomsg_session_claimed(janus_transport_session *transport, guint64 session_id);\njson_t *janus_nanomsg_query_transport(json_t *request);\n\n\n/* Transport setup */\nstatic janus_transport janus_nanomsg_transport =\n\tJANUS_TRANSPORT_INIT (\n\t\t.init = janus_nanomsg_init,\n\t\t.destroy = janus_nanomsg_destroy,\n\n\t\t.get_api_compatibility = janus_nanomsg_get_api_compatibility,\n\t\t.get_version = janus_nanomsg_get_version,\n\t\t.get_version_string = janus_nanomsg_get_version_string,\n\t\t.get_description = janus_nanomsg_get_description,\n\t\t.get_name = janus_nanomsg_get_name,\n\t\t.get_author = janus_nanomsg_get_author,\n\t\t.get_package = janus_nanomsg_get_package,\n\n\t\t.is_janus_api_enabled = janus_nanomsg_is_janus_api_enabled,\n\t\t.is_admin_api_enabled = janus_nanomsg_is_admin_api_enabled,\n\n\t\t.send_message = janus_nanomsg_send_message,\n\t\t.session_created = janus_nanomsg_session_created,\n\t\t.session_over = janus_nanomsg_session_over,\n\t\t.session_claimed = janus_nanomsg_session_claimed,\n\n\t\t.query_transport = janus_nanomsg_query_transport,\n\t);\n\n/* Transport creator */\njanus_transport *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_NANOMSG_NAME);\n\treturn &janus_nanomsg_transport;\n}\n\n\n/* Useful stuff */\nstatic gint initialized = 0, stopping = 0;\nstatic janus_transport_callbacks *gateway = NULL;\nstatic gboolean notify_events = TRUE;\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n#define BUFFER_SIZE\t\t8192\n\n/* Parameter validation (for tweaking and queries via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"events\", JANUS_JSON_BOOL, 0},\n\t{\"json\", JSON_STRING, 0},\n};\n/* Error codes (for the tweaking and queries via Admin API) */\n#define JANUS_NANOMSG_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_NANOMSG_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_NANOMSG_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_NANOMSG_ERROR_UNKNOWN_ERROR\t\t499\n\n\n/* Nanomsg server thread */\nstatic GThread *nanomsg_thread = NULL;\nvoid *janus_nanomsg_thread(void *data);\n\n/* Nanomsg servers */\nstatic int nfd = -1, nfd_addr = -1, admin_nfd = -1, admin_nfd_addr = -1;\n/* Pipeline to notify about the need for outgoing data */\nstatic int write_nfd[2];\n\n/* Nanomsg client session */\ntypedef struct janus_nanomsg_client {\n\tgboolean admin;\t\t\t\t\t/* Whether this client is for the Admin or Janus API */\n\tGAsyncQueue *messages;\t\t\t/* Queue of outgoing messages to push */\n\tjanus_transport_session *ts;\t/* Janus core-transport session */\n} janus_nanomsg_client;\n/* We only handle a single client per API, since we use NN_PAIR and we bind locally */\nstatic janus_nanomsg_client client, admin_client;\n\n\n/* Transport implementation */\nint janus_nanomsg_init(janus_transport_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_NANOMSG_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_NANOMSG_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_NANOMSG_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\t/* Handle configuration */\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_category *config_admin = janus_config_get_create(config, NULL, janus_config_type_category, \"admin\");\n\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\t\tif(item && item->value) {\n\t\t\t/* Check how we need to format/serialize the JSON output */\n\t\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t}\n\t\t}\n\n\t\t/* Check if we need to send events to handlers */\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_NANOMSG_NAME);\n\t\t}\n\n\t\t/* First of all, initialize the pipeline for writeable notifications */\n\t\twrite_nfd[0] = nn_socket(AF_SP, NN_PULL);\n\t\twrite_nfd[1] = nn_socket(AF_SP, NN_PUSH);\n\t\tif(nn_bind(write_nfd[0], \"inproc://janus\") < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Error configuring internal Nanomsg pipeline... %d (%s)\\n\", errno, nn_strerror(errno));\n\t\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t\t}\n\t\tif(nn_connect(write_nfd[1], \"inproc://janus\") < 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Error configuring internal Nanomsg pipeline...%d (%s)\\n\", errno, nn_strerror(errno));\n\t\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t\t}\n\n\t\t/* Setup the Janus API Nanomsg server(s) */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Nanomsg server disabled (Janus API)\\n\");\n\t\t} else {\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"address\");\n\t\t\tconst char *address = item && item->value ? item->value : NULL;\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"mode\");\n\t\t\tconst char *mode = item && item->value ? item->value : NULL;\n\t\t\tif(mode == NULL)\n\t\t\t\tmode = \"bind\";\n\t\t\tnfd = nn_socket(AF_SP, NN_PAIR);\n\t\t\tif(nfd < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating Janus API Nanomsg socket: %d (%s)\\n\", errno, nn_strerror(errno));\n\t\t\t} else {\n\t\t\t\tif(!strcasecmp(mode, \"bind\")) {\n\t\t\t\t\t/* Bind to this address */\n\t\t\t\t\tnfd_addr = nn_bind(nfd, address);\n\t\t\t\t\tif(nfd_addr < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error binding Janus API Nanomsg socket to address '%s': %d (%s)\\n\",\n\t\t\t\t\t\t\taddress, errno, nn_strerror(errno));\n\t\t\t\t\t\tnn_close(nfd);\n\t\t\t\t\t\tnfd = -1;\n\t\t\t\t\t}\n\t\t\t\t} else if(!strcasecmp(mode, \"connect\")) {\n\t\t\t\t\t/* Connect to this address */\n\t\t\t\t\tnfd_addr = nn_connect(nfd, address);\n\t\t\t\t\tif(nfd_addr < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error connecting Janus API Nanomsg socket to address '%s': %d (%s)\\n\",\n\t\t\t\t\t\t\taddress, errno, nn_strerror(errno));\n\t\t\t\t\t\tnn_close(nfd);\n\t\t\t\t\t\tnfd = -1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Unsupported mode */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported mode '%s'\\n\", mode);\n\t\t\t\t\tnn_close(nfd);\n\t\t\t\t\tnfd = -1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t/* Do the same for the Admin API, if enabled */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_enabled\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Nanomsg server disabled (Admin API)\\n\");\n\t\t} else {\n\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_address\");\n\t\t\tconst char *address = item && item->value ? item->value : NULL;\n\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_mode\");\n\t\t\tconst char *mode = item && item->value ? item->value : NULL;\n\t\t\tif(mode == NULL)\n\t\t\t\tmode = \"bind\";\n\t\t\tadmin_nfd = nn_socket(AF_SP, NN_PAIR);\n\t\t\tif(admin_nfd < 0) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating Admin API Nanomsg socket: %d (%s)\\n\", errno, nn_strerror(errno));\n\t\t\t} else {\n\t\t\t\tif(!strcasecmp(mode, \"bind\")) {\n\t\t\t\t\t/* Bind to this address */\n\t\t\t\t\tadmin_nfd_addr = nn_bind(admin_nfd, address);\n\t\t\t\t\tif(admin_nfd_addr < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error binding Admin API Nanomsg socket to address '%s': %d (%s)\\n\",\n\t\t\t\t\t\t\taddress, errno, nn_strerror(errno));\n\t\t\t\t\t\tnn_close(admin_nfd);\n\t\t\t\t\t\tadmin_nfd = -1;\n\t\t\t\t\t}\n\t\t\t\t} else if(!strcasecmp(mode, \"connect\")) {\n\t\t\t\t\t/* Connect to this address */\n\t\t\t\t\tadmin_nfd_addr = nn_connect(admin_nfd, address);\n\t\t\t\t\tif(admin_nfd_addr < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error connecting Admin API Nanomsg socket to address '%s': %d (%s)\\n\",\n\t\t\t\t\t\t\taddress, errno, nn_strerror(errno));\n\t\t\t\t\t\tnn_close(admin_nfd);\n\t\t\t\t\t\tadmin_nfd = -1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Unsupported mode */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported mode '%s'\\n\", mode);\n\t\t\t\t\tnn_close(admin_nfd);\n\t\t\t\t\tadmin_nfd = -1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\tif(nfd < 0 && admin_nfd < 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No Nanomsg server started, giving up...\\n\");\n\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t}\n\n\t/* Create the clients */\n\tmemset(&client, 0, sizeof(janus_nanomsg_client));\n\tif(nfd > -1) {\n\t\tclient.admin = FALSE;\n\t\tclient.messages = g_async_queue_new();\n\t\t/* Create a transport instance as well */\n\t\tclient.ts = janus_transport_session_create(&client, NULL);\n\t\t/* Notify handlers about this new transport */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"created\"));\n\t\t\tjson_object_set_new(info, \"admin_api\", json_false());\n\t\t\tjson_object_set_new(info, \"socket\", json_integer(nfd));\n\t\t\tgateway->notify_event(&janus_nanomsg_transport, client.ts, info);\n\t\t}\n\t}\n\tmemset(&admin_client, 0, sizeof(janus_nanomsg_client));\n\tif(admin_nfd > -1) {\n\t\tadmin_client.admin = TRUE;\n\t\tadmin_client.messages = g_async_queue_new();\n\t\t/* Create a transport instance as well */\n\t\tadmin_client.ts = janus_transport_session_create(&admin_client, NULL);\n\t\t/* Notify handlers about this new transport */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"created\"));\n\t\t\tjson_object_set_new(info, \"admin_api\", json_true());\n\t\t\tjson_object_set_new(info, \"socket\", json_integer(admin_nfd));\n\t\t\tgateway->notify_event(&janus_nanomsg_transport, admin_client.ts, info);\n\t\t}\n\t}\n\n\t/* Start the Nanomsg service thread */\n\tGError *error = NULL;\n\tnanomsg_thread = g_thread_try_new(\"nanomsg thread\", &janus_nanomsg_thread, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Nanomsg thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\n\t/* Done */\n\tg_atomic_int_set(&initialized, 1);\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_NANOMSG_NAME);\n\treturn 0;\n}\n\nvoid janus_nanomsg_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\t/* Stop the service thread */\n\t(void)nn_send(write_nfd[1], \"x\", 1, 0);\n\n\tif(nanomsg_thread != NULL) {\n\t\tg_thread_join(nanomsg_thread);\n\t\tnanomsg_thread = NULL;\n\t}\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_NANOMSG_NAME);\n}\n\nint janus_nanomsg_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_TRANSPORT_API_VERSION;\n}\n\nint janus_nanomsg_get_version(void) {\n\treturn JANUS_NANOMSG_VERSION;\n}\n\nconst char *janus_nanomsg_get_version_string(void) {\n\treturn JANUS_NANOMSG_VERSION_STRING;\n}\n\nconst char *janus_nanomsg_get_description(void) {\n\treturn JANUS_NANOMSG_DESCRIPTION;\n}\n\nconst char *janus_nanomsg_get_name(void) {\n\treturn JANUS_NANOMSG_NAME;\n}\n\nconst char *janus_nanomsg_get_author(void) {\n\treturn JANUS_NANOMSG_AUTHOR;\n}\n\nconst char *janus_nanomsg_get_package(void) {\n\treturn JANUS_NANOMSG_PACKAGE;\n}\n\ngboolean janus_nanomsg_is_janus_api_enabled(void) {\n\treturn nfd > -1;\n}\n\ngboolean janus_nanomsg_is_admin_api_enabled(void) {\n\treturn admin_nfd > -1;\n}\n\nint janus_nanomsg_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message) {\n\tif(message == NULL)\n\t\treturn -1;\n\t/* Convert to string */\n\tchar *payload = json_dumps(message, json_format);\n\tjson_decref(message);\n\tif(payload == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\treturn -1;\n\t}\n\t/* Enqueue the packet and have poll tell us when it's time to send it */\n\tg_async_queue_push(admin ? admin_client.messages : client.messages, payload);\n\t/* Notify the thread there's data to send */\n\t(void)nn_send(write_nfd[1], \"x\", 1, 0);\n\treturn 0;\n}\n\nvoid janus_nanomsg_session_created(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care */\n}\n\nvoid janus_nanomsg_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed) {\n\t/* We don't care */\n}\n\nvoid janus_nanomsg_session_claimed(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care about this. We should start receiving messages from the core about this session: no action necessary */\n\t/* FIXME Is the above statement accurate? Should we care? Unlike the HTTP transport, there is no hashtable to update */\n}\n\njson_t *janus_nanomsg_query_transport(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this request to dynamically change the behaviour of\n\t * the transport plugin, and/or query for some specific information */\n\tjson_t *response = json_object();\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_NANOMSG_ERROR_MISSING_ELEMENT, JANUS_NANOMSG_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"configure\")) {\n\t\t/* We only allow for the configuration of some basic properties:\n\t\t * changing more complex things (e.g., port to bind to, etc.)\n\t\t * would likely require restarting backends, so just too much */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, configure_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_NANOMSG_ERROR_MISSING_ELEMENT, JANUS_NANOMSG_ERROR_INVALID_ELEMENT);\n\t\t/* Check if we now need to send events to handlers */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjson_t *notes = NULL;\n\t\tgboolean events = json_is_true(json_object_get(request, \"events\"));\n\t\tif(events && !gateway->events_is_enabled()) {\n\t\t\t/* Notify that this will be ignored */\n\t\t\tnotes = json_array();\n\t\t\tjson_array_append_new(notes, json_string(\"Event handlers disabled at the core level\"));\n\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t}\n\t\tif(events != notify_events) {\n\t\t\tnotify_events = events;\n\t\t\tif(!notify_events && gateway->events_is_enabled()) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_NANOMSG_NAME);\n\t\t\t}\n\t\t}\n\t\tconst char *indentation = json_string_value(json_object_get(request, \"json\"));\n\t\tif(indentation != NULL) {\n\t\t\tif(!strcasecmp(indentation, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', ignoring tweak\\n\", indentation);\n\t\t\t\t/* Notify that this will be ignored */\n\t\t\t\tif(notes == NULL) {\n\t\t\t\t\tnotes = json_array();\n\t\t\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t\t\t}\n\t\t\t\tjson_array_append_new(notes, json_string(\"Ignored unsupported indentation format\"));\n\t\t\t}\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_NANOMSG_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n\n/* Thread */\nvoid *janus_nanomsg_thread(void *data) {\n\tJANUS_LOG(LOG_INFO, \"Nanomsg thread started\\n\");\n\n\tint fds = 0;\n\tstruct nn_pollfd poll_nfds[3];\t/* FIXME Should we allow for more clients? */\n\tchar buffer[BUFFER_SIZE];\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\t/* Prepare poll list of file descriptors */\n\t\tfds = 0;\n\t\t/* Writeable monitor */\n\t\tpoll_nfds[fds].fd = write_nfd[0];\n\t\tpoll_nfds[fds].events = NN_POLLIN;\n\t\tfds++;\n\t\tif(nfd > -1) {\n\t\t\t/* Janus API */\n\t\t\tpoll_nfds[fds].fd = nfd;\n\t\t\tpoll_nfds[fds].events = NN_POLLIN;\n\t\t\tif(client.messages != NULL && g_async_queue_length(client.messages) > 0)\n\t\t\t\tpoll_nfds[fds].events |= NN_POLLOUT;\n\t\t\tfds++;\n\t\t}\n\t\tif(admin_nfd > -1) {\n\t\t\t/* Admin API */\n\t\t\tpoll_nfds[fds].fd = admin_nfd;\n\t\t\tpoll_nfds[fds].events = NN_POLLIN;\n\t\t\tif(admin_client.messages != NULL && g_async_queue_length(admin_client.messages) > 0)\n\t\t\t\tpoll_nfds[fds].events |= NN_POLLOUT;\n\t\t\tfds++;\n\t\t}\n\t\t/* Start polling */\n\t\tint res = nn_poll(poll_nfds, fds, -1);\n\t\tif(res == 0)\n\t\t\tcontinue;\n\t\tif(res < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got an EINTR (%s) polling the Nanomsg descriptors, ignoring...\\n\", nn_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"poll() failed: %d (%s)\\n\", errno, nn_strerror(errno));\n\t\t\tbreak;\n\t\t}\n\t\tint i = 0;\n\t\tfor(i=0; i<fds; i++) {\n\t\t\t/* FIXME Is there a Nanomsg equivalent of POLLERR? */\n\t\t\tif(poll_nfds[i].revents & NN_POLLOUT) {\n\t\t\t\t/* Find the client from its file descriptor */\n\t\t\t\tif(poll_nfds[i].fd == nfd || poll_nfds[i].fd == admin_nfd) {\n\t\t\t\t\tchar *payload = NULL;\n\t\t\t\t\twhile((payload = g_async_queue_try_pop(poll_nfds[i].fd == nfd ? client.messages : admin_client.messages)) != NULL) {\n\t\t\t\t\t\tint res = nn_send(poll_nfds[i].fd, payload, strlen(payload), 0);\n\t\t\t\t\t\t/* FIXME Should we check if sent everything? */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Written %d/%zu bytes on %d\\n\", res, strlen(payload), poll_nfds[i].fd);\n\t\t\t\t\t\tfree(payload);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif(poll_nfds[i].revents & NN_POLLIN) {\n\t\t\t\tif(poll_nfds[i].fd == write_nfd[0]) {\n\t\t\t\t\t/* Read and ignore: we use this to unlock the poll if there's data to write */\n\t\t\t\t\t(void)nn_recv(poll_nfds[i].fd, buffer, BUFFER_SIZE, 0);\n\t\t\t\t} else if(poll_nfds[i].fd == nfd || poll_nfds[i].fd == admin_nfd) {\n\t\t\t\t\t/* Janus/Admin API: get the message from the client */\n\t\t\t\t\tint res = nn_recv(poll_nfds[i].fd, buffer, BUFFER_SIZE, 0);\n\t\t\t\t\tif(res < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error receiving %s API message... %d (%s)\\n\",\n\t\t\t\t\t\t\tpoll_nfds[i].fd == nfd ? \"Janus\" : \"Admin\", errno, nn_strerror(errno));\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* If we got here, there's data to handle */\n\t\t\t\t\tbuffer[res] = '\\0';\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Got %s API message (%d bytes)\\n\",\n\t\t\t\t\t\tpoll_nfds[i].fd == nfd ? \"Janus\" : \"Admin\", res);\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"%s\\n\", buffer);\n\t\t\t\t\t/* Parse the JSON payload */\n\t\t\t\t\tjson_error_t error;\n\t\t\t\t\tjson_t *root = json_loads(buffer, 0, &error);\n\t\t\t\t\t/* Notify the core, passing both the object and, since it may be needed, the error */\n\t\t\t\t\tgateway->incoming_request(&janus_nanomsg_transport,\n\t\t\t\t\t\tpoll_nfds[i].fd == nfd ? client.ts : admin_client.ts,\n\t\t\t\t\t\tNULL,\n\t\t\t\t\t\tpoll_nfds[i].fd == nfd ? FALSE : TRUE,\n\t\t\t\t\t\troot, &error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tnn_close(write_nfd[0]);\n\tnn_close(write_nfd[1]);\n\tif(nfd > -1) {\n\t\tnn_shutdown(nfd, nfd_addr);\n\t\tnn_close(nfd);\n\t\tjanus_transport_session_destroy(client.ts);\n\t\tclient.ts = NULL;\n\t}\n\tif(admin_nfd > -1) {\n\t\tnn_shutdown(admin_nfd, admin_nfd_addr);\n\t\tnn_close(admin_nfd);\n\t\tjanus_transport_session_destroy(admin_client.ts);\n\t\tadmin_client.ts = NULL;\n\t}\n\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"Nanomsg thread ended\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/transports/janus_pfunix.c",
    "content": "/*! \\file   janus_pfunix.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus Unix Sockets transport plugin\n * \\details  This is an implementation of a Unix Sockets transport for the\n * Janus API. This means that, with the help of this module, local\n * applications can use Unix Sockets to make requests to Janus.\n * This plugin can make use of either the \\c SOCK_SEQPACKET or the\n * \\c SOCK_DGRAM socket type according to what you configure, so make\n * sure you're using the right one when writing a client application.\n * Pretty much as it happens with WebSockets, the same client socket can\n * be used for both sending requests and receiving notifications, without\n * any need for long polls. At the same time, without the concept of a\n * REST path, requests sent through the Unix Sockets interface will need\n * to include, when needed, additional pieces of information like\n * \\c session_id and \\c handle_id. That is, where you'd send a Janus\n * request related to a specific session to the \\c /janus/\\<session> path,\n * with Unix Sockets you'd have to send the same request with an additional\n * \\c session_id field in the JSON payload. The same applies for the handle.\n * \\note When you create a session using Unix Sockets, a subscription to\n * the events related to it is done automatically, so no need for an\n * explicit request as the GET in the plain HTTP API. Closing a client\n * Unix Socket will also destroy all the sessions it created.\n *\n * \\ingroup transports\n * \\ref transports\n */\n\n#include \"transport.h\"\n\n#include <arpa/inet.h>\n#include <sys/socket.h>\n#include <poll.h>\n#include <sys/un.h>\n\n#ifdef  HAVE_LIBSYSTEMD\n#include \"systemd/sd-daemon.h\"\n#endif /* HAVE_LIBSYSTEMD */\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n\n\n/* Transport plugin information */\n#define JANUS_PFUNIX_VERSION\t\t\t1\n#define JANUS_PFUNIX_VERSION_STRING\t\t\"0.0.1\"\n#define JANUS_PFUNIX_DESCRIPTION\t\t\"This transport plugin adds Unix Sockets support to the Janus API.\"\n#define JANUS_PFUNIX_NAME\t\t\t\t\"JANUS Unix Sockets transport plugin\"\n#define JANUS_PFUNIX_AUTHOR\t\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_PFUNIX_PACKAGE\t\t\t\"janus.transport.pfunix\"\n\n/* Transport methods */\njanus_transport *create(void);\nint janus_pfunix_init(janus_transport_callbacks *callback, const char *config_path);\nvoid janus_pfunix_destroy(void);\nint janus_pfunix_get_api_compatibility(void);\nint janus_pfunix_get_version(void);\nconst char *janus_pfunix_get_version_string(void);\nconst char *janus_pfunix_get_description(void);\nconst char *janus_pfunix_get_name(void);\nconst char *janus_pfunix_get_author(void);\nconst char *janus_pfunix_get_package(void);\ngboolean janus_pfunix_is_janus_api_enabled(void);\ngboolean janus_pfunix_is_admin_api_enabled(void);\nint janus_pfunix_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message);\nvoid janus_pfunix_session_created(janus_transport_session *transport, guint64 session_id);\nvoid janus_pfunix_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed);\nvoid janus_pfunix_session_claimed(janus_transport_session *transport, guint64 session_id);\njson_t *janus_pfunix_query_transport(json_t *request);\n\n\n/* Transport setup */\nstatic janus_transport janus_pfunix_transport =\n\tJANUS_TRANSPORT_INIT (\n\t\t.init = janus_pfunix_init,\n\t\t.destroy = janus_pfunix_destroy,\n\n\t\t.get_api_compatibility = janus_pfunix_get_api_compatibility,\n\t\t.get_version = janus_pfunix_get_version,\n\t\t.get_version_string = janus_pfunix_get_version_string,\n\t\t.get_description = janus_pfunix_get_description,\n\t\t.get_name = janus_pfunix_get_name,\n\t\t.get_author = janus_pfunix_get_author,\n\t\t.get_package = janus_pfunix_get_package,\n\n\t\t.is_janus_api_enabled = janus_pfunix_is_janus_api_enabled,\n\t\t.is_admin_api_enabled = janus_pfunix_is_admin_api_enabled,\n\n\t\t.send_message = janus_pfunix_send_message,\n\t\t.session_created = janus_pfunix_session_created,\n\t\t.session_over = janus_pfunix_session_over,\n\t\t.session_claimed = janus_pfunix_session_claimed,\n\n\t\t.query_transport = janus_pfunix_query_transport,\n\t);\n\n/* Transport creator */\njanus_transport *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_PFUNIX_NAME);\n\treturn &janus_pfunix_transport;\n}\n\n\n/* Useful stuff */\nstatic gint initialized = 0, stopping = 0;\nstatic janus_transport_callbacks *gateway = NULL;\nstatic gboolean notify_events = TRUE;\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n#define BUFFER_SIZE\t\t8192\n\n/* Parameter validation (for tweaking and queries via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"events\", JANUS_JSON_BOOL, 0},\n\t{\"json\", JSON_STRING, 0},\n};\n/* Error codes (for the tweaking and queries via Admin API) */\n#define JANUS_PFUNIX_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_PFUNIX_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_PFUNIX_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_PFUNIX_ERROR_UNKNOWN_ERROR\t\t499\n\n\nstruct sockaddr_un sizecheck;\n#ifndef UNIX_PATH_MAX\n#define UNIX_PATH_MAX sizeof(sizecheck.sun_path)\n#endif\n\n/* Unix Sockets server thread */\nstatic GThread *pfunix_thread = NULL;\nvoid *janus_pfunix_thread(void *data);\n\n/* Unix Sockets servers (and whether they should be SOCK_SEQPACKET or SOCK_DGRAM) */\nstatic int pfd = -1, admin_pfd = -1;\nstatic gboolean dgram = FALSE, admin_dgram = FALSE;\n#ifdef HAVE_LIBSYSTEMD\nstatic gboolean sd_socket = FALSE, admin_sd_socket = FALSE;\n#endif /* HAVE_LIBSYSTEMD */\n/* Socket pair to notify about the need for outgoing data */\nstatic int write_fd[2];\n\n/* Unix Sockets client session */\ntypedef struct janus_pfunix_client {\n\tint fd;\t\t\t\t\t\t\t/* Client socket (in case SOCK_SEQPACKET is used) */\n\tstruct sockaddr_un addr;\t\t/* Client address (in case SOCK_DGRAM is used) */\n\tgboolean admin;\t\t\t\t\t/* Whether this client is for the Admin or Janus API */\n\tGAsyncQueue *messages;\t\t\t/* Queue of outgoing messages to push */\n\tgboolean session_timeout;\t\t/* Whether a Janus session timeout occurred in the core */\n\tjanus_transport_session *ts;\t/* Janus core-transport session */\n} janus_pfunix_client;\nstatic GHashTable *clients = NULL, *clients_by_fd = NULL, *clients_by_path = NULL;\nstatic janus_mutex clients_mutex = JANUS_MUTEX_INITIALIZER;\n\nstatic void janus_pfunix_client_free(void *client_ref) {\n\tif(!client_ref)\n\t\treturn;\n\tJANUS_LOG(LOG_INFO, \"Freeing unix sockets client\\n\");\n\tjanus_pfunix_client *client = (janus_pfunix_client *) client_ref;\n\tif(client->messages != NULL) {\n\t\tchar *response = NULL;\n\t\twhile((response = g_async_queue_try_pop(client->messages)) != NULL) {\n\t\t\tg_free(response);\n\t\t}\n\t\tg_async_queue_unref(client->messages);\n\t}\n\tg_free(client);\n}\n\n\n/* Helper to create a named Unix Socket out of the path to link to */\nstatic int janus_pfunix_create_socket(char *pfname, gboolean use_dgram) {\n\tif(pfname == NULL)\n\t\treturn -1;\n\tint fd = -1;\n\tif(strnlen(pfname, UNIX_PATH_MAX + 1) > UNIX_PATH_MAX) {\n\t\tJANUS_LOG(LOG_WARN, \"The provided path name (%s) is longer than %lu characters, it will be truncated\\n\", pfname, UNIX_PATH_MAX);\n\t\tpfname[UNIX_PATH_MAX] = '\\0';\n\t}\n\t/* Create socket */\n\tint flags = use_dgram ? SOCK_DGRAM | SOCK_NONBLOCK : SOCK_SEQPACKET | SOCK_NONBLOCK;\n\tfd = socket(use_dgram ? AF_UNIX : PF_UNIX, flags, 0);\n\tif(fd < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Unix Sockets %s creation failed: %d, %s\\n\", pfname, errno, g_strerror(errno));\n\t} else {\n\t\t/* Unlink before binding */\n\t\tunlink(pfname);\n\t\t/* Let's bind to the provided path now */\n\t\tstruct sockaddr_un address;\n\t\tmemset(&address, 0, sizeof(address));\n\t\taddress.sun_family = AF_UNIX;\n\t\tg_snprintf(address.sun_path, UNIX_PATH_MAX, \"%s\", pfname);\n\t\tJANUS_LOG(LOG_VERB, \"Binding Unix Socket %s... (Janus API)\\n\", pfname);\n\t\tif(bind(fd, (struct sockaddr *)&address, sizeof(address)) != 0) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Bind for Unix Socket %s failed: %d, %s\\n\", pfname, errno, g_strerror(errno));\n\t\t\tclose(fd);\n\t\t\tfd = -1;\n\t\t\treturn fd;\n\t\t}\n\t\tif(!use_dgram) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Listening on Unix Socket %s...\\n\", pfname);\n\t\t\tif(listen(fd, 128) != 0) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Listening on Unix Socket %s failed: %d, %s\\n\", pfname, errno, g_strerror(errno));\n\t\t\t\tclose(fd);\n\t\t\t\tfd = -1;\n\t\t\t}\n\t\t}\n\t}\n\treturn fd;\n}\n\n/* Transport implementation */\nint janus_pfunix_init(janus_transport_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_PFUNIX_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_PFUNIX_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_PFUNIX_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\t/* Handle configuration */\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_category *config_admin = janus_config_get_create(config, NULL, janus_config_type_category, \"admin\");\n\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\t\tif(item && item->value) {\n\t\t\t/* Check how we need to format/serialize the JSON output */\n\t\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t}\n\t\t}\n\n\t\t/* Check if we need to send events to handlers */\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_PFUNIX_NAME);\n\t\t}\n\n\t\t/* First of all, initialize the socketpair for writeable notifications */\n\t\tif(socketpair(PF_LOCAL, SOCK_STREAM, 0, write_fd) < 0) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Error creating socket pair for writeable events: %d, %s\\n\", errno, g_strerror(errno));\n\t\t\treturn -1;\n\t\t}\n\n\t\t/* Setup the Janus API Unix Sockets server(s) */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Unix Sockets server disabled (Janus API)\\n\");\n\t\t} else {\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"path\");\n\t\t\tchar *pfname = (char *)(item && item->value ? item->value : NULL);\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"type\");\n\t\t\tconst char *type = item && item->value ? item->value : \"SOCK_SEQPACKET\";\n\t\t\tdgram = FALSE;\n\t\t\tif(!strcasecmp(type, \"SOCK_SEQPACKET\")) {\n\t\t\t\tdgram = FALSE;\n\t\t\t} else if(!strcasecmp(type, \"SOCK_DGRAM\")) {\n\t\t\t\tdgram = TRUE;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unknown type %s, assuming SOCK_SEQPACKET\\n\", type);\n\t\t\t\ttype = \"SOCK_SEQPACKET\";\n\t\t\t}\n\t\t\tif(pfname == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"No path configured, skipping Unix Sockets server (Janus API)\\n\");\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Configuring %s Unix Sockets server (Janus API)\\n\", type);\n#ifdef HAVE_LIBSYSTEMD\n\t\t\t\tif (sd_listen_fds(0) > 0) {\n\t\t\t\t\tpfd = SD_LISTEN_FDS_START + 0;\n\t\t\t\t\tsd_socket = TRUE;\n\t\t\t\t} else\n#endif /* HAVE_LIBSYSTEMD */\n\t\t\t\tpfd = janus_pfunix_create_socket(pfname, dgram);\n\t\t\t}\n\t\t}\n\t\t/* Do the same for the Admin API, if enabled */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_enabled\");\n\t\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Unix Sockets server disabled (Admin API)\\n\");\n\t\t} else {\n\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_path\");\n\t\t\tchar *pfname = (char *)(item && item->value ? item->value : NULL);\n\t\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_type\");\n\t\t\tconst char *type = item && item->value ? item->value : \"SOCK_SEQPACKET\";\n\t\t\tif(!strcasecmp(type, \"SOCK_SEQPACKET\")) {\n\t\t\t\tadmin_dgram = FALSE;\n\t\t\t} else if(!strcasecmp(type, \"SOCK_DGRAM\")) {\n\t\t\t\tadmin_dgram = TRUE;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unknown type %s, assuming SOCK_SEQPACKET\\n\", type);\n\t\t\t\ttype = \"SOCK_SEQPACKET\";\n\t\t\t}\n\t\t\tif(pfname == NULL) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"No path configured, skipping Unix Sockets server (Admin API)\\n\");\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Configuring %s Unix Sockets server (Admin API)\\n\", type);\n#ifdef HAVE_LIBSYSTEMD\n\t\t\t\tif (sd_listen_fds(0) > 1) {\n\t\t\t\t\tadmin_pfd = SD_LISTEN_FDS_START + 1;\n\t\t\t\t\tadmin_sd_socket = TRUE;\n\t\t\t\t} else\n#endif /* HAVE_LIBSYSTEMD */\n\t\t\t\tadmin_pfd = janus_pfunix_create_socket(pfname, admin_dgram);\n\t\t\t}\n\t\t}\n\t}\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\tif(pfd < 0 && admin_pfd < 0) {\n\t\tJANUS_LOG(LOG_WARN, \"No Unix Sockets server started, giving up...\\n\");\n\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t}\n\n\t/* Create a couple of hashtables for all clients */\n\tclients = g_hash_table_new(NULL, NULL);\n\tclients_by_fd = g_hash_table_new(NULL, NULL);\n\tclients_by_path = g_hash_table_new(g_str_hash, g_str_equal);\n\n\t/* Start the Unix Sockets service thread */\n\tGError *error = NULL;\n\tpfunix_thread = g_thread_try_new(\"pfunix thread\", &janus_pfunix_thread, NULL, &error);\n\tif(error != NULL) {\n\t\tg_atomic_int_set(&initialized, 0);\n\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the Unix Sockets thread...\\n\",\n\t\t\terror->code, error->message ? error->message : \"??\");\n\t\tg_error_free(error);\n\t\treturn -1;\n\t}\n\n\t/* Done */\n\tg_atomic_int_set(&initialized, 1);\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_PFUNIX_NAME);\n\treturn 0;\n}\n\nvoid janus_pfunix_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\t/* Stop the service thread */\n\tint res = 0;\n\tdo {\n\t\tres = write(write_fd[1], \"x\", 1);\n\t} while(res == -1 && errno == EINTR);\n\n\tif(pfunix_thread != NULL) {\n\t\tg_thread_join(pfunix_thread);\n\t\tpfunix_thread = NULL;\n\t}\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_PFUNIX_NAME);\n}\n\nint janus_pfunix_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_TRANSPORT_API_VERSION;\n}\n\nint janus_pfunix_get_version(void) {\n\treturn JANUS_PFUNIX_VERSION;\n}\n\nconst char *janus_pfunix_get_version_string(void) {\n\treturn JANUS_PFUNIX_VERSION_STRING;\n}\n\nconst char *janus_pfunix_get_description(void) {\n\treturn JANUS_PFUNIX_DESCRIPTION;\n}\n\nconst char *janus_pfunix_get_name(void) {\n\treturn JANUS_PFUNIX_NAME;\n}\n\nconst char *janus_pfunix_get_author(void) {\n\treturn JANUS_PFUNIX_AUTHOR;\n}\n\nconst char *janus_pfunix_get_package(void) {\n\treturn JANUS_PFUNIX_PACKAGE;\n}\n\ngboolean janus_pfunix_is_janus_api_enabled(void) {\n\treturn pfd > -1;\n}\n\ngboolean janus_pfunix_is_admin_api_enabled(void) {\n\treturn admin_pfd > -1;\n}\n\nint janus_pfunix_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message) {\n\tif(message == NULL)\n\t\treturn -1;\n\tif(transport == NULL || transport->transport_p == NULL) {\n\t\tjson_decref(message);\n\t\treturn -1;\n\t}\n\t/* Make sure this is related to a still valid Unix Sockets session */\n\tjanus_pfunix_client *client = (janus_pfunix_client *)transport->transport_p;\n\tjanus_mutex_lock(&clients_mutex);\n\tif(g_hash_table_lookup(clients, client) == NULL) {\n\t\tjanus_mutex_unlock(&clients_mutex);\n\t\tJANUS_LOG(LOG_WARN, \"Outgoing message for invalid client %p\\n\", client);\n\t\tjson_decref(message);\n\t\tmessage = NULL;\n\t\treturn -1;\n\t}\n\tjanus_mutex_unlock(&clients_mutex);\n\t/* Convert to string */\n\tchar *payload = json_dumps(message, json_format);\n\tjson_decref(message);\n\tif(payload == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\treturn -1;\n\t}\n\tif(client->fd != -1) {\n\t\t/* SOCK_SEQPACKET, enqueue the packet and have poll tell us when it's time to send it */\n\t\tg_async_queue_push(client->messages, payload);\n\t\t/* Notify the thread there's data to send */\n\t\tint res = 0;\n\t\tdo {\n\t\t\tres = write(write_fd[1], \"x\", 1);\n\t\t} while(res == -1 && errno == EINTR);\n\t} else {\n\t\t/* SOCK_DGRAM, send it right away */\n\t\tint res = 0;\n\t\tdo {\n\t\t\tres = sendto(client->admin ? admin_pfd : pfd, payload, strlen(payload), 0, (struct sockaddr *)&client->addr, sizeof(struct sockaddr_un));\n\t\t} while(res == -1 && errno == EINTR);\n\t\tfree(payload);\n\t}\n\treturn 0;\n}\n\nvoid janus_pfunix_session_created(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care */\n}\n\nvoid janus_pfunix_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed) {\n\t/* We only care if it's a timeout: if so, close the connection */\n\tif(transport == NULL || transport->transport_p == NULL || !timeout)\n\t\treturn;\n\t/* FIXME Should we really close the connection in case of a timeout? */\n\tjanus_pfunix_client *client = (janus_pfunix_client *)transport->transport_p;\n\tjanus_mutex_lock(&clients_mutex);\n\tif(g_hash_table_lookup(clients, client) != NULL) {\n\t\tclient->session_timeout = TRUE;\n\t\t/* Notify the thread about this */\n\t\tint res = 0;\n\t\tdo {\n\t\t\tres = write(write_fd[1], \"x\", 1);\n\t\t} while(res == -1 && errno == EINTR);\n\t}\n\tjanus_mutex_unlock(&clients_mutex);\n}\n\nvoid janus_pfunix_session_claimed(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care about this. We should start receiving messages from the core about this session: no action necessary */\n\t/* FIXME Is the above statement accurate? Should we care? Unlike the HTTP transport, there is no hashtable to update */\n}\n\njson_t *janus_pfunix_query_transport(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this request to dynamically change the behaviour of\n\t * the transport plugin, and/or query for some specific information */\n\tjson_t *response = json_object();\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_PFUNIX_ERROR_MISSING_ELEMENT, JANUS_PFUNIX_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"configure\")) {\n\t\t/* We only allow for the configuration of some basic properties:\n\t\t * changing more complex things (e.g., port to bind to, etc.)\n\t\t * would likely require restarting backends, so just too much */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, configure_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_PFUNIX_ERROR_MISSING_ELEMENT, JANUS_PFUNIX_ERROR_INVALID_ELEMENT);\n\t\t/* Check if we now need to send events to handlers */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjson_t *notes = NULL;\n\t\tgboolean events = json_is_true(json_object_get(request, \"events\"));\n\t\tif(events && !gateway->events_is_enabled()) {\n\t\t\t/* Notify that this will be ignored */\n\t\t\tnotes = json_array();\n\t\t\tjson_array_append_new(notes, json_string(\"Event handlers disabled at the core level\"));\n\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t}\n\t\tif(events != notify_events) {\n\t\t\tnotify_events = events;\n\t\t\tif(!notify_events && gateway->events_is_enabled()) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_PFUNIX_NAME);\n\t\t\t}\n\t\t}\n\t\tconst char *indentation = json_string_value(json_object_get(request, \"json\"));\n\t\tif(indentation != NULL) {\n\t\t\tif(!strcasecmp(indentation, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', ignoring tweak\\n\", indentation);\n\t\t\t\t/* Notify that this will be ignored */\n\t\t\t\tif(notes == NULL) {\n\t\t\t\t\tnotes = json_array();\n\t\t\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t\t\t}\n\t\t\t\tjson_array_append_new(notes, json_string(\"Ignored unsupported indentation format\"));\n\t\t\t}\n\t\t}\n\t} else if(!strcasecmp(request_text, \"connections\")) {\n\t\t/* Return the number of active connections currently handled by the plugin */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjanus_mutex_lock(&clients_mutex);\n\t\tguint connections = g_hash_table_size(clients);\n\t\tjanus_mutex_unlock(&clients_mutex);\n\t\tjson_object_set_new(response, \"connections\", json_integer(connections));\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_PFUNIX_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n\n/* Thread */\nvoid *janus_pfunix_thread(void *data) {\n\tJANUS_LOG(LOG_INFO, \"Unix Sockets thread started\\n\");\n\n\tint fds = 0;\n\tstruct pollfd poll_fds[1024];\t/* FIXME Should we allow for more clients? */\n\tchar buffer[BUFFER_SIZE];\n\tstruct iovec iov[1];\n\tstruct msghdr msg;\n\tmemset(&msg, 0, sizeof(msg));\n\tmemset(iov, 0, sizeof(iov));\n\tiov[0].iov_base = buffer;\n\tiov[0].iov_len = sizeof(buffer);\n\tmsg.msg_iov = iov;\n\tmsg.msg_iovlen = 1;\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\t/* Prepare poll list of file descriptors */\n\t\tfds = 0;\n\t\t/* Writeable monitor */\n\t\tpoll_fds[fds].fd = write_fd[0];\n\t\tpoll_fds[fds].events = POLLIN;\n\t\tfds++;\n\t\tif(pfd > -1) {\n\t\t\t/* Janus API */\n\t\t\tpoll_fds[fds].fd = pfd;\n\t\t\tpoll_fds[fds].events = POLLIN;\n\t\t\tfds++;\n\t\t}\n\t\tif(admin_pfd > -1) {\n\t\t\t/* Admin API */\n\t\t\tpoll_fds[fds].fd = admin_pfd;\n\t\t\tpoll_fds[fds].events = POLLIN;\n\t\t\tfds++;\n\t\t}\n\t\t/* Iterate on available clients, to see if we need to POLLIN or POLLOUT too */\n\t\tjanus_mutex_lock(&clients_mutex);\n\t\tGHashTableIter iter;\n\t\tgpointer value;\n\t\tg_hash_table_iter_init(&iter, clients_by_fd);\n\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\tjanus_pfunix_client *client = value;\n\t\t\tif(client->fd > -1) {\n\t\t\t\tpoll_fds[fds].fd = client->fd;\n\t\t\t\tpoll_fds[fds].events = g_async_queue_length(client->messages) > 0 ? POLLIN | POLLOUT : POLLIN;\n\t\t\t\tfds++;\n\t\t\t}\n\t\t}\n\t\tjanus_mutex_unlock(&clients_mutex);\n\n\t\t/* Start polling */\n\t\tint res = poll(poll_fds, fds, -1);\n\t\tif(res == 0)\n\t\t\tcontinue;\n\t\tif(res < 0) {\n\t\t\tif(errno == EINTR) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got an EINTR (%s) polling the Unix Sockets descriptors, ignoring...\\n\", g_strerror(errno));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_ERR, \"poll() failed: %d (%s)\\n\", errno, g_strerror(errno));\n\t\t\tbreak;\n\t\t}\n\t\tint i = 0;\n\t\tfor(i=0; i<fds; i++) {\n\t\t\tif(poll_fds[i].revents & (POLLERR | POLLHUP)) {\n\t\t\t\t/* Socket error? Shall we do something? */\n\t\t\t\tif(poll_fds[i].fd == write_fd[0]) {\n\t\t\t\t\t/* Error in the wake-up socketpair, that sucks: try recreating it */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error polling wake-up socketpair: %s...\\n\",\n\t\t\t\t\t\tpoll_fds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\");\n\t\t\t\t\tclose(write_fd[0]);\n\t\t\t\t\twrite_fd[0] = -1;\n\t\t\t\t\tclose(write_fd[1]);\n\t\t\t\t\twrite_fd[1] = -1;\n\t\t\t\t\tif(socketpair(PF_LOCAL, SOCK_STREAM, 0, write_fd) < 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_FATAL, \"Error creating socket pair for writeable events: %d, %s\\n\", errno, g_strerror(errno));\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else if(poll_fds[i].fd == pfd) {\n\t\t\t\t\t/* Error in the Janus API socket */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error polling Unix Sockets Janus API interface (%s), disabling it\\n\",\n\t\t\t\t\t\tpoll_fds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\");\n\t\t\t\t\tclose(pfd);\n\t\t\t\t\tpfd = -1;\n\t\t\t\t\tcontinue;\n\t\t\t\t} else if(poll_fds[i].fd == admin_pfd) {\n\t\t\t\t\t/* Error in the Admin API socket */\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Error polling Unix Sockets Admin API interface (%s), disabling it\\n\",\n\t\t\t\t\t\tpoll_fds[i].revents & POLLERR ? \"POLLERR\" : \"POLLHUP\");\n\t\t\t\t\tclose(admin_pfd);\n\t\t\t\t\tadmin_pfd = -1;\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\t/* Error in a client socket, find and remove it */\n\t\t\t\t\tjanus_mutex_lock(&clients_mutex);\n\t\t\t\t\tjanus_pfunix_client *client = g_hash_table_lookup(clients_by_fd, GINT_TO_POINTER(poll_fds[i].fd));\n\t\t\t\t\tif(client == NULL) {\n\t\t\t\t\t\t/* We're not handling this, ignore */\n\t\t\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Unix Sockets client disconnected (%d)\\n\", poll_fds[i].fd);\n\t\t\t\t\t/* Notify core */\n\t\t\t\t\tgateway->transport_gone(&janus_pfunix_transport, client->ts);\n\t\t\t\t\t/* Notify handlers about this transport being gone */\n\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"disconnected\"));\n\t\t\t\t\t\tgateway->notify_event(&janus_pfunix_transport, client->ts, info);\n\t\t\t\t\t}\n\t\t\t\t\t/* Close socket */\n\t\t\t\t\tshutdown(SHUT_RDWR, poll_fds[i].fd);\n\t\t\t\t\tclose(poll_fds[i].fd);\n\t\t\t\t\tclient->fd = -1;\n\t\t\t\t\t/* Destroy the client */\n\t\t\t\t\tg_hash_table_remove(clients_by_fd, GINT_TO_POINTER(poll_fds[i].fd));\n\t\t\t\t\tg_hash_table_remove(clients, client);\n\t\t\t\t\t/* Unref the transport instance */\n\t\t\t\t\tjanus_transport_session_destroy(client->ts);\n\t\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif(poll_fds[i].revents & POLLOUT) {\n\t\t\t\t/* Find the client from its file descriptor */\n\t\t\t\tjanus_mutex_lock(&clients_mutex);\n\t\t\t\tjanus_pfunix_client *client = g_hash_table_lookup(clients_by_fd, GINT_TO_POINTER(poll_fds[i].fd));\n\t\t\t\tif(client != NULL) {\n\t\t\t\t\tchar *payload = NULL;\n\t\t\t\t\twhile((payload = g_async_queue_try_pop(client->messages)) != NULL) {\n\t\t\t\t\t\tint res = 0;\n\t\t\t\t\t\tdo {\n\t\t\t\t\t\t\tif(client->fd < 0)\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tres = write(client->fd, payload, strlen(payload));\n\t\t\t\t\t\t} while(res == -1 && errno == EINTR);\n\t\t\t\t\t\t/* FIXME Should we check if sent everything? */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Written %d/%zu bytes on %d\\n\", res, strlen(payload), client->fd);\n\t\t\t\t\t\tg_free(payload);\n\t\t\t\t\t}\n\t\t\t\t\tif(client->session_timeout) {\n\t\t\t\t\t\t/* We should actually get rid of this connection, now */\n\t\t\t\t\t\tshutdown(SHUT_RDWR, poll_fds[i].fd);\n\t\t\t\t\t\tclose(poll_fds[i].fd);\n\t\t\t\t\t\tclient->fd = -1;\n\t\t\t\t\t\t/* Destroy the client */\n\t\t\t\t\t\tg_hash_table_remove(clients_by_fd, GINT_TO_POINTER(poll_fds[i].fd));\n\t\t\t\t\t\tg_hash_table_remove(clients, client);\n\t\t\t\t\t\tif(client->messages != NULL) {\n\t\t\t\t\t\t\tchar *response = NULL;\n\t\t\t\t\t\t\twhile((response = g_async_queue_try_pop(client->messages)) != NULL) {\n\t\t\t\t\t\t\t\tg_free(response);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tg_async_queue_unref(client->messages);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg_free(client);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t}\n\t\t\tif(poll_fds[i].revents & POLLIN) {\n\t\t\t\tif(poll_fds[i].fd == write_fd[0]) {\n\t\t\t\t\t/* Read and ignore: we use this to unlock the poll if there's data to write */\n\t\t\t\t\t(void)read(poll_fds[i].fd, buffer, BUFFER_SIZE);\n\t\t\t\t} else if(poll_fds[i].fd == pfd || poll_fds[i].fd == admin_pfd) {\n\t\t\t\t\t/* Janus/Admin API: accept the new client (SOCK_SEQPACKET) or receive data (SOCK_DGRAM) */\n\t\t\t\t\tstruct sockaddr_un address;\n\t\t\t\t\tsocklen_t addrlen = sizeof(address);\n\t\t\t\t\tif((poll_fds[i].fd == pfd && !dgram) || (poll_fds[i].fd == admin_pfd && !admin_dgram)) {\n\t\t\t\t\t\t/* SOCK_SEQPACKET */\n\t\t\t\t\t\tint cfd = accept(poll_fds[i].fd, (struct sockaddr *) &address, &addrlen);\n\t\t\t\t\t\tif(cfd > -1) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Got new Unix Sockets %s API client: %d\\n\",\n\t\t\t\t\t\t\t\tpoll_fds[i].fd == pfd ? \"Janus\" : \"Admin\", cfd);\n\t\t\t\t\t\t\t/* Allocate new client */\n\t\t\t\t\t\t\tjanus_pfunix_client *client = g_malloc(sizeof(janus_pfunix_client));\n\t\t\t\t\t\t\tclient->fd = cfd;\n\t\t\t\t\t\t\tmemset(&client->addr, 0, sizeof(client->addr));\n\t\t\t\t\t\t\tclient->admin = (poll_fds[i].fd == admin_pfd);\t/* API client type */\n\t\t\t\t\t\t\tclient->messages = g_async_queue_new();\n\t\t\t\t\t\t\tclient->session_timeout = FALSE;\n\t\t\t\t\t\t\t/* Create a transport instance as well */\n\t\t\t\t\t\t\tclient->ts = janus_transport_session_create(client, janus_pfunix_client_free);\n\t\t\t\t\t\t\t/* Take note of this new client */\n\t\t\t\t\t\t\tjanus_mutex_lock(&clients_mutex);\n\t\t\t\t\t\t\tg_hash_table_insert(clients_by_fd, GINT_TO_POINTER(cfd), client);\n\t\t\t\t\t\t\tg_hash_table_insert(clients, client, client);\n\t\t\t\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t\t\t\t\t/* Notify handlers about this new transport */\n\t\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"connected\"));\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"admin_api\", client->admin ? json_true() : json_false());\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"fd\", json_integer(client->fd));\n\t\t\t\t\t\t\t\tgateway->notify_event(&janus_pfunix_transport, client->ts, info);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* SOCK_DGRAM */\n\t\t\t\t\t\tstruct sockaddr_storage address;\n\t\t\t\t\t\tres = recvfrom(poll_fds[i].fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&address, &addrlen);\n\t\t\t\t\t\tif(res < 0) {\n\t\t\t\t\t\t\tif(errno != EAGAIN && errno != EWOULDBLOCK) {\n\t\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error reading from client (%s API)...\\n\",\n\t\t\t\t\t\t\t\t\tpoll_fds[i].fd == pfd ? \"Janus\" : \"Admin\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbuffer[res] = '\\0';\n\t\t\t\t\t\t/* Is this a new client, or one we knew about already? */\n\t\t\t\t\t\tstruct sockaddr_un *uaddr = (struct sockaddr_un *)&address;\n\t\t\t\t\t\tif(strlen(uaddr->sun_path) == 0) {\n\t\t\t\t\t\t\t/* No path provided, drop the packet */\n\t\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Dropping packet from unknown source (no path provided)\\n\");\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_lock(&clients_mutex);\n\t\t\t\t\t\tjanus_pfunix_client *client = g_hash_table_lookup(clients_by_path, uaddr->sun_path);\n\t\t\t\t\t\tif(client == NULL) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Got new Unix Sockets %s API client: %s\\n\",\n\t\t\t\t\t\t\t\tpoll_fds[i].fd == pfd ? \"Janus\" : \"Admin\", uaddr->sun_path);\n\t\t\t\t\t\t\t/* Allocate new client */\n\t\t\t\t\t\t\tclient = g_malloc(sizeof(janus_pfunix_client));\n\t\t\t\t\t\t\tclient->fd = -1;\n\t\t\t\t\t\t\tmemcpy(&client->addr, uaddr, sizeof(struct sockaddr_un));\n\t\t\t\t\t\t\tclient->admin = (poll_fds[i].fd == admin_pfd);\t/* API client type */\n\t\t\t\t\t\t\tclient->messages = g_async_queue_new();\n\t\t\t\t\t\t\tclient->session_timeout = FALSE;\n\t\t\t\t\t\t\t/* Create a transport instance as well */\n\t\t\t\t\t\t\tclient->ts = janus_transport_session_create(client, janus_pfunix_client_free);\n\t\t\t\t\t\t\t/* Take note of this new client */\n\t\t\t\t\t\t\tg_hash_table_insert(clients_by_path, uaddr->sun_path, client);\n\t\t\t\t\t\t\tg_hash_table_insert(clients, client, client);\n\t\t\t\t\t\t\t/* Notify handlers about this new transport */\n\t\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"connected\"));\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"admin_api\", client->admin ? json_true() : json_false());\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"fd\", json_integer(client->fd));\n\t\t\t\t\t\t\t\tjson_object_set_new(info, \"type\", json_string(\"SOCK_DGRAM\"));\n\t\t\t\t\t\t\t\tgateway->notify_event(&janus_pfunix_transport, client->ts, info);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Message from client %s (%d bytes)\\n\", uaddr->sun_path, res);\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"%s\\n\", buffer);\n\t\t\t\t\t\t/* Parse the JSON payload */\n\t\t\t\t\t\tjson_error_t error;\n\t\t\t\t\t\tjson_t *root = json_loads(buffer, 0, &error);\n\t\t\t\t\t\t/* Notify the core, passing both the object and, since it may be needed, the error */\n\t\t\t\t\t\tgateway->incoming_request(&janus_pfunix_transport, client->ts, NULL, client->admin, root, &error);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t/* Client data: receive message */\n\t\t\t\t\tiov[0].iov_len = sizeof(buffer);\n\t\t\t\t\tres = recvmsg(poll_fds[i].fd, &msg, MSG_WAITALL);\n\t\t\t\t\tif(res < 0) {\n\t\t\t\t\t\tif(errno != EAGAIN && errno != EWOULDBLOCK) {\n\t\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Error reading from client %d...\\n\", poll_fds[i].fd);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(msg.msg_flags & MSG_TRUNC) {\n\t\t\t\t\t\t/* Apparently our buffer is not large enough? */\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Incoming message from client %d truncated (%d bytes), dropping it...\\n\", poll_fds[i].fd, res);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t/* Find the client from its file descriptor */\n\t\t\t\t\tjanus_mutex_lock(&clients_mutex);\n\t\t\t\t\tjanus_pfunix_client *client = g_hash_table_lookup(clients_by_fd, GINT_TO_POINTER(poll_fds[i].fd));\n\t\t\t\t\tif(client == NULL) {\n\t\t\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Got data from unknown Unix Sockets client %d, closing connection...\\n\", poll_fds[i].fd);\n\t\t\t\t\t\t/* Close socket */\n\t\t\t\t\t\tshutdown(SHUT_RDWR, poll_fds[i].fd);\n\t\t\t\t\t\tclose(poll_fds[i].fd);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif(res == 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Unix Sockets client disconnected (%d)\\n\", poll_fds[i].fd);\n\t\t\t\t\t\t/* Notify core */\n\t\t\t\t\t\tgateway->transport_gone(&janus_pfunix_transport, client->ts);\n\t\t\t\t\t\t/* Notify handlers about this transport being gone */\n\t\t\t\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\t\t\t\tjson_t *info = json_object();\n\t\t\t\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"disconnected\"));\n\t\t\t\t\t\t\tgateway->notify_event(&janus_pfunix_transport, client->ts, info);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Close socket */\n\t\t\t\t\t\tshutdown(SHUT_RDWR, poll_fds[i].fd);\n\t\t\t\t\t\tclose(poll_fds[i].fd);\n\t\t\t\t\t\tclient->fd = -1;\n\t\t\t\t\t\t/* Destroy the client */\n\t\t\t\t\t\tg_hash_table_remove(clients_by_fd, GINT_TO_POINTER(poll_fds[i].fd));\n\t\t\t\t\t\tg_hash_table_remove(clients, client);\n\t\t\t\t\t\t/* Unref the transport instance */\n\t\t\t\t\t\tjanus_transport_session_destroy(client->ts);\n\t\t\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&clients_mutex);\n\t\t\t\t\t/* If we got here, there's data to handle */\n\t\t\t\t\tbuffer[res] = '\\0';\n\t\t\t\t\tJANUS_LOG(LOG_VERB, \"Message from client %d (%d bytes)\\n\", poll_fds[i].fd, res);\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"%s\\n\", buffer);\n\t\t\t\t\t/* Parse the JSON payload */\n\t\t\t\t\tjson_error_t error;\n\t\t\t\t\tjson_t *root = json_loads(buffer, 0, &error);\n\t\t\t\t\t/* Notify the core, passing both the object and, since it may be needed, the error */\n\t\t\t\t\tgateway->incoming_request(&janus_pfunix_transport, client->ts, NULL, client->admin, root, &error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid *addr = g_malloc(sizeof(struct sockaddr_un)+1);\n\tif(pfd > -1) {\n\t\tsocklen_t addrlen = sizeof(struct sockaddr_un);\n\t\t/* Unlink the path name first */\n#ifdef HAVE_LIBSYSTEMD\n\t\tif((getsockname(pfd, (struct sockaddr *)addr, &addrlen) != -1) && (FALSE == sd_socket)) {\n#else\n\t\tif(getsockname(pfd, (struct sockaddr *)addr, &addrlen) != -1) {\n#endif\n\t\t\tJANUS_LOG(LOG_INFO, \"Unlinking %s\\n\", ((struct sockaddr_un *)addr)->sun_path);\n\t\t\tunlink(((struct sockaddr_un *)addr)->sun_path);\n\t\t}\n\t\t/* Close the socket */\n\t\tclose(pfd);\n\t}\n\tpfd = -1;\n\tif(admin_pfd > -1) {\n\t\tsocklen_t addrlen = sizeof(struct sockaddr_un);\n\t\t/* Unlink the path name first */\n#ifdef HAVE_LIBSYSTEMD\n\t\tif((getsockname(admin_pfd, (struct sockaddr *)addr, &addrlen) != -1) && (FALSE == admin_sd_socket)) {\n#else\n\t\tif(getsockname(admin_pfd, (struct sockaddr *)addr, &addrlen) != -1) {\n#endif\n\t\t\tJANUS_LOG(LOG_INFO, \"Unlinking %s\\n\", ((struct sockaddr_un *)addr)->sun_path);\n\t\t\tunlink(((struct sockaddr_un *)addr)->sun_path);\n\t\t}\n\t\t/* Close the socket */\n\t\tclose(admin_pfd);\n\t}\n\tadmin_pfd = -1;\n\tg_free(addr);\n\n\tg_hash_table_destroy(clients_by_path);\n\tg_hash_table_destroy(clients_by_fd);\n\tg_hash_table_destroy(clients);\n\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"Unix Sockets thread ended\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/transports/janus_rabbitmq.c",
    "content": "/*! \\file   janus_rabbitmq.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus RabbitMQ transport plugin\n * \\details  This is an implementation of a RabbitMQ transport for the\n * Janus API, using the rabbitmq-c library (https://github.com/alanxz/rabbitmq-c).\n * This means that this module adds support for RabbitMQ based messaging as\n * an alternative \"transport\" for API requests, responses and notifications.\n * This is only useful when you're wrapping Janus requests in your server\n * application, and handling the communication with clients your own way.\n * At the moment, only a single \"application\" can be handled at the same\n * time, meaning that Janus won't implement multiple queues to handle\n * multiple concurrent \"application servers\" taking advantage of its\n * features. Support for this is planned, though (e.g., through some kind\n * of negotiation to create queues on the fly). Right now, you can only\n * configure the address of the RabbitMQ server to use, and the queues to\n * make use of to receive (to-janus) and send (from-janus) messages\n * from/to an external application. As with WebSockets, considering that\n * requests wouldn't include a path to address some mandatory information,\n * these requests addressed to Janus should include as part of their payload,\n * when needed, additional pieces of information like \\c session_id and\n * \\c handle_id. That is, where you'd send a Janus request related to a\n * specific session to the \\c /janus/\\<session> path, with RabbitMQ\n * you'd have to send the same request with an additional \\c session_id\n * field in the JSON payload.\n * \\note When you create a session using RabbitMQ, a subscription to the\n * events related to it is done automatically through the outgoing queue,\n * so no need for an explicit request as the GET in the plain HTTP API.\n *\n * \\ingroup transports\n * \\ref transports\n */\n\n#include \"transport.h\"\n\n/* Latest RabbitMQ-C library changes the library paths from 0.12.0.0 onwards */\n#ifdef HAVE_RABBITMQ_C_AMQP_H\n#include <rabbitmq-c/amqp.h>\n#include <rabbitmq-c/framing.h>\n#include <rabbitmq-c/tcp_socket.h>\n#include <rabbitmq-c/ssl_socket.h>\n#else\n#include <amqp.h>\n#include <amqp_framing.h>\n#include <amqp_tcp_socket.h>\n#include <amqp_ssl_socket.h>\n#endif\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n\n\n/* Transport plugin information */\n#define JANUS_RABBITMQ_VERSION\t\t\t1\n#define JANUS_RABBITMQ_VERSION_STRING\t\"0.0.1\"\n#define JANUS_RABBITMQ_DESCRIPTION\t\t\"This transport plugin adds RabbitMQ support to the Janus API via rabbitmq-c.\"\n#define JANUS_RABBITMQ_NAME\t\t\t\t\"JANUS RabbitMQ transport plugin\"\n#define JANUS_RABBITMQ_AUTHOR\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_RABBITMQ_PACKAGE\t\t\t\"janus.transport.rabbitmq\"\n\n/* Transport methods */\njanus_transport *create(void);\nint janus_rabbitmq_init(janus_transport_callbacks *callback, const char *config_path);\nvoid janus_rabbitmq_destroy(void);\nint janus_rabbitmq_get_api_compatibility(void);\nint janus_rabbitmq_get_version(void);\nconst char *janus_rabbitmq_get_version_string(void);\nconst char *janus_rabbitmq_get_description(void);\nconst char *janus_rabbitmq_get_name(void);\nconst char *janus_rabbitmq_get_author(void);\nconst char *janus_rabbitmq_get_package(void);\ngboolean janus_rabbitmq_is_janus_api_enabled(void);\ngboolean janus_rabbitmq_is_admin_api_enabled(void);\nint janus_rabbitmq_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message);\nvoid janus_rabbitmq_session_created(janus_transport_session *transport, guint64 session_id);\nvoid janus_rabbitmq_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed);\nvoid janus_rabbitmq_session_claimed(janus_transport_session *transport, guint64 session_id);\njson_t *janus_rabbitmq_query_transport(json_t *request);\n\n/* Internal methods */\nstatic int janus_rabbitmq_connect(void);\n\n/* Transport setup */\nstatic janus_transport janus_rabbitmq_transport =\n\tJANUS_TRANSPORT_INIT (\n\t\t.init = janus_rabbitmq_init,\n\t\t.destroy = janus_rabbitmq_destroy,\n\n\t\t.get_api_compatibility = janus_rabbitmq_get_api_compatibility,\n\t\t.get_version = janus_rabbitmq_get_version,\n\t\t.get_version_string = janus_rabbitmq_get_version_string,\n\t\t.get_description = janus_rabbitmq_get_description,\n\t\t.get_name = janus_rabbitmq_get_name,\n\t\t.get_author = janus_rabbitmq_get_author,\n\t\t.get_package = janus_rabbitmq_get_package,\n\n\t\t.is_janus_api_enabled = janus_rabbitmq_is_janus_api_enabled,\n\t\t.is_admin_api_enabled = janus_rabbitmq_is_admin_api_enabled,\n\n\t\t.send_message = janus_rabbitmq_send_message,\n\t\t.session_created = janus_rabbitmq_session_created,\n\t\t.session_over = janus_rabbitmq_session_over,\n\t\t.session_claimed = janus_rabbitmq_session_claimed,\n\n\t\t.query_transport = janus_rabbitmq_query_transport,\n\t);\n\n/* Transport creator */\njanus_transport *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_RABBITMQ_NAME);\n\treturn &janus_rabbitmq_transport;\n}\n\n\n/* Useful stuff */\nstatic gint initialized = 0, stopping = 0;\nstatic janus_transport_callbacks *gateway = NULL;\nstatic gboolean rmq_janus_api_enabled = FALSE;\nstatic gboolean rmq_admin_api_enabled = FALSE;\nstatic gboolean notify_events = TRUE;\nstatic guint rmq_reconnect_backoff_initial = 100000; /* 100ms */\nstatic guint rmq_reconnect_backoff_max = 5000000; /* 5s */\nstatic gfloat rmq_reconnect_backoff_multiplier = 1.5;\n\n#define JANUS_RABBITMQ_EXCHANGE_TYPE \"fanout\"\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Parameter validation (for tweaking and queries via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"events\", JANUS_JSON_BOOL, 0},\n\t{\"json\", JSON_STRING, 0},\n};\n/* Error codes (for the tweaking and queries via Admin API) */\n#define JANUS_RABBITMQ_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_RABBITMQ_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_RABBITMQ_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_RABBITMQ_ERROR_UNKNOWN_ERROR\t\t\t499\n\n\n/* RabbitMQ client session: we only create a single one as of now */\ntypedef struct janus_rabbitmq_client {\n\tamqp_connection_state_t rmq_conn;\t\t/* AMQP connection state */\n\tamqp_channel_t rmq_channel;\t\t\t\t/* AMQP channel */\n\tgboolean janus_api_enabled;\t\t\t\t/* Whether the Janus API via RabbitMQ is enabled */\n\tamqp_bytes_t janus_exchange;\t\t\t/* AMQP exchange for outgoing messages */\n\tamqp_bytes_t to_janus_queue;\t\t\t/* AMQP outgoing messages queue (Janus API) */\n\tgboolean admin_api_enabled;\t\t\t\t/* Whether the Janus API via RabbitMQ is enabled */\n\tamqp_bytes_t to_janus_admin_queue;\t\t/* AMQP outgoing messages queue (Admin API) */\n\tGThread *in_thread, *out_thread;\t\t/* Threads to handle incoming and outgoing queues */\n\tGAsyncQueue *messages;\t\t\t\t\t/* Queue of outgoing messages to push */\n\tjanus_mutex mutex;\t\t\t\t\t\t/* Mutex to lock/unlock this session */\n\tgboolean session_timeout;\t\t\t\t/* Whether a Janus session timeout occurred in the core */\n\tgboolean destroy;\t\t\t\t\t\t/* Flag to trigger a lazy session destruction */\n\tgboolean connected;\t\t\t\t\t\t/* Flag to specify whether or not RabbitMQ connection is deemed to be up */\n} janus_rabbitmq_client;\n\n/* RabbitMQ response */\ntypedef struct janus_rabbitmq_response {\n\tgboolean admin;\t\t\t/* Whether this is a Janus or Admin API response */\n\tchar *correlation_id;\t/* Correlation ID, if any */\n\tchar *payload;\t\t\t/* Payload to send to the client */\n} janus_rabbitmq_response;\nstatic janus_rabbitmq_response exit_message;\n\n/* Threads */\nvoid *janus_rmq_in_thread(void *data);\nvoid *janus_rmq_out_thread(void *data);\n\n\n/* We only handle a single client per time, as the queues are fixed */\nstatic janus_rabbitmq_client *rmq_client = NULL;\nstatic janus_transport_session *rmq_session = NULL;\n\n/* Global properties */\nstatic char *rmqhost = NULL, *vhost = NULL, *username = NULL, *password = NULL,\n\t*ssl_cacert_file = NULL, *ssl_cert_file = NULL, *ssl_key_file = NULL,\n\t*to_janus = NULL, *from_janus = NULL, *to_janus_admin = NULL, *from_janus_admin = NULL, *janus_exchange = NULL, *janus_exchange_type = NULL,\n\t*queue_name = NULL, *queue_name_admin = NULL;\nstatic uint16_t rmqport = AMQP_PROTOCOL_PORT;\nstatic gboolean ssl_enabled = FALSE, ssl_verify_peer = FALSE, ssl_verify_hostname = FALSE;\nstatic gboolean declare_outgoing_queue = FALSE, declare_outgoing_queue_admin = FALSE;\namqp_boolean_t queue_durable = 0, queue_exclusive = 0, queue_autodelete = 0,\n\tqueue_durable_admin = 0, queue_exclusive_admin = 0, queue_autodelete_admin = 0;\nstatic uint16_t heartbeat = 0;\n\n/* Transport implementation */\nint janus_rabbitmq_init(janus_transport_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_RABBITMQ_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_RABBITMQ_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_RABBITMQ_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL)\n\t\tjanus_config_print(config);\n\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\tjanus_config_category *config_admin = janus_config_get_create(config, NULL, janus_config_type_category, \"admin\");\n\n\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\tif(item && item->value) {\n\t\t/* Check how we need to format/serialize the JSON output */\n\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t/* Compact, so no spaces between separators */\n\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t}\n\t}\n\n\t/* Check if we need to send events to handlers */\n\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\tif(events != NULL && events->value != NULL)\n\t\tnotify_events = janus_is_true(events->value);\n\tif(!notify_events && callback->events_is_enabled()) {\n\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_RABBITMQ_NAME);\n\t}\n\n\t/* Handle configuration, starting from the server details */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"host\");\n\tif(item && item->value)\n\t\trmqhost = g_strdup(item->value);\n\telse\n\t\trmqhost = g_strdup(\"localhost\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"port\");\n\tif(item && item->value && janus_string_to_uint16(item->value, &rmqport) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s), falling back to default\\n\", item->value);\n\t\trmqport = AMQP_PROTOCOL_PORT;\n\t}\n\n\t/* Credentials and Virtual Host */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"vhost\");\n\tif(item && item->value)\n\t\tvhost = g_strdup(item->value);\n\telse\n\t\tvhost = g_strdup(\"/\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"username\");\n\tif(item && item->value)\n\t\tusername = g_strdup(item->value);\n\telse\n\t\tusername = g_strdup(\"guest\");\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"password\");\n\tif(item && item->value)\n\t\tpassword = g_strdup(item->value);\n\telse\n\t\tpassword = g_strdup(\"guest\");\n\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_enabled\");\n\tif(item == NULL) {\n\t\t/* Try legacy property */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_enable\");\n\t\tif (item && item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Found deprecated 'ssl_enable' property, please update it to 'ssl_enabled' instead\\n\");\n\t\t}\n\t}\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_INFO, \"RabbitMQ SSL support disabled\\n\");\n\t} else {\n\t\tssl_enabled = TRUE;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_cacert\");\n\t\tif(item && item->value)\n\t\t\tssl_cacert_file = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_cert\");\n\t\tif(item && item->value)\n\t\t\tssl_cert_file = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_key\");\n\t\tif(item && item->value)\n\t\t\tssl_key_file = g_strdup(item->value);\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_verify_peer\");\n\t\tif(item && item->value && janus_is_true(item->value))\n\t\t\tssl_verify_peer = TRUE;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ssl_verify_hostname\");\n\t\tif(item && item->value && janus_is_true(item->value))\n\t\t\tssl_verify_hostname = TRUE;\n\t}\n\n\t/* Heartbeat config */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"heartbeat\");\n\tif(item && item->value && janus_string_to_uint16(item->value, &heartbeat) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid heartbeat timeout (%s), falling back to default (0, disabling heartbeat)\\n\", item->value);\n\t\theartbeat = 0;\n\t}\n\n\t/* Now check if the Janus API must be supported */\n\titem = janus_config_get(config, config_general, janus_config_type_item, \"enabled\");\n\tif(item == NULL) {\n\t\t/* Try legacy property */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"enable\");\n\t\tif (item && item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Found deprecated 'enable' property, please update it to 'enabled' instead\\n\");\n\t\t}\n\t}\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_VERB, \"RabbitMQ support disabled (Janus API)\\n\");\n\t} else {\n\n\t\t/* Get exchange name config, or set to default exchange */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"janus_exchange\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_INFO, \"Missing name of outgoing exchange for RabbitMQ integration, using default\\n\");\n\t\t} else {\n\t\t\tjanus_exchange = g_strdup(item->value);\n\t\t}\n\n\t\t/* Get exchange type config, or set to default */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"janus_exchange_type\");\n\t\tif(!item || !item->value) {\n\t\t\tjanus_exchange_type = g_strdup((char *)JANUS_RABBITMQ_EXCHANGE_TYPE);\n\t\t} else {\n\t\t\tjanus_exchange_type = g_strdup(item->value);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"queue_name\");\n\t\tif(item && item->value) {\n\t\t\tqueue_name = g_strdup(item->value);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"to_janus\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Missing name of incoming queue/topic for RabbitMQ integration...\\n\");\n\t\t\tgoto error;\n\t\t}\n\t\tto_janus = g_strdup(item->value);\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"from_janus\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Missing name of outgoing routing key for RabbitMQ integration...\\n\");\n\t\t\tgoto error;\n\t\t}\n\t\tfrom_janus = g_strdup(item->value);\n\n\t\t/* Set queue options */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"queue_durable\");\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tqueue_durable = 1;\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"queue_exclusive\");\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tqueue_exclusive = 1;\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"queue_autodelete\");\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tqueue_autodelete = 1;\n\t\t}\n\n\t\t/* By default, declare the outgoing queue */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"declare_outgoing_queue\");\n\t\tif(!item || !item->value || janus_is_true(item->value)) {\n\t\t\tdeclare_outgoing_queue = TRUE;\n\t\t}\n\n\t\tif(janus_exchange == NULL) {\n\t\t\tJANUS_LOG(LOG_INFO, \"RabbitMQ support for Janus API enabled, %s:%d (%s/%s)  exchange_type:%s \\n\", rmqhost, rmqport, to_janus, from_janus, janus_exchange_type);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_INFO, \"RabbitMQ support for Janus API enabled, %s:%d (%s/%s) exch: (%s) exchange_type:%s \\n\", rmqhost, rmqport, to_janus, from_janus, janus_exchange, janus_exchange_type);\n\t\t}\n\t\trmq_janus_api_enabled = TRUE;\n\t}\n\t/* Do the same for the admin API */\n\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_enabled\");\n\tif(item == NULL) {\n\t\t/* Try legacy property */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"admin_enable\");\n\t\tif (item && item->value) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Found deprecated 'admin_enable' property, please update it to 'admin_enabled' instead\\n\");\n\t\t}\n\t}\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_VERB, \"RabbitMQ support disabled (Admin API)\\n\");\n\t} else {\n\t\t/* Parse configuration */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"queue_name_admin\");\n\t\tif(item && item->value) {\n\t\t\tqueue_name_admin = g_strdup(item->value);\n\t\t}\n\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"to_janus_admin\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Missing name of incoming queue for RabbitMQ integration...\\n\");\n\t\t\tgoto error;\n\t\t}\n\t\tto_janus_admin = g_strdup(item->value);\n\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"from_janus_admin\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Missing name of outgoing routing key for RabbitMQ integration...\\n\");\n\t\t\tgoto error;\n\t\t}\n\n\t\tfrom_janus_admin = g_strdup(item->value);\n\n\t\t/* Set queue options */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"queue_durable_admin\");\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tqueue_durable_admin = 1;\n\t\t}\n\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"queue_exclusive_admin\");\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tqueue_exclusive_admin = 1;\n\t\t}\n\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"queue_autodelete_admin\");\n\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\tqueue_autodelete_admin = 1;\n\t\t}\n\n\t\t/* By default, declare the outgoing queue */\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"declare_outgoing_queue_admin\");\n\t\tif(!item || !item->value || janus_is_true(item->value)) {\n\t\t\tdeclare_outgoing_queue_admin = TRUE;\n\t\t}\n\n\t\tJANUS_LOG(LOG_INFO, \"RabbitMQ support for Admin API enabled, %s:%d (%s/%s)\\n\", rmqhost, rmqport, to_janus_admin, from_janus_admin);\n\t\trmq_admin_api_enabled = TRUE;\n\t}\n\tif(!rmq_janus_api_enabled && !rmq_admin_api_enabled) {\n\t\tJANUS_LOG(LOG_WARN, \"RabbitMQ support disabled for both Janus and Admin API, giving up\\n\");\n\t\tgoto error;\n\t} else {\n\t\t/* FIXME We currently support a single application, create a new janus_rabbitmq_client instance */\n\t\trmq_client = g_malloc0(sizeof(janus_rabbitmq_client));\n\n\t\t/* Connect */\n\t\tint result = janus_rabbitmq_connect();\n\t\tif(result < 0) {\n\t\t\tgoto error;\n\t\t}\n\n\t\trmq_client->messages = g_async_queue_new();\n\t\trmq_client->destroy = FALSE;\n\t\t/* Prepare the transport session (again, just one) */\n\t\trmq_session = janus_transport_session_create(rmq_client, NULL);\n\t\t/* Start the threads */\n\t\tGError *error = NULL;\n\t\trmq_client->in_thread = g_thread_try_new(\"rmq_in_thread\", &janus_rmq_in_thread, rmq_client, &error);\n\t\tif(error != NULL) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to launch the RabbitMQ incoming thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tjanus_transport_session_destroy(rmq_session);\n\t\t\tg_free(rmq_client);\n\t\t\tjanus_config_destroy(config);\n\t\t\treturn -1;\n\t\t}\n\t\trmq_client->out_thread = g_thread_try_new(\"rmq_out_thread\", &janus_rmq_out_thread, rmq_client, &error);\n\t\tif(error != NULL) {\n\t\t\t/* Something went wrong... */\n\t\t\tJANUS_LOG(LOG_FATAL, \"Got error %d (%s) trying to launch the RabbitMQ outgoing thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\tjanus_transport_session_destroy(rmq_session);\n\t\t\tg_free(rmq_client);\n\t\t\tjanus_config_destroy(config);\n\t\t\treturn -1;\n\t\t}\n\n\t\tjanus_mutex_init(&rmq_client->mutex);\n\t\t/* Done */\n\t\tJANUS_LOG(LOG_INFO, \"Setup of RabbitMQ integration completed\\n\");\n\t\t/* Notify handlers about this new transport */\n\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\tjson_t *info = json_object();\n\t\t\tjson_object_set_new(info, \"event\", json_string(\"connected\"));\n\t\t\tgateway->notify_event(&janus_rabbitmq_transport, rmq_session, info);\n\t\t}\n\t}\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\n\t/* Done */\n\tg_atomic_int_set(&initialized, 1);\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_RABBITMQ_NAME);\n\treturn 0;\n\nerror:\n\t/* If we got here, something went wrong */\n\tg_free(rmq_client);\n\tg_free(rmqhost);\n\tg_free(vhost);\n\tg_free(username);\n\tg_free(password);\n\tg_free(janus_exchange);\n\tg_free(janus_exchange_type);\n\tg_free(queue_name);\n\tg_free(queue_name_admin);\n\tg_free(to_janus);\n\tg_free(from_janus);\n\tg_free(to_janus_admin);\n\tg_free(from_janus_admin);\n\tg_free(ssl_cacert_file);\n\tg_free(ssl_cert_file);\n\tg_free(ssl_key_file);\n\tif(config)\n\t\tjanus_config_destroy(config);\n\treturn -1;\n}\n\nint janus_rabbitmq_connect(void) {\n\trmq_client->connected = FALSE;\n\t/* Connect */\n\trmq_client->rmq_conn = amqp_new_connection();\n\tamqp_socket_t *socket = NULL;\n\tamqp_queue_declare_ok_t *declare = NULL;\n\tint status;\n\tJANUS_LOG(LOG_VERB, \"Creating RabbitMQ socket...\\n\");\n\tif(ssl_enabled) {\n\t\tsocket = amqp_ssl_socket_new(rmq_client->rmq_conn);\n\t\tif(socket == NULL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error creating socket...\\n\");\n\t\t\treturn -1;\n\t\t}\n\t\tamqp_ssl_socket_set_verify_peer(socket, ssl_verify_peer);\n\t\tamqp_ssl_socket_set_verify_hostname(socket, ssl_verify_hostname);\n\n\t\tif(ssl_cacert_file) {\n\t\t\tstatus = amqp_ssl_socket_set_cacert(socket, ssl_cacert_file);\n\t\t\tif(status != AMQP_STATUS_OK) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error setting CA certificate... (%s)\\n\", amqp_error_string2(status));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t\tif(ssl_cert_file && ssl_key_file) {\n\t\t\tstatus = amqp_ssl_socket_set_key(socket, ssl_cert_file, ssl_key_file);\n\t\t\tif(status != AMQP_STATUS_OK) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error setting key... (%s)\\n\", amqp_error_string2(status));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t} else {\n\t\tsocket = amqp_tcp_socket_new(rmq_client->rmq_conn);\n\t\tif(socket == NULL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error creating socket...\\n\");\n\t\t\treturn -1;\n\t\t}\n\t}\n\tJANUS_LOG(LOG_VERB, \"Connecting to RabbitMQ server...\\n\");\n\tstatus = amqp_socket_open(socket, rmqhost, rmqport);\n\tif(status != AMQP_STATUS_OK) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error opening socket... (%s)\\n\", amqp_error_string2(status));\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Logging in...\\n\");\n\tamqp_rpc_reply_t result = amqp_login(rmq_client->rmq_conn, vhost, 0, 131072, heartbeat, AMQP_SASL_METHOD_PLAIN, username, password);\n\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error logging in... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\treturn -1;\n\t}\n\trmq_client->rmq_channel = 1;\n\tJANUS_LOG(LOG_VERB, \"Opening channel...\\n\");\n\tamqp_channel_open(rmq_client->rmq_conn, rmq_client->rmq_channel);\n\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error opening channel... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\treturn -1;\n\t}\n\trmq_client->janus_exchange = amqp_empty_bytes;\n\tif(janus_exchange != NULL) {\n\t\tJANUS_LOG(LOG_VERB, \"Declaring exchange...\\n\");\n\t\trmq_client->janus_exchange = amqp_cstring_bytes(janus_exchange);\n\t\tamqp_exchange_declare(rmq_client->rmq_conn, rmq_client->rmq_channel, rmq_client->janus_exchange, amqp_cstring_bytes(janus_exchange_type), 0, 0, 0, 0, amqp_empty_table);\n\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error declaring exchange... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\treturn -1;\n\t\t}\n\t}\n\trmq_client->janus_api_enabled = FALSE;\n\tif(rmq_janus_api_enabled) {\n\t\trmq_client->janus_api_enabled = TRUE;\n\n\t\t/* Case when we have a queue_name, and to_janus is the name of the topic to bind on (if exchange_type is topic) */\n\t\tif(queue_name != NULL) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Declaring incoming queue (using queue_name)... (%s)\\n\", queue_name);\n\t\t\tdeclare = amqp_queue_declare(rmq_client->rmq_conn, rmq_client->rmq_channel, amqp_cstring_bytes(queue_name), 0, queue_durable, queue_exclusive, queue_autodelete, amqp_empty_table);\n\t\t\trmq_client->to_janus_queue = declare->queue;\n\t\t\tJANUS_LOG(LOG_VERB, \"Incoming queue declared: (%s)\\n\", (char *) rmq_client->to_janus_queue.bytes);\n\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error declaring queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tif(strcmp(janus_exchange_type, \"topic\") == 0 || strcmp(janus_exchange_type, \"direct\") == 0) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Binding queue (%s) to routing key (%s)\\n\", queue_name, to_janus);\n\t\t\t\tamqp_queue_bind(rmq_client->rmq_conn, rmq_client->rmq_channel, rmq_client->to_janus_queue, rmq_client->janus_exchange, amqp_cstring_bytes(to_janus), amqp_empty_table);\n\t\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error binding queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t/* Case when to_janus is the name of the queue (and there's no binding) */\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"Declaring incoming queue (using to_janus)... (%s)\\n\", to_janus);\n\t\t\tdeclare = amqp_queue_declare(rmq_client->rmq_conn, rmq_client->rmq_channel, amqp_cstring_bytes(to_janus), 0, queue_durable, queue_exclusive, queue_autodelete, amqp_empty_table);\n\t\t\trmq_client->to_janus_queue = declare->queue;\n\t\t\tJANUS_LOG(LOG_VERB, \"Incoming queue declared: (%s)\\n\", (char *)rmq_client->to_janus_queue.bytes);\n\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error declaring queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\n\t\tif (declare_outgoing_queue) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Declaring outgoing queue... (%s)\\n\", from_janus);\n\t\t\tamqp_queue_declare(rmq_client->rmq_conn, rmq_client->rmq_channel, amqp_cstring_bytes(from_janus), 0, 0, 0, 0, amqp_empty_table);\n\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error declaring queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\n\t\tamqp_basic_consume(rmq_client->rmq_conn, rmq_client->rmq_channel, rmq_client->to_janus_queue, amqp_empty_bytes, 0, 1, 0, amqp_empty_table);\n\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error consuming... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\treturn -1;\n\t\t}\n\t}\n\trmq_client->admin_api_enabled = FALSE;\n\tif(rmq_admin_api_enabled) {\n\t\trmq_client->admin_api_enabled = TRUE;\n\n\t\t/* Case when we have a queue_name_admin, and to_janus_admin is the name of the routing key to bind on (if exchange_type is topic or direct) */\n\t\tif(queue_name_admin != NULL) {\n\t\t\tJANUS_LOG(LOG_VERB, \"Declaring incoming admin queue (using queue_name_admin)... (%s)\\n\", queue_name_admin);\n\t\t\tdeclare = amqp_queue_declare(rmq_client->rmq_conn, rmq_client->rmq_channel, amqp_cstring_bytes(queue_name_admin), 0, queue_durable_admin, queue_exclusive_admin, queue_autodelete_admin, amqp_empty_table);\n\t\t\trmq_client->to_janus_admin_queue = declare->queue;\n\t\t\tJANUS_LOG(LOG_VERB, \"Incoming admin queue declared: (%s)\\n\", (char *) rmq_client->to_janus_queue.bytes);\n\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error declaring queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tif(strcmp(janus_exchange_type, \"topic\") == 0 || strcmp(janus_exchange_type, \"direct\") == 0) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Binding queue (%s) to routing key (%s)\\n\", queue_name_admin, to_janus_admin);\n\t\t\t\tamqp_queue_bind(rmq_client->rmq_conn, rmq_client->rmq_channel, rmq_client->to_janus_admin_queue, rmq_client->janus_exchange, amqp_cstring_bytes(to_janus_admin), amqp_empty_table);\n\t\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error binding queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t/* Case when to_janus_admin is the name of the queue (and there's no binding */\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_VERB, \"Declaring incoming admin queue (using to_janus_admin)... (%s)\\n\", to_janus_admin);\n\t\t\trmq_client->to_janus_admin_queue = amqp_cstring_bytes(to_janus_admin);\n\t\t\tdeclare = amqp_queue_declare(rmq_client->rmq_conn, rmq_client->rmq_channel, rmq_client->to_janus_admin_queue, 0, queue_durable_admin, queue_exclusive_admin, queue_autodelete_admin, amqp_empty_table);\n\t\t\trmq_client->to_janus_admin_queue = declare->queue;\n\t\t\tJANUS_LOG(LOG_VERB, \"Incoming admin queue declared: (%s)\\n\", (char *) rmq_client->to_janus_queue.bytes);\n\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error declaring queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\n\t\tif (declare_outgoing_queue_admin){\n\t\t\tJANUS_LOG(LOG_VERB, \"Declaring outgoing queue... (%s)\\n\", from_janus_admin);\n\t\t\tamqp_queue_declare(rmq_client->rmq_conn, rmq_client->rmq_channel, amqp_cstring_bytes(from_janus_admin), 0, 0, 0, 0, amqp_empty_table);\n\t\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error declaring queue... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\n\t\tamqp_basic_consume(rmq_client->rmq_conn, rmq_client->rmq_channel, rmq_client->to_janus_admin_queue, amqp_empty_bytes, 0, 1, 0, amqp_empty_table);\n\t\tresult = amqp_get_rpc_reply(rmq_client->rmq_conn);\n\t\tif(result.reply_type != AMQP_RESPONSE_NORMAL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Can't connect to RabbitMQ server: error consuming... %s, %s\\n\", amqp_error_string2(result.library_error), amqp_method_name(result.reply.id));\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\trmq_client->connected = TRUE;\n\n\tJANUS_LOG(LOG_INFO, \"RabbitMQ Connected successfully\\n\");\n\n\treturn 0;\n}\n\nvoid janus_rabbitmq_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n\n\tif(rmq_client) {\n\t\trmq_client->destroy = TRUE;\n\t\tg_async_queue_push(rmq_client->messages, &exit_message);\n\t\tif(rmq_client->in_thread)\n\t\t\tg_thread_join(rmq_client->in_thread);\n\t\tif(rmq_client->out_thread)\n\t\t\tg_thread_join(rmq_client->out_thread);\n\t\tif(rmq_client->rmq_conn) {\n\t\t\tamqp_destroy_connection(rmq_client->rmq_conn);\n\t\t}\n\t}\n\tg_free(rmq_client);\n\tjanus_transport_session_destroy(rmq_session);\n\n\tg_free(rmqhost);\n\tg_free(vhost);\n\tg_free(username);\n\tg_free(password);\n\tg_free(janus_exchange);\n\tg_free(janus_exchange_type);\n\tg_free(queue_name);\n\tg_free(queue_name_admin);\n\tg_free(to_janus);\n\tg_free(from_janus);\n\tg_free(to_janus_admin);\n\tg_free(from_janus_admin);\n\tg_free(ssl_cacert_file);\n\tg_free(ssl_cert_file);\n\tg_free(ssl_key_file);\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_RABBITMQ_NAME);\n}\n\nint janus_rabbitmq_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_TRANSPORT_API_VERSION;\n}\n\nint janus_rabbitmq_get_version(void) {\n\treturn JANUS_RABBITMQ_VERSION;\n}\n\nconst char *janus_rabbitmq_get_version_string(void) {\n\treturn JANUS_RABBITMQ_VERSION_STRING;\n}\n\nconst char *janus_rabbitmq_get_description(void) {\n\treturn JANUS_RABBITMQ_DESCRIPTION;\n}\n\nconst char *janus_rabbitmq_get_name(void) {\n\treturn JANUS_RABBITMQ_NAME;\n}\n\nconst char *janus_rabbitmq_get_author(void) {\n\treturn JANUS_RABBITMQ_AUTHOR;\n}\n\nconst char *janus_rabbitmq_get_package(void) {\n\treturn JANUS_RABBITMQ_PACKAGE;\n}\n\ngboolean janus_rabbitmq_is_janus_api_enabled(void) {\n\treturn rmq_janus_api_enabled;\n}\n\ngboolean janus_rabbitmq_is_admin_api_enabled(void) {\n\treturn rmq_admin_api_enabled;\n}\n\nint janus_rabbitmq_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message) {\n\tif(rmq_client == NULL)\n\t\treturn -1;\n\tif(message == NULL)\n\t\treturn -1;\n\tif(transport == NULL || transport->transport_p == NULL || g_atomic_int_get(&transport->destroyed)) {\n\t\tjson_decref(message);\n\t\treturn -1;\n\t}\n\tJANUS_LOG(LOG_HUGE, \"Sending %s API %s via RabbitMQ\\n\", admin ? \"admin\" : \"Janus\", request_id ? \"response\" : \"event\");\n\t/* FIXME Add to the queue of outgoing messages */\n\tjanus_rabbitmq_response *response = g_malloc(sizeof(janus_rabbitmq_response));\n\tresponse->admin = admin;\n\tresponse->payload = json_dumps(message, json_format);\n\tjson_decref(message);\n\tif(response->payload == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\tg_free(response);\n\t\treturn -1;\n\t}\n\tresponse->correlation_id = (char *)request_id;\n\tg_async_queue_push(rmq_client->messages, response);\n\treturn 0;\n}\n\nvoid janus_rabbitmq_session_created(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care */\n}\n\nvoid janus_rabbitmq_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed) {\n\t/* We don't care, not even if it's a timeout (should we?), our client is always up */\n}\n\nvoid janus_rabbitmq_session_claimed(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care about this. We should start receiving messages from the core about this session: no action necessary */\n\t/* FIXME Is the above statement accurate? Should we care? Unlike the HTTP transport, there is no hashtable to update */\n}\n\njson_t *janus_rabbitmq_query_transport(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this request to dynamically change the behaviour of\n\t * the transport plugin, and/or query for some specific information */\n\tjson_t *response = json_object();\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_RABBITMQ_ERROR_MISSING_ELEMENT, JANUS_RABBITMQ_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"configure\")) {\n\t\t/* We only allow for the configuration of some basic properties:\n\t\t * changing more complex things (e.g., port to bind to, etc.)\n\t\t * would likely require restarting backends, so just too much */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, configure_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_RABBITMQ_ERROR_MISSING_ELEMENT, JANUS_RABBITMQ_ERROR_INVALID_ELEMENT);\n\t\t/* Check if we now need to send events to handlers */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjson_t *notes = NULL;\n\t\tgboolean events = json_is_true(json_object_get(request, \"events\"));\n\t\tif(events && !gateway->events_is_enabled()) {\n\t\t\t/* Notify that this will be ignored */\n\t\t\tnotes = json_array();\n\t\t\tjson_array_append_new(notes, json_string(\"Event handlers disabled at the core level\"));\n\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t}\n\t\tif(events != notify_events) {\n\t\t\tnotify_events = events;\n\t\t\tif(!notify_events && gateway->events_is_enabled()) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_RABBITMQ_NAME);\n\t\t\t}\n\t\t}\n\t\tconst char *indentation = json_string_value(json_object_get(request, \"json\"));\n\t\tif(indentation != NULL) {\n\t\t\tif(!strcasecmp(indentation, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', ignoring tweak\\n\", indentation);\n\t\t\t\t/* Notify that this will be ignored */\n\t\t\t\tif(notes == NULL) {\n\t\t\t\t\tnotes = json_array();\n\t\t\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t\t\t}\n\t\t\t\tjson_array_append_new(notes, json_string(\"Ignored unsupported indentation format\"));\n\t\t\t}\n\t\t}\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_RABBITMQ_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n\n/* Threads */\nvoid *janus_rmq_in_thread(void *data) {\n\tif(rmq_client == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No RabbitMQ connection??\\n\");\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Joining RabbitMQ in thread\\n\");\n\n\tstruct timeval timeout;\n\ttimeout.tv_sec = 0;\n\ttimeout.tv_usec = 20000;\n\tamqp_frame_t frame;\n\tguint rmq_reconnect_backoff = rmq_reconnect_backoff_initial;\n\n\twhile(!rmq_client->destroy && !g_atomic_int_get(&stopping)) {\n\t\tamqp_maybe_release_buffers(rmq_client->rmq_conn);\n\t\t/* Wait for a frame */\n\t\tint res = amqp_simple_wait_frame_noblock(rmq_client->rmq_conn, &frame, &timeout);\n\t\tif(res != AMQP_STATUS_OK) {\n\t\t\t/* No data */\n\t\t\tif(res == AMQP_STATUS_TIMEOUT || res == AMQP_STATUS_SSL_ERROR)\n\t\t\t\tcontinue;\n\t\t\tJANUS_LOG(LOG_VERB, \"Error on amqp_simple_wait_frame_noblock: %d (%s)\\n\", res, amqp_error_string2(res));\n\n\t\t\trmq_client->connected = FALSE;\n\n\t\t\t/* Try and reconnect */\n\t\t\tif(rmq_client->rmq_conn) {\n\t\t\t\tamqp_destroy_connection(rmq_client->rmq_conn);\n\t\t\t}\n\n\t\t\tif(!g_atomic_int_get(&stopping)) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Trying to reconnect with RabbitMQ Server\\n\");\n\t\t\t\tint result = janus_rabbitmq_connect();\n\t\t\t\tif(result < 0) {\n\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Failed to reconnect to RabbitMQ Server. Retrying in %fs...\\n\", (gfloat)rmq_reconnect_backoff/1000000);\n\t\t\t\t\tg_usleep(rmq_reconnect_backoff);\n\t\t\t\t\trmq_reconnect_backoff *= rmq_reconnect_backoff_multiplier;\n\t\t\t\t\tif(rmq_reconnect_backoff >= rmq_reconnect_backoff_max)\n\t\t\t\t\t\trmq_reconnect_backoff = rmq_reconnect_backoff_max;\n\t\t\t\t} else {\n\t\t\t\t\trmq_reconnect_backoff = rmq_reconnect_backoff_initial;\n\t\t\t\t}\n\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\t/* We expect method first */\n\t\tJANUS_LOG(LOG_VERB, \"Frame type %d, channel %d\\n\", frame.frame_type, frame.channel);\n\t\tif(frame.frame_type != AMQP_FRAME_METHOD)\n\t\t\tcontinue;\n\t\tJANUS_LOG(LOG_VERB, \"Method %s\\n\", amqp_method_name(frame.payload.method.id));\n\t\tgboolean admin = FALSE;\n\t\tif(frame.payload.method.id == AMQP_BASIC_DELIVER_METHOD) {\n\t\t\tamqp_basic_deliver_t *d = (amqp_basic_deliver_t *)frame.payload.method.decoded;\n\t\t\tJANUS_LOG(LOG_VERB, \"Delivery #%u, %.*s\\n\", (unsigned) d->delivery_tag, (int) d->routing_key.len, (char *) d->routing_key.bytes);\n\t\t\t/* Check if this is a Janus or Admin API request */\n\t\t\tif(rmq_client->admin_api_enabled) {\n\t\t\t\tchar incoming_topic[d->routing_key.len + 2];\n\t\t\t\t/* Convert the amqp_bytes_t back to char* */\n\t\t\t\tg_strlcpy(incoming_topic, (char *)d->routing_key.bytes, d->routing_key.len + 1);\n\t\t\t\tif(strcmp(incoming_topic, to_janus_admin) == 0) {\n\t\t\t\t\tadmin = TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- This is %s API request\\n\", admin ? \"an admin\" : \"a Janus\");\n\t\t}\n\t\t/* Then the header */\n\t\tamqp_simple_wait_frame(rmq_client->rmq_conn, &frame);\n\t\tJANUS_LOG(LOG_VERB, \"Frame type %d, channel %d\\n\", frame.frame_type, frame.channel);\n\t\tif(frame.frame_type != AMQP_FRAME_HEADER)\n\t\t\tcontinue;\n\t\tamqp_basic_properties_t *p = (amqp_basic_properties_t *)frame.payload.properties.decoded;\n\t\tif(p->_flags & AMQP_BASIC_REPLY_TO_FLAG) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Reply-to: %.*s\\n\", (int) p->reply_to.len, (char *) p->reply_to.bytes);\n\t\t}\n\t\tchar *correlation = NULL;\n\t\tif(p->_flags & AMQP_BASIC_CORRELATION_ID_FLAG) {\n\t\t\tcorrelation = g_malloc0(p->correlation_id.len+1);\n\t\t\tsprintf(correlation, \"%.*s\", (int) p->correlation_id.len, (char *) p->correlation_id.bytes);\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Correlation-id: %s\\n\", correlation);\n\t\t}\n\t\tif(p->_flags & AMQP_BASIC_CONTENT_TYPE_FLAG) {\n\t\t\tJANUS_LOG(LOG_VERB, \"  -- Content-type: %.*s\\n\", (int) p->content_type.len, (char *) p->content_type.bytes);\n\t\t}\n\t\t/* And the body */\n\t\tuint64_t total = frame.payload.properties.body_size, received = 0;\n\t\tchar *payload = g_malloc0(total+1), *index = payload;\n\t\twhile(received < total) {\n\t\t\tamqp_simple_wait_frame(rmq_client->rmq_conn, &frame);\n\t\t\tJANUS_LOG(LOG_VERB, \"Frame type %d, channel %d\\n\", frame.frame_type, frame.channel);\n\t\t\tif(frame.frame_type != AMQP_FRAME_BODY)\n\t\t\t\tbreak;\n\t\t\tsprintf(index, \"%.*s\", (int) frame.payload.body_fragment.len, (char *) frame.payload.body_fragment.bytes);\n\t\t\treceived += frame.payload.body_fragment.len;\n\t\t\tindex = payload+received;\n\t\t}\n\t\tJANUS_LOG(LOG_VERB, \"Got %\"SCNu64\"/%\"SCNu64\" bytes from the %s queue (%\"SCNu64\")\\n\",\n\t\t\treceived, total, admin ? \"admin API\" : \"Janus API\", frame.payload.body_fragment.len);\n\t\tJANUS_LOG(LOG_VERB, \"%s\\n\", payload);\n\t\t/* Parse the JSON payload */\n\t\tjson_error_t error;\n\t\tjson_t *root = json_loadb(payload, frame.payload.body_fragment.len, 0, &error);\n\t\tg_free(payload);\n\t\t/* Notify the core, passing both the object and, since it may be needed, the error\n\t\t * We also specify the correlation ID as an opaque request identifier: we'll need it later */\n\t\tgateway->incoming_request(&janus_rabbitmq_transport, rmq_session, correlation, admin, root, &error);\n\t}\n\tJANUS_LOG(LOG_INFO, \"Leaving RabbitMQ in thread\\n\");\n\treturn NULL;\n}\n\nvoid *janus_rmq_out_thread(void *data) {\n\tif(rmq_client == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"No RabbitMQ connection??\\n\");\n\t\treturn NULL;\n\t}\n\tJANUS_LOG(LOG_VERB, \"Joining RabbitMQ out thread\\n\");\n\tguint rmq_reconnect_backoff = rmq_reconnect_backoff_initial;\n\twhile(!rmq_client->destroy && !g_atomic_int_get(&stopping)) {\n\n\t\tif(!rmq_client->connected) {\n\t\t\tg_usleep(rmq_reconnect_backoff);\n\t\t\trmq_reconnect_backoff *= rmq_reconnect_backoff_multiplier;\n\t\t\tif (rmq_reconnect_backoff >= rmq_reconnect_backoff_max)\n\t\t\t\trmq_reconnect_backoff = rmq_reconnect_backoff_max;\n\n\t\t\tcontinue;\n\t\t}\n\n\t\trmq_reconnect_backoff = rmq_reconnect_backoff_initial;\n\n\t\t/* We send messages from here as well, not only notifications */\n\t\tjanus_rabbitmq_response *response = g_async_queue_pop(rmq_client->messages);\n\t\tif(response == &exit_message)\n\t\t\tbreak;\n\t\tif(!rmq_client->destroy && !g_atomic_int_get(&stopping) && response->payload) {\n\t\t\tjanus_mutex_lock(&rmq_client->mutex);\n\t\t\t/* Gotcha! Convert json_t to string */\n\t\t\tchar *payload_text = response->payload;\n\t\t\tJANUS_LOG(LOG_VERB, \"Sending %s API message to RabbitMQ (%zu bytes) on exchange %s with routing key %s...\\n\", response->admin ? \"Admin\" : \"Janus\", strlen(payload_text), janus_exchange, response->admin ? from_janus_admin : from_janus);\n\t\t\tJANUS_LOG(LOG_VERB, \"%s\\n\", payload_text);\n\t\t\tamqp_basic_properties_t props;\n\t\t\tprops._flags = 0;\n\t\t\tprops._flags |= AMQP_BASIC_REPLY_TO_FLAG;\n\t\t\tprops.reply_to = amqp_cstring_bytes(\"Janus\");\n\t\t\tif(response->correlation_id) {\n\t\t\t\tprops._flags |= AMQP_BASIC_CORRELATION_ID_FLAG;\n\t\t\t\tprops.correlation_id = amqp_cstring_bytes(response->correlation_id);\n\t\t\t}\n\t\t\tprops._flags |= AMQP_BASIC_CONTENT_TYPE_FLAG;\n\t\t\tprops.content_type = amqp_cstring_bytes(\"application/json\");\n\t\t\tamqp_bytes_t message = amqp_cstring_bytes(payload_text);\n\t\t\tint status = amqp_basic_publish(rmq_client->rmq_conn, rmq_client->rmq_channel, rmq_client->janus_exchange,\n\t\t\t\tresponse->admin ? amqp_cstring_bytes(from_janus_admin) : amqp_cstring_bytes(from_janus),\n\t\t\t\t0, 0, &props, message);\n\t\t\tif(status != AMQP_STATUS_OK) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error publishing... %d, %s\\n\", status, amqp_error_string2(status));\n\t\t\t}\n\t\t\tjanus_mutex_unlock(&rmq_client->mutex);\n\t\t}\n\t\t/* Free the message */\n\t\tg_free(response->correlation_id);\n\t\tresponse->correlation_id = NULL;\n\t\tif(response->payload != NULL)\n\t\t\tfree(response->payload);\n\t\tresponse->payload = NULL;\n\t\tg_free(response);\n\t\tresponse = NULL;\n\t}\n\tg_async_queue_unref(rmq_client->messages);\n\tJANUS_LOG(LOG_INFO, \"Leaving RabbitMQ out thread\\n\");\n\treturn NULL;\n}\n"
  },
  {
    "path": "src/transports/janus_websockets.c",
    "content": "/*! \\file   janus_websockets.c\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus WebSockets transport plugin\n * \\details  This is an implementation of a WebSockets transport for the\n * Janus API, using the libwebsockets library (http://libwebsockets.org).\n * This means that, with the help of this module, browsers or applications\n * (e.g., nodejs server side implementations) can also make use of\n * WebSockets to make requests to Janus. In that case, the same\n * WebSocket can be used for both sending requests and receiving\n * notifications, without the need for long polls. At the same time,\n * without the concept of a REST path, requests sent through the\n * WebSockets interface will need to include, when needed, additional\n * pieces of information like \\c session_id and \\c handle_id. That is,\n * where you'd send a Janus request related to a specific session to the\n * \\c /janus/\\<session> path, with WebSockets you'd have to send the same\n * request with an additional \\c session_id field in the JSON payload.\n * The same applies for the handle. The JavaScript library (janus.js)\n * implements all of this on the client side automatically.\n * \\note When you create a session using WebSockets, a subscription to\n * the events related to it is done automatically, so no need for an\n * explicit request as the GET in the plain HTTP API. Closing a WebSocket\n * will also destroy all the sessions it created.\n *\n * \\ingroup transports\n * \\ref transports\n */\n\n#include \"transport.h\"\n\n#include <arpa/inet.h>\n#include <net/if.h>\n#include <ifaddrs.h>\n\n#include <libwebsockets.h>\n\n#include \"../debug.h\"\n#include \"../apierror.h\"\n#include \"../config.h\"\n#include \"../mutex.h\"\n#include \"../utils.h\"\n\n\n/* Transport plugin information */\n#define JANUS_WEBSOCKETS_VERSION\t\t\t1\n#define JANUS_WEBSOCKETS_VERSION_STRING\t\t\"0.0.1\"\n#define JANUS_WEBSOCKETS_DESCRIPTION\t\t\"This transport plugin adds WebSockets support to the Janus API via libwebsockets.\"\n#define JANUS_WEBSOCKETS_NAME\t\t\t\t\"JANUS WebSockets transport plugin\"\n#define JANUS_WEBSOCKETS_AUTHOR\t\t\t\t\"Meetecho s.r.l.\"\n#define JANUS_WEBSOCKETS_PACKAGE\t\t\t\"janus.transport.websockets\"\n\n/* Transport methods */\njanus_transport *create(void);\nint janus_websockets_init(janus_transport_callbacks *callback, const char *config_path);\nvoid janus_websockets_destroy(void);\nint janus_websockets_get_api_compatibility(void);\nint janus_websockets_get_version(void);\nconst char *janus_websockets_get_version_string(void);\nconst char *janus_websockets_get_description(void);\nconst char *janus_websockets_get_name(void);\nconst char *janus_websockets_get_author(void);\nconst char *janus_websockets_get_package(void);\ngboolean janus_websockets_is_janus_api_enabled(void);\ngboolean janus_websockets_is_admin_api_enabled(void);\nint janus_websockets_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message);\nvoid janus_websockets_session_created(janus_transport_session *transport, guint64 session_id);\nvoid janus_websockets_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed);\nvoid janus_websockets_session_claimed(janus_transport_session *transport, guint64 session_id);\njson_t *janus_websockets_query_transport(json_t *request);\n\n#define WS_LIST_TERM 0, NULL, 0\n\n/* Transport setup */\nstatic janus_transport janus_websockets_transport =\n\tJANUS_TRANSPORT_INIT (\n\t\t.init = janus_websockets_init,\n\t\t.destroy = janus_websockets_destroy,\n\n\t\t.get_api_compatibility = janus_websockets_get_api_compatibility,\n\t\t.get_version = janus_websockets_get_version,\n\t\t.get_version_string = janus_websockets_get_version_string,\n\t\t.get_description = janus_websockets_get_description,\n\t\t.get_name = janus_websockets_get_name,\n\t\t.get_author = janus_websockets_get_author,\n\t\t.get_package = janus_websockets_get_package,\n\n\t\t.is_janus_api_enabled = janus_websockets_is_janus_api_enabled,\n\t\t.is_admin_api_enabled = janus_websockets_is_admin_api_enabled,\n\n\t\t.send_message = janus_websockets_send_message,\n\t\t.session_created = janus_websockets_session_created,\n\t\t.session_over = janus_websockets_session_over,\n\t\t.session_claimed = janus_websockets_session_claimed,\n\n\t\t.query_transport = janus_websockets_query_transport,\n\t);\n\n/* Transport creator */\njanus_transport *create(void) {\n\tJANUS_LOG(LOG_VERB, \"%s created!\\n\", JANUS_WEBSOCKETS_NAME);\n\treturn &janus_websockets_transport;\n}\n\n\n/* Useful stuff */\nstatic gint initialized = 0, stopping = 0;\nstatic janus_transport_callbacks *gateway = NULL;\nstatic gboolean ws_janus_api_enabled = FALSE;\nstatic gboolean ws_admin_api_enabled = FALSE;\nstatic gboolean notify_events = TRUE;\n\n/* Clients maps */\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\nstatic GHashTable *clients = NULL, *writable_clients = NULL;\n#endif\nstatic janus_mutex writable_mutex = JANUS_MUTEX_INITIALIZER;\n\n/* JSON serialization options */\nstatic size_t json_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\n/* Parameter validation (for tweaking and queries via Admin API) */\nstatic struct janus_json_parameter request_parameters[] = {\n\t{\"request\", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}\n};\nstatic struct janus_json_parameter configure_parameters[] = {\n\t{\"events\", JANUS_JSON_BOOL, 0},\n\t{\"json\", JSON_STRING, 0},\n\t{\"logging\", JSON_STRING, 0},\n};\n/* Error codes (for the tweaking and queries via Admin API) */\n#define JANUS_WEBSOCKETS_ERROR_INVALID_REQUEST\t\t411\n#define JANUS_WEBSOCKETS_ERROR_MISSING_ELEMENT\t\t412\n#define JANUS_WEBSOCKETS_ERROR_INVALID_ELEMENT\t\t413\n#define JANUS_WEBSOCKETS_ERROR_UNKNOWN_ERROR\t\t499\n\n\n/* Logging */\nstatic int ws_log_level = 0;\nstatic const char *janus_websockets_get_level_str(int level) {\n\tswitch(level) {\n\t\tcase LLL_ERR:\n\t\t\treturn \"ERR\";\n\t\tcase LLL_WARN:\n\t\t\treturn \"WARN\";\n\t\tcase LLL_NOTICE:\n\t\t\treturn \"NOTICE\";\n\t\tcase LLL_INFO:\n\t\t\treturn \"INFO\";\n\t\tcase LLL_DEBUG:\n\t\t\treturn \"DEBUG\";\n\t\tcase LLL_PARSER:\n\t\t\treturn \"PARSER\";\n\t\tcase LLL_HEADER:\n\t\t\treturn \"HEADER\";\n\t\tcase LLL_EXT:\n\t\t\treturn \"EXT\";\n\t\tcase LLL_CLIENT:\n\t\t\treturn \"CLIENT\";\n\t\tcase LLL_LATENCY:\n\t\t\treturn \"LATENCY\";\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\tcase LLL_USER:\n\t\t\treturn \"USER\";\n#endif\n\t\tcase LLL_COUNT:\n\t\t\treturn \"COUNT\";\n\t\tdefault:\n\t\t\treturn NULL;\n\t}\n}\nstatic void janus_websockets_log_emit_function(int level, const char *line) {\n\t/* FIXME Do we want to use different Janus debug levels according to the level here? */\n\tJANUS_LOG(LOG_INFO, \"[libwebsockets][%s] %s\", janus_websockets_get_level_str(level), line);\n}\n\n/* WebSockets service thread */\nstatic GThread *ws_thread = NULL;\nvoid *janus_websockets_thread(void *data);\n\n\n/* WebSocket client session */\ntypedef struct janus_websockets_client {\n\tstruct lws *wsi;\t\t\t\t\t\t/* The libwebsockets client instance */\n\tGAsyncQueue *messages;\t\t\t\t\t/* Queue of outgoing messages to push */\n\tchar *incoming;\t\t\t\t\t\t\t/* Buffer containing the incoming message to process (in case there are fragments) */\n\tunsigned char *buffer;\t\t\t\t\t/* Buffer containing the message to send */\n\tsize_t buflen;\t\t\t\t\t\t\t\t/* Length of the buffer (may be resized after re-allocations) */\n\tsize_t bufpending;\t\t\t\t\t\t\t/* Data an interrupted previous write couldn't send */\n\tsize_t bufoffset;\t\t\t\t\t\t\t/* Offset from where the interrupted previous write should resume */\n\tvolatile gint destroyed;\t\t\t\t/* Whether this libwebsockets client instance has been closed */\n\tjanus_transport_session *ts;\t\t\t/* Janus core-transport session */\n} janus_websockets_client;\n\n\n/* libwebsockets WS context */\nstatic struct lws_context *wsc = NULL;\n/* Callbacks for HTTP-related events (automatically rejected) */\nstatic int janus_websockets_callback_http(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len);\nstatic int janus_websockets_callback_https(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len);\n/* Callbacks for WebSockets-related events */\nstatic int janus_websockets_callback(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len);\nstatic int janus_websockets_callback_secure(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len);\nstatic int janus_websockets_admin_callback(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len);\nstatic int janus_websockets_admin_callback_secure(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len);\n/* Protocol mappings */\nstatic struct lws_protocols ws_protocols[] = {\n\t{ \"http-only\", janus_websockets_callback_http, 0, 0, WS_LIST_TERM },\n\t{ \"janus-protocol\", janus_websockets_callback, sizeof(janus_websockets_client), 0, WS_LIST_TERM },\n\t{ NULL, NULL, 0, 0, WS_LIST_TERM }\n};\nstatic struct lws_protocols sws_protocols[] = {\n\t{ \"http-only\", janus_websockets_callback_https, 0, 0, WS_LIST_TERM },\n\t{ \"janus-protocol\", janus_websockets_callback_secure, sizeof(janus_websockets_client), 0, WS_LIST_TERM },\n\t{ NULL, NULL, 0, 0, WS_LIST_TERM }\n};\nstatic struct lws_protocols admin_ws_protocols[] = {\n\t{ \"http-only\", janus_websockets_callback_http, 0, 0, WS_LIST_TERM },\n\t{ \"janus-admin-protocol\", janus_websockets_admin_callback, sizeof(janus_websockets_client), 0, WS_LIST_TERM },\n\t{ NULL, NULL, 0, 0, WS_LIST_TERM }\n};\nstatic struct lws_protocols admin_sws_protocols[] = {\n\t{ \"http-only\", janus_websockets_callback_https, 0, 0, WS_LIST_TERM },\n\t{ \"janus-admin-protocol\", janus_websockets_admin_callback_secure, sizeof(janus_websockets_client), 0, WS_LIST_TERM },\n\t{ NULL, NULL, 0, 0, WS_LIST_TERM }\n};\n/* Helper for debugging reasons */\n#define CASE_STR(name) case name: return #name\nstatic const char *janus_websockets_reason_string(enum lws_callback_reasons reason) {\n\tswitch(reason) {\n\t\tCASE_STR(LWS_CALLBACK_ESTABLISHED);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_CONNECTION_ERROR);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_ESTABLISHED);\n\t\tCASE_STR(LWS_CALLBACK_CLOSED);\n\t\tCASE_STR(LWS_CALLBACK_CLOSED_HTTP);\n\t\tCASE_STR(LWS_CALLBACK_RECEIVE);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_RECEIVE);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_RECEIVE_PONG);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_WRITEABLE);\n\t\tCASE_STR(LWS_CALLBACK_SERVER_WRITEABLE);\n\t\tCASE_STR(LWS_CALLBACK_HTTP);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_BODY);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_BODY_COMPLETION);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_FILE_COMPLETION);\n\t\tCASE_STR(LWS_CALLBACK_HTTP_WRITEABLE);\n\t\tCASE_STR(LWS_CALLBACK_ADD_HEADERS);\n\t\tCASE_STR(LWS_CALLBACK_FILTER_NETWORK_CONNECTION);\n\t\tCASE_STR(LWS_CALLBACK_FILTER_HTTP_CONNECTION);\n\t\tCASE_STR(LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED);\n\t\tCASE_STR(LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION);\n\t\tCASE_STR(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS);\n\t\tCASE_STR(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS);\n\t\tCASE_STR(LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER);\n\t\tCASE_STR(LWS_CALLBACK_CONFIRM_EXTENSION_OKAY);\n\t\tCASE_STR(LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED);\n\t\tCASE_STR(LWS_CALLBACK_PROTOCOL_INIT);\n\t\tCASE_STR(LWS_CALLBACK_PROTOCOL_DESTROY);\n\t\tCASE_STR(LWS_CALLBACK_WSI_CREATE);\n\t\tCASE_STR(LWS_CALLBACK_WSI_DESTROY);\n\t\tCASE_STR(LWS_CALLBACK_GET_THREAD_ID);\n\t\tCASE_STR(LWS_CALLBACK_ADD_POLL_FD);\n\t\tCASE_STR(LWS_CALLBACK_DEL_POLL_FD);\n\t\tCASE_STR(LWS_CALLBACK_CHANGE_MODE_POLL_FD);\n\t\tCASE_STR(LWS_CALLBACK_LOCK_POLL);\n\t\tCASE_STR(LWS_CALLBACK_UNLOCK_POLL);\n\t\tCASE_STR(LWS_CALLBACK_USER);\n\t\tCASE_STR(LWS_CALLBACK_RECEIVE_PONG);\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn NULL;\n}\n\n#if (LWS_LIBRARY_VERSION_MAJOR >= 4)\nstatic lws_retry_bo_t pingpong = { 0 };\n#endif\n\n/* Helper method to return the interface associated with a local IP address */\nstatic char *janus_websockets_get_interface_name(const char *ip) {\n\tstruct ifaddrs *addrs = NULL, *iap = NULL;\n\tif(getifaddrs(&addrs) == -1)\n\t\treturn NULL;\n\tfor(iap = addrs; iap != NULL; iap = iap->ifa_next) {\n\t\tif(iap->ifa_addr && (iap->ifa_flags & IFF_UP)) {\n\t\t\tif(iap->ifa_addr->sa_family == AF_INET) {\n\t\t\t\tstruct sockaddr_in *sa = (struct sockaddr_in *)(iap->ifa_addr);\n\t\t\t\tchar buffer[16];\n\t\t\t\tinet_ntop(iap->ifa_addr->sa_family, (void *)&(sa->sin_addr), buffer, sizeof(buffer));\n\t\t\t\tif(!strcmp(ip, buffer)) {\n\t\t\t\t\tchar *iface = g_strdup(iap->ifa_name);\n\t\t\t\t\tfreeifaddrs(addrs);\n\t\t\t\t\treturn iface;\n\t\t\t\t}\n\t\t\t} else if(iap->ifa_addr->sa_family == AF_INET6) {\n\t\t\t\tstruct sockaddr_in6 *sa = (struct sockaddr_in6 *)(iap->ifa_addr);\n\t\t\t\tchar buffer[48];\n\t\t\t\tinet_ntop(iap->ifa_addr->sa_family, (void *)&(sa->sin6_addr), buffer, sizeof(buffer));\n\t\t\t\tif(!strcmp(ip, buffer)) {\n\t\t\t\t\tchar *iface = g_strdup(iap->ifa_name);\n\t\t\t\t\tfreeifaddrs(addrs);\n\t\t\t\t\treturn iface;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfreeifaddrs(addrs);\n\treturn NULL;\n}\n\n/* Custom Access-Control-Allow-Origin value, if specified */\nstatic char *allow_origin = NULL;\nstatic gboolean enforce_cors = FALSE;\n\n/* WebSockets ACL list for both Janus and Admin API */\nstatic GList *janus_websockets_access_list = NULL, *janus_websockets_admin_access_list = NULL;\nstatic gboolean janus_websockets_check_xff = FALSE, janus_websockets_admin_check_xff = FALSE;\nstatic janus_mutex access_list_mutex;\nstatic void janus_websockets_allow_address(const char *ip, gboolean admin) {\n\tif(ip == NULL)\n\t\treturn;\n\t/* Is this an IP or an interface? */\n\tjanus_mutex_lock(&access_list_mutex);\n\tif(!admin)\n\t\tjanus_websockets_access_list = g_list_append(janus_websockets_access_list, (gpointer)ip);\n\telse\n\t\tjanus_websockets_admin_access_list = g_list_append(janus_websockets_admin_access_list, (gpointer)ip);\n\tjanus_mutex_unlock(&access_list_mutex);\n}\nstatic gboolean janus_websockets_is_allowed(const char *ip, gboolean admin) {\n\tif(ip == NULL)\n\t\treturn FALSE;\n\tjanus_mutex_lock(&access_list_mutex);\n\tif(!admin && janus_websockets_access_list == NULL) {\n\t\tjanus_mutex_unlock(&access_list_mutex);\n\t\treturn TRUE;\n\t}\n\tif(admin && janus_websockets_admin_access_list == NULL) {\n\t\tjanus_mutex_unlock(&access_list_mutex);\n\t\treturn TRUE;\n\t}\n\tGList *temp = admin ? janus_websockets_admin_access_list : janus_websockets_access_list;\n\twhile(temp) {\n\t\tconst char *allowed = (const char *)temp->data;\n\t\tif(allowed != NULL && strstr(ip, allowed)) {\n\t\t\tjanus_mutex_unlock(&access_list_mutex);\n\t\t\treturn TRUE;\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tjanus_mutex_unlock(&access_list_mutex);\n\treturn FALSE;\n}\n\nstatic struct lws_vhost* janus_websockets_create_ws_server(\n\t\tjanus_config *config,\n\t\tjanus_config_container *config_container,\n\t\tjanus_config_container *config_certs,\n\t\tconst char *prefix,\n\t\tconst char *name,\n\t\tstruct lws_protocols ws_protocols[],\n\t\tgboolean secure,\n\t\tuint16_t default_port)\n{\n\tjanus_config_item *item;\n\tchar item_name[255];\n\n\titem = janus_config_get(config, config_container, janus_config_type_item, prefix);\n\tif(!item || !item->value || !janus_is_true(item->value)) {\n\t\tJANUS_LOG(LOG_VERB, \"%s server disabled\\n\", name);\n\t\treturn NULL;\n\t}\n\n\tuint16_t wsport = default_port;\n\tg_snprintf(item_name, 255, \"%s_port\", prefix);\n\titem = janus_config_get(config, config_container, janus_config_type_item, item_name);\n\tif(item && item->value && janus_string_to_uint16(item->value, &wsport) < 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid port (%s), falling back to default\\n\", item->value);\n\t\twsport = default_port;\n\t}\n\n\tchar *interface = NULL;\n\tg_snprintf(item_name, 255, \"%s_interface\", prefix);\n\titem = janus_config_get(config, config_container, janus_config_type_item, item_name);\n\tif(item && item->value)\n\t\tinterface = (char *)item->value;\n\n\tchar *ip = NULL;\n\tint ipv4_only = 0;\n\tg_snprintf(item_name, 255, \"%s_ip\", prefix);\n\titem = janus_config_get(config, config_container, janus_config_type_item, item_name);\n\tif(item && item->value) {\n\t\tip = (char *)item->value;\n\t\tstruct in_addr addr;\n\t\tif(inet_pton(AF_INET, ip, &addr) == 1)\n\t\t\tipv4_only = 1;\n\t\tchar *iface = janus_websockets_get_interface_name(ip);\n\t\tif(iface == NULL) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"No interface associated with %s?\\n\", ip);\n\t\t\treturn NULL;\n\t\t}\n\t\telse {\n\t\t\tg_free(iface);\n\t\t}\n\t}\n\n\tg_snprintf(item_name, 255, \"%s_unix\", prefix);\n\titem = janus_config_get(config, config_container, janus_config_type_item, item_name);\n#if defined(LWS_USE_UNIX_SOCK) || defined(LWS_WITH_UNIX_SOCK)\n\tchar *unixpath = NULL;\n\tif(item && item->value)\n\t\tunixpath = (char *)item->value;\n#else\n\tif(item && item->value)\n\t\tJANUS_LOG(LOG_WARN, \"WebSockets option '%s' is not supported because libwebsockets compiled without UNIX sockets\\n\", item_name);\n#endif\n\n\tchar *server_pem = NULL;\n\tchar *server_key = NULL;\n\tchar *password = NULL;\n\tchar *ciphers = NULL;\n\n\tif (secure) {\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_pem\");\n\t\tif(!item || !item->value) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Missing certificate/key path\\n\");\n\t\t\treturn NULL;\n\t\t}\n\t\tserver_pem = (char *)item->value;\n\t\tserver_key = (char *)item->value;\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_key\");\n\t\tif(item && item->value)\n\t\t\tserver_key = (char *)item->value;\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"cert_pwd\");\n\t\tif(item && item->value)\n\t\t\tpassword = (char *)item->value;\n\t\tJANUS_LOG(LOG_VERB, \"Using certificates:\\n\\t%s\\n\\t%s\\n\", server_pem, server_key);\n\t\titem = janus_config_get(config, config_certs, janus_config_type_item, \"ciphers\");\n\t\tif(item && item->value)\n\t\t\tciphers = (char *)item->value;\n\t}\n\n\t/* Prepare context */\n\tstruct lws_context_creation_info info;\n\tmemset(&info, 0, sizeof info);\n#if defined(LWS_USE_UNIX_SOCK) || defined(LWS_WITH_UNIX_SOCK)\n\tinfo.port = unixpath ? 0 : wsport;\n\tinfo.iface = unixpath ? unixpath : (ip ? ip : interface);\n#else\n\tinfo.port = wsport;\n\tinfo.iface = ip ? ip : interface;\n#endif\n\tinfo.protocols = ws_protocols;\n\tinfo.extensions = NULL;\n\tinfo.ssl_cert_filepath = server_pem;\n\tinfo.ssl_private_key_filepath = server_key;\n\tinfo.ssl_private_key_password = password;\n\tinfo.ssl_cipher_list = ciphers;\n\tinfo.gid = -1;\n\tinfo.uid = -1;\n\tinfo.options = 0;\n\n\tif (server_pem) {\n#if (LWS_LIBRARY_VERSION_MAJOR == 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR > 3)\n\t\tinfo.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_FAIL_UPON_UNABLE_TO_BIND;\n#elif LWS_LIBRARY_VERSION_MAJOR >= 2\n\t\tinfo.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;\n#endif\n\t}\n\n\tif (ipv4_only) {\n\t\tinfo.options |= LWS_SERVER_OPTION_DISABLE_IPV6;\n\t\tipv4_only = 0;\n\t}\n#if defined(LWS_USE_UNIX_SOCK) || defined(LWS_WITH_UNIX_SOCK)\n\tif (unixpath)\n\t\tinfo.options |= LWS_SERVER_OPTION_UNIX_SOCK;\n#endif\n#if (LWS_LIBRARY_VERSION_MAJOR == 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR > 3)\n\tinfo.options |= LWS_SERVER_OPTION_FAIL_UPON_UNABLE_TO_BIND;\n\n#endif\n\t/* Create the WebSocket context */\n\tstruct lws_vhost *vhost = lws_create_vhost(wsc, &info);\n\tif(vhost == NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error creating vhost for %s server...\\n\", name);\n#if defined(LWS_USE_UNIX_SOCK) || defined(LWS_WITH_UNIX_SOCK)\n\t} else if (unixpath) {\n\t\tJANUS_LOG(LOG_INFO, \"%s server started (UNIX socket %s)...\\n\", name, unixpath);\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_INFO, \"%s server started (port %d)...\\n\", name, wsport);\n\t}\n\treturn vhost;\n}\n\n/* Transport implementation */\nint janus_websockets_init(janus_transport_callbacks *callback, const char *config_path) {\n\tif(g_atomic_int_get(&stopping)) {\n\t\t/* Still stopping from before */\n\t\treturn -1;\n\t}\n\tif(callback == NULL || config_path == NULL) {\n\t\t/* Invalid arguments */\n\t\treturn -1;\n\t}\n\n#ifndef LWS_WITH_IPV6\n\tJANUS_LOG(LOG_WARN, \"libwebsockets has been built without IPv6 support, will bind to IPv4 only\\n\");\n#endif\n\n\t/* This is the callback we'll need to invoke to contact the Janus core */\n\tgateway = callback;\n\n\t/* Prepare the common context */\n\tstruct lws_context_creation_info wscinfo;\n\tmemset(&wscinfo, 0, sizeof wscinfo);\n\twscinfo.options |= LWS_SERVER_OPTION_EXPLICIT_VHOSTS;\n\n\t/* We use vhosts on the same context to address both APIs, secure or not */\n\tstruct lws_vhost *wss = NULL, *swss = NULL,\n\t\t*admin_wss = NULL, *admin_swss = NULL;\n\n\t/* Read configuration */\n\tchar filename[255];\n\tg_snprintf(filename, 255, \"%s/%s.jcfg\", config_path, JANUS_WEBSOCKETS_PACKAGE);\n\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\tjanus_config *config = janus_config_parse(filename);\n\tif(config == NULL) {\n\t\tJANUS_LOG(LOG_WARN, \"Couldn't find .jcfg configuration file (%s), trying .cfg\\n\", JANUS_WEBSOCKETS_PACKAGE);\n\t\tg_snprintf(filename, 255, \"%s/%s.cfg\", config_path, JANUS_WEBSOCKETS_PACKAGE);\n\t\tJANUS_LOG(LOG_VERB, \"Configuration file: %s\\n\", filename);\n\t\tconfig = janus_config_parse(filename);\n\t}\n\tif(config != NULL) {\n\t\tjanus_config_print(config);\n\t\tjanus_config_category *config_general = janus_config_get_create(config, NULL, janus_config_type_category, \"general\");\n\t\tjanus_config_category *config_admin = janus_config_get_create(config, NULL, janus_config_type_category, \"admin\");\n\t\tjanus_config_category *config_cors = janus_config_get_create(config, NULL, janus_config_type_category, \"cors\");\n\t\tjanus_config_category *config_certs = janus_config_get_create(config, NULL, janus_config_type_category, \"certificates\");\n\n\t\t/* Handle configuration */\n\t\tjanus_config_item *item = janus_config_get(config, config_general, janus_config_type_item, \"json\");\n\t\tif(item && item->value) {\n\t\t\t/* Check how we need to format/serialize the JSON output */\n\t\t\tif(!strcasecmp(item->value, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(item->value, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', using default (indented)\\n\", item->value);\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t}\n\t\t}\n\n\t\t/* Check if we need to send events to handlers */\n\t\tjanus_config_item *events = janus_config_get(config, config_general, janus_config_type_item, \"events\");\n\t\tif(events != NULL && events->value != NULL)\n\t\t\tnotify_events = janus_is_true(events->value);\n\t\tif(!notify_events && callback->events_is_enabled()) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_WEBSOCKETS_NAME);\n\t\t}\n\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ws_logging\");\n\t\tif(item && item->value) {\n\t\t\t/* libwebsockets uses a mask to set log levels, as documented here:\n\t\t\t * https://libwebsockets.org/lws-api-doc-master/html/group__log.html */\n\t\t\tif(strstr(item->value, \"none\")) {\n\t\t\t\t/* Disable libwebsockets logging completely (the default) */\n\t\t\t} else if(strstr(item->value, \"all\")) {\n\t\t\t\t/* Enable all libwebsockets logging */\n\t\t\t\tws_log_level = LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO |\n\t\t\t\t\tLLL_DEBUG | LLL_PARSER | LLL_HEADER | LLL_EXT |\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\t\t\tLLL_CLIENT | LLL_LATENCY | LLL_USER | LLL_COUNT;\n#else\n\t\t\t\t\tLLL_CLIENT | LLL_LATENCY | LLL_COUNT;\n#endif\n\t\t\t} else {\n\t\t\t\t/* Only enable some of the properties */\n\t\t\t\tif(strstr(item->value, \"err\"))\n\t\t\t\t\tws_log_level |= LLL_ERR;\n\t\t\t\tif(strstr(item->value, \"warn\"))\n\t\t\t\t\tws_log_level |= LLL_WARN;\n\t\t\t\tif(strstr(item->value, \"notice\"))\n\t\t\t\t\tws_log_level |= LLL_NOTICE;\n\t\t\t\tif(strstr(item->value, \"info\"))\n\t\t\t\t\tws_log_level |= LLL_INFO;\n\t\t\t\tif(strstr(item->value, \"debug\"))\n\t\t\t\t\tws_log_level |= LLL_DEBUG;\n\t\t\t\tif(strstr(item->value, \"parser\"))\n\t\t\t\t\tws_log_level |= LLL_PARSER;\n\t\t\t\tif(strstr(item->value, \"header\"))\n\t\t\t\t\tws_log_level |= LLL_HEADER;\n\t\t\t\tif(strstr(item->value, \"ext\"))\n\t\t\t\t\tws_log_level |= LLL_EXT;\n\t\t\t\tif(strstr(item->value, \"client\"))\n\t\t\t\t\tws_log_level |= LLL_CLIENT;\n\t\t\t\tif(strstr(item->value, \"latency\"))\n\t\t\t\t\tws_log_level |= LLL_LATENCY;\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\t\tif(strstr(item->value, \"user\"))\n\t\t\t\t\tws_log_level |= LLL_USER;\n#endif\n\t\t\t\tif(strstr(item->value, \"count\"))\n\t\t\t\t\tws_log_level |= LLL_COUNT;\n\t\t\t}\n\t\t}\n\t\tJANUS_LOG(LOG_INFO, \"libwebsockets logging: %d\\n\", ws_log_level);\n\t\tlws_set_log_level(ws_log_level, janus_websockets_log_emit_function);\n\n\t\t/* Any ACL for either the Janus or Admin API? */\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ws_acl\");\n\t\tif(item && item->value) {\n\t\t\tgchar **list = g_strsplit(item->value, \",\", -1);\n\t\t\tgchar *index = list[0];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\tif(strlen(index) > 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Adding '%s' to the Janus API allowed list...\\n\", index);\n\t\t\t\t\t\tjanus_websockets_allow_address(g_strdup(index), FALSE);\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_strfreev(list);\n\t\t\tlist = NULL;\n\t\t\t/* Check if we should use the value of X-Forwarded-For for checks too */\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"ws_acl_forwarded\");\n\t\t\tif(item && item->value)\n\t\t\t\tjanus_websockets_check_xff = janus_is_true(item->value);\n\t\t}\n\t\titem = janus_config_get(config, config_admin, janus_config_type_item, \"admin_ws_acl\");\n\t\tif(item && item->value) {\n\t\t\tgchar **list = g_strsplit(item->value, \",\", -1);\n\t\t\tgchar *index = list[0];\n\t\t\tif(index != NULL) {\n\t\t\t\tint i=0;\n\t\t\t\twhile(index != NULL) {\n\t\t\t\t\tif(strlen(index) > 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_INFO, \"Adding '%s' to the Admin/monitor allowed list...\\n\", index);\n\t\t\t\t\t\tjanus_websockets_allow_address(g_strdup(index), TRUE);\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t\tindex = list[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\tg_strfreev(list);\n\t\t\tlist = NULL;\n\t\t\t/* Check if we should use the value of X-Forwarded-For for checks too */\n\t\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"admin_ws_acl_forwarded\");\n\t\t\tif(item && item->value)\n\t\t\t\tjanus_websockets_admin_check_xff = janus_is_true(item->value);\n\t\t}\n\n\t\t/* Any custom value for the Access-Control-Allow-Origin header? */\n\t\titem = janus_config_get(config, config_cors, janus_config_type_item, \"allow_origin\");\n\t\tif(item && item->value) {\n\t\t\tallow_origin = g_strdup(item->value);\n\t\t\tJANUS_LOG(LOG_INFO, \"Restricting Access-Control-Allow-Origin to '%s'\\n\", allow_origin);\n\t\t}\n\t\tif(allow_origin != NULL) {\n\t\t\titem = janus_config_get(config, config_cors, janus_config_type_item, \"enforce_cors\");\n\t\t\tif(item && item->value && janus_is_true(item->value)) {\n\t\t\t\tenforce_cors = TRUE;\n\t\t\t\tJANUS_LOG(LOG_INFO, \"Going to enforce CORS by rejecting WebSocket connections\\n\");\n\t\t\t}\n\t\t}\n\n\t\t/* Check if we need to enable the transport level ping/pong mechanism */\n\t\tint pingpong_trigger = 0, pingpong_timeout = 0;\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"pingpong_trigger\");\n\t\tif(item && item->value) {\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 1) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\tpingpong_trigger = atoi(item->value);\n\t\t\tif(pingpong_trigger < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid value for pingpong_trigger (%d), ignoring...\\n\", pingpong_trigger);\n\t\t\t\tpingpong_trigger = 0;\n\t\t\t}\n#else\n\t\t\tJANUS_LOG(LOG_WARN, \"WebSockets ping/pong only supported in libwebsockets >= 2.1\\n\");\n#endif\n\t\t}\n\t\titem = janus_config_get(config, config_general, janus_config_type_item, \"pingpong_timeout\");\n\t\tif(item && item->value) {\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 1) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\tpingpong_timeout = atoi(item->value);\n\t\t\tif(pingpong_timeout < 0) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Invalid value for pingpong_timeout (%d), ignoring...\\n\", pingpong_timeout);\n\t\t\t\tpingpong_timeout = 0;\n\t\t\t}\n#else\n\t\t\tJANUS_LOG(LOG_WARN, \"WebSockets ping/pong only supported in libwebsockets >= 2.1\\n\");\n#endif\n\t\t}\n\t\tif((pingpong_trigger && !pingpong_timeout) || (!pingpong_trigger && pingpong_timeout)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"pingpong_trigger and pingpong_timeout not both set, ignoring...\\n\");\n\t\t}\n#if (LWS_LIBRARY_VERSION_MAJOR >= 4)\n\t\t/* libwebsockets 4 has a different API, that works differently\n\t\t * https://github.com/warmcat/libwebsockets/blob/master/READMEs/README.lws_retry.md */\n\t\tif(pingpong_trigger > 0 && pingpong_timeout > 0) {\n\t\t\tpingpong.secs_since_valid_ping = pingpong_trigger;\n\t\t\tpingpong.secs_since_valid_hangup = pingpong_trigger + pingpong_timeout;\n\t\t\twscinfo.retry_and_idle_policy = &pingpong;\n\t\t}\n#else\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 1) || (LWS_LIBRARY_VERSION_MAJOR == 3)\n\t\tif(pingpong_trigger > 0 && pingpong_timeout > 0) {\n\t\t\twscinfo.ws_ping_pong_interval = pingpong_trigger;\n\t\t\twscinfo.timeout_secs = pingpong_timeout;\n\t\t}\n#endif\n#endif\n\t\t/* Force single-thread server */\n\t\twscinfo.count_threads = 1;\n\n\t\t/* Create the base context */\n\t\twsc = lws_create_context(&wscinfo);\n\t\tif(wsc == NULL) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Error creating libwebsockets context...\\n\");\n\t\t\tjanus_config_destroy(config);\n\t\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t\t}\n\n\t\t/* Setup the Janus API WebSockets server(s) */\n\t\twss = janus_websockets_create_ws_server(config, config_general, NULL, \"ws\",\n\t\t\t\t\"Websockets\", ws_protocols, FALSE, 8188);\n\t\tswss = janus_websockets_create_ws_server(config, config_general, config_certs, \"wss\",\n\t\t\t\t\"Secure Websockets\", sws_protocols, TRUE, 8989);\n\t\t/* Do the same for the Admin API, if enabled */\n\t\tadmin_wss = janus_websockets_create_ws_server(config, config_admin, NULL, \"admin_ws\",\n\t\t\t\t\"Admin Websockets\", admin_ws_protocols, FALSE, 7188);\n\t\tadmin_swss = janus_websockets_create_ws_server(config, config_admin, config_certs, \"admin_wss\",\n\t\t\t\t\"Secure Admin Websockets\", admin_sws_protocols, TRUE, 7989);\n\t}\n\tjanus_config_destroy(config);\n\tconfig = NULL;\n\tif(!wss && !swss && !admin_wss && !admin_swss) {\n\t\tJANUS_LOG(LOG_WARN, \"No WebSockets server started, giving up...\\n\");\n\t\tlws_context_destroy(wsc);\n\t\treturn -1;\t/* No point in keeping the plugin loaded */\n\t}\n\tws_janus_api_enabled = wss || swss;\n\tws_admin_api_enabled = admin_wss || admin_swss;\n\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\tclients = g_hash_table_new(NULL, NULL);\n\twritable_clients = g_hash_table_new(NULL, NULL);\n#endif\n\n\tg_atomic_int_set(&initialized, 1);\n\n\tGError *error = NULL;\n\t/* Start the WebSocket service thread */\n\tif(ws_janus_api_enabled || ws_admin_api_enabled) {\n\t\tws_thread = g_thread_try_new(\"ws thread\", &janus_websockets_thread, wsc, &error);\n\t\tif(error != NULL) {\n\t\t\tg_atomic_int_set(&initialized, 0);\n\t\t\tJANUS_LOG(LOG_ERR, \"Got error %d (%s) trying to launch the WebSockets thread...\\n\",\n\t\t\t\terror->code, error->message ? error->message : \"??\");\n\t\t\tg_error_free(error);\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"%s initialized!\\n\", JANUS_WEBSOCKETS_NAME);\n\treturn 0;\n}\n\nvoid janus_websockets_destroy(void) {\n\tif(!g_atomic_int_get(&initialized))\n\t\treturn;\n\tg_atomic_int_set(&stopping, 1);\n#if ((LWS_LIBRARY_VERSION_MAJOR == 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || LWS_LIBRARY_VERSION_MAJOR >= 4)\n\tlws_cancel_service(wsc);\n#endif\n\n\t/* Stop the service thread */\n\tif(ws_thread != NULL) {\n\t\tg_thread_join(ws_thread);\n\t\tws_thread = NULL;\n\t}\n\n\t/* Destroy the context */\n\tif(wsc != NULL) {\n\t\tlws_context_destroy(wsc);\n\t\twsc = NULL;\n\t}\n\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\tjanus_mutex_lock(&writable_mutex);\n\tg_hash_table_destroy(clients);\n\tclients = NULL;\n\tg_hash_table_destroy(writable_clients);\n\twritable_clients = NULL;\n\tjanus_mutex_unlock(&writable_mutex);\n#endif\n\n\tg_atomic_int_set(&initialized, 0);\n\tg_atomic_int_set(&stopping, 0);\n\tJANUS_LOG(LOG_INFO, \"%s destroyed!\\n\", JANUS_WEBSOCKETS_NAME);\n}\n\nstatic void janus_websockets_destroy_client(\n\t\tjanus_websockets_client *ws_client,\n\t\tstruct lws *wsi,\n\t\tconst char *log_prefix) {\n\tif(!ws_client || !ws_client->ts)\n\t\treturn;\n\tjanus_mutex_lock(&ws_client->ts->mutex);\n\tif(!g_atomic_int_compare_and_exchange(&ws_client->destroyed, 0, 1)) {\n\t\tjanus_mutex_unlock(&ws_client->ts->mutex);\n\t\treturn;\n\t}\n\t/* Cleanup */\n\tJANUS_LOG(LOG_INFO, \"[%s-%p] Destroying WebSocket client\\n\", log_prefix, wsi);\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\tjanus_mutex_lock(&writable_mutex);\n\tg_hash_table_remove(clients, ws_client);\n\tg_hash_table_remove(writable_clients, ws_client);\n\tjanus_mutex_unlock(&writable_mutex);\n#endif\n\tws_client->wsi = NULL;\n\t/* Notify handlers about this transport being gone */\n\tif(notify_events && gateway->events_is_enabled()) {\n\t\tjson_t *info = json_object();\n\t\tjson_object_set_new(info, \"event\", json_string(\"disconnected\"));\n\t\tgateway->notify_event(&janus_websockets_transport, ws_client->ts, info);\n\t}\n\tws_client->ts->transport_p = NULL;\n\t/* Remove messages queue too, if needed */\n\tif(ws_client->messages != NULL) {\n\t\tchar *response = NULL;\n\t\twhile((response = g_async_queue_try_pop(ws_client->messages)) != NULL) {\n\t\t\tg_free(response);\n\t\t}\n\t\tg_async_queue_unref(ws_client->messages);\n\t}\n\t/* ... and the shared buffers */\n\tg_free(ws_client->incoming);\n\tws_client->incoming = NULL;\n\tg_free(ws_client->buffer);\n\tws_client->buffer = NULL;\n\tws_client->buflen = 0;\n\tws_client->bufpending = 0;\n\tws_client->bufoffset = 0;\n\tjanus_mutex_unlock(&ws_client->ts->mutex);\n\t/* Notify core */\n\tgateway->transport_gone(&janus_websockets_transport, ws_client->ts);\n\tjanus_transport_session_destroy(ws_client->ts);\n}\n\nint janus_websockets_get_api_compatibility(void) {\n\t/* Important! This is what your plugin MUST always return: don't lie here or bad things will happen */\n\treturn JANUS_TRANSPORT_API_VERSION;\n}\n\nint janus_websockets_get_version(void) {\n\treturn JANUS_WEBSOCKETS_VERSION;\n}\n\nconst char *janus_websockets_get_version_string(void) {\n\treturn JANUS_WEBSOCKETS_VERSION_STRING;\n}\n\nconst char *janus_websockets_get_description(void) {\n\treturn JANUS_WEBSOCKETS_DESCRIPTION;\n}\n\nconst char *janus_websockets_get_name(void) {\n\treturn JANUS_WEBSOCKETS_NAME;\n}\n\nconst char *janus_websockets_get_author(void) {\n\treturn JANUS_WEBSOCKETS_AUTHOR;\n}\n\nconst char *janus_websockets_get_package(void) {\n\treturn JANUS_WEBSOCKETS_PACKAGE;\n}\n\ngboolean janus_websockets_is_janus_api_enabled(void) {\n\treturn ws_janus_api_enabled;\n}\n\ngboolean janus_websockets_is_admin_api_enabled(void) {\n\treturn ws_admin_api_enabled;\n}\n\nint janus_websockets_send_message(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message) {\n\tif(message == NULL)\n\t\treturn -1;\n\tif(transport == NULL || g_atomic_int_get(&transport->destroyed)) {\n\t\tjson_decref(message);\n\t\treturn -1;\n\t}\n\tjanus_mutex_lock(&transport->mutex);\n\tjanus_websockets_client *client = (janus_websockets_client *)transport->transport_p;\n\tif(!client || !client->wsi || g_atomic_int_get(&client->destroyed)) {\n\t\tjson_decref(message);\n\t\tjanus_mutex_unlock(&transport->mutex);\n\t\treturn -1;\n\t}\n\t/* Convert to string and enqueue */\n\tchar *payload = json_dumps(message, json_format);\n\tif(payload == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Failed to stringify message...\\n\");\n\t\tjson_decref(message);\n\t\tjanus_mutex_unlock(&transport->mutex);\n\t\treturn -1;\n\t}\n\tg_async_queue_push(client->messages, payload);\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t/* On libwebsockets >= 3.x we use lws_cancel_service */\n\tjanus_mutex_lock(&writable_mutex);\n\tif(g_hash_table_lookup(clients, client) == client)\n\t\tg_hash_table_insert(writable_clients, client, client);\n\tjanus_mutex_unlock(&writable_mutex);\n\tlws_cancel_service(wsc);\n#else\n\t/* On libwebsockets < 3.x we use lws_callback_on_writable */\n\tjanus_mutex_lock(&writable_mutex);\n\tlws_callback_on_writable(client->wsi);\n\tjanus_mutex_unlock(&writable_mutex);\n#endif\n\tjanus_mutex_unlock(&transport->mutex);\n\tjson_decref(message);\n\treturn 0;\n}\n\nvoid janus_websockets_session_created(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care */\n}\n\nvoid janus_websockets_session_over(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed) {\n\t/* We don't care either: transport timeouts can be detected using the ping/pong mechanism */\n}\n\nvoid janus_websockets_session_claimed(janus_transport_session *transport, guint64 session_id) {\n\t/* We don't care about this. We should start receiving messages from the core about this session: no action necessary */\n\t/* FIXME Is the above statement accurate? Should we care? Unlike the HTTP transport, there is no hashtable to update */\n}\n\njson_t *janus_websockets_query_transport(json_t *request) {\n\tif(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) {\n\t\treturn NULL;\n\t}\n\t/* We can use this request to dynamically change the behaviour of\n\t * the transport plugin, and/or query for some specific information */\n\tjson_t *response = json_object();\n\tint error_code = 0;\n\tchar error_cause[512];\n\tJANUS_VALIDATE_JSON_OBJECT(request, request_parameters,\n\t\terror_code, error_cause, TRUE,\n\t\tJANUS_WEBSOCKETS_ERROR_MISSING_ELEMENT, JANUS_WEBSOCKETS_ERROR_INVALID_ELEMENT);\n\tif(error_code != 0)\n\t\tgoto plugin_response;\n\t/* Get the request */\n\tconst char *request_text = json_string_value(json_object_get(request, \"request\"));\n\tif(!strcasecmp(request_text, \"configure\")) {\n\t\t/* We only allow for the configuration of some basic properties:\n\t\t * changing more complex things (e.g., port to bind to, etc.)\n\t\t * would likely require restarting backends, so just too much */\n\t\tJANUS_VALIDATE_JSON_OBJECT(request, configure_parameters,\n\t\t\terror_code, error_cause, TRUE,\n\t\t\tJANUS_WEBSOCKETS_ERROR_MISSING_ELEMENT, JANUS_WEBSOCKETS_ERROR_INVALID_ELEMENT);\n\t\t/* Check if we now need to send events to handlers */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n\t\tjson_t *notes = NULL;\n\t\tgboolean events = json_is_true(json_object_get(request, \"events\"));\n\t\tif(events && !gateway->events_is_enabled()) {\n\t\t\t/* Notify that this will be ignored */\n\t\t\tnotes = json_array();\n\t\t\tjson_array_append_new(notes, json_string(\"Event handlers disabled at the core level\"));\n\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t}\n\t\tif(events != notify_events) {\n\t\t\tnotify_events = events;\n\t\t\tif(!notify_events && gateway->events_is_enabled()) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Notification of events to handlers disabled for %s\\n\", JANUS_WEBSOCKETS_NAME);\n\t\t\t}\n\t\t}\n\t\tconst char *indentation = json_string_value(json_object_get(request, \"json\"));\n\t\tif(indentation != NULL) {\n\t\t\tif(!strcasecmp(indentation, \"indented\")) {\n\t\t\t\t/* Default: indented, we use three spaces for that */\n\t\t\t\tjson_format = JSON_INDENT(3) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"plain\")) {\n\t\t\t\t/* Not indented and no new lines, but still readable */\n\t\t\t\tjson_format = JSON_INDENT(0) | JSON_PRESERVE_ORDER;\n\t\t\t} else if(!strcasecmp(indentation, \"compact\")) {\n\t\t\t\t/* Compact, so no spaces between separators */\n\t\t\t\tjson_format = JSON_COMPACT | JSON_PRESERVE_ORDER;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unsupported JSON format option '%s', ignoring tweak\\n\", indentation);\n\t\t\t\t/* Notify that this will be ignored */\n\t\t\t\tif(notes == NULL) {\n\t\t\t\t\tnotes = json_array();\n\t\t\t\t\tjson_object_set_new(response, \"notes\", notes);\n\t\t\t\t}\n\t\t\t\tjson_array_append_new(notes, json_string(\"Ignored unsupported indentation format\"));\n\t\t\t}\n\t\t}\n\t\tconst char *logging = json_string_value(json_object_get(request, \"logging\"));\n\t\tif(logging != NULL) {\n\t\t\t/* libwebsockets uses a mask to set log levels, as documented here:\n\t\t\t * https://libwebsockets.org/lws-api-doc-master/html/group__log.html */\n\t\t\tif(strstr(logging, \"none\")) {\n\t\t\t\t/* Disable libwebsockets logging completely (the default) */\n\t\t\t} else if(strstr(logging, \"all\")) {\n\t\t\t\t/* Enable all libwebsockets logging */\n\t\t\t\tws_log_level = LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO |\n\t\t\t\t\tLLL_DEBUG | LLL_PARSER | LLL_HEADER | LLL_EXT |\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\t\t\tLLL_CLIENT | LLL_LATENCY | LLL_USER | LLL_COUNT;\n#else\n\t\t\t\t\tLLL_CLIENT | LLL_LATENCY | LLL_COUNT;\n#endif\n\t\t\t} else {\n\t\t\t\t/* Only enable some of the properties */\n\t\t\t\tws_log_level = 0;\n\t\t\t\tif(strstr(logging, \"err\"))\n\t\t\t\t\tws_log_level |= LLL_ERR;\n\t\t\t\tif(strstr(logging, \"warn\"))\n\t\t\t\t\tws_log_level |= LLL_WARN;\n\t\t\t\tif(strstr(logging, \"notice\"))\n\t\t\t\t\tws_log_level |= LLL_NOTICE;\n\t\t\t\tif(strstr(logging, \"info\"))\n\t\t\t\t\tws_log_level |= LLL_INFO;\n\t\t\t\tif(strstr(logging, \"debug\"))\n\t\t\t\t\tws_log_level |= LLL_DEBUG;\n\t\t\t\tif(strstr(logging, \"parser\"))\n\t\t\t\t\tws_log_level |= LLL_PARSER;\n\t\t\t\tif(strstr(logging, \"header\"))\n\t\t\t\t\tws_log_level |= LLL_HEADER;\n\t\t\t\tif(strstr(logging, \"ext\"))\n\t\t\t\t\tws_log_level |= LLL_EXT;\n\t\t\t\tif(strstr(logging, \"client\"))\n\t\t\t\t\tws_log_level |= LLL_CLIENT;\n\t\t\t\tif(strstr(logging, \"latency\"))\n\t\t\t\t\tws_log_level |= LLL_LATENCY;\n#if (LWS_LIBRARY_VERSION_MAJOR >= 2 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\t\tif(strstr(logging, \"user\"))\n\t\t\t\t\tws_log_level |= LLL_USER;\n#endif\n\t\t\t\tif(strstr(logging, \"count\"))\n\t\t\t\t\tws_log_level |= LLL_COUNT;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_INFO, \"libwebsockets logging: %d\\n\", ws_log_level);\n\t\t\tlws_set_log_level(ws_log_level, janus_websockets_log_emit_function);\n\t\t}\n\t} else if(!strcasecmp(request_text, \"connections\")) {\n\t\t/* Return the number of active connections currently handled by the plugin */\n\t\tjson_object_set_new(response, \"result\", json_integer(200));\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\tjanus_mutex_lock(&writable_mutex);\n\t\tguint connections = g_hash_table_size(clients);\n\t\tjanus_mutex_unlock(&writable_mutex);\n\t\tjson_object_set_new(response, \"connections\", json_integer(connections));\n#endif\n\t} else {\n\t\tJANUS_LOG(LOG_VERB, \"Unknown request '%s'\\n\", request_text);\n\t\terror_code = JANUS_WEBSOCKETS_ERROR_INVALID_REQUEST;\n\t\tg_snprintf(error_cause, 512, \"Unknown request '%s'\", request_text);\n\t}\n\nplugin_response:\n\t\t{\n\t\t\tif(error_code != 0) {\n\t\t\t\t/* Prepare JSON error event */\n\t\t\t\tjson_object_set_new(response, \"error_code\", json_integer(error_code));\n\t\t\t\tjson_object_set_new(response, \"error\", json_string(error_cause));\n\t\t\t}\n\t\t\treturn response;\n\t\t}\n}\n\n\n/* Thread */\nvoid *janus_websockets_thread(void *data) {\n\tstruct lws_context *service = (struct lws_context *)data;\n\tif(service == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid service\\n\");\n\t\treturn NULL;\n\t}\n\n\tJANUS_LOG(LOG_INFO, \"WebSockets thread started\\n\");\n\n\twhile(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {\n\t\t/* libwebsockets is single thread, we cycle through events here */\n\t\tlws_service(service, 50);\n\t}\n\n\t/* Get rid of the WebSockets server */\n\tlws_cancel_service(service);\n\t/* Done */\n\tJANUS_LOG(LOG_INFO, \"WebSockets thread ended\\n\");\n\treturn NULL;\n}\n\n\n/* WebSockets */\nstatic int janus_websockets_callback_http(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len)\n{\n\t/* This endpoint cannot be used for HTTP */\n\tswitch(reason) {\n\t\tcase LWS_CALLBACK_HTTP:\n\t\t\tJANUS_LOG(LOG_VERB, \"Rejecting incoming HTTP request on WebSockets endpoint\\n\");\n\t\t\tlws_return_http_status(wsi, 403, NULL);\n\t\t\t/* Close and free connection */\n\t\t\treturn -1;\n\t\tcase LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:\n\t\t\tif (!in) {\n\t\t\t\tJANUS_LOG(LOG_VERB, \"Rejecting incoming HTTP request on WebSockets endpoint: no sub-protocol specified\\n\");\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tbreak;\n\t\tcase LWS_CALLBACK_GET_THREAD_ID:\n\t\t\treturn (uint64_t)pthread_self();\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn 0;\n}\n\nstatic int janus_websockets_callback_https(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len)\n{\n\t/* We just forward the event to the HTTP handler */\n\treturn janus_websockets_callback_http(wsi, reason, user, in, len);\n}\n\n/* Use ~ 2xMTU as chunk size */\n#define MESSAGE_CHUNK_SIZE 2800\n\n/* This callback handles Janus API requests */\nstatic int janus_websockets_common_callback(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len, gboolean admin)\n{\n\tconst char *log_prefix = admin ? \"AdminWSS\" : \"WSS\";\n\tjanus_websockets_client *ws_client = (janus_websockets_client *)user;\n\tswitch(reason) {\n\t\tcase LWS_CALLBACK_ESTABLISHED: {\n\t\t\t/* Is there any filtering we should apply? */\n\t\t\tchar ip[256];\n#ifdef HAVE_LIBWEBSOCKETS_PEER_SIMPLE\n\t\t\tlws_get_peer_simple(wsi, ip, 256);\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p] WebSocket connection opened from %s\\n\", log_prefix, wsi, ip);\n#else\n\t\t\tchar name[256];\n\t\t\tlws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), name, 256, ip, 256);\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p] WebSocket connection opened from %s by %s\\n\", log_prefix, wsi, ip, name);\n#endif\n\t\t\tif(!janus_websockets_is_allowed(ip, admin)) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s-%p] IP %s is unauthorized to connect to the WebSockets %s API interface\\n\", log_prefix, wsi, ip, admin ? \"Admin\" : \"Janus\");\n\t\t\t\t/* Close the connection */\n\t\t\t\tlws_callback_on_writable(wsi);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\t/* Check if an X-Forwarded-For header was provided */\n\t\t\tchar xff[1024] = {0};\n\t\t\tif(lws_hdr_copy(wsi, xff, 1023, WSI_TOKEN_X_FORWARDED_FOR) > 0) {\n\t\t\t\t/* If the ACL is enabled, are we supposed to use this header too for checks? */\n\t\t\t\tif(((!admin && janus_websockets_check_xff) || (admin && janus_websockets_admin_check_xff)) && !janus_websockets_is_allowed(xff, admin)) {\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s-%p] IP %s is unauthorized to connect to the WebSockets %s API interface\\n\",\n\t\t\t\t\t\tlog_prefix, wsi, xff, admin ? \"Admin\" : \"Janus\");\n\t\t\t\t\t/* Close the connection */\n\t\t\t\t\tlws_callback_on_writable(wsi);\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p] WebSocket connection accepted\\n\", log_prefix, wsi);\n\t\t\tif(ws_client == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s-%p] Invalid WebSocket client instance...\\n\", log_prefix, wsi);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\t/* Prepare the session */\n\t\t\tws_client->wsi = wsi;\n\t\t\tws_client->messages = g_async_queue_new();\n\t\t\tws_client->buffer = NULL;\n\t\t\tws_client->buflen = 0;\n\t\t\tws_client->bufpending = 0;\n\t\t\tws_client->bufoffset = 0;\n\t\t\tg_atomic_int_set(&ws_client->destroyed, 0);\n\t\t\tws_client->ts = janus_transport_session_create(ws_client, NULL);\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t\tjanus_mutex_lock(&writable_mutex);\n\t\t\tg_hash_table_insert(clients, ws_client, ws_client);\n\t\t\tjanus_mutex_unlock(&writable_mutex);\n#endif\n\t\t\t/* Let us know when the WebSocket channel becomes writeable */\n\t\t\tlws_callback_on_writable(wsi);\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p]   -- Ready to be used!\\n\", log_prefix, wsi);\n\t\t\t/* Notify handlers about this new transport */\n\t\t\tif(notify_events && gateway->events_is_enabled()) {\n\t\t\t\tjson_t *info = json_object();\n\t\t\t\tjson_object_set_new(info, \"event\", json_string(\"connected\"));\n\t\t\t\tjson_object_set_new(info, \"admin_api\", admin ? json_true() : json_false());\n\t\t\t\tjson_object_set_new(info, \"ip\", json_string(ip));\n\t\t\t\tgateway->notify_event(&janus_websockets_transport, ws_client->ts, info);\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t\tcase LWS_CALLBACK_ADD_HEADERS: {\n\t\t\t/* If CORS is enabled, check the headers and add our own */\n\t\t\tstruct lws_process_html_args *args = (struct lws_process_html_args *)in;\n\t\t\tif(allow_origin == NULL) {\n\t\t\t\t/* Return a wildcard for the Access-Control-Allow-Origin header */\n\t\t\t\tif(lws_add_http_header_by_name(wsi,\n\t\t\t\t\t\t(unsigned char *)\"Access-Control-Allow-Origin:\",\n\t\t\t\t\t\t(unsigned char *)\"*\", 1,\n\t\t\t\t\t\t(unsigned char **)&args->p,\n\t\t\t\t\t\t(unsigned char *)args->p + args->max_len))\n\t\t\t\t\treturn 1;\n\t\t\t} else {\n\t\t\t\t/* Return the configured origin in the header */\n\t\t\t\tif(lws_add_http_header_by_name(wsi,\n\t\t\t\t\t\t(unsigned char *)\"Access-Control-Allow-Origin:\",\n\t\t\t\t\t\t(unsigned char *)allow_origin, strlen(allow_origin),\n\t\t\t\t\t\t(unsigned char **)&args->p,\n\t\t\t\t\t\t(unsigned char *)args->p + args->max_len))\n\t\t\t\t\treturn 1;\n\t\t\t\tchar origin[256], headers[256], methods[256];\n\t\t\t\torigin[0] = '\\0';\n\t\t\t\theaders[0] = '\\0';\n\t\t\t\tmethods[0] = '\\0';\n\t\t\t\tint olen = lws_hdr_total_length(wsi, WSI_TOKEN_ORIGIN);\n\t\t\t\tif(olen > 0 && olen < 255) {\n\t\t\t\t\tlws_hdr_copy(wsi, origin, sizeof(origin), WSI_TOKEN_ORIGIN);\n\t\t\t\t}\n\t\t\t\tint hlen = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_AC_REQUEST_HEADERS);\n\t\t\t\tif(hlen > 0 && hlen < 255) {\n\t\t\t\t\tlws_hdr_copy(wsi, headers, sizeof(headers), WSI_TOKEN_HTTP_AC_REQUEST_HEADERS);\n\t\t\t\t\tif(lws_add_http_header_by_name(wsi,\n\t\t\t\t\t\t\t(unsigned char *)\"Access-Control-Allow-Headers:\",\n\t\t\t\t\t\t\t(unsigned char *)headers, strlen(headers),\n\t\t\t\t\t\t\t(unsigned char **)&args->p,\n\t\t\t\t\t\t\t(unsigned char *)args->p + args->max_len))\n\t\t\t\t\t\treturn 1;\n\t\t\t\t}\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3 && LWS_LIBRARY_VERSION_MINOR >= 2) || (LWS_LIBRARY_VERSION_MAJOR >= 4)\n\t\t\t\tint mlen = lws_hdr_custom_length(wsi, \"Access-Control-Request-Methods\", strlen(\"Access-Control-Request-Methods\"));\n\t\t\t\tif(mlen > 0 && mlen < 255) {\n\t\t\t\t\tlws_hdr_custom_copy(wsi, methods, sizeof(methods),\n\t\t\t\t\t\t\"Access-Control-Request-Methods\", strlen(\"Access-Control-Request-Methods\"));\n\t\t\t\t\tif(lws_add_http_header_by_name(wsi,\n\t\t\t\t\t\t\t(unsigned char *)\"Access-Control-Allow-Methods:\",\n\t\t\t\t\t\t\t(unsigned char *)methods, strlen(methods),\n\t\t\t\t\t\t\t(unsigned char **)&args->p,\n\t\t\t\t\t\t\t(unsigned char *)args->p + args->max_len))\n\t\t\t\t\t\treturn 1;\n\t\t\t\t}\n#endif\n\t\t\t\t/* WebSockets are not bound by CORS, but we can enforce this */\n\t\t\t\tif(enforce_cors) {\n\t\t\t\t\tif(strlen(origin) == 0 || strstr(origin, allow_origin) != origin) {\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s-%p] Invalid origin, rejecting...\\n\", log_prefix, wsi);\n\t\t\t\t\t\treturn -1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t\tcase LWS_CALLBACK_RECEIVE: {\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Got %zu bytes:\\n\", log_prefix, wsi, len);\n\t\t\tif(ws_client == NULL || ws_client->wsi == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s-%p] Invalid WebSocket client instance...\\n\", log_prefix, wsi);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif(g_atomic_int_get(&ws_client->destroyed))\n\t\t\t\treturn 0;\n#if (LWS_LIBRARY_VERSION_MAJOR >= 4)\n\t\t\t/* Refresh the lws connection validity (avoid sending a ping) */\n\t\t\tlws_validity_confirmed(ws_client->wsi);\n#endif\n\t\t\tsize_t incoming_length;\n\t\t\t/* Is this a new message, or part of a fragmented one? */\n\t\t\tconst size_t remaining = lws_remaining_packet_payload(wsi);\n\t\t\tif(ws_client->incoming == NULL) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] First fragment: %zu bytes, %zu remaining\\n\", log_prefix, wsi, len, remaining);\n\t\t\t\tws_client->incoming = g_malloc(len+1);\n\t\t\t\tmemcpy(ws_client->incoming, in, len);\n\t\t\t\tincoming_length = len;\n\t\t\t\tws_client->incoming[incoming_length] = '\\0';\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"%s\\n\", ws_client->incoming);\n\t\t\t} else {\n\t\t\t\tsize_t offset = strlen(ws_client->incoming);\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Appending fragment: offset %zu, %zu bytes, %zu remaining\\n\", log_prefix, wsi, offset, len, remaining);\n\t\t\t\tws_client->incoming = g_realloc(ws_client->incoming, offset+len+1);\n\t\t\t\tmemcpy(ws_client->incoming+offset, in, len);\n\t\t\t\tincoming_length = offset+len;\n\t\t\t\tws_client->incoming[incoming_length] = '\\0';\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"%s\\n\", ws_client->incoming+offset);\n\t\t\t}\n\t\t\tif(remaining > 0 || !lws_is_final_fragment(wsi)) {\n\t\t\t\t/* Still waiting for some more fragments */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Waiting for more fragments\\n\", log_prefix, wsi);\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Done, parsing message: %zu bytes\\n\", log_prefix, wsi, incoming_length);\n\t\t\t/* If we got here, the message is complete: parse the JSON payload */\n\t\t\tconst char *incoming_curr = ws_client->incoming;\n\t\t\tconst char *incoming_end = ws_client->incoming + incoming_length;\n\t\t\tint message_buffer_count = 0;\n\t\t\tjson_t **message_buffer = NULL;\n\t\t\t/* Load all JSON messages from the websocket incoming */\n\t\t\tdo {\n\t\t\t\tjson_error_t error;\n\t\t\t\tjson_t *message = json_loads(incoming_curr, JSON_DISABLE_EOF_CHECK, &error);\n\t\t\t\tif(message != NULL) {\n\t\t\t\t\t/* Position is set to bytes read on success when EOF_CHECK is disabled as above. */\n\t\t\t\t\tincoming_curr += error.position;\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Parsed JSON message - consumed %zu/%zu bytes\\n\",\n\t\t\t\t\t\tlog_prefix, wsi, (size_t)(incoming_curr - ws_client->incoming), incoming_length);\n\t\t\t\t\t/* Trailing whitespace after the last message results in invalid JSON error */\n\t\t\t\t\twhile (incoming_curr < incoming_end && isspace(*incoming_curr))\n\t\t\t\t\t\tincoming_curr++;\n\t\t\t\t\tif(incoming_curr == incoming_end) {\n\t\t\t\t\t\tif(message_buffer != NULL) {\n\t\t\t\t\t\t\t/* Process messages in order */\n\t\t\t\t\t\t\tjson_t **msg = message_buffer;\n\t\t\t\t\t\t\tjson_t **msg_end = message_buffer + message_buffer_count;\n\t\t\t\t\t\t\twhile(msg != msg_end) {\n\t\t\t\t\t\t\t\t/* Notify the core, no error since we know there weren't any */\n\t\t\t\t\t\t\t\tgateway->incoming_request(&janus_websockets_transport, ws_client->ts, NULL, admin, *msg++, NULL);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/* Notify the core, no error since we know there weren't any */\n\t\t\t\t\t\tgateway->incoming_request(&janus_websockets_transport, ws_client->ts, NULL, admin, message, NULL);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t/* Buffer the message */\n\t\t\t\t\t\tmessage_buffer = (json_t**)g_realloc(message_buffer, sizeof(json_t*) * (message_buffer_count + 1));\n\t\t\t\t\t\tmessage_buffer[message_buffer_count++] = message;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif(message_buffer != NULL) {\n\t\t\t\t\t\t/* Release any buffered messages */\n\t\t\t\t\t\tjson_t **msg = message_buffer;\n\t\t\t\t\t\tjson_t **msg_end = message_buffer + message_buffer_count;\n\t\t\t\t\t\twhile(msg != msg_end) {\n\t\t\t\t\t\t\tjson_decref(*msg++);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t/* Notify the core, passing the error since we have no message */\n\t\t\t\t\tgateway->incoming_request(&janus_websockets_transport, ws_client->ts, NULL, admin, NULL, &error);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} while(incoming_curr < incoming_end);\n\t\t\tg_free(message_buffer);\n\t\t\tg_free(ws_client->incoming);\n\t\t\tws_client->incoming = NULL;\n\t\t\treturn 0;\n\t\t}\n#if (LWS_LIBRARY_VERSION_MAJOR >= 3)\n\t\t/* On libwebsockets >= 3.x, we use this event to mark connections as writable in the event loop */\n\t\tcase LWS_CALLBACK_EVENT_WAIT_CANCELLED: {\n\t\t\tjanus_mutex_lock(&writable_mutex);\n\t\t\t/* We iterate on all the clients we marked as writable and act on them */\n\t\t\tGHashTableIter iter;\n\t\t\tgpointer value;\n\t\t\tg_hash_table_iter_init(&iter, writable_clients);\n\t\t\twhile(g_hash_table_iter_next(&iter, NULL, &value)) {\n\t\t\t\tjanus_websockets_client *client = value;\n\t\t\t\tif(client == NULL || client->wsi == NULL)\n\t\t\t\t\tcontinue;\n\t\t\t\tlws_callback_on_writable(client->wsi);\n\t\t\t}\n\t\t\tg_hash_table_remove_all(writable_clients);\n\t\t\tjanus_mutex_unlock(&writable_mutex);\n\t\t\treturn 0;\n\t\t}\n#endif\n\t\tcase LWS_CALLBACK_SERVER_WRITEABLE: {\n\t\t\tif(ws_client == NULL || ws_client->wsi == NULL) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"[%s-%p] Invalid WebSocket client instance...\\n\", log_prefix, wsi);\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif(!g_atomic_int_get(&ws_client->destroyed) && !g_atomic_int_get(&stopping)) {\n\t\t\t\tjanus_mutex_lock(&ws_client->ts->mutex);\n\n\t\t\t\t/* Check if Websockets send pipe is choked */\n\t\t\t\tif(lws_send_pipe_choked(wsi)) {\n\t\t\t\t\tif(ws_client->buffer && ws_client->bufpending > 0 && ws_client->bufoffset > 0) {\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Websockets choked with buffer: %zu, trying again\\n\", ws_client->bufpending);\n\t\t\t\t\t\tlws_callback_on_writable(wsi);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tgint qlen = g_async_queue_length(ws_client->messages);\n\t\t\t\t\t\tJANUS_LOG(LOG_WARN, \"Websockets choked with queue: %d, trying again\\n\", qlen);\n\t\t\t\t\t\tif(qlen > 0) {\n\t\t\t\t\t\t\tlws_callback_on_writable(wsi);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjanus_mutex_unlock(&ws_client->ts->mutex);\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\t/* Check if we have a pending/partial write to complete first */\n\t\t\t\tif(ws_client->buffer && ws_client->bufpending > 0 && ws_client->bufoffset > 0) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Completing pending WebSocket write (still need to write last %zu bytes)...\\n\",\n\t\t\t\t\t\tlog_prefix, wsi, ws_client->bufpending);\n\t\t\t\t} else {\n\t\t\t\t\t/* Shoot all the pending messages */\n\t\t\t\t\tchar *response = g_async_queue_try_pop(ws_client->messages);\n\t\t\t\t\tif (!response) {\n\t\t\t\t\t\t/* No messages found */\n\t\t\t\t\t\tjanus_mutex_unlock(&ws_client->ts->mutex);\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\tif (g_atomic_int_get(&ws_client->destroyed) || g_atomic_int_get(&stopping)) {\n\t\t\t\t\t\tfree(response);\n\t\t\t\t\t\tjanus_mutex_unlock(&ws_client->ts->mutex);\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\t/* Gotcha! */\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Sending WebSocket message (%zu bytes)...\\n\", log_prefix, wsi, strlen(response));\n\t\t\t\t\tsize_t buflen = LWS_PRE + strlen(response);\n\t\t\t\t\tif (buflen > ws_client->buflen) {\n\t\t\t\t\t\t/* We need a larger shared buffer */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] Re-allocating to %zu bytes (was %zu, response is %zu bytes)\\n\", log_prefix, wsi, buflen, ws_client->buflen, strlen(response));\n\t\t\t\t\t\tws_client->buflen = buflen;\n\t\t\t\t\t\tws_client->buffer = g_realloc(ws_client->buffer, buflen);\n\t\t\t\t\t}\n\t\t\t\t\tmemcpy(ws_client->buffer + LWS_PRE, response, strlen(response));\n\t\t\t\t\t/* Initialize pending bytes count and buffer offset */\n\t\t\t\t\tws_client->bufpending = strlen(response);\n\t\t\t\t\tws_client->bufoffset = LWS_PRE;\n\t\t\t\t\t/* We can get rid of the message */\n\t\t\t\t\tfree(response);\n\t\t\t\t}\n\n\t\t\t\tif (g_atomic_int_get(&ws_client->destroyed) || g_atomic_int_get(&stopping)) {\n\t\t\t\t\tjanus_mutex_unlock(&ws_client->ts->mutex);\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\t/* Evaluate amount of data to send according to MESSAGE_CHUNK_SIZE */\n\t\t\t\tint amount = ws_client->bufpending <= MESSAGE_CHUNK_SIZE ? ws_client->bufpending : MESSAGE_CHUNK_SIZE;\n\t\t\t\t/* Set fragment flags */\n\t\t\t\tint flags = lws_write_ws_flags(LWS_WRITE_TEXT, ws_client->bufoffset == LWS_PRE, ws_client->bufpending <= (size_t)amount);\n\t\t\t\t/* Send the fragment with proper flags */\n\t\t\t\tint sent = lws_write(wsi, ws_client->buffer + ws_client->bufoffset, (size_t)amount, flags);\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p]   -- First=%d, Last=%d, Requested=%d bytes, Sent=%d bytes, Missing=%zu bytes\\n\", log_prefix, wsi, ws_client->bufoffset <= LWS_PRE, ws_client->bufpending <= (size_t)amount, amount, sent, ws_client->bufpending - amount);\n\t\t\t\tif(sent < amount) {\n\t\t\t\t\t/* Error on sending, abort operation */\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Websocket sent only %d bytes (expected %d)\\n\", sent, amount);\n\t\t\t\t\tws_client->bufpending = 0;\n\t\t\t\t\tws_client->bufoffset = 0;\n\t\t\t\t} else {\n\t\t\t\t\t/* Fragment successfully sent, update status */\n\t\t\t\t\tws_client->bufpending -= amount;\n\t\t\t\t\tws_client->bufoffset += amount;\n\t\t\t\t\tif(ws_client->bufpending > 0) {\n\t\t\t\t\t\t/* We couldn't send everything in a single write, we'll complete this in the next round */\n\t\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p]   -- Couldn't write all bytes (%zu missing), setting offset %zu\\n\",\n\t\t\t\t\t\t\tlog_prefix, wsi, ws_client->bufpending, ws_client->bufoffset);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t/* Done for this round, check the next response/notification later */\n\t\t\t\tlws_callback_on_writable(wsi);\n\t\t\t\tjanus_mutex_unlock(&ws_client->ts->mutex);\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t\tcase LWS_CALLBACK_CLOSED: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p] WS connection down, closing\\n\", log_prefix, wsi);\n\t\t\tjanus_websockets_destroy_client(ws_client, wsi, log_prefix);\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p]   -- closed\\n\", log_prefix, wsi);\n\t\t\treturn 0;\n\t\t}\n\t\tcase LWS_CALLBACK_WSI_DESTROY: {\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p] WS connection down, destroying\\n\", log_prefix, wsi);\n\t\t\tjanus_websockets_destroy_client(ws_client, wsi, log_prefix);\n\t\t\tJANUS_LOG(LOG_VERB, \"[%s-%p]   -- destroyed\\n\", log_prefix, wsi);\n\t\t\treturn 0;\n\t\t}\n\t\tdefault:\n\t\t\tif(wsi != NULL) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s-%p] %d (%s)\\n\", log_prefix, wsi, reason, janus_websockets_reason_string(reason));\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"[%s] %d (%s)\\n\", log_prefix, reason, janus_websockets_reason_string(reason));\n\t\t\t}\n\t\t\tbreak;\n\t}\n\treturn 0;\n}\n\n/* This callback handles Janus API requests */\nstatic int janus_websockets_callback(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len)\n{\n\treturn janus_websockets_common_callback(wsi, reason, user, in, len, FALSE);\n}\n\nstatic int janus_websockets_callback_secure(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len)\n{\n\t/* We just forward the event to the Janus API handler */\n\treturn janus_websockets_callback(wsi, reason, user, in, len);\n}\n\n/* This callback handles Admin API requests */\nstatic int janus_websockets_admin_callback(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len)\n{\n\treturn janus_websockets_common_callback(wsi, reason, user, in, len, TRUE);\n}\n\nstatic int janus_websockets_admin_callback_secure(\n\t\tstruct lws *wsi,\n\t\tenum lws_callback_reasons reason,\n\t\tvoid *user, void *in, size_t len)\n{\n\t/* We just forward the event to the Admin API handler */\n\treturn janus_websockets_admin_callback(wsi, reason, user, in, len);\n}\n"
  },
  {
    "path": "src/transports/transport.c",
    "content": "/*! \\file   transport.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Modular Janus API transports\n * \\details  This header contains the definition of the callbacks both\n * the gateway and all the transports need too implement to interact with\n * each other. The structures to make the communication possible are\n * defined here as well.\n *\n * \\ingroup transportapi\n * \\ref transportapi\n */\n\n#include \"transport.h\"\n\nstatic void janus_transport_session_free(const janus_refcount *transport_ref) {\n\tjanus_transport_session *session = janus_refcount_containerof(transport_ref, janus_transport_session, ref);\n\t/* This session can be destroyed, free all the resources */\n\tif(session->p_free)\n\t\tsession->p_free(session->transport_p);\n\tjanus_mutex_destroy(&session->mutex);\n\tg_free(session);\n}\n\njanus_transport_session *janus_transport_session_create(void *transport_p, void (*p_free)(void *)) {\n\tjanus_transport_session *tp = g_malloc0(sizeof(janus_transport_session));\n\tif(tp == NULL)\n\t\treturn NULL;\n\ttp->transport_p = transport_p;\n\ttp->p_free = p_free;\n\tg_atomic_int_set(&tp->destroyed, 0);\n\tjanus_refcount_init(&tp->ref, janus_transport_session_free);\n\tjanus_mutex_init(&tp->mutex);\n\treturn tp;\n}\n\nvoid janus_transport_session_destroy(janus_transport_session *session) {\n\tif(session && g_atomic_int_compare_and_exchange(&session->destroyed, 0, 1))\n\t\tjanus_refcount_decrease(&session->ref);\n}\n"
  },
  {
    "path": "src/transports/transport.h",
    "content": "/*! \\file   transport.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Modular Janus API transports (headers)\n * \\details  This header contains the definition of the callbacks both\n * the Janus core and all the transports need to implement to interact with\n * each other. The structures to make the communication possible are\n * defined here as well.\n *\n * In particular, the Janus core implements the \\c janus_transport_callbacks\n * interface. This means that, as a transport plugin, you can use the\n * methods it exposes to contact the core, e.g., in order to notify\n * an incoming message. In particular, the methods the core exposes\n * to transport plugins are:\n *\n * - \\c incoming_request(): to notify an incoming JSON message/event\n * from one of the transport clients.\n *\n * On the other hand, a transport plugin that wants to register at the\n * Janus core needs to implement the \\c janus_transport interface. Besides,\n * as a transport plugin is a shared object, and as such external to the\n * core itself, in order to be dynamically loaded at startup it needs\n * to implement the \\c create_t() hook as well, that should return a\n * pointer to the plugin instance. This is an example of such a step:\n *\n\\verbatim\nstatic janus_transport mytransport = {\n\t[..]\n};\n\njanus_transport *create(void) {\n\tJANUS_LOG(LOG_VERB, , \"%s created!\\n\", MY_TRANSPORT_NAME);\n\treturn &mytransport;\n}\n\\endverbatim\n *\n * This will make sure that your transport plugin is loaded at startup\n * by the Janus core, if it is deployed in the proper folder.\n *\n * As anticipated and described in the above example, a transport plugin\n * must basically be an instance of the \\c janus_transport type. As such,\n * it must implement the following methods and callbacks for the core:\n *\n * - \\c init(): this is called by the Janus core as soon as your transport\n * plugin is started; this is where you should setup your transport plugin\n * (e.g., static stuff and reading the configuration file);\n * - \\c destroy(): on the other hand, this is called by the core when it\n * is shutting down, and your transport plugin should too;\n * - \\c get_api_compatibility(): this method MUST return JANUS_TRANSPORT_API_VERSION;\n * - \\c get_version(): this method should return a numeric version identifier (e.g., 3);\n * - \\c get_version_string(): this method should return a verbose version identifier (e.g., \"v1.0.1\");\n * - \\c get_description(): this method should return a verbose description of your transport plugin (e.g., \"This is my avian carrier transport plugin for the Janus API\");\n * - \\c get_name(): this method should return a short display name for your transport plugin (e.g., \"My Amazing Transport\");\n * - \\c get_package(): this method should return a unique package identifier for your transport plugin (e.g., \"janus.transport.mytransport\");\n * - \\c is_janus_api_enabled(): this method should return TRUE if Janus API can be used with this transport, and support has been enabled by the user;\n * - \\c is_admin_api_enabled(): this method should return TRUE if Admin API can be used with this transport, and support has been enabled by the user;\n * - \\c send_message(): this method asks the transport to send a message (be it a response or an event) to a client on the specified transport;\n * - \\c session_created(): this method notifies the transport that a Janus session has been created by one of its requests;\n * - \\c session_over(): this method notifies the transport that one of its Janus sessionss is now over, whether because of a timeout or not.\n * - \\c session_claimed(): this method notifies the transport that it has claimed a session.\n *\n * All the above methods and callbacks are mandatory: the Janus core will\n * reject a transport plugin that doesn't implement any of the\n * mandatory callbacks.\n *\n * The Janus core \\c janus_transport_callbacks interface is provided to a\n * transport plugin, together with the path to the configurations files\n * folder, in the \\c init() method. This path can be used to read and\n * parse a configuration file for the transport plugin: the transport\n * plugins we made available out of the box use the package name as a\n * name for the file (e.g., \\c janus.transport.http.cfg for the HTTP/HTTPS\n * transport plugin), but you're free to use a different one, as long\n * as it doesn't collide with existing ones. Besides, the existing transport\n * plugins use the same INI format for configuration files the core\n * uses (relying on the \\c janus_config helpers for the purpose) but\n * again, if you prefer a different format (XML, JSON, etc.) that's up to you.\n *\n * \\ingroup transportapi\n * \\ref transportapi\n */\n\n#ifndef JANUS_TRANSPORT_H\n#define JANUS_TRANSPORT_H\n\n#include <stdlib.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <string.h>\n#include <ctype.h>\n#include <unistd.h>\n#include <inttypes.h>\n\n#include <glib.h>\n#include <jansson.h>\n\n#include \"../refcount.h\"\n\n\n/*! \\brief Version of the API, to match the one transport plugins were compiled against */\n#define JANUS_TRANSPORT_API_VERSION\t\t8\n\n/*! \\brief Initialization of all transport plugin properties to NULL\n *\n * \\note All transport plugins MUST add this as the FIRST line when initializing\n * their transport plugin structure, e.g.:\n *\n\\verbatim\nstatic janus_transport janus_http_transport_plugin =\n\t{\n\t\tJANUS_TRANSPORT_INIT,\n\n\t\t.init = janus_http_init,\n\t\t[..]\n\\endverbatim\n * */\n#define JANUS_TRANSPORT_INIT(...) {\t\t\\\n\t\t.init = NULL,\t\t\t\t\t\\\n\t\t.destroy = NULL,\t\t\t\t\\\n\t\t.get_api_compatibility = NULL,\t\\\n\t\t.get_version = NULL,\t\t\t\\\n\t\t.get_version_string = NULL,\t\t\\\n\t\t.get_description = NULL,\t\t\\\n\t\t.get_name = NULL,\t\t\t\t\\\n\t\t.get_author = NULL,\t\t\t\t\\\n\t\t.get_package = NULL,\t\t\t\\\n\t\t.is_janus_api_enabled = NULL,\t\\\n\t\t.is_admin_api_enabled = NULL,\t\\\n\t\t.send_message = NULL,\t\t\t\\\n\t\t.session_created = NULL,\t\t\\\n\t\t.session_over = NULL,\t\t\t\\\n\t\t.session_claimed = NULL,\t\t\\\n\t\t.query_transport = NULL,\t\t\\\n\t\t## __VA_ARGS__ }\n\n\n/*! \\brief Callbacks to contact the Janus core */\ntypedef struct janus_transport_callbacks janus_transport_callbacks;\n/*! \\brief The transport plugin session and callbacks interface */\ntypedef struct janus_transport janus_transport;\n/*! \\brief Transport-Gateway session mapping */\ntypedef struct janus_transport_session janus_transport_session;\n\n\n/*! \\brief Transport-Gateway session mapping */\nstruct janus_transport_session {\n\t/*! \\brief Opaque pointer to the transport session */\n\tvoid *transport_p;\n\t/*! \\brief Pointer to the transport-provided function, if needed, that will be used to free the opaque transport session instance */\n\tvoid (*p_free)(void *);\n\t/*! \\brief Whether this mapping has been destroyed definitely or not: if so,\n\t * the transport shouldn't make use of it anymore */\n\tvolatile gint destroyed;\n\t/*! \\brief Mutex to protect changes to transport_p */\n\tjanus_mutex mutex;\n\t/*! \\brief Reference counter for this instance */\n\tjanus_refcount ref;\n};\n/*! \\brief Helper to create a janus_transport_session instance\n * @note This helper automatically initializes the reference counter\n * @param transport_p Pointer to the transport-side session instance (won't be touched by the core)\n * @param p_free Pointer to the transport-provided function, if needed, that will be used to free the opaque transport-side session instance (won't be touched by the core)\n * @returns Pointer to a valid janus_transport_session, if successful, NULL otherwise */\njanus_transport_session *janus_transport_session_create(void *transport_p, void (*p_free)(void *));\n/*! \\brief Helper to mark a janus_transport_session instance as destroyed\n * @note Only use this helper when that specific transport session must not be\n * used by the core anymore: e.g., a WebSocket connection was closed, an\n * HTTP connection associated with a pending request was lost, etc. Remember\n * to decrease the counter in case you increased it in other methods (this\n * method does this automatically as far as the create was concerned).\n * @param session Pointer to the janus_transport_session instance */\nvoid janus_transport_session_destroy(janus_transport_session *session);\n\n\n/*! \\brief The transport plugin session and callbacks interface */\nstruct janus_transport {\n\t/*! \\brief Transport plugin initialization/constructor\n\t * @param[in] callback The callback instance the transport plugin can use to contact the Janus core\n\t * @param[in] config_path Path of the folder where the configuration for this transport plugin can be found\n\t * @returns 0 in case of success, a negative integer in case of error */\n\tint (* const init)(janus_transport_callbacks *callback, const char *config_path);\n\t/*! \\brief Transport plugin deinitialization/destructor */\n\tvoid (* const destroy)(void);\n\n\t/*! \\brief Informative method to request the API version this transport plugin was compiled against\n\t *  \\note All transport plugins MUST implement this method and return JANUS_TRANSPORT_API_VERSION\n\t * to make this work, or they will be rejected by the core. */\n\tint (* const get_api_compatibility)(void);\n\t/*! \\brief Informative method to request the numeric version of the transport plugin */\n\tint (* const get_version)(void);\n\t/*! \\brief Informative method to request the string version of the transport plugin */\n\tconst char *(* const get_version_string)(void);\n\t/*! \\brief Informative method to request a description of the transport plugin */\n\tconst char *(* const get_description)(void);\n\t/*! \\brief Informative method to request the name of the transport plugin */\n\tconst char *(* const get_name)(void);\n\t/*! \\brief Informative method to request the author of the transport plugin */\n\tconst char *(* const get_author)(void);\n\t/*! \\brief Informative method to request the package name of the transport plugin (what will be used in web applications to refer to it) */\n\tconst char *(* const get_package)(void);\n\n\t/*! \\brief Informative method to check whether any Janus API support is currently enabled in this transport */\n\tgboolean (* const is_janus_api_enabled)(void);\n\t/*! \\brief Informative method to check whether any Admin API support is currently enabled in this transport */\n\tgboolean (* const is_admin_api_enabled)(void);\n\n\t/*! \\brief Method to send a message to a client over a transport session\n\t * \\note It's the transport plugin's responsibility to free the message.\n\t * Besides, a successful return does not necessarily mean the message has been\n\t * actually sent, but only that it has been accepted by the transport plugim\n\t * @param[in] transport Pointer to the transport session instance\n\t * @param[in] request_id Will be not-NULL in case this is a response to a previous request\n\t * @param[in] admin Whether this is an admin API or a Janus API message\n\t * @param[in] message The message data as a Jansson json_t object\n\t * @returns 0 on success, a negative integer otherwise */\n\tint (* const send_message)(janus_transport_session *transport, void *request_id, gboolean admin, json_t *message);\n\t/*! \\brief Method to notify the transport plugin that a new session has been created from this transport\n\t * \\note A transport plugin may decide to close the connection as a result of such an event\n\t * @param[in] transport Pointer to the transport session instance\n\t * @param[in] session_id The session ID that was created (if the transport cares) */\n\tvoid (* const session_created)(janus_transport_session *transport, guint64 session_id);\n\t/*! \\brief Method to notify the transport plugin that a session it originated timed out\n\t * \\note A transport plugin may decide to close the connection as a result of such an event\n\t * @param[in] transport Pointer to the transport session instance\n\t * @param[in] session_id The session ID that was closed (if the transport cares)\n\t * @param[in] timeout Whether the cause for the session closure is a timeout (this may interest transport plugins more)\n\t * @param[in] claimed Whether the cause for the session closure is due to someone claiming the session */\n\tvoid (* const session_over)(janus_transport_session *transport, guint64 session_id, gboolean timeout, gboolean claimed);\n\t/*! \\brief Method to notify the transport plugin that a session it owned was claimed by another transport\n\t * \\note A transport plugin should close the connection as a result of such an event\n\t * @param[in] transport Pointer to the new transport session instance that has claimed the session\n\t * @param[in] session_id The session ID that was claimed (if the transport cares) */\n\tvoid (* const session_claimed)(janus_transport_session *transport, guint64 session_id);\n\n\t/*! \\brief Method to send a management request to this specific transport plugin\n\t * \\details The method takes a Jansson json_t, that contains all the info related\n\t * to the request. This object will come from an Admin API request, and is\n\t * meant to represent a synchronous request. Since each transport plugin can have\n\t * its own bells and whistles, there's no constraint on what this object should\n\t * contain, which is entirely handler specific. A json_t object needs to be\n\t * returned as a response, which will be sent in response to the Admin API call.\n\t * This can be useful to tweak settings in real-time, or to probe the internals\n\t * of the transport plugin for monitoring purposes.\n\t * @param[in] request Jansson object containing the request\n\t * @returns A Jansson object containing the response for the client */\n\tjson_t *(* const query_transport)(json_t *request);\n\n};\n\n/*! \\brief Callbacks to contact the Janus core */\nstruct janus_transport_callbacks {\n\t/*! \\brief Callback to notify a new incoming request\n\t * @param[in] handle The transport session that should be associated to this client\n\t * @param[in] transport Pointer to the transport session instance that received the event\n\t * @param[in] request_id Opaque pointer to a transport plugin specific value that identifies this request, so that an incoming response coming later can be matched\n\t * @param[in] admin Whether this is an admin API or a Janus API request\n\t * @param[in] message The message data as a Jansson json_t object */\n\tvoid (* const incoming_request)(janus_transport *plugin, janus_transport_session *transport, void *request_id, gboolean admin, json_t *message, json_error_t *error);\n\t/*! \\brief Callback to notify an existing transport instance went away\n\t * \\note Be careful in calling this method, as the core will assume this\n\t * client is gone for good, and will tear down all sessions it originated.\n\t * So, it makes sense to call it, for instance, when a WebSocket connection\n\t * was lost (the user went away). Not as much if you're handling connections\n\t * and their matching with clients your own way (e.g., HTTP/HTTPS connections\n\t * will come and go).\n\t * @param[in] handle The transport session that went away\n\t * @param[in] transport Pointer to the transport session instance that went away */\n\tvoid (* const transport_gone)(janus_transport *plugin, janus_transport_session *transport);\n\t/*! \\brief Callback to check with the core if an API secret must be provided\n\t * @param[in] apisecret The API secret to validate\n\t * @returns TRUE if an API secret is needed, FALSE otherwise */\n\tgboolean (* const is_api_secret_needed)(janus_transport *plugin);\n\t/*! \\brief Callback to check with the core if a provided API secret is valid\n\t * \\note This callback should only be needed when, for any reason, the transport needs to\n\t * validate requests directly, as in general requests will be validated by the core itself.\n\t * It is the case, for instance, of HTTP long polls to get session events, as those never\n\t * pass through the core and so need to be validated by the transport plugin on its behalf.\n\t * @param[in] apisecret The API secret to validate\n\t * @returns TRUE if the API secret is correct, FALSE otherwise */\n\tgboolean (* const is_api_secret_valid)(janus_transport *plugin, const char *apisecret);\n\t/*! \\brief Callback to check with the core if an authentication token is needed\n\t * @returns TRUE if an auth token is needed, FALSE otherwise */\n\tgboolean (* const is_auth_token_needed)(janus_transport *plugin);\n\t/*! \\brief Callback to check with the core if a provided authentication token is valid\n\t * \\note This callback should only be needed when, for any reason, the transport needs to\n\t * validate requests directly, as in general requests will be validated by the core itself.\n\t * It is the case, for instance, of HTTP long polls to get session events, as those never\n\t * pass through the core and so need to be validated by the transport plugin on its behalf.\n\t * @param[in] token The auth token to validate\n\t * @returns TRUE if the auth token is valid, FALSE otherwise */\n\tgboolean (* const is_auth_token_valid)(janus_transport *plugin, const char *token);\n\n\t/*! \\brief Callback to check whether the event handlers mechanism is enabled\n\t * @returns TRUE if it is, FALSE if it isn't (which means notify_event should NOT be called) */\n\tgboolean (* const events_is_enabled)(void);\n\t/*! \\brief Callback to notify an event to the registered and subscribed event handlers\n\t * \\note Don't unref the event object, the core will do that for you\n\t * @param[in] plugin The transport originating the event\n\t * @param[in] event The event to notify as a Jansson json_t object */\n\tvoid (* const notify_event)(janus_transport *plugin, void *transport, json_t *event);\n};\n\n/*! \\brief The hook that transport plugins need to implement to be created from the Janus core */\ntypedef janus_transport* create_t(void);\n\n#endif\n"
  },
  {
    "path": "src/turnrest.c",
    "content": "/*! \\file    utils.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    TURN REST API client\n * \\details  Implementation of the \\c draft-uberti-rtcweb-turn-rest-00\n * draft, that is a REST API that can be used to access TURN services,\n * more specifically credentials to use. Currently implemented in both\n * rfc5766-turn-server and coturn, and so should be generic enough to\n * be usable here.\n * \\note This implementation depends on \\c libcurl and is optional.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifdef HAVE_TURNRESTAPI\n\n#include <netdb.h>\n#include <stdlib.h>\n#include <string.h>\n#include <curl/curl.h>\n#include <jansson.h>\n#include <agent.h>\n\n#include \"turnrest.h\"\n#include \"debug.h\"\n#include \"mutex.h\"\n#include \"ip-utils.h\"\n#include \"utils.h\"\n\nstatic const char *api_server = NULL;\nstatic const char *api_key = NULL;\nstatic gboolean api_http_get = FALSE;\nstatic uint api_timeout;\nstatic janus_mutex api_mutex = JANUS_MUTEX_INITIALIZER;\n\n\n/* Buffer we use to receive the response via libcurl */\ntypedef struct janus_turnrest_buffer {\n\tchar *buffer;\n\tsize_t size;\n} janus_turnrest_buffer;\n\n/* Callback we use to progressively receive the whole response via libcurl in the buffer */\nstatic size_t janus_turnrest_callback(void *payload, size_t size, size_t nmemb, void *data) {\n\tsize_t realsize = size * nmemb;\n\tjanus_turnrest_buffer *buf = (struct janus_turnrest_buffer *)data;\n\t/* (Re)allocate if needed */\n\tbuf->buffer = g_realloc(buf->buffer, buf->size+realsize+1);\n\t/* Update the buffer */\n\tmemcpy(&(buf->buffer[buf->size]), payload, realsize);\n\tbuf->size += realsize;\n\tbuf->buffer[buf->size] = 0;\n\t/* Done! */\n\treturn realsize;\n}\n\n\nvoid janus_turnrest_init(void) {\n\t/* Initialize libcurl, needed for contacting the TURN REST API backend */\n\tcurl_global_init(CURL_GLOBAL_ALL);\n}\n\nvoid janus_turnrest_deinit(void) {\n\t/* Cleanup the libcurl initialization */\n\tcurl_global_cleanup();\n\tjanus_mutex_lock(&api_mutex);\n\tg_free((char *)api_server);\n\tg_free((char *)api_key);\n\tjanus_mutex_unlock(&api_mutex);\n}\n\nvoid janus_turnrest_set_backend(const char *server, const char *key, const char *method, const uint timeout) {\n\tjanus_mutex_lock(&api_mutex);\n\n\t/* Get rid of the old values first */\n\tg_free((char *)api_server);\n\tapi_server = NULL;\n\tg_free((char *)api_key);\n\tapi_key = NULL;\n\n\tif(server != NULL) {\n\t\t/* Set a new server now */\n\t\tapi_server = g_strdup(server);\n\t\tif(key != NULL)\n\t\t\tapi_key = g_strdup(key);\n\t\tif(method != NULL) {\n\t\t\tif(!strcasecmp(method, \"get\")) {\n\t\t\t\tapi_http_get = TRUE;\n\t\t\t} else if(!strcasecmp(method, \"post\")) {\n\t\t\t\tapi_http_get = FALSE;\n\t\t\t} else {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"Unknown method '%s' for TURN REST API, assuming POST\\n\", method);\n\t\t\t\tapi_http_get = FALSE;\n\t\t\t}\n\t\t}\n\t\tapi_timeout = timeout;\n\t}\n\tjanus_mutex_unlock(&api_mutex);\n}\n\nconst char *janus_turnrest_get_backend(void) {\n\treturn api_server;\n}\n\nstatic void janus_turnrest_instance_destroy(gpointer data) {\n\tjanus_turnrest_instance *instance = (janus_turnrest_instance *)data;\n\tif(instance == NULL)\n\t\treturn;\n\tg_free(instance->server);\n\tg_free(instance);\n}\n\nvoid janus_turnrest_response_destroy(janus_turnrest_response *response) {\n\tif(response == NULL)\n\t\treturn;\n\tg_free(response->username);\n\tg_free(response->password);\n\tg_list_free_full(response->servers, janus_turnrest_instance_destroy);\n\tg_free(response);\n}\n\njanus_turnrest_response *janus_turnrest_request(const char *user) {\n\tjanus_mutex_lock(&api_mutex);\n\tif(api_server == NULL) {\n\t\tjanus_mutex_unlock(&api_mutex);\n\t\treturn NULL;\n\t}\n\t/* Prepare the libcurl context */\n\tCURLcode res;\n\tCURL *curl = curl_easy_init();\n\tif(curl == NULL) {\n\t\tjanus_mutex_unlock(&api_mutex);\n\t\tJANUS_LOG(LOG_ERR, \"libcurl error\\n\");\n\t\treturn NULL;\n\t}\n\t/* Prepare the request URI */\n\tchar query_string[512];\n\tg_snprintf(query_string, 512, \"service=turn\");\n\tif(api_key != NULL) {\n\t\t/* Note: we've been using 'api' as a query string parameter for\n\t\t * a while, but the expired draft this implementation follows\n\t\t * suggested 'key' instead: as such, we send them both\n\t\t * See https://github.com/meetecho/janus-gateway/issues/1416 */\n\t\tchar buffer[256];\n\t\tchar *encoded_key = curl_easy_escape(curl, api_key, 0);\n\t\tg_snprintf(buffer, 256, \"&api=%s\", encoded_key);\n\t\tjanus_strlcat(query_string, buffer, 512);\n\t\tg_snprintf(buffer, 256, \"&key=%s\", encoded_key);\n\t\tjanus_strlcat(query_string, buffer, 512);\n\t\tcurl_free(encoded_key);\n\t}\n\tif(user != NULL) {\n\t\t/* Note: 'username' is supposedly optional, but a commonly used\n\t\t * TURN REST API server implementation requires it. As such, we\n\t\t * now send that too, letting the Janus core tell us what to use\n\t\t * See https://github.com/meetecho/janus-gateway/issues/2199 */\n\t\tchar buffer[256];\n\t\tchar *encoded_user = curl_easy_escape(curl, user, 0);\n\t\tg_snprintf(buffer, 256, \"&username=%s\", encoded_user);\n\t\tjanus_strlcat(query_string, buffer, 512);\n\t\tcurl_free(encoded_user);\n\t}\n\tchar request_uri[1024];\n\tg_snprintf(request_uri, 1024, \"%s?%s\", api_server, query_string);\n\tJANUS_LOG(LOG_VERB, \"Sending request: %s\\n\", request_uri);\n\tcurl_easy_setopt(curl, CURLOPT_URL, request_uri);\n\tcurl_easy_setopt(curl, (api_http_get ? CURLOPT_HTTPGET : CURLOPT_POST), 1);\n\tif(!api_http_get) {\n\t\t/* FIXME Some servers don't like a POST with no data */\n\t\tcurl_easy_setopt(curl, CURLOPT_POSTFIELDS, query_string);\n\t}\n\tcurl_easy_setopt(curl, CURLOPT_TIMEOUT, api_timeout);\n\tjanus_mutex_unlock(&api_mutex);\n\t/* For getting data, we use an helper struct and the libcurl callback */\n\tjanus_turnrest_buffer data;\n\tdata.buffer = g_malloc0(1);\n\tdata.size = 0;\n\tcurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, janus_turnrest_callback);\n\tcurl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&data);\n\tcurl_easy_setopt(curl, CURLOPT_USERAGENT, \"Janus/1.0\");\n\t/* Send the request */\n\tres = curl_easy_perform(curl);\n\tif(res != CURLE_OK) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't send the request: %s\\n\", curl_easy_strerror(res));\n\t\tg_free(data.buffer);\n\t\tcurl_easy_cleanup(curl);\n\t\treturn NULL;\n\t}\n\t/* Cleanup the libcurl context */\n\tcurl_easy_cleanup(curl);\n\t/* Process the response */\n\tJANUS_LOG(LOG_VERB, \"Got %zu bytes from the TURN REST API server\\n\", data.size);\n\tJANUS_LOG(LOG_VERB, \"%s\\n\", data.buffer);\n\tjson_error_t error;\n\tjson_t *root = json_loads(data.buffer, 0, &error);\n\tif(!root) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't parse response: error on line %d: %s\", error.line, error.text);\n\t\tg_free(data.buffer);\n\t\treturn NULL;\n\t}\n\tg_free(data.buffer);\n\tjson_t *username = json_object_get(root, \"username\");\n\tif(!username) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid response: missing username\\n\");\n\t\treturn NULL;\n\t}\n\tif(!json_is_string(username)) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid response: username should be a string\\n\");\n\t\treturn NULL;\n\t}\n\tjson_t *password = json_object_get(root, \"password\");\n\tif(!password) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid response: missing password\\n\");\n\t\treturn NULL;\n\t}\n\tif(!json_is_string(password)) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid response: password should be a string\\n\");\n\t\treturn NULL;\n\t}\n\tjson_t *ttl = json_object_get(root, \"ttl\");\n\tif(ttl && (!json_is_integer(ttl) || json_integer_value(ttl) < 0)) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid response: ttl should be a positive integer\\n\");\n\t\treturn NULL;\n\t}\n\tjson_t *uris = json_object_get(root, \"uris\");\n\tif(!uris) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid response: missing uris\\n\");\n\t\treturn NULL;\n\t}\n\tif(!json_is_array(uris) || json_array_size(uris) == 0) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid response: uris should be a non-empty array\\n\");\n\t\treturn NULL;\n\t}\n\t/* Turn the response into a janus_turnrest_response object we can use */\n\tjanus_turnrest_response *response = g_malloc(sizeof(janus_turnrest_response));\n\tresponse->username = g_strdup(json_string_value(username));\n\tresponse->password = g_strdup(json_string_value(password));\n\tresponse->ttl = ttl ? json_integer_value(ttl) : 0;\n\tresponse->servers = NULL;\n\tsize_t i = 0;\n\tfor(i=0; i<json_array_size(uris); i++) {\n\t\tjson_t *uri = json_array_get(uris, i);\n\t\tif(uri == NULL || !json_is_string(uri)) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Skipping invalid TURN URI (not a string)...\\n\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst char *turn_uri = json_string_value(uri);\n\t\tif(strstr(turn_uri, \"turn:\") != turn_uri && strstr(turn_uri, \"turns:\") != turn_uri) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Skipping invalid TURN URI '%s' (not a TURN URI)...\\n\", turn_uri);\n\t\t\tcontinue;\n\t\t}\n\t\tjanus_turnrest_instance *instance = g_malloc(sizeof(janus_turnrest_instance));\n\t\tinstance->transport = NICE_RELAY_TYPE_TURN_UDP;\n\t\tif(strstr(turn_uri, \"turns:\") == turn_uri || strstr(turn_uri, \"transport=tls\") != NULL)\n\t\t\tinstance->transport = NICE_RELAY_TYPE_TURN_TLS;\n\t\telse if(strstr(turn_uri, \"transport=tcp\") != NULL)\n\t\t\tinstance->transport = NICE_RELAY_TYPE_TURN_TCP;\n\t\tgchar **parts = NULL;\n\t\tif(strstr(turn_uri, \"?\") != NULL) {\n\t\t\tparts = g_strsplit(turn_uri, \"?\", -1);\n\t\t\tturn_uri = parts[0];\n\t\t}\n\t\tgchar **uri_parts = g_strsplit(turn_uri, \":\", -1);\n\t\t/* Resolve the TURN URI address */\n\t\tstruct addrinfo *res = NULL;\n\t\tjanus_network_address addr;\n\t\tjanus_network_address_string_buffer addr_buf;\n\t\tif(getaddrinfo(uri_parts[1], NULL, NULL, &res) != 0 ||\n\t\t\t\tjanus_network_address_from_sockaddr(res->ai_addr, &addr) != 0 ||\n\t\t\t\tjanus_network_address_to_string_buffer(&addr, &addr_buf) != 0) {\n\t\t\tJANUS_LOG(LOG_WARN, \"Skipping invalid TURN URI '%s' (could not resolve the address)...\\n\", uri_parts[1]);\n\t\t\tif(res != NULL)\n\t\t\t\tfreeaddrinfo(res);\n\t\t\tg_strfreev(uri_parts);\n\t\t\tg_strfreev(parts);\n\t\t\tjanus_turnrest_instance_destroy(instance);\n\t\t\tcontinue;\n\t\t}\n\t\tfreeaddrinfo(res);\n\t\tinstance->server = g_strdup(janus_network_address_string_from_buffer(&addr_buf));\n\t\tif(uri_parts[2] == NULL) {\n\t\t\t/* No port? Use 3478 by default */\n\t\t\tinstance->port = 3478;\n\t\t} else if(janus_string_to_uint16(uri_parts[2], &instance->port) < 0) {\n\t\t\tJANUS_LOG(LOG_ERR, \"Invalid TURN instance port: %s (falling back to 3478)\\n\", uri_parts[2]);\n\t\t\tinstance->port = 3478;\n\t\t}\n\t\tg_strfreev(uri_parts);\n\t\tg_strfreev(parts);\n\t\t/* Add the server to the list */\n\t\tresponse->servers = g_list_append(response->servers, instance);\n\t}\n\tif(response->servers == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Couldn't find any valid TURN URI in the response...\\n\");\n\t\tjanus_turnrest_response_destroy(response);\n\t\treturn NULL;\n\t}\n\t/* Done */\n\treturn response;\n}\n\n#endif\n"
  },
  {
    "path": "src/turnrest.h",
    "content": "/*! \\file    utils.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    TURN REST API client (headers)\n * \\details  Implementation of the \\c draft-uberti-rtcweb-turn-rest-00\n * draft, that is a REST API that can be used to access TURN services,\n * more specifically credentials to use. Currently implemented in both\n * rfc5766-turn-server and coturn, and so should be generic enough to\n * be usable here.\n * \\note This implementation depends on \\c libcurl and is optional.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_TURNREST_H\n#define JANUS_TURNREST_H\n\n#ifdef HAVE_TURNRESTAPI\n\n#include <glib.h>\n\n/*! \\brief Initialize the TURN REST API client stack */\nvoid janus_turnrest_init(void);\n/*! \\brief De-initialize the TURN REST API client stack */\nvoid janus_turnrest_deinit(void);\n\n\n/*! \\brief Set (or reset, in case the server is NULL) the backend that\n * needs to be contacted, and optionally the API key, if required\n * @param server The REST API server address (pass NULL to disable the\n * TURN REST API entirely)\n * @param key The API key, if any (pass NULL if it's not required)\n * @param method The HTTP method to use, POST or GET (NULL means POST)\n * @param timeout The timeout in seconds */\nvoid janus_turnrest_set_backend(const char *server, const char *key, const char *method, const uint timeout);\n/*! \\brief Get the currently set TURN REST API backend\n * @returns The currently set TURN REST API backend */\nconst char *janus_turnrest_get_backend(void);\n\n\n/*! \\brief Complete response from the TURN REST API service */\ntypedef struct janus_turnrest_response {\n\t/*! \\brief TURN username */\n\tchar *username;\n\t/*! \\brief TURN password */\n\tchar *password;\n\t/*! \\brief Time-to-live of the credentials, in seconds */\n\tguint32 ttl;\n\t/*! \\brief List of TURN servers */\n\tGList *servers;\n} janus_turnrest_response;\n\n/*! \\brief Instance of TURN server as returned by TURN REST API service */\ntypedef struct janus_turnrest_instance {\n\t/*! \\brief TURN server address */\n\tchar *server;\n\t/*! \\brief TURN server port */\n\tguint16 port;\n\t/*! \\brief TURN server transport type */\n\tint transport;\n} janus_turnrest_instance;\n/*! \\brief De-allocate a janus_turnrest_response instance\n * @param response The janus_turnrest_response instance to destroy */\nvoid janus_turnrest_response_destroy(janus_turnrest_response *response);\n\n\n/*! \\brief Retrieve address and credentials for one or more TURN servers\n * @note Use janus_turnrest_response_destroy to get rid of the response, once done\n * @param[in] user Username to provide in the TURN REST API request\n * @returns A valid janus_turnrest_response instance, if successful, NULL otherwise */\njanus_turnrest_response *janus_turnrest_request(const char *user);\n\n#endif\n\n#endif\n"
  },
  {
    "path": "src/utils.c",
    "content": "/*! \\file    utils.c\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Utilities and helpers\n * \\details  Implementations of a few methods that may be of use here\n * and there in the code.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#include <stdlib.h>\n#include <string.h>\n#include <sys/stat.h>\n#include <errno.h>\n#include <fcntl.h>\n#include <sys/file.h>\n#include <sys/types.h>\n#include <unistd.h>\n#include <arpa/inet.h>\n#include <inttypes.h>\n\n#include <zlib.h>\n#include <openssl/rand.h>\n\n#include \"utils.h\"\n#include \"rtp.h\"\n#include \"debug.h\"\n#include \"mutex.h\"\n\n#if __MACH__\n#include \"mach_gettime.h\"\n#endif\n\ngint64 janus_get_monotonic_time_internal(void) {\n\tstruct timespec ts;\n\tclock_gettime (CLOCK_MONOTONIC, &ts);\n\treturn (ts.tv_sec*G_GINT64_CONSTANT(1000000)) + (ts.tv_nsec/G_GINT64_CONSTANT(1000));\n}\n\nstatic gint64 janus_started = 0;\nvoid janus_mark_started(void) {\n\tif(janus_started == 0)\n\t\tjanus_started = janus_get_monotonic_time_internal();\n}\n\ngint64 janus_get_monotonic_time(void) {\n\treturn janus_get_monotonic_time_internal() - janus_started;\n}\n\ngint64 janus_get_real_time(void) {\n\tstruct timespec ts;\n\tclock_gettime (CLOCK_REALTIME, &ts);\n\treturn (ts.tv_sec*G_GINT64_CONSTANT(1000000)) + (ts.tv_nsec/G_GINT64_CONSTANT(1000));\n}\n\ngboolean janus_is_true(const char *value) {\n\treturn value && (!strcasecmp(value, \"yes\") || !strcasecmp(value, \"true\") || !strcasecmp(value, \"1\"));\n}\n\ngboolean janus_strcmp_const_time(const void *str1, const void *str2) {\n\tif(str1 == NULL || str2 == NULL)\n\t\treturn FALSE;\n\tconst unsigned char *string1 = (const unsigned char *)str1;\n\tconst unsigned char *string2 = (const unsigned char *)str2;\n\tsize_t maxlen = strlen((char *)string1);\n\tif(strlen((char *)string2) > maxlen)\n\t\tmaxlen = strlen((char *)string2);\n\tunsigned char *buf1 = g_malloc0(maxlen+1);\n\tmemcpy(buf1, string1, strlen(str1));\n\tunsigned char *buf2 = g_malloc0(maxlen+1);\n\tmemcpy(buf2, string2, strlen(str2));\n\tunsigned char result = 0;\n\tsize_t i = 0;\n\tfor (i = 0; i < maxlen; i++) {\n\t\tresult |= buf1[i] ^ buf2[i];\n\t}\n\tg_free(buf1);\n\tbuf1 = NULL;\n\tg_free(buf2);\n\tbuf2 = NULL;\n\treturn result == 0;\n}\n\nguint32 janus_random_uint32(void) {\n\tguint32 ret = 0;\n\tif(RAND_bytes((void *)&ret, sizeof(ret)) != 1) {\n\t\tJANUS_LOG(LOG_WARN, \"Safe RAND_bytes() failed, falling back to unsafe PRNG\\n\");\n\t\treturn g_random_int();\n\t}\n\treturn ret;\n}\n\nguint64 janus_random_uint64_full(void) {\n\tguint64 ret = 0;\n\tif(RAND_bytes((void *)&ret, sizeof(ret)) != 1) {\n\t\tJANUS_LOG(LOG_WARN, \"Safe RAND_bytes() failed, falling back to unsafe PRNG\\n\");\n\t\treturn ((guint64)g_random_int() << 32) | g_random_int();\n\t}\n\treturn ret;\n}\n\nguint64 janus_random_uint64(void) {\n\treturn janus_random_uint64_full() & 0x1FFFFFFFFFFFFF;\n}\n\nchar *janus_random_uuid(void) {\n#if GLIB_CHECK_VERSION(2, 52, 0)\n\treturn g_uuid_string_random();\n#else\n\t/* g_uuid_string_random is only available from glib 2.52, so if it's\n\t * not available we have to do it manually: the following code is\n\t * heavily based on https://github.com/rxi/uuid4 (MIT license) */\n\tconst char *template = \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\";\n\tconst char *samples = \"0123456789abcdef\";\n\tunion { unsigned char b[16]; uint64_t word[2]; } rnd;\n\trnd.word[0] = janus_random_uint64_full();\n\trnd.word[1] = janus_random_uint64_full();\n\t/* Generate the string */\n\tchar uuid[37], *dst = uuid;\n\tconst char *p = template;\n\tint i = 0, n = 0;\n\twhile(*p) {\n\t\tn = rnd.b[i >> 1];\n\t\tn = (i & 1) ? (n >> 4) : (n & 0xf);\n\t\tswitch (*p) {\n\t\t\tcase 'x':\n\t\t\t\t*dst = samples[n];\n\t\t\t\ti++;\n\t\t\t\tbreak;\n\t\t\tcase 'y':\n\t\t\t\t*dst = samples[(n & 0x3) + 8];\n\t\t\t\ti++;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t*dst = *p;\n\t\t}\n\t\tp++;\n\t\tdst++;\n\t}\n\tuuid[36] = '\\0';\n\treturn g_strdup(uuid);\n#endif\n}\n\nguint64 *janus_uint64_dup(guint64 num) {\n\tguint64 *numdup = g_malloc(sizeof(guint64));\n\t*numdup = num;\n\treturn numdup;\n}\n\nguint64 janus_uint64_hash(guint64 num) {\n\tnum = (num ^ (num >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);\n\tnum = (num ^ (num >> 27)) * UINT64_C(0x94d049bb133111eb);\n\tnum = num ^ (num >> 31);\n\treturn num;\n}\n\nint janus_string_to_uint8(const char *str, uint8_t *num) {\n\tif(str == NULL || num == NULL)\n\t\treturn -EINVAL;\n\tlong int val = strtol(str, 0, 10);\n\tif(val < 0 || val > UINT8_MAX)\n\t\treturn -ERANGE;\n\t*num = val;\n\treturn 0;\n}\n\nint janus_string_to_uint16(const char *str, uint16_t *num) {\n\tif(str == NULL || num == NULL)\n\t\treturn -EINVAL;\n\tlong int val = strtol(str, 0, 10);\n\tif(val < 0 || val > UINT16_MAX)\n\t\treturn -ERANGE;\n\t*num = val;\n\treturn 0;\n}\n\nint janus_string_to_uint32(const char *str, uint32_t *num) {\n\tif(str == NULL || num == NULL)\n\t\treturn -EINVAL;\n\tlong long int val = strtoll(str, 0, 10);\n\tif(val < 0 || val > UINT32_MAX)\n\t\treturn -ERANGE;\n\t*num = val;\n\treturn 0;\n}\n\nvoid janus_flags_reset(janus_flags *flags) {\n\tif(flags != NULL)\n\t\tg_atomic_pointer_set(flags, 0);\n}\n\nvoid janus_flags_set(janus_flags *flags, gsize flag) {\n\tif(flags != NULL) {\n\t\tg_atomic_pointer_or(flags, flag);\n\t}\n}\n\nvoid janus_flags_clear(janus_flags *flags, gsize flag) {\n\tif(flags != NULL) {\n\t\tg_atomic_pointer_and(flags, ~(flag));\n\t}\n}\n\ngboolean janus_flags_is_set(janus_flags *flags, gsize flag) {\n\tif(flags != NULL) {\n\t\tgsize bit = ((gsize) g_atomic_pointer_get(flags)) & flag;\n\t\treturn (bit != 0);\n\t}\n\treturn FALSE;\n}\n\n/* Easy way to replace multiple occurrences of a string with another */\nchar *janus_string_replace(char *message, const char *old_string, const char *new_string)\n{\n\tif(!message || !old_string || !new_string)\n\t\treturn NULL;\n\n\tif(!strstr(message, old_string)) {\t/* Nothing to be done (old_string is not there) */\n\t\treturn message;\n\t}\n\tif(!strcmp(old_string, new_string)) {\t/* Nothing to be done (old_string=new_string) */\n\t\treturn message;\n\t}\n\tif(strlen(old_string) == strlen(new_string)) {\t/* Just overwrite */\n\t\tchar *outgoing = message;\n\t\tchar *pos = strstr(outgoing, old_string), *tmp = NULL;\n\t\twhile(pos) {\n\t\t\tmemcpy(pos, new_string, strlen(new_string));\n\t\t\tpos += strlen(old_string);\n\t\t\ttmp = strstr(pos, old_string);\n\t\t\tpos = tmp;\n\t\t}\n\t\treturn outgoing;\n\t} else {\t/* We need to resize */\n\t\tchar *outgoing = g_strdup(message);\n\t\tg_free(message);\n\t\tif(outgoing == NULL) {\n\t\t\treturn NULL;\n\t\t}\n\t\tint diff = strlen(new_string) - strlen(old_string);\n\t\t/* Count occurrences */\n\t\tint counter = 0;\n\t\tchar *pos = strstr(outgoing, old_string), *tmp = NULL;\n\t\twhile(pos) {\n\t\t\tcounter++;\n\t\t\tpos += strlen(old_string);\n\t\t\ttmp = strstr(pos, old_string);\n\t\t\tpos = tmp;\n\t\t}\n\t\tuint16_t old_stringlen = strlen(outgoing)+1, new_stringlen = old_stringlen + diff*counter;\n\t\tif(diff > 0) {\t/* Resize now */\n\t\t\ttmp = g_realloc(outgoing, new_stringlen);\n\t\t\toutgoing = tmp;\n\t\t}\n\t\t/* Replace string */\n\t\tpos = strstr(outgoing, old_string);\n\t\twhile(pos) {\n\t\t\tif(diff > 0) {\t/* Move to the right (new_string is larger than old_string) */\n\t\t\t\tuint16_t len = strlen(pos)+1;\n\t\t\t\tmemmove(pos + diff, pos, len);\n\t\t\t\tmemcpy(pos, new_string, strlen(new_string));\n\t\t\t\tpos += strlen(new_string);\n\t\t\t\ttmp = strstr(pos, old_string);\n\t\t\t} else {\t/* Move to the left (new_string is smaller than old_string) */\n\t\t\t\tuint16_t len = strlen(pos - diff)+1;\n\t\t\t\tmemmove(pos, pos - diff, len);\n\t\t\t\tmemcpy(pos, new_string, strlen(new_string));\n\t\t\t\tpos += strlen(old_string);\n\t\t\t\ttmp = strstr(pos, old_string);\n\t\t\t}\n\t\t\tpos = tmp;\n\t\t}\n\t\tif(diff < 0) {\t/* We skipped the resize previously (shrinking memory) */\n\t\t\ttmp = g_realloc(outgoing, new_stringlen);\n\t\t\toutgoing = tmp;\n\t\t}\n\t\toutgoing[strlen(outgoing)] = '\\0';\n\t\treturn outgoing;\n\t}\n}\n\nsize_t janus_strlcat(char *dest, const char *src, size_t dest_size) {\n\tsize_t ret = g_strlcat(dest, src, dest_size);\n\tif(ret >= dest_size)\n\t\tJANUS_LOG(LOG_ERR, \"Truncation occurred, %lu >= %lu\\n\", ret, dest_size);\n\treturn ret;\n}\n\nint janus_strlcat_fast(char *dest, const char *src, size_t dest_size, size_t *offset) {\n\tif(dest == NULL || src == NULL || offset == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Invalid arguments\\n\");\n\t\treturn -1;\n\t}\n\tif(*offset >= dest_size) {\n\t\tJANUS_LOG(LOG_ERR, \"Offset is beyond the buffer size\\n\");\n\t\treturn -2;\n\t}\n\tchar *p = memccpy(dest + *offset, src, 0, dest_size - *offset);\n\tif(p == NULL) {\n\t\tJANUS_LOG(LOG_ERR, \"Truncation occurred, %lu >= %lu\\n\",\n\t\t\t*offset + strlen(src), dest_size);\n\t\t*offset = dest_size;\n\t\t*(dest + dest_size -1) = '\\0';\n\t\treturn -3;\n\t}\n\t*offset = (p - dest - 1);\n\treturn 0;\n}\n\nint janus_mkdir(const char *dir, mode_t mode) {\n\tchar tmp[256];\n\tchar *p = NULL;\n\tsize_t len;\n\n\tint res = 0;\n\tg_snprintf(tmp, sizeof(tmp), \"%s\", dir);\n\tlen = strlen(tmp);\n\tif(tmp[len - 1] == '/')\n\t\ttmp[len - 1] = 0;\n\tfor(p = tmp + 1; *p; p++) {\n\t\tif(*p == '/') {\n\t\t\t*p = 0;\n\t\t\tres = mkdir(tmp, mode);\n\t\t\tif(res != 0 && errno != EEXIST) {\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Error creating folder %s\\n\", tmp);\n\t\t\t\treturn res;\n\t\t\t}\n\t\t\t*p = '/';\n\t\t}\n\t}\n\tres = mkdir(tmp, mode);\n\tif(res != 0 && errno != EEXIST)\n\t\treturn res;\n\treturn 0;\n}\n\ngchar *janus_make_absolute_path(const gchar *base_dir, const gchar *path) {\n\tif(!path)\n\t\treturn NULL;\n\tif(g_path_is_absolute(path))\n\t\treturn g_strdup(path);\n\tif(!base_dir)\n\t\treturn NULL;\n\treturn g_build_filename(base_dir, path, NULL);\n}\n\nint janus_get_codec_pt(const char *sdp, const char *codec) {\n\tif(!sdp || !codec)\n\t\treturn -1;\n\tint video = 0;\n\tconst char *format = NULL, *format2 = NULL;\n\tif(!strcasecmp(codec, \"opus\")) {\n\t\tvideo = 0;\n\t\tformat = \"opus/48000/2\";\n\t\tformat2 = \"OPUS/48000/2\";\n\t} else if(!strcasecmp(codec, \"pcmu\")) {\n\t\t/* We know the payload type is 0: we just need to make sure it's there */\n\t\tvideo = 0;\n\t\tformat = \"pcmu/8000\";\n\t\tformat2 = \"PCMU/8000\";\n\t} else if(!strcasecmp(codec, \"pcma\")) {\n\t\t/* We know the payload type is 8: we just need to make sure it's there */\n\t\tvideo = 0;\n\t\tformat = \"pcma/8000\";\n\t\tformat2 = \"PCMA/8000\";\n\t} else if(!strcasecmp(codec, \"g722\")) {\n\t\t/* We know the payload type is 9: we just need to make sure it's there */\n\t\tvideo = 0;\n\t\tformat = \"g722/8000\";\n\t\tformat2 = \"G722/8000\";\n\t} else if(!strcasecmp(codec, \"isac16\")) {\n\t\tvideo = 0;\n\t\tformat = \"isac/16000\";\n\t\tformat2 = \"ISAC/16000\";\n\t} else if(!strcasecmp(codec, \"isac32\")) {\n\t\tvideo = 0;\n\t\tformat = \"isac/32000\";\n\t\tformat2 = \"ISAC/32000\";\n\t} else if(!strcasecmp(codec, \"l16-48\")) {\n\t\tvideo = 0;\n\t\tformat = \"l16/48000\";\n\t\tformat2 = \"L16/48000\";\n\t} else if(!strcasecmp(codec, \"l16\")) {\n\t\tvideo = 0;\n\t\tformat = \"l16/16000\";\n\t\tformat2 = \"L16/16000\";\n\t} else if(!strcasecmp(codec, \"vp8\")) {\n\t\tvideo = 1;\n\t\tformat = \"vp8/90000\";\n\t\tformat2 = \"VP8/90000\";\n\t} else if(!strcasecmp(codec, \"vp9\")) {\n\t\tvideo = 1;\n\t\tformat = \"vp9/90000\";\n\t\tformat2 = \"VP9/90000\";\n\t} else if(!strcasecmp(codec, \"h264\")) {\n\t\tvideo = 1;\n\t\tformat = \"h264/90000\";\n\t\tformat2 = \"H264/90000\";\n\t} else if(!strcasecmp(codec, \"av1\")) {\n\t\tvideo = 1;\n\t\tformat = \"av1/90000\";\n\t\tformat2 = \"AV1/90000\";\n\t} else if(!strcasecmp(codec, \"h265\")) {\n\t\tvideo = 1;\n\t\tformat = \"h265/90000\";\n\t\tformat2 = \"H265/90000\";\n\t} else {\n\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec '%s'\\n\", codec);\n\t\treturn -1;\n\t}\n\t/* First of all, let's check if the codec is there */\n\tif(!video) {\n\t\tif(!strstr(sdp, \"m=audio\") || (!strstr(sdp, format) && !strstr(sdp, format2)))\n\t\t\treturn -2;\n\t} else {\n\t\tif(!strstr(sdp, \"m=video\") || (!strstr(sdp, format) && !strstr(sdp, format2)))\n\t\t\treturn -2;\n\t}\n\tchar rtpmap[50], rtpmap2[50];\n\tg_snprintf(rtpmap, 50, \"a=rtpmap:%%d %s\", format);\n\tg_snprintf(rtpmap2, 50, \"a=rtpmap:%%d %s\", format2);\n\t/* Look for the mapping */\n\tconst char *line = strstr(sdp, video ? \"m=video\" : \"m=audio\");\n\twhile(line) {\n\t\tchar *next = strchr(line, '\\n');\n\t\tif(next) {\n\t\t\t*next = '\\0';\n\t\t\tif(strstr(line, \"a=rtpmap\") && strstr(line, format)) {\n\t\t\t\t/* Gotcha! */\n\t\t\t\tint pt = 0;\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wformat-nonliteral\"\n\t\t\t\tif(sscanf(line, rtpmap, &pt) == 1) {\n\t\t\t\t\t*next = '\\n';\n\t\t\t\t\treturn pt;\n\t\t\t\t}\n\t\t\t} else if(strstr(line, \"a=rtpmap\") && strstr(line, format2)) {\n\t\t\t\t/* Gotcha! */\n\t\t\t\tint pt = 0;\n\t\t\t\tif(sscanf(line, rtpmap2, &pt) == 1) {\n#pragma GCC diagnostic pop\n\t\t\t\t\t*next = '\\n';\n\t\t\t\t\treturn pt;\n\t\t\t\t}\n\t\t\t}\n\t\t\t*next = '\\n';\n\t\t}\n\t\tline = next ? (next+1) : NULL;\n\t}\n\treturn -3;\n}\n\nconst char *janus_get_codec_from_pt(const char *sdp, int pt) {\n\tif(!sdp || pt < 0)\n\t\treturn NULL;\n\tif(pt == 0)\n\t\treturn \"pcmu\";\n\tif(pt == 8)\n\t\treturn \"pcma\";\n\tif(pt == 9)\n\t\treturn \"g722\";\n\t/* Look for the mapping */\n\tchar rtpmap[50];\n\tg_snprintf(rtpmap, 50, \"a=rtpmap:%d \", pt);\n\tconst char *line = strstr(sdp, \"m=\");\n\twhile(line) {\n\t\tchar *next = strchr(line, '\\n');\n\t\tif(next) {\n\t\t\t*next = '\\0';\n\t\t\tif(strstr(line, rtpmap)) {\n\t\t\t\t/* Gotcha! */\n\t\t\t\tchar name[100];\n\t\t\t\tif(sscanf(line, \"a=rtpmap:%d %99s\", &pt, name) == 2) {\n\t\t\t\t\t*next = '\\n';\n\t\t\t\t\tif(strstr(name, \"vp8\") || strstr(name, \"VP8\"))\n\t\t\t\t\t\treturn \"vp8\";\n\t\t\t\t\tif(strstr(name, \"vp9\") || strstr(name, \"VP9\"))\n\t\t\t\t\t\treturn \"vp9\";\n\t\t\t\t\tif(strstr(name, \"h264\") || strstr(name, \"H264\"))\n\t\t\t\t\t\treturn \"h264\";\n\t\t\t\t\tif(strstr(name, \"av1\") || strstr(name, \"AV1\"))\n\t\t\t\t\t\treturn \"av1\";\n\t\t\t\t\tif(strstr(name, \"h265\") || strstr(name, \"H265\"))\n\t\t\t\t\t\treturn \"h265\";\n\t\t\t\t\tif(strstr(name, \"opus\") || strstr(name, \"OPUS\"))\n\t\t\t\t\t\treturn \"opus\";\n\t\t\t\t\tif(strstr(name, \"pcmu\") || strstr(name, \"PCMU\"))\n\t\t\t\t\t\treturn \"pcmu\";\n\t\t\t\t\tif(strstr(name, \"pcma\") || strstr(name, \"PCMA\"))\n\t\t\t\t\t\treturn \"pcma\";\n\t\t\t\t\tif(strstr(name, \"g722\") || strstr(name, \"G722\"))\n\t\t\t\t\t\treturn \"g722\";\n\t\t\t\t\tif(strstr(name, \"isac/16\") || strstr(name, \"ISAC/16\"))\n\t\t\t\t\t\treturn \"isac16\";\n\t\t\t\t\tif(strstr(name, \"isac/32\") || strstr(name, \"ISAC/32\"))\n\t\t\t\t\t\treturn \"isac32\";\n\t\t\t\t\tif(strstr(name, \"l16/48\") || strstr(name, \"L16/48\"))\n\t\t\t\t\t\treturn \"l16-48\";\n\t\t\t\t\tif(strstr(name, \"l16/16\") || strstr(name, \"L16/16\"))\n\t\t\t\t\t\treturn \"l16\";\n\t\t\t\t\tif(strstr(name, \"red\"))\n\t\t\t\t\t\treturn NULL;\n\t\t\t\t\tJANUS_LOG(LOG_ERR, \"Unsupported codec '%s'\\n\", name);\n\t\t\t\t\treturn NULL;\n\t\t\t\t}\n\t\t\t}\n\t\t\t*next = '\\n';\n\t\t}\n\t\tline = next ? (next+1) : NULL;\n\t}\n\treturn NULL;\n}\n\n/* PID file management */\nstatic char *pidfile = NULL;\nstatic int pidfd = -1;\nstatic FILE *pidf = NULL;\nint janus_pidfile_create(const char *file) {\n\tif(file == NULL)\n\t\treturn 0;\n\tpidfile = g_strdup(file);\n\t/* Try creating a PID file (or opening an existing one) */\n\tpidfd = open(pidfile, O_RDWR|O_CREAT|O_TRUNC, 0644);\n\tif(pidfd < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error opening/creating PID file %s, does Janus have enough permissions?\\n\", pidfile);\n\t\treturn -1;\n\t}\n\tpidf = fdopen(pidfd, \"r+\");\n\tif(pidf == NULL) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error opening/creating PID file %s, does Janus have enough permissions?\\n\", pidfile);\n\t\tclose(pidfd);\n\t\treturn -1;\n\t}\n\t/* Try locking the PID file */\n\tint pid = 0;\n\tif(flock(pidfd, LOCK_EX|LOCK_NB) < 0) {\n\t\tif(fscanf(pidf, \"%d\", &pid) == 1) {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Error locking PID file (lock held by PID %d?)\\n\", pid);\n\t\t} else {\n\t\t\tJANUS_LOG(LOG_FATAL, \"Error locking PID file (lock held by unknown PID?)\\n\");\n\t\t}\n\t\tfclose(pidf);\n\t\treturn -1;\n\t}\n\t/* Write the PID */\n\tpid = getpid();\n\tif(fprintf(pidf, \"%d\\n\", pid) < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error writing PID in file, error %d (%s)\\n\", errno, g_strerror(errno));\n\t\tfclose(pidf);\n\t\treturn -1;\n\t}\n\tfflush(pidf);\n\t/* We're done */\n\treturn 0;\n}\n\nint janus_pidfile_remove(void) {\n\tif(pidfile == NULL || pidfd < 0 || pidf == NULL)\n\t\treturn 0;\n\t/* Unlock the PID file and remove it */\n\tif(flock(pidfd, LOCK_UN) < 0) {\n\t\tJANUS_LOG(LOG_FATAL, \"Error unlocking PID file\\n\");\n\t\tfclose(pidf);\n\t\tclose(pidfd);\n\t\treturn -1;\n\t}\n\tfclose(pidf);\n\tunlink(pidfile);\n\tg_free(pidfile);\n\treturn 0;\n}\n\n/* Protected folders management */\nstatic GList *protected_folders = NULL;\nstatic janus_mutex pf_mutex = JANUS_MUTEX_INITIALIZER;\n\nvoid janus_protected_folder_add(const char *folder) {\n\tif(folder == NULL)\n\t\treturn;\n\tjanus_mutex_lock(&pf_mutex);\n\tprotected_folders = g_list_append(protected_folders, g_strdup(folder));\n\tjanus_mutex_unlock(&pf_mutex);\n}\n\ngboolean janus_is_folder_protected(const char *path) {\n\t/* We need a valid pathname (can't start with a space, we don't trim) */\n\tif(path == NULL || *path == ' ')\n\t\treturn TRUE;\n\t/* Resolve the pathname to its real path first */\n\tchar resolved[PATH_MAX+1];\n\tresolved[0] = '\\0';\n\tif(realpath(path, resolved) == NULL && errno != ENOENT) {\n\t\tJANUS_LOG(LOG_ERR, \"Error resolving path '%s'... %d (%s)\\n\",\n\t\t\tpath, errno, g_strerror(errno));\n\t\treturn TRUE;\n\t}\n\t/* Traverse the list of protected folders to see if any match */\n\tjanus_mutex_lock(&pf_mutex);\n\tif(protected_folders == NULL) {\n\t\t/* No protected folder in the list */\n\t\tjanus_mutex_unlock(&pf_mutex);\n\t\treturn FALSE;\n\t}\n\tgboolean protected = FALSE;\n\tGList *temp = protected_folders;\n\twhile(temp) {\n\t\tchar *folder = (char *)temp->data;\n\t\tif(folder && (strstr(resolved, folder) == resolved)) {\n\t\t\tprotected = TRUE;\n\t\t\tbreak;\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\tjanus_mutex_unlock(&pf_mutex);\n\treturn protected;\n}\n\nvoid janus_protected_folders_clear(void) {\n\tjanus_mutex_lock(&pf_mutex);\n\tg_list_free_full(protected_folders, (GDestroyNotify)g_free);\n\tjanus_mutex_unlock(&pf_mutex);\n}\n\n\nvoid janus_get_json_type_name(int jtype, unsigned int flags, char *type_name) {\n\t/* Longest possible combination is \"a non-empty boolean\" plus one for null char */\n\tgsize req_size = 20;\n\t/* Don't allow for both \"positive\" and \"non-empty\" because that needlessly increases the size. */\n\tif((flags & JANUS_JSON_PARAM_POSITIVE) != 0) {\n\t\tg_strlcpy(type_name, \"a positive \", req_size);\n\t}\n\telse if((flags & JANUS_JSON_PARAM_NONEMPTY) != 0) {\n\t\tg_strlcpy(type_name, \"a non-empty \", req_size);\n\t}\n\telse if(jtype == JSON_INTEGER || jtype == JSON_ARRAY || jtype == JSON_OBJECT) {\n\t\tg_strlcpy(type_name, \"an \", req_size);\n\t}\n\telse {\n\t\tg_strlcpy(type_name, \"a \", req_size);\n\t}\n\tswitch(jtype) {\n\t\tcase JSON_TRUE:\n\t\t\tjanus_strlcat(type_name, \"boolean\", req_size);\n\t\t\tbreak;\n\t\tcase JSON_INTEGER:\n\t\t\tjanus_strlcat(type_name, \"integer\", req_size);\n\t\t\tbreak;\n\t\tcase JSON_REAL:\n\t\t\tjanus_strlcat(type_name, \"real\", req_size);\n\t\t\tbreak;\n\t\tcase JSON_STRING:\n\t\t\tjanus_strlcat(type_name, \"string\", req_size);\n\t\t\tbreak;\n\t\tcase JSON_ARRAY:\n\t\t\tjanus_strlcat(type_name, \"array\", req_size);\n\t\t\tbreak;\n\t\tcase JSON_OBJECT:\n\t\t\tjanus_strlcat(type_name, \"object\", req_size);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n}\n\ngboolean janus_json_is_valid(json_t *val, json_type jtype, unsigned int flags) {\n\tgboolean is_valid = (json_typeof(val) == jtype || (jtype == JSON_TRUE && json_typeof(val) == JSON_FALSE));\n\tif(!is_valid)\n\t\treturn FALSE;\n\tif((flags & JANUS_JSON_PARAM_POSITIVE) != 0) {\n\t\tswitch(jtype) {\n\t\t\tcase JSON_INTEGER:\n\t\t\t\tis_valid = (json_integer_value(val) >= 0);\n\t\t\t\tbreak;\n\t\t\tcase JSON_REAL:\n\t\t\t\tis_valid = (json_real_value(val) >= 0);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\telse if((flags & JANUS_JSON_PARAM_NONEMPTY) != 0) {\n\t\tswitch(jtype) {\n\t\t\tcase JSON_STRING:\n\t\t\t\tis_valid = (strlen(json_string_value(val)) > 0);\n\t\t\t\tbreak;\n\t\t\tcase JSON_ARRAY:\n\t\t\t\tis_valid = (json_array_size(val) > 0);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\treturn is_valid;\n}\n\n/* The following code is more related to codec specific helpers */\n#if defined(__ppc__) || defined(__ppc64__)\n\t# define swap2(d)  \\\n\t((d&0x000000ff)<<8) |  \\\n\t((d&0x0000ff00)>>8)\n#else\n\t# define swap2(d) d\n#endif\n\ngboolean janus_vp8_is_keyframe(const char *buffer, int len) {\n\tif(!buffer || len < 16)\n\t\treturn FALSE;\n\t/* Parse VP8 header now */\n\tuint8_t vp8pd = *buffer;\n\tuint8_t xbit = (vp8pd & 0x80);\n\tuint8_t sbit = (vp8pd & 0x10);\n\tif(xbit) {\n\t\tJANUS_LOG(LOG_HUGE, \"  -- X bit is set!\\n\");\n\t\t/* Read the Extended control bits octet */\n\t\tbuffer++;\n\t\tvp8pd = *buffer;\n\t\tuint8_t ibit = (vp8pd & 0x80);\n\t\tuint8_t lbit = (vp8pd & 0x40);\n\t\tuint8_t tbit = (vp8pd & 0x20);\n\t\tuint8_t kbit = (vp8pd & 0x10);\n\t\tif(ibit) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- I bit is set!\\n\");\n\t\t\t/* Read the PictureID octet */\n\t\t\tbuffer++;\n\t\t\tvp8pd = *buffer;\n\t\t\tuint16_t picid = vp8pd, wholepicid = picid;\n\t\t\tuint8_t mbit = (vp8pd & 0x80);\n\t\t\tif(mbit) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  -- M bit is set!\\n\");\n\t\t\t\tmemcpy(&picid, buffer, sizeof(uint16_t));\n\t\t\t\twholepicid = ntohs(picid);\n\t\t\t\tpicid = (wholepicid & 0x7FFF);\n\t\t\t\tbuffer++;\n\t\t\t}\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- -- PictureID: %\"SCNu16\"\\n\", picid);\n\t\t}\n\t\tif(lbit) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- L bit is set!\\n\");\n\t\t\t/* Read the TL0PICIDX octet */\n\t\t\tbuffer++;\n\t\t\tvp8pd = *buffer;\n\t\t}\n\t\tif(tbit || kbit) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- T/K bit is set!\\n\");\n\t\t\t/* Read the TID/KEYIDX octet */\n\t\t\tbuffer++;\n\t\t\tvp8pd = *buffer;\n\t\t}\n\t}\n\tbuffer++;\t/* Now we're in the payload */\n\tif(sbit) {\n\t\tJANUS_LOG(LOG_HUGE, \"  -- S bit is set!\\n\");\n\t\tunsigned long int vp8ph = 0;\n\t\tmemcpy(&vp8ph, buffer, 4);\n\t\tvp8ph = ntohl(vp8ph);\n\t\tuint8_t pbit = ((vp8ph & 0x01000000) >> 24);\n\t\tif(!pbit) {\n\t\t\tJANUS_LOG(LOG_HUGE, \"  -- P bit is NOT set!\\n\");\n\t\t\t/* It is a key frame! Get resolution for debugging */\n\t\t\tunsigned char *c = (unsigned char *)buffer+3;\n\t\t\t/* vet via sync code */\n\t\t\tif(c[0]!=0x9d||c[1]!=0x01||c[2]!=0x2a) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"First 3-bytes after header not what they're supposed to be?\\n\");\n\t\t\t} else {\n\t\t\t\tunsigned short val3, val5;\n\t\t\t\tmemcpy(&val3,c+3,sizeof(short));\n\t\t\t\tint vp8w = swap2(val3)&0x3fff;\n\t\t\t\tint vp8ws = swap2(val3)>>14;\n\t\t\t\tmemcpy(&val5,c+5,sizeof(short));\n\t\t\t\tint vp8h = swap2(val5)&0x3fff;\n\t\t\t\tint vp8hs = swap2(val5)>>14;\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got a VP8 key frame: %dx%d (scale=%dx%d)\\n\", vp8w, vp8h, vp8ws, vp8hs);\n\t\t\t\treturn TRUE;\n\t\t\t}\n\t\t}\n\t}\n\t/* If we got here it's not a key frame */\n\treturn FALSE;\n}\n\ngboolean janus_vp9_is_keyframe(const char *buffer, int len) {\n\tif(!buffer || len < 16)\n\t\treturn FALSE;\n\t/* Parse VP9 header now */\n\tuint8_t vp9pd = *buffer;\n\tuint8_t ibit = (vp9pd & 0x80);\n\tuint8_t pbit = (vp9pd & 0x40);\n\tuint8_t lbit = (vp9pd & 0x20);\n\tuint8_t fbit = (vp9pd & 0x10);\n\tuint8_t vbit = (vp9pd & 0x02);\n\tbuffer++;\n\tlen--;\n\tif(ibit) {\n\t\t/* Read the PictureID octet */\n\t\tvp9pd = *buffer;\n\t\tuint16_t picid = vp9pd, wholepicid = picid;\n\t\tuint8_t mbit = (vp9pd & 0x80);\n\t\tif(!mbit) {\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t} else {\n\t\t\tmemcpy(&picid, buffer, sizeof(uint16_t));\n\t\t\twholepicid = ntohs(picid);\n\t\t\tpicid = (wholepicid & 0x7FFF);\n\t\t\tbuffer += 2;\n\t\t\tlen -= 2;\n\t\t}\n\t}\n\tif(lbit) {\n\t\tbuffer++;\n\t\tlen--;\n\t\tif(!fbit) {\n\t\t\t/* Non-flexible mode, skip TL0PICIDX */\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t}\n\t}\n\tif(fbit && pbit) {\n\t\t/* Skip reference indices */\n\t\tuint8_t nbit = 1;\n\t\twhile(nbit) {\n\t\t\tvp9pd = *buffer;\n\t\t\tnbit = (vp9pd & 0x01);\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t\tif(len == 0)\t/* Make sure we don't overflow */\n\t\t\t\treturn FALSE;\n\t\t}\n\t}\n\tif(vbit) {\n\t\t/* Parse and skip SS */\n\t\tvp9pd = *buffer;\n\t\tuint n_s = (vp9pd & 0xE0) >> 5;\n\t\tn_s++;\n\t\tuint8_t ybit = (vp9pd & 0x10);\n\t\tif(ybit) {\n\t\t\t/* Iterate on all spatial layers and get resolution */\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t\tif(len == 0)\t/* Make sure we don't overflow */\n\t\t\t\treturn FALSE;\n\t\t\tuint i=0;\n\t\t\tfor(i=0; i<n_s && len>=4; i++,len-=4) {\n\t\t\t\t/* Width */\n\t\t\t\tuint16_t w;\n\t\t\t\tmemcpy(&w, buffer, sizeof(uint16_t));\n\t\t\t\tint vp9w = ntohs(w);\n\t\t\t\tbuffer += 2;\n\t\t\t\t/* Height */\n\t\t\t\tuint16_t h;\n\t\t\t\tmemcpy(&h, buffer, sizeof(uint16_t));\n\t\t\t\tint vp9h = ntohs(h);\n\t\t\t\tbuffer += 2;\n\t\t\t\tif(vp9w || vp9h) {\n\t\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got a VP9 key frame: %dx%d\\n\", vp9w, vp9h);\n\t\t\t\t\treturn TRUE;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t/* If we got here it's not a key frame */\n\treturn FALSE;\n}\n\nstatic gboolean janus_h264_contains_nal(const char *buffer, int len, int val) {\n\tif(!buffer || len < 6)\n\t\treturn FALSE;\n\t/* Parse H264 header now */\n\tuint8_t fragment = *buffer & 0x1F;\n\tuint8_t nal = *(buffer+1) & 0x1F;\n\tif(fragment == val || ((fragment == 28 || fragment == 29) && nal == val && (*(buffer+1) & 0x80))) {\n\t\tJANUS_LOG(LOG_HUGE, \"Got an H264 NAL %d\\n\", val);\n\t\treturn TRUE;\n\t} else if(fragment == 24) {\n\t\t/* May we find it in this STAP-A? */\n\t\tbuffer++;\n\t\tlen--;\n\t\tuint16_t psize = 0;\n\t\t/* We're reading 3 bytes */\n\t\twhile(len > 2) {\n\t\t\tmemcpy(&psize, buffer, 2);\n\t\t\tpsize = ntohs(psize);\n\t\t\tbuffer += 2;\n\t\t\tlen -= 2;\n\t\t\tint nal = *buffer & 0x1F;\n\t\t\tif(nal == val) {\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"Got an H264 NAL %d\\n\", val);\n\t\t\t\treturn TRUE;\n\t\t\t}\n\t\t\tbuffer += psize;\n\t\t\tlen -= psize;\n\t\t}\n\t}\n\t/* If we got here we didn't find it */\n\treturn FALSE;\n}\n\ngboolean janus_h264_is_keyframe(const char *buffer, int len) {\n\treturn janus_h264_contains_nal(buffer, len, 7);\n}\n\ngboolean janus_h264_is_i_frame(const char *buffer, int len) {\n\treturn janus_h264_contains_nal(buffer, len, 5);\n}\n\ngboolean janus_h264_is_b_frame(const char *buffer, int len) {\n\treturn janus_h264_contains_nal(buffer, len, 1);\n}\n\ngboolean janus_av1_is_keyframe(const char *buffer, int len) {\n\tif(!buffer || len < 3)\n\t\treturn FALSE;\n\t/* Read the aggregation header */\n\tuint8_t aggrh = *buffer;\n\tuint8_t zbit = (aggrh & 0x80) >> 7;\n\tuint8_t nbit = (aggrh & 0x08) >> 3;\n\t/* FIXME Ugly hack: we consider a packet with Z=0 and N=1 a keyframe */\n\treturn (!zbit && nbit);\n}\n\ngboolean janus_h265_is_keyframe(const char *buffer, int len) {\n\tif(!buffer || len < 2)\n\t\treturn FALSE;\n\t/* Parse the NAL unit */\n\tuint16_t unit = 0;\n\tmemcpy(&unit, buffer, sizeof(uint16_t));\n\tunit = ntohs(unit);\n\tuint8_t type = (unit & 0x7E00) >> 9;\n\tif(type == 32 || type == 33 || type == 34 || type == 16 || type == 17 || type == 18 || type == 19 || type == 20 || type == 21) {\n\t\t/* FIXME We return TRUE for more than just VPS and SPS, as\n\t\t * suggested in https://github.com/meetecho/janus-gateway/issues/2323 */\n\t\treturn TRUE;\n\t}\n\treturn FALSE;\n}\n\ngboolean janus_is_keyframe(int codec, const char *buffer, int len) {\n\tswitch(codec) {\n\t\tcase JANUS_VIDEOCODEC_VP8:\n\t\t\treturn janus_vp8_is_keyframe(buffer, len);\n\t\tcase JANUS_VIDEOCODEC_VP9:\n\t\t\treturn janus_vp9_is_keyframe(buffer, len);\n\t\tcase JANUS_VIDEOCODEC_H264:\n\t\t\treturn janus_h264_is_keyframe(buffer, len);\n\t\tcase JANUS_VIDEOCODEC_AV1:\n\t\t\treturn janus_av1_is_keyframe(buffer, len);\n\t\tcase JANUS_VIDEOCODEC_H265:\n\t\t\treturn janus_h265_is_keyframe(buffer, len);\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn FALSE;\n}\n\nint janus_vp8_parse_descriptor(char *buffer, int len,\n\t\tgboolean *m, uint16_t *picid, uint8_t *tl0picidx, uint8_t *tid, uint8_t *y, uint8_t *keyidx) {\n\tif(!buffer || len < 6)\n\t\treturn -1;\n\tif(picid)\n\t\t*picid = 0;\n\tif(tl0picidx)\n\t\t*tl0picidx = 0;\n\tif(tid)\n\t\t*tid = 0;\n\tif(y)\n\t\t*y = 0;\n\tif(keyidx)\n\t\t*keyidx = 0;\n\tuint8_t vp8pd = *buffer;\n\tuint8_t xbit = (vp8pd & 0x80);\n\t/* Read the Extended control bits octet */\n\tif(xbit) {\n\t\tbuffer++;\n\t\tvp8pd = *buffer;\n\t\tuint8_t ibit = (vp8pd & 0x80);\n\t\tuint8_t lbit = (vp8pd & 0x40);\n\t\tuint8_t tbit = (vp8pd & 0x20);\n\t\tuint8_t kbit = (vp8pd & 0x10);\n\t\tif(ibit) {\n\t\t\t/* Read the PictureID octet */\n\t\t\tbuffer++;\n\t\t\tvp8pd = *buffer;\n\t\t\tuint16_t partpicid = vp8pd, wholepicid = partpicid;\n\t\t\tuint8_t mbit = (vp8pd & 0x80);\n\t\t\tif(mbit) {\n\t\t\t\tmemcpy(&partpicid, buffer, sizeof(uint16_t));\n\t\t\t\twholepicid = ntohs(partpicid);\n\t\t\t\tpartpicid = (wholepicid & 0x7FFF);\n\t\t\t\tbuffer++;\n\t\t\t}\n\t\t\tif(m)\n\t\t\t\t*m = (mbit ? TRUE : FALSE);\n\t\t\tif(picid)\n\t\t\t\t*picid = partpicid;\n\t\t}\n\t\tif(lbit) {\n\t\t\t/* Read the TL0PICIDX octet */\n\t\t\tbuffer++;\n\t\t\tvp8pd = *buffer;\n\t\t\tif(tl0picidx)\n\t\t\t\t*tl0picidx = vp8pd;\n\t\t}\n\t\tif(tbit || kbit) {\n\t\t\t/* Read the TID/Y/KEYIDX octet */\n\t\t\tbuffer++;\n\t\t\tvp8pd = *buffer;\n\t\t\tif(tid)\n\t\t\t\t*tid = (vp8pd & 0xC0) >> 6;\n\t\t\tif(y)\n\t\t\t\t*y = (vp8pd & 0x20) >> 5;\n\t\t\tif(keyidx)\n\t\t\t\t*keyidx = (vp8pd & 0x1F) >> 4;\n\t\t}\n\t}\n\treturn 0;\n}\n\nstatic int janus_vp8_replace_descriptor(char *buffer, int len, gboolean m, uint16_t picid, uint8_t tl0picidx) {\n\tif(!buffer || len < 6)\n\t\treturn -1;\n\tuint8_t vp8pd = *buffer;\n\tuint8_t xbit = (vp8pd & 0x80);\n\t/* Read the Extended control bits octet */\n\tif(xbit) {\n\t\tbuffer++;\n\t\tvp8pd = *buffer;\n\t\tuint8_t ibit = (vp8pd & 0x80);\n\t\tuint8_t lbit = (vp8pd & 0x40);\n\t\tuint8_t tbit = (vp8pd & 0x20);\n\t\tuint8_t kbit = (vp8pd & 0x10);\n\t\tif(ibit) {\n\t\t\t/* Overwrite the PictureID octet */\n\t\t\tbuffer++;\n\t\t\tvp8pd = *buffer;\n\t\t\tuint8_t mbit = (vp8pd & 0x80);\n\t\t\tif(!mbit || !m) {\n\t\t\t\t*buffer = picid;\n\t\t\t} else {\n\t\t\t\tuint16_t wholepicid = htons(picid);\n\t\t\t\tmemcpy(buffer, &wholepicid, 2);\n\t\t\t\t*buffer |= 0x80;\n\t\t\t\tbuffer++;\n\t\t\t}\n\t\t}\n\t\tif(lbit) {\n\t\t\t/* Overwrite the TL0PICIDX octet */\n\t\t\tbuffer++;\n\t\t\t*buffer = tl0picidx;\n\t\t}\n\t\tif(tbit || kbit) {\n\t\t\t/* Should we overwrite the TID/Y/KEYIDX octet? */\n\t\t\tbuffer++;\n\t\t}\n\t}\n\treturn 0;\n}\n\nvoid janus_vp8_simulcast_context_reset(janus_vp8_simulcast_context *context) {\n\tif(context == NULL)\n\t\treturn;\n\t/* Reset the context values */\n\tcontext->last_picid = 0;\n\tcontext->base_picid = 0;\n\tcontext->base_picid_prev = 0;\n\tcontext->last_tlzi = 0;\n\tcontext->base_tlzi = 0;\n\tcontext->base_tlzi_prev = 0;\n}\n\nvoid janus_vp8_simulcast_descriptor_update(char *buffer, int len, janus_vp8_simulcast_context *context, gboolean switched) {\n\tif(!buffer || len < 0)\n\t\treturn;\n\tgboolean m = FALSE;\n\tuint16_t picid = 0;\n\tuint8_t tlzi = 0;\n\tuint8_t tid = 0;\n\tuint8_t ybit = 0;\n\tuint8_t keyidx = 0;\n\t/* Parse the identifiers in the VP8 payload descriptor */\n\tif(janus_vp8_parse_descriptor(buffer, len, &m, &picid, &tlzi, &tid, &ybit, &keyidx) < 0)\n\t\treturn;\n\tif(switched) {\n\t\tcontext->base_picid_prev = context->last_picid;\n\t\tcontext->base_picid = picid;\n\t\tcontext->base_tlzi_prev = context->last_tlzi;\n\t\tcontext->base_tlzi = tlzi;\n\t}\n\tcontext->last_picid = (picid-context->base_picid)+context->base_picid_prev+1;\n\tif(!m && context->last_picid > 127) {\n\t\tcontext->last_picid -= 128;\n\t\tif(context->last_picid > 127)\n\t\t\tcontext->last_picid = 0;\n\t} else if(m && context->last_picid > 32767) {\n\t\tcontext->last_picid -= 32768;\n\t}\n\tcontext->last_tlzi = (tlzi-context->base_tlzi)+context->base_tlzi_prev+1;\n\t/* Overwrite the values in the VP8 payload descriptors with the ones we have */\n\tjanus_vp8_replace_descriptor(buffer, len, m, context->last_picid, context->last_tlzi);\n}\n\n/* Helper method to parse a VP9 RTP video frame and get some SVC-related info:\n * notice that this only works with VP9, right now, on an experimental basis */\nint janus_vp9_parse_svc(char *buffer, int len, gboolean *found, janus_vp9_svc_info *info) {\n\tif(!buffer || len < 8)\n\t\treturn -1;\n\t/* VP9 depay: */\n\t\t/* https://tools.ietf.org/html/draft-ietf-payload-vp9-04 */\n\t/* Read the first octet (VP9 Payload Descriptor) */\n\tuint8_t vp9pd = *buffer;\n\tuint8_t ibit = (vp9pd & 0x80) >> 7;\n\tuint8_t pbit = (vp9pd & 0x40) >> 6;\n\tuint8_t lbit = (vp9pd & 0x20) >> 5;\n\tuint8_t fbit = (vp9pd & 0x10) >> 4;\n\tuint8_t bbit = (vp9pd & 0x08) >> 3;\n\tuint8_t ebit = (vp9pd & 0x04) >> 2;\n\tuint8_t vbit = (vp9pd & 0x02) >> 1;\n\tif(!lbit) {\n\t\t/* No Layer indices present, no need to go on */\n\t\tif(found)\n\t\t\t*found = FALSE;\n\t\treturn 0;\n\t}\n\t/* Move to the next octet and see what's there */\n\tbuffer++;\n\tlen--;\n\tif(ibit) {\n\t\t/* Read the PictureID octet */\n\t\tvp9pd = *buffer;\n\t\tuint16_t picid = vp9pd, wholepicid = picid;\n\t\tuint8_t mbit = (vp9pd & 0x80);\n\t\tif(!mbit) {\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t} else {\n\t\t\tmemcpy(&picid, buffer, sizeof(uint16_t));\n\t\t\twholepicid = ntohs(picid);\n\t\t\tpicid = (wholepicid & 0x7FFF);\n\t\t\tbuffer += 2;\n\t\t\tlen -= 2;\n\t\t}\n\t}\n\tif(lbit) {\n\t\t/* Read the octet and parse the layer indices now */\n\t\tvp9pd = *buffer;\n\t\tint tlid = (vp9pd & 0xE0) >> 5;\n\t\tuint8_t ubit = (vp9pd & 0x10) >> 4;\n\t\tint slid = (vp9pd & 0x0E) >> 1;\n\t\tuint8_t dbit = (vp9pd & 0x01);\n\t\tJANUS_LOG(LOG_HUGE, \"%s Mode, Layer indices: Temporal: %d (u=%u), Spatial: %d (d=%u)\\n\",\n\t\t\tfbit ? \"Flexible\" : \"Non-flexible\", tlid, ubit, slid, dbit);\n\t\tif(info) {\n\t\t\tinfo->temporal_layer = tlid;\n\t\t\tinfo->spatial_layer = slid;\n\t\t\tinfo->fbit = fbit;\n\t\t\tinfo->pbit = pbit;\n\t\t\tinfo->dbit = dbit;\n\t\t\tinfo->ubit = ubit;\n\t\t\tinfo->bbit = bbit;\n\t\t\tinfo->ebit = ebit;\n\t\t}\n\t\tif(found)\n\t\t\t*found = TRUE;\n\t\t/* Go on, just to get to the SS, if available (which we currently ignore anyway) */\n\t\tbuffer++;\n\t\tlen--;\n\t\tif(!fbit) {\n\t\t\t/* Non-flexible mode, skip TL0PICIDX */\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t}\n\t}\n\tif(fbit && pbit) {\n\t\t/* Skip reference indices */\n\t\tuint8_t nbit = 1;\n\t\twhile(nbit) {\n\t\t\tvp9pd = *buffer;\n\t\t\tnbit = (vp9pd & 0x01);\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t\tif(len == 0)\t/* Make sure we don't overflow */\n\t\t\t\treturn -1;\n\t\t}\n\t}\n\tif(vbit) {\n\t\t/* Parse and skip SS */\n\t\tvp9pd = *buffer;\n\t\tint n_s = (vp9pd & 0xE0) >> 5;\n\t\tn_s++;\n\t\tJANUS_LOG(LOG_HUGE, \"There are %d spatial layers\\n\", n_s);\n\t\tuint8_t ybit = (vp9pd & 0x10);\n\t\tuint8_t gbit = (vp9pd & 0x08);\n\t\tif(ybit) {\n\t\t\t/* Iterate on all spatial layers and get resolution */\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t\tif(len == 0)\t/* Make sure we don't overflow */\n\t\t\t\treturn -1;\n\t\t\tint i=0;\n\t\t\tfor(i=0; i<n_s; i++) {\n\t\t\t\t/* Been there, done that: skip skip skip */\n\t\t\t\tbuffer += 4;\n\t\t\t\tlen -= 4;\n\t\t\t\tif(len <= 0)\t/* Make sure we don't overflow */\n\t\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t\tif(gbit) {\n\t\t\tif(!ybit) {\n\t\t\t\tbuffer++;\n\t\t\t\tlen--;\n\t\t\t\tif(len == 0)\t/* Make sure we don't overflow */\n\t\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tuint8_t n_g = *buffer;\n\t\t\tJANUS_LOG(LOG_HUGE, \"There are %u frames in a GOF\\n\", n_g);\n\t\t\tbuffer++;\n\t\t\tlen--;\n\t\t\tif(len == 0)\t/* Make sure we don't overflow */\n\t\t\t\treturn -1;\n\t\t\tif(n_g > 0) {\n\t\t\t\tint i=0;\n\t\t\t\tfor(i=0; i<n_g; i++) {\n\t\t\t\t\t/* Read the R bits */\n\t\t\t\t\tvp9pd = *buffer;\n\t\t\t\t\tint r = (vp9pd & 0x0C) >> 2;\n\t\t\t\t\tif(r > 0) {\n\t\t\t\t\t\t/* Skip reference indices */\n\t\t\t\t\t\tbuffer += r;\n\t\t\t\t\t\tlen -= r;\n\t\t\t\t\t\tif(len <= 0)\t/* Make sure we don't overflow */\n\t\t\t\t\t\t\treturn -1;\n\t\t\t\t\t}\n\t\t\t\t\tbuffer++;\n\t\t\t\t\tlen--;\n\t\t\t\t\tif(len == 0)\t/* Make sure we don't overflow */\n\t\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn 0;\n}\n\n/* RED parsing and building utilities */\nGList *janus_red_parse_blocks(char *buffer, int len) {\n\tif(buffer == NULL || len < 0)\n\t\treturn NULL;\n\t/* TODO This whole method should be fuzzed */\n\tchar *payload = buffer;\n\tint plen = len;\n\t/* Find out how many generations are in the RED packet */\n\tint gens = 0;\n\tuint32_t red_block;\n\tuint8_t follow = 0, block_pt = 0;\n\tuint16_t ts_offset = 0, block_len = 0;\n\tGList *blocks = NULL;\n\tjanus_red_block *rb = NULL;\n\t/* Parse the header */\n\twhile(payload != NULL && plen > 0) {\n\t\t/* Go through the header for the different generations */\n\t\tgens++;\n\t\tfollow = ((*payload) & 0x80) >> 7;\n\t\tblock_pt = (*payload) & 0x7F;\n\t\tif(follow && plen > 3) {\n\t\t\t/* Read the rest of the header */\n\t\t\tmemcpy(&red_block, payload, sizeof(red_block));\n\t\t\tred_block = ntohl(red_block);\n\t\t\tts_offset = (red_block & 0x00FFFC00) >> 10;\n\t\t\tblock_len = (red_block & 0x000003FF);\n\t\t\tJANUS_LOG(LOG_HUGE, \"  [%d] f=%u, pt=%u, tsoff=%\"SCNu16\", blen=%\"SCNu16\"\\n\",\n\t\t\t\tgens, follow, block_pt, ts_offset, block_len);\n\t\t\trb = g_malloc0(sizeof(janus_red_block));\n\t\t\trb->pt = block_pt;\n\t\t\trb->ts_offset = ts_offset;\n\t\t\trb->length = block_len;\n\t\t\tblocks = g_list_append(blocks, rb);\n\t\t\tpayload += 4;\n\t\t\tplen -= 4;\n\t\t} else {\n\t\t\t/* Header parsed */\n\t\t\tpayload++;\n\t\t\tplen--;\n\t\t\tJANUS_LOG(LOG_HUGE, \"  [%d] f=%u, pt=%u, tsoff=0, blen=TBD.\\n\",\n\t\t\t\tgens, follow, block_pt);\n\t\t\tbreak;\n\t\t}\n\t}\n\t/* Go through the blocks, iterating on the lengths to get a pointer to the data */\n\tif(blocks != NULL) {\n\t\tgens = 0;\n\t\tuint16_t length = 0;\n\t\tGList *temp = blocks;\n\t\twhile(temp != NULL) {\n\t\t\tgens++;\n\t\t\trb = (janus_red_block *)temp->data;\n\t\t\tlength = rb->length;\n\t\t\tif(length > plen) {\n\t\t\t\tJANUS_LOG(LOG_WARN, \"  >> [%d] Broken red payload:\\n\", gens);\n\t\t\t\tg_list_free_full(blocks, (GDestroyNotify)g_free);\n\t\t\t\treturn NULL;\n\t\t\t}\n\t\t\tif(length > 0) {\n\t\t\t\t/* Redundant data, take note of where the block is */\n\t\t\t\tJANUS_LOG(LOG_HUGE, \"  >> [%d] plen=%\"SCNu16\"\\n\", gens, length);\n\t\t\t\trb->data = (uint8_t *)payload;\n\t\t\t\tpayload += length;\n\t\t\t\tplen -= length;\n\t\t\t}\n\t\t\ttemp = temp->next;\n\t\t}\n\t}\n\tif(plen > 0) {\n\t\t/* The last block is the primary data, add it to the list */\n\t\tgens++;\n\t\tJANUS_LOG(LOG_HUGE, \"  >> [%d] plen=%d\\n\", gens, plen);\n\t\trb = g_malloc0(sizeof(janus_red_block));\n\t\trb->pt = block_pt;\n\t\trb->length = plen;\n\t\trb->data = (uint8_t *)payload;\n\t\tblocks = g_list_append(blocks, rb);\n\t}\n\n\treturn blocks;\n}\nint janus_red_pack_blocks(char *buffer, int len, GList *blocks) {\n\tif(buffer == NULL || len < 0)\n\t\treturn 1;\n\tint required = 0, written = 0;\n\tjanus_red_block *rb = NULL;\n\t/* Write all headers to the buffer */\n\tuint32_t red_block = 0;\n\tuint8_t *payload = (uint8_t *)buffer;\n\tGList *temp = blocks;\n\twhile(temp != NULL) {\n\t\trb = (janus_red_block *)temp->data;\n\t\trequired += (temp->next ? 4 : 1);\n\t\trequired += rb->length;\n\t\tif(len < required) {\n\t\t\tJANUS_LOG(LOG_ERR, \"RED buffer too small (%d bytes, at least %d needed)\\n\", len, required);\n\t\t\treturn -2;\n\t\t}\n\t\tif(temp->next != NULL) {\n\t\t\t/* There's going to be a follow-up, write 4 bytes (F=1 and info) */\n\t\t\tred_block =\n\t\t\t\t0x80000000 +\t\t\t\t\t\t\t/* F=1 */\n\t\t\t\t(0x7F000000 & (rb->pt << 24)) + \t\t/* Payload type */\n\t\t\t\t(0x00FFFC00 & (rb->ts_offset << 10)) + \t/* Timestamp offset */\n\t\t\t\t(0x000003FF & rb->length);\t\t\t\t/* Data length */\n\t\t\tred_block = htonl(red_block);\n\t\t\tmemcpy(payload + written, &red_block, sizeof(red_block));\n\t\t\twritten += 4;\n\t\t} else {\n\t\t\t/* Primary data, 1 byte (F=0 and payload type) */\n\t\t\tuint8_t pt = rb->pt;\n\t\t\t*(payload + written) = pt;\n\t\t\twritten++;\n\t\t}\n\t\ttemp = temp->next;\n\t}\n\t/* Now write all data to the buffer too */\n\ttemp = blocks;\n\twhile(temp != NULL) {\n\t\trb = (janus_red_block *)temp->data;\n\t\t/* Write the data itself */\n\t\tmemcpy(payload + written, rb->data, rb->length);\n\t\twritten += rb->length;\n\t\ttemp = temp->next;\n\t}\n\treturn written;\n}\nint janus_red_replace_block_pt(char *buffer, int len, int pt) {\n\tif(buffer == NULL || len < 0 || pt < 0 || pt > 127)\n\t\treturn -1;\n\t/* TODO This whole method should be fuzzed */\n\tchar *payload = buffer;\n\tint plen = len;\n\tuint8_t follow = 0;\n\t/* Parse the header */\n\twhile(payload != NULL && plen > 0) {\n\t\t/* Go through the block headers */\n\t\tfollow = ((*payload) & 0x80) >> 7;\n\t\t*payload = (0x80 & (follow << 7)) + (0x7F & pt);\n\t\tif(follow && plen > 3) {\n\t\t\t/* Move to the next block header */\n\t\t\tpayload += 4;\n\t\t\tplen -= 4;\n\t\t} else {\n\t\t\t/* We're done */\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn 0;\n}\n\n/* Bit manipulation (mostly for TWCC) */\ninline guint32 janus_push_bits(guint32 word, size_t num, guint32 val) {\n\tif(num == 0)\n\t\treturn word;\n\treturn (word << num) | (val & (0xFFFFFFFF>>(32-num)));\n}\n\ninline void janus_set1(guint8 *data,size_t i,guint8 val) {\n\tdata[i] = val;\n}\n\ninline void janus_set2(guint8 *data,size_t i,guint32 val) {\n\tdata[i+1] = (guint8)(val);\n\tdata[i]   = (guint8)(val>>8);\n}\n\ninline void janus_set3(guint8 *data,size_t i,guint32 val) {\n\tdata[i+2] = (guint8)(val);\n\tdata[i+1] = (guint8)(val>>8);\n\tdata[i]   = (guint8)(val>>16);\n}\n\ninline void janus_set4(guint8 *data,size_t i,guint32 val) {\n\tdata[i+3] = (guint8)(val);\n\tdata[i+2] = (guint8)(val>>8);\n\tdata[i+1] = (guint8)(val>>16);\n\tdata[i]   = (guint8)(val>>24);\n}\n\nuint8_t janus_bitstream_getbit(uint8_t *base, uint32_t offset) {\n\treturn ((*(base + (offset >> 0x3))) >> (0x7 - (offset & 0x7))) & 0x1;\n}\n\nuint32_t janus_bitstream_getbits(uint8_t *base, uint8_t num, uint32_t *offset) {\n\tuint32_t res = 0;\n\tint32_t i = 0;\n\tfor(i=num-1; i>=0; i--) {\n\t\tres |= janus_bitstream_getbit(base, (*offset)++) << i;\n\t}\n\treturn res;\n}\n\n#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION\nsize_t janus_gzip_compress(int compression, char *text, size_t tlen, char *compressed, size_t zlen) {\n\tif(text == NULL || tlen < 1 || compressed == NULL || zlen < 1)\n\t\treturn 0;\n\tif(compression < 0 || compression > 9) {\n\t\tJANUS_LOG(LOG_WARN, \"Invalid compression factor %d, falling back to default compression...\\n\", compression);\n\t\tcompression = Z_DEFAULT_COMPRESSION;\n\t}\n\n\t/* Initialize the deflater, and clarify we need gzip */\n\tz_stream zs = { 0 };\n\tzs.zalloc = Z_NULL;\n\tzs.zfree = Z_NULL;\n\tzs.opaque = Z_NULL;\n\tzs.next_in = (Bytef *)text;\n\tzs.avail_in = (uInt)tlen;\n\tzs.next_out = (Bytef *)compressed;\n\tzs.avail_out = (uInt)zlen;\n\tint res = deflateInit2(&zs, compression, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY);\n\tif(res != Z_OK) {\n\t\tJANUS_LOG(LOG_ERR, \"deflateInit error: %d\\n\", res);\n\t\treturn 0;\n\t}\n\t/* Deflate the string */\n\tres = deflate(&zs, Z_FINISH);\n\tif(res != Z_STREAM_END) {\n\t\tJANUS_LOG(LOG_ERR, \"deflate error: %d\\n\", res);\n\t\treturn 0;\n\t}\n\tres = deflateEnd(&zs);\n\tif(res != Z_OK) {\n\t\tJANUS_LOG(LOG_ERR, \"deflateEnd error: %d\\n\", res);\n\t\treturn 0;\n\t}\n\n\t/* Done, return the size of the compressed data */\n\treturn zs.total_out;\n}\n#endif\n"
  },
  {
    "path": "src/utils.h",
    "content": "/*! \\file    utils.h\n * \\author   Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief    Utilities and helpers (headers)\n * \\details  Implementations of a few methods that may be of use here\n * and there in the code.\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_UTILS_H\n#define JANUS_UTILS_H\n\n#include <stdint.h>\n#include <glib.h>\n#include <jansson.h>\n\n#define JANUS_JSON_STRING\t\t\tJSON_STRING\n#define JANUS_JSON_INTEGER\t\t\tJSON_INTEGER\n#define JANUS_JSON_OBJECT\t\t\tJSON_OBJECT\n#define JANUS_JSON_ARRAY\t\t\tJSON_ARRAY\n/* Use JANUS_JSON_BOOL instead of the non-existing JSON_BOOLEAN */\n#define JANUS_JSON_BOOL\t\t\t\tJSON_TRUE\n#define JANUS_JSON_PARAM_REQUIRED\t1\n#define JANUS_JSON_PARAM_POSITIVE\t2\n#define JANUS_JSON_PARAM_NONEMPTY\t4\n\nstruct janus_json_parameter {\n\tconst gchar *name;\n\tjson_type jtype;\n\tunsigned int flags;\n};\n\n#ifndef htonll\n#define htonll(x) ((1==htonl(1)) ? (x) : ((guint64)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32))\n#endif\n#ifndef ntohll\n#define ntohll(x) ((1==ntohl(1)) ? (x) : ((guint64)ntohl((x) & 0xFFFFFFFF) << 32) | ntohl((x) >> 32))\n#endif\n\n\n/*! Helper method used by the core to mark when Janus started */\nvoid janus_mark_started(void);\n\n/*! \\brief Helper to retrieve the system monotonic time, as Glib's\n * g_get_monotonic_time may not be available (only since 2.28)\n * @returns The system monotonic time */\ngint64 janus_get_monotonic_time_internal(void);\n\n/*! \\brief Helper to retrieve the system monotonic time, as Glib's\n * g_get_monotonic_time may not be available (only since 2.28)\n * @note The monotonic time will be normalized from the Janus start time\n * @returns The system monotonic time */\ngint64 janus_get_monotonic_time(void);\n\n/*! \\brief Helper to retrieve the system real time, as Glib's\n * g_get_real_time may not be available (only since 2.28)\n * @returns The system real time */\ngint64 janus_get_real_time(void);\n\n/*! \\brief Helper to replace strings\n * @param message The string that contains the text to replace, which may be\n * freed if it is too short\n * @param old_string The old text to replace\n * @param new_string The new text\n * @returns A pointer to the updated text string (re-allocated or just updated) */\nchar *janus_string_replace(char *message, const char *old_string, const char *new_string) G_GNUC_WARN_UNUSED_RESULT;\n\n/*! \\brief Helper method to concatenate strings and log an error if truncation occurred\n * @param[in] dest Destination buffer, already containing one nul-terminated string\n * @param[in] src Source buffer\n * @param[in] dest_size Length of dest buffer in bytes (not length of existing string inside dest)\n * @returns Size of attempted result, if retval >= dest_size, truncation occurred (and an error will be logged). */\nsize_t janus_strlcat(char *dest, const char *src, size_t dest_size);\n\n/*! \\brief Alternative helper method to concatenate strings and log an error if truncation occurred,\n * which uses memccpy instead of g_strlcat and so is supposed to be faster\n * @note The offset attribute is input/output, and updated any time the method is called\n * @param[in] dest Destination buffer, already containing one nul-terminated string\n * @param[in] src Source buffer\n * @param[in] dest_size Length of dest buffer in bytes (not length of existing string inside dest)\n * @param[in] offset Offset of where to start appending, in the destination buffer\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_strlcat_fast(char *dest, const char *src, size_t dest_size, size_t *offset);\n\n/*! \\brief Helper to parse yes/no|true/false configuration values\n * @param value The configuration value to parse\n * @returns true if the value contains a \"yes\", \"YES\", \"true\", TRUE\", \"1\", false otherwise */\ngboolean janus_is_true(const char *value);\n\n/*! \\brief Helper to compare strings in constant time\n * @param str1 The first string to compare\n * @param str2 The second string to compare\n * @returns true if the strings are the same, false otherwise */\ngboolean janus_strcmp_const_time(const void *str1, const void *str2);\n\n/*! \\brief Helper to generate random 32-bit unsigned integers (useful for SSRCs, etc.)\n * @note Warning: this will fall back to a non-cryptographically safe PRNG in case\n * the crypto library RAND_bytes() call fails.\n * @returns A (mostly crypto-safe) random 32-bit unsigned integer */\nguint32 janus_random_uint32(void);\n\n/*! \\brief Helper to generate random 64-bit unsigned integers\n * @note Unlike janus_random_uint64(), which actually only generates 52 bits, this\n * generates the full 64 bits. See the janus_random_uint64() docstring for details.\n * Warning: this will fall back to a non-cryptographically safe PRNG in case\n * the crypto library RAND_bytes() call fails.\n * @returns A (mostly crypto-safe) random 52-bit unsigned integer */\nguint64 janus_random_uint64_full(void);\n\n/*! \\brief Helper to generate random 52 bit unsigned integers\n * @note The reason for 52 instead of 64 bits: Javascript does not have real integers,\n * its builtin \"number\" type is a float64. Thus, only integer values up to\n * <code>Number.MAX_SAFE_INTEGER == 2^53 - 1 == 9007199254740991</code>\n * can be safely represented in Javascript. This method returns such numbers.\n * Use this method instead of janus_random_uint64_full() whenever you generate numbers which\n * might end up in Javascript (via JSON API).\n * This method is called janus_random_uint64() instead of janus_random_uint52() (or similar)\n * for backwards compatibility.\n * Warning: this will fall back to a non-cryptographically safe PRNG in case\n * the crypto library RAND_bytes() call fails.\n * @returns A (mostly crypto-safe) random 64-bit unsigned integer */\nguint64 janus_random_uint64(void);\n\n/*! \\brief Helper to generate random UUIDs (needed by some plugins)\n * Warning: this will fall back to a non-cryptographically safe PRNG in case\n * the crypto library RAND_bytes() call fails.\n * @returns A (mostly crypto-safe) random UUID string, which must be deallocated with \\c g_free */\nchar *janus_random_uuid(void);\n\n/*! \\brief Helper to generate an allocated copy of a guint64 number\n * @note While apparently silly, this is needed in order to make sure guint64 values\n * used as keys in GHashTable operations are not lost: using temporary guint64 numbers\n * in a g_hash_table_insert, for instance, will cause the key to contain garbage as\n * soon as the temporary variable is lost, and all opererations on the key to fail\n * @param num The guint64 number to duplicate\n * @returns A pointer to a guint64 number, if successful, NULL otherwise */\nguint64 *janus_uint64_dup(guint64 num);\n\n/*! \\brief Helper to hash a guint64 number to another guint64 number\n * @note We currently only use for event handlers\n * @param num The guint64 number to hash\n * @returns The hashed number */\nguint64 janus_uint64_hash(guint64 num);\n\n/*! \\brief Helper method to convert a string to a uint8_t\n * @note The value of \\c num should be ignored, if the method returned an error\n * @param[in] str The string to convert\n * @param[out] num Pointer to the converted number\n * @returns 0 if successful, or a negative integer otherwise (e.g., \\c -ERANGE if the value is out of range) */\nint janus_string_to_uint8(const char *str, uint8_t *num);\n\n/*! \\brief Helper method to convert a string to a uint16_t\n * @note The value of \\c num should be ignored, if the method returned an error\n * @param[in] str The string to convert\n * @param[out] num Pointer to the converted number\n * @returns 0 if successful, or a negative integer otherwise (e.g., \\c -ERANGE if the value is out of range) */\nint janus_string_to_uint16(const char *str, uint16_t *num);\n\n/*! \\brief Helper method to convert a string to a uint32_t\n * @note The value of \\c num should be ignored, if the method returned an error\n * @param[in] str The string to convert\n * @param[out] num Pointer to the converted number\n * @returns 0 if successful, or a negative integer otherwise (e.g., \\c -ERANGE if the value is out of range) */\nint janus_string_to_uint32(const char *str, uint32_t *num);\n\n/** @name Flags helper methods\n */\n///@{\n/*! \\brief Janus flags container */\ntypedef gsize janus_flags;\n\n/*! \\brief Janus flags reset method\n * \\param[in] flags The janus_flags instance to reset */\nvoid janus_flags_reset(janus_flags *flags);\n\n/*! \\brief Janus flags set method\n * \\param[in] flags The janus_flags instance to update\n * \\param[in] flag The flag to set */\nvoid janus_flags_set(janus_flags *flags, gsize flag);\n\n/*! \\brief Janus flags clear method\n * \\param[in] flags The janus_flags instance to update\n * \\param[in] flag The flag to clear */\nvoid janus_flags_clear(janus_flags *flags, gsize flag);\n\n/*! \\brief Janus flags check method\n * \\param[in] flags The janus_flags instance to check\n * \\param[in] flag The flag to check\n * \\returns true if the flag is set, false otherwise */\ngboolean janus_flags_is_set(janus_flags *flags, gsize flag);\n///@}\n\n/*! \\brief Helper to create a new directory, and recursively create parent directories if needed\n * @param dir Path to the new folder to create\n * @param mode File permissions for the new directory file\n * @returns An integer like the regular mkdir does\n * @note A failure may indicate that creating any of the subdirectories failed: some may still have been created */\nint janus_mkdir(const char *dir, mode_t mode);\n\n/*! \\brief Helper to convert \\c path relative to \\c base_dir to absolute path.\n *  If \\c path already represents absolute path then just g_strdup it.\n * @param[in] base_dir Path which will be prepended to \\c path if it's relative\n * @param[in] path Some relative or absolute path\n * @returns g_strdup'ed absolute path. Should be freed with g_free() when no longer needed */\ngchar *janus_make_absolute_path(const gchar *base_dir, const gchar *path);\n\n/*! \\brief Ugly and dirty helper to quickly get the payload type associated with a codec in an SDP\n * @param sdp The SDP to parse\n * @param codec The codec to look for\n * @returns The payload type, if found, -1 otherwise */\nint janus_get_codec_pt(const char *sdp, const char *codec);\n\n/*! \\brief Ugly and dirty helper to quickly get the codec associated with a payload type in an SDP\n * @param sdp The SDP to parse\n * @param pt The payload type to look for\n * @returns The codec name, if found, NULL otherwise */\nconst char *janus_get_codec_from_pt(const char *sdp, int pt);\n\n/*! \\brief Create and lock a PID file\n * @param file Path to the PID file to use\n * @returns 0 if successful, a negative integer otherwise */\nint janus_pidfile_create(const char *file);\n\n/*! \\brief Unlock and remove a previously created PID file\n * @returns 0 if successful, a negative integer otherwise */\nint janus_pidfile_remove(void);\n\n/*! \\brief Add a folder to the protected list (meaning we won't create\n * files there, like recordings or pcap dumps)\n * @param folder Folder to protect */\nvoid janus_protected_folder_add(const char *folder);\n\n/*! \\brief Check if the path points to a protected folder\n * @param path Path we need to check\n * @returns TRUE if the folder is protected, and FALSE otherwise */\ngboolean janus_is_folder_protected(const char *path);\n\n/*! \\brief Cleanup the list of protected folder */\nvoid janus_protected_folders_clear(void);\n\n/*! \\brief Creates a string describing the JSON type and constraint\n * @param jtype The JSON type, e.g., JSON_STRING\n * @param flags Indicates constraints for the described type\n * @param[out] type_name The type description, e.g., \"a positive integer\"; required size is 19 characters */\nvoid janus_get_json_type_name(int jtype, unsigned int flags, char *type_name);\n\n/*! \\brief Checks whether the JSON value matches the type and constraint\n * @param val The JSON value to be checked\n * @param jtype The JSON type, e.g., JSON_STRING\n * @param flags Indicates constraints for the described type\n * @returns TRUE if the value is valid */\ngboolean janus_json_is_valid(json_t *val, json_type jtype, unsigned int flags);\n\n/*! \\brief Validates the JSON object against the description of its parameters\n * @param missing_format printf format to indicate a missing required parameter; needs one %s for the parameter name\n * @param invalid_format printf format to indicate an invalid parameter; needs two %s for parameter name and type description from janus_get_json_type_name\n * @param obj The JSON object to be validated\n * @param params Array of struct janus_json_parameter to describe the parameters; the array has to be a global or stack variable to make sizeof work\n * @param[out] error_code int to return error code\n * @param[out] error_cause Array of char or NULL to return the error descriptions; the array has to be a global or stack variable to make sizeof work; the required size is the length of the format string plus the length of the longest parameter name plus 19 for the type description\n * @param log_error If TRUE, log any error with JANUS_LOG(LOG_ERR)\n * @param missing_code The code to be returned in error_code if a parameter is missing\n * @param invalid_code The code to be returned in error_code if a parameter is invalid */\n#define JANUS_VALIDATE_JSON_OBJECT_FORMAT(missing_format, invalid_format, obj, params, error_code, error_cause, log_error, missing_code, invalid_code) \\\n\tdo { \\\n\t\terror_code = 0; \\\n\t\tunsigned int i; \\\n\t\tfor(i = 0; i < sizeof(params) / sizeof(struct janus_json_parameter); i++) { \\\n\t\t\tjson_t *val = json_object_get(obj, params[i].name); \\\n\t\t\tif(!val) { \\\n\t\t\t\tif((params[i].flags & JANUS_JSON_PARAM_REQUIRED) != 0) {\t\\\n\t\t\t\t\terror_code = (missing_code); \\\n\t\t\t\t\tif(log_error) \\\n\t\t\t\t\t\tJANUS_LOG(LOG_ERR, missing_format \"\\n\", params[i].name); \\\n\t\t\t\t\tif(error_cause != NULL) \\\n\t\t\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), missing_format, params[i].name); \\\n\t\t\t\t\tbreak; \\\n\t\t\t\t} \\\n\t\t\t\tcontinue; \\\n\t\t\t} \\\n\t\t\tif(!janus_json_is_valid(val, params[i].jtype, params[i].flags)) { \\\n\t\t\t\terror_code = (invalid_code); \\\n\t\t\t\tchar type_name[20]; \\\n\t\t\t\tjanus_get_json_type_name(params[i].jtype, params[i].flags, type_name); \\\n\t\t\t\tif(log_error) \\\n\t\t\t\t\tJANUS_LOG(LOG_ERR, invalid_format \"\\n\", params[i].name, type_name); \\\n\t\t\t\tif(error_cause != NULL) \\\n\t\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), invalid_format, params[i].name, type_name); \\\n\t\t\t\tbreak; \\\n\t\t\t} \\\n\t\t} \\\n\t} while(0)\n\n/*! \\brief Validates the JSON object against the description of its parameters\n * @param obj The JSON object to be validated\n * @param params Array of struct janus_json_parameter to describe the parameters; the array has to be a global or stack variable to make sizeof work\n * @param[out] error_code int to return error code\n * @param[out] error_cause Array of char or NULL to return the error descriptions; the array has to be a global or stack variable to make sizeof work; the required size is the length of the longest parameter name plus 54 for the format string and type description\n * @param log_error If TRUE, log any error with JANUS_LOG(LOG_ERR)\n * @param missing_code The code to be returned in error_code if a parameter is missing\n * @param invalid_code The code to be returned in error_code if a parameter is invalid */\n#define JANUS_VALIDATE_JSON_OBJECT(obj, params, error_code, error_cause, log_error, missing_code, invalid_code) \\\n\tJANUS_VALIDATE_JSON_OBJECT_FORMAT(\"Missing mandatory element (%s)\", \"Invalid element type (%s should be %s)\", obj, params, error_code, error_cause, log_error, missing_code, invalid_code)\n\n/*! \\brief If the secret isn't NULL, check the secret after validating the specified member of the JSON object\n * @param secret The secret to be checked; no check if the secret is NULL\n * @param obj The JSON object to be validated\n * @param member The JSON member with the secret, usually \"secret\" or \"pin\"\n * @param[out] error_code int to return error code\n * @param[out] error_cause Array of char or NULL to return the error descriptions; the array has to be a global or stack variable to make sizeof work; the required size is 60\n * @param missing_code The code to be returned in error_code if a parameter is missing\n * @param invalid_code The code to be returned in error_code if a parameter is invalid\n * @param unauthorized_code The code to be returned in error_code if the secret doesn't match */\n#define JANUS_CHECK_SECRET(secret, obj, member, error_code, error_cause, missing_code, invalid_code, unauthorized_code) \\\n\tdo { \\\n\t\tif (secret) { \\\n\t\t\tstatic struct janus_json_parameter secret_parameters[] = { \\\n\t\t\t\t{member, JSON_STRING, JANUS_JSON_PARAM_REQUIRED} \\\n\t\t\t}; \\\n\t\t\tJANUS_VALIDATE_JSON_OBJECT(obj, secret_parameters, error_code, error_cause, TRUE, missing_code, invalid_code); \\\n\t\t\tif(error_code == 0 && !janus_strcmp_const_time((secret), json_string_value(json_object_get(obj, member)))) { \\\n\t\t\t\terror_code = (unauthorized_code); \\\n\t\t\t\tJANUS_LOG(LOG_ERR, \"Unauthorized (wrong %s)\\n\", member); \\\n\t\t\t\tif(error_cause != NULL) \\\n\t\t\t\t\tg_snprintf(error_cause, sizeof(error_cause), \"Unauthorized (wrong %s)\", member); \\\n\t\t\t} \\\n\t\t} \\\n\t} while(0)\n\n/*! \\brief Helper method to check if a VP8 frame is a keyframe or not\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's a keyframe, FALSE otherwise */\ngboolean janus_vp8_is_keyframe(const char *buffer, int len);\n\n/*! \\brief Helper method to check if a VP9 frame is a keyframe or not\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's a keyframe, FALSE otherwise */\ngboolean janus_vp9_is_keyframe(const char *buffer, int len);\n\n/*! \\brief Helper method to check if an H.264 frame is a keyframe or not\n * @note This checks the presence of an SPS NAL (7), nor an I-Frame (5),\n * since SPS/PPS are what's needed for a browser to actually be able to\n * decode a stream. If for some reason you want to check for I-Frames\n * instead, use the janus_h264_is_i_frame() function\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's a keyframe, FALSE otherwise */\ngboolean janus_h264_is_keyframe(const char *buffer, int len);\n\n/*! \\brief Helper method to check if an H.264 frame contains an I-Frame or not\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's an I-Frame, FALSE otherwise */\ngboolean janus_h264_is_i_frame(const char *buffer, int len);\n\n/*! \\brief Helper method to check if an H.264 frame contains a B-Frame or not\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's a B-Frame, FALSE otherwise */\ngboolean janus_h264_is_b_frame(const char *buffer, int len);\n\n/*! \\brief Helper method to check if an AV1 frame is a keyframe or not\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's a keyframe, FALSE otherwise */\ngboolean janus_av1_is_keyframe(const char *buffer, int len);\n\n/*! \\brief Helper method to check if an H.265 frame is a keyframe or not\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's a keyframe, FALSE otherwise */\ngboolean janus_h265_is_keyframe(const char *buffer, int len);\n\n/*! \\brief Helper method to check if keyframe or not, using one of the\n * codec specific helper methods according to the provided codec type\n * @param[in] codec The \\ref janus_videocodec used for this RTP payload\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns TRUE if it's a keyframe, FALSE otherwise */\ngboolean janus_is_keyframe(int codec, const char *buffer, int len);\n\n/*! \\brief VP8 simulcasting context, in order to make sure SSRC changes result in coherent picid/temporal level increases */\ntypedef struct janus_vp8_simulcast_context {\n\tuint16_t last_picid, base_picid, base_picid_prev;\n\tuint8_t last_tlzi, base_tlzi, base_tlzi_prev;\n} janus_vp8_simulcast_context;\n\n/*! \\brief Set (or reset) the context fields to their default values\n * @param[in] context The context to (re)set */\nvoid janus_vp8_simulcast_context_reset(janus_vp8_simulcast_context *context);\n\n/*! \\brief Helper method to parse a VP8 payload descriptor for useful info (e.g., when simulcasting)\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @param[out] m Whether the Picture ID is 15 bit or 7 bit\n * @param[out] picid The Picture ID\n * @param[out] tl0picidx Temporal level zero index\n * @param[out] tid Temporal-layer index\n * @param[out] y Layer sync bit\n * @param[out] keyidx Temporal key frame index\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_vp8_parse_descriptor(char *buffer, int len,\n\t\tgboolean *m, uint16_t *picid, uint8_t *tl0picidx, uint8_t *tid, uint8_t *y, uint8_t *keyidx);\n\n/*! \\brief Use the context info to update the RTP header of a packet, if needed\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @param[in] context The context to use as a reference\n * @param[in] switched Whether there has been a source switch or not (important to compute offsets) */\nvoid janus_vp8_simulcast_descriptor_update(char *buffer, int len, janus_vp8_simulcast_context *context, gboolean switched);\n\n/*! \\brief VP9 SVC info, as parsed from a payload descriptor */\ntypedef struct janus_vp9_svc_info {\n\tint spatial_layer, temporal_layer;\n\tuint8_t fbit, pbit, dbit, ubit, bbit, ebit;\n} janus_vp9_svc_info;\n\n/*! \\brief Helper method to parse a VP9 payload descriptor for SVC-related info (e.g., when SVC is enabled)\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @param[out] found Whether any SVC related info has been found or not\n * @param[out] info Pointer to a janus_vp9_svc_info structure for passing the parsed info back\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_vp9_parse_svc(char *buffer, int len, gboolean *found, janus_vp9_svc_info *info);\n\n/*! \\brief Helper struct to address a specific RED block */\ntypedef struct janus_red_block {\n\tuint8_t pt;\n\tuint32_t ts_offset;\n\tuint8_t *data;\n\tuint16_t length;\n} janus_red_block;\n/*! \\brief Helper method to parse an RTP payload to return a list of RED blocks\n * \\note The returned list is owned by the caller, and must be freed\n * @param[in] buffer The RTP payload to process\n * @param[in] len The length of the RTP payload\n * @returns An allocated GList of janus_red_block, if successful, NULL otherwise */\nGList *janus_red_parse_blocks(char *buffer, int len);\n/*! \\brief Helper method to pack multiple buffers in a RED payload\n * @param[in] buffer The RTP payload to write to\n * @param[in] len The size of the RTP payload buffer\n * @param[in] blocks Linked list of janus_red_block instances to add to the payload\n * @returns The size of the RED payload in case of success, a negative integer otherwise */\nint janus_red_pack_blocks(char *buffer, int len, GList *blocks);\n/*! \\brief Helper method to overwrite all RTP payload types in RED blocks\n * @param[in] buffer The RED block payload to update\n * @param[in] len The size of the payload buffer\n * @param[in] pt The RTP payload type to set in all blocks\n * @returns 0 in case of success, a negative integer otherwise */\nint janus_red_replace_block_pt(char *buffer, int len, int pt);\n\n/*! \\brief Helper method to push individual bits at the end of a word\n * @param[in] word Initial value of word\n * @param[in] num Number of bits to push\n * @param[in] val Value of bits to push\n * @returns 0  New word value*/\nguint32 janus_push_bits(guint32 word, size_t num, guint32 val);\n\n/*! \\brief Helper method to set one byte at a memory position\n * @param[in] data memory data pointer\n * @param[in] i position in memory to change\n * @param[in] val value to write\n */\nvoid janus_set1(guint8 *data, size_t i, guint8 val);\n\n/*! \\brief Helper method to set two bytes at a memory position\n * @param[in] data memory data pointer\n * @param[in] i position in memory to change\n * @param[in] val value to write\n */\nvoid janus_set2(guint8 *data, size_t i, guint32 val);\n\n/*! \\brief Helper method to set three bytes at a memory position\n * @param[in] data memory data pointer\n * @param[in] i position in memory to change\n * @param[in] val value to write\n */\nvoid janus_set3(guint8 *data, size_t i, guint32 val);\n\n/*! \\brief Helper method to set four bytes at a memory position\n * @param[in] data memory data pointer\n * @param[in] i position in memory to change\n * @param[in] val value to write\n */\nvoid janus_set4(guint8 *data, size_t i, guint32 val);\n\n/*! \\brief Helpers to read a bit from a bitstream\n * @param[in] base Pointer to the start of the bitstream\n * @param[in] offset Offset in bits from the start\n * @returns The value of the bit */\nuint8_t janus_bitstream_getbit(uint8_t *base, uint32_t offset);\n/*! \\brief Helpers to read agroup of bits from a bitstream\n * @param[in] base Pointer to the start of the bitstream\n * @param[in] num The number of bits to read\n * @param[in] offset Offset in bits from the start\n * @returns The value of the bits */\nuint32_t janus_bitstream_getbits(uint8_t *base, uint8_t num, uint32_t *offset);\n\n/*! \\brief Helper method to compress a string to gzip (using zlib)\n * \\note It's up to you to provide a buffer large enough for the compressed\n * data: in case the buffer isn't large enough, the request will fail\n * @param[in] compression Compression factor (1=fastest, 9=best compression)\n * @param[in] text Pointer to the string to compress\n * @param[in] tlen Length of the string to compress\n * @param[in] compressed Pointer to the buffer where to compress the string to\n * @param[in] zlen Size of the output buffer\n * @returns The size of the compressed data, if successful, or 0 otherwise\n */\nsize_t janus_gzip_compress(int compression, char *text, size_t tlen, char *compressed, size_t zlen);\n\n#endif\n"
  },
  {
    "path": "src/version.h",
    "content": "/*! \\file   version.h\n * \\author Lorenzo Miniero <lorenzo@meetecho.com>\n * \\copyright GNU General Public License v3\n * \\brief  Janus GitHub versioning (headers)\n * \\details This exposes a quick an easy way to display the commit the\n * compiled version of Janus implements, and when it has been built. It\n * is based on this excellent comment: http://stackoverflow.com/a/1843783\n *\n * \\ingroup core\n * \\ref core\n */\n\n#ifndef JANUS_VERSION_H\n#define JANUS_VERSION_H\n\nextern int janus_version;\nextern const char *janus_version_string;\nextern const char *janus_build_git_time;\nextern const char *janus_build_git_sha;\n\n/* Dependencies (those we can't get programmatically) */\nextern const char *libnice_version_string;\n\n#endif\n"
  },
  {
    "path": "test/README.md",
    "content": "# Janus testing\n\nThe files in this sub-folder are intended to be used for testing Janus.\n\n## aiortc functional testing\n\nWe implemented some RTC Python clients based on [aiortc](https://github.com/aiortc/aiortc).\nIn order to use them you'll need Python >= 3.9.\n\nTo install required libraries:\n\n```bash\npython3 -m venv .venv\n.venv/bin/pip install -r requirements.txt\n```\n\n### echo.py\n\nThis script does a quick echotest with a specified Janus instance.\nThe source code has been largely inspired by the examples on the aiortc repository.\nThe program basically initiates a Janus session through a WebSocket connection, then creates a new echotest handle and starts an audio/video negotiation according to the echotest API.\n\nOnce everything has been succesfully set up, the client waits for 5 seconds and then checks the following assertions:\n* WebSocket is connected\n* ICE Connection State is completed\n* DTLS state is connected\n* outbound RTP packets are greater or equal than inbound RTP packets\n\nIf any of these assertion fails, the client returns a non-zero value.\nNo assertion has been made on the RTP packets to check if they contains valid media or not, I guess this is something that might be added in future.\n\nThe script is invoked like this:\n\n```bash\npython3 echo.py ws://localhost:8188/ --play-from media_file --verbose\n```\n\nThe websocket endpoint default is `ws://localhost:8188/`.\nThe media_file is optional and if omitted dummy audio/video tracks will be generated.\n\n### The test_aiortc.sh helper script\n\nWe have added a `test_aiortc.sh` helper script in order to easily launch a Janus aiortc-based test.\nThe scripts must be invoked like this:\n\n```bash\n./test_aiortc.sh echo.py ws://localhost:8188/\n```\n\nIt will start a Janus instance in the background taking the binary files from the Janus sources directory.\nThen it will wait for some seconds before invoking the Python script specified in the first parameter.\nFinally it will check the exit status of the Python script and kill the Janus instance.\n"
  },
  {
    "path": "test/echo.py",
    "content": "import argparse\nimport asyncio\nimport logging\nimport random\nimport string\nimport sys\n\nimport websockets as ws\nimport json\n\n\nfrom aiortc import RTCPeerConnection, RTCSessionDescription\nfrom aiortc.mediastreams import AudioStreamTrack, VideoStreamTrack\nfrom aiortc.contrib.media import MediaPlayer\n\n\nlogger = logging.getLogger('echo')\n\n\nclass WebSocketClient():\n\n    def __init__(self, url='ws://localhost:8188/'):\n        self._url = url\n        self.connection = None\n        self._transactions = {}\n\n    async def connect(self):\n        self.connection = await ws.connect(self._url,\n                                           subprotocols=['janus-protocol'],\n                                           ping_interval=10,\n                                           ping_timeout=10,\n                                           compression=None)\n        if self.connection.state == ws.State.OPEN:\n            asyncio.ensure_future(self.receiveMessage())\n            logger.info('WebSocket connected')\n            return self\n\n    def transaction_id(self):\n        return ''.join(random.choice(string.ascii_letters) for x in range(12))\n\n    async def send(self, message):\n        tx_id = self.transaction_id()\n        message.update({'transaction': tx_id})\n        tx = asyncio.get_event_loop().create_future()\n        tx_in = {'tx': tx, 'request': message['janus']}\n        self._transactions[tx_id] = tx_in\n        try:\n            await asyncio.gather(self.connection.send(json.dumps(message)), tx)\n        except Exception as e:\n            tx.set_result(e)\n        return tx.result()\n\n    async def receiveMessage(self):\n        try:\n            async for message in self.connection:\n                data = json.loads(message)\n                tx_id = data.get('transaction')\n                response = data['janus']\n\n                # Handle ACK\n                if tx_id and response == 'ack':\n                    logger.debug(f'Received ACK for transaction {tx_id}')\n                    if tx_id in self._transactions:\n                        tx_in = self._transactions[tx_id]\n                        if tx_in['request'] == 'keepalive':\n                            tx = tx_in['tx']\n                            tx.set_result(data)\n                            del self._transactions[tx_id]\n                            logger.debug(f'Closed transaction {tx_id}'\n                                         f' with {response}')\n                    continue\n\n                # Handle Success / Event / Error\n                if response not in {'success', 'error'}:\n                    logger.info(f'Janus Event --> {response}')\n                if tx_id and tx_id in self._transactions:\n                    tx_in = self._transactions[tx_id]\n                    tx = tx_in['tx']\n                    tx.set_result(data)\n                    del self._transactions[tx_id]\n                    logger.debug(f'Closed transaction {tx_id}'\n                                 f' with {response}')\n        except Exception:\n            logger.error('WebSocket failure')\n        logger.info('Connection closed')\n\n    async def close(self):\n        if self.connection:\n            await self.connection.close()\n            self.connection = None\n        self._transactions = {}\n\n\nclass JanusPlugin:\n    def __init__(self, session, handle_id):\n        self._session = session\n        self._handle_id = handle_id\n\n    async def sendMessage(self, message):\n        logger.info('Sending message to the plugin')\n        message.update({'janus': 'message', 'handle_id': self._handle_id})\n        response = await self._session.send(message)\n        return response\n\n\nclass JanusSession:\n    def __init__(self, url='ws://localhost:8188/'):\n        self._websocket = None\n        self._url = url\n        self._handles = {}\n        self._session_id = None\n        self._ka_interval = 15\n        self._ka_task = None\n\n    async def send(self, message):\n        message.update({'session_id': self._session_id})\n        response = await self._websocket.send(message)\n        return response\n\n    async def create(self):\n        logger.info('Creating session')\n        self._websocket = await WebSocketClient(self._url).connect()\n        message = {'janus': 'create'}\n        response = await self.send(message)\n        assert response['janus'] == 'success'\n        session_id = response['data']['id']\n        self._session_id = session_id\n        self._ka_task = asyncio.ensure_future(self._keepalive())\n        logger.info('Session created')\n\n    async def attach(self, plugin):\n        logger.info('Attaching handle')\n        message = {'janus': 'attach', 'plugin': plugin}\n        response = await self.send(message)\n        assert response['janus'] == 'success'\n        handle_id = response['data']['id']\n        handle = JanusPlugin(self, handle_id)\n        self._handles[handle_id] = handle\n        logger.info('Handle attached')\n        return handle\n\n    async def destroy(self):\n        logger.info('Destroying session')\n        if self._session_id:\n            message = {'janus': 'destroy'}\n            await self.send(message)\n            self._session_id = None\n        if self._ka_task:\n            self._ka_task.cancel()\n            try:\n                await self._ka_task\n            except asyncio.CancelledError:\n                pass\n            self._ka_task = None\n        self._handles = {}\n        logger.info('Session destroyed')\n\n        logger.info('Closing WebSocket')\n        if self._websocket:\n            await self._websocket.close()\n            self._websocket = None\n\n    async def _keepalive(self):\n        while True:\n            logger.info('Sending keepalive')\n            message = {'janus': 'keepalive'}\n            await self.send(message)\n            logger.info('Keepalive OK')\n            await asyncio.sleep(self._ka_interval)\n\n\nasync def run(pc, player, session, bitrate=512000, record=False):\n    @pc.on('track')\n    def on_track(track):\n        logger.info(f'Track {track.kind} received')\n\n        @track.on('ended')\n        def on_ended():\n            print(f'Track {track.kind} ended')\n\n    @pc.on('iceconnectionstatechange')\n    def on_ice_state_change():\n        # logger.info(f'ICE state changed to {pc.iceConnectionState}')\n        pass\n\n    # create session\n    await session.create()\n\n    # configure media\n    media = {'audio': True, 'video': True}\n    if player and player.audio:\n        pc.addTrack(player.audio)\n    else:\n        pc.addTrack(AudioStreamTrack())\n    if player and player.video:\n        pc.addTrack(player.video)\n    else:\n        pc.addTrack(VideoStreamTrack())\n\n    # attach to echotest plugin\n    plugin = await session.attach('janus.plugin.echotest')\n\n    # create data-channel\n    channel = pc.createDataChannel('JanusDataChannel')\n    logger.info(f'DataChannel ({channel.label}) created')\n    dc_probe_message = 'echo-ping'\n    dc_open = False\n    dc_probe_received = False\n\n    @channel.on('open')\n    def on_open():\n        nonlocal dc_open\n        dc_open = True\n        logger.info(f'DataChannel ({channel.label}) open')\n        logger.info(\n            f'DataChannel ({channel.label}) sending: {dc_probe_message}')\n        channel.send(dc_probe_message)\n\n    @channel.on('close')\n    def on_close():\n        nonlocal dc_open\n        dc_open = False\n        logger.info(f'DataChannel ({channel.label}) closed')\n\n    @channel.on('message')\n    def on_message(message):\n        logger.info(f'DataChannel ({channel.label}) received: {message}')\n        if dc_probe_message in message:\n            nonlocal dc_probe_received\n            dc_probe_received = True\n\n    # send offer\n    await pc.setLocalDescription(await pc.createOffer())\n    request = {'record': record, 'bitrate': bitrate}\n    request.update(media)\n    response = await plugin.sendMessage(\n        {\n            'body': request,\n            'jsep': {\n                'sdp': pc.localDescription.sdp,\n                'trickle': False,\n                'type': pc.localDescription.type,\n            },\n        }\n    )\n    assert response['plugindata']['data']['result'] == 'ok'\n\n    # apply answer\n    answer = RTCSessionDescription(\n        sdp=response['jsep']['sdp'],\n        type=response['jsep']['type']\n    )\n    await pc.setRemoteDescription(answer)\n\n    logger.info('Running for a while...')\n    await asyncio.sleep(5)\n\n    # Check WebSocket status\n    assert session._websocket.connection.state == ws.State.OPEN\n\n    # Get RTC stats and check the status\n    rtcstats = await pc.getStats()\n    rtp = {'audio': {'in': 0, 'out': 0}, 'video': {'in': 0, 'out': 0}}\n    dtls_state = None\n    for stat in rtcstats.values():\n        if stat.type == 'inbound-rtp':\n            rtp[stat.kind]['in'] = stat.packetsReceived\n        elif stat.type == 'outbound-rtp':\n            rtp[stat.kind]['out'] = stat.packetsSent\n        elif stat.type == 'transport':\n            dtls_state = stat.dtlsState\n    # ICE succeded\n    assert pc.iceConnectionState == 'completed'\n    # DTLS succeded\n    assert dtls_state == 'connected'\n    # Janus echoed the sent packets\n    assert rtp['audio']['out'] >= rtp['audio']['in'] > 0\n    assert rtp['video']['out'] >= rtp['video']['in'] > 0\n    # DataChannels worked\n    assert dc_open\n    assert dc_probe_received\n\n    logger.info('Ending the test now')\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser(description='Janus')\n    parser.add_argument('url',\n                        help='Janus root URL, e.g. ws://localhost:8188/')\n    parser.add_argument('--play-from',\n                        help='Read the media from a file and sent it.'),\n    parser.add_argument('--verbose', '-v', action='count')\n    args = parser.parse_args()\n\n    if args.verbose:\n        logging.basicConfig(level=logging.DEBUG)\n    else:\n        logging.basicConfig(level=logging.INFO)\n\n    # create signaling and peer connection\n    session = JanusSession(args.url)\n    pc = RTCPeerConnection()\n\n    # create media source\n    if args.play_from:\n        player = MediaPlayer(args.play_from)\n    else:\n        player = None\n\n    loop = asyncio.new_event_loop()\n    asyncio.set_event_loop(loop)\n    try:\n        loop.run_until_complete(\n            run(pc=pc, player=player, session=session)\n        )\n        logger.info('Test Passed')\n        sys.exit(0)\n    except Exception:\n        logger.exception('Test Failed')\n        sys.exit(1)\n    finally:\n        loop.run_until_complete(pc.close())\n        loop.run_until_complete(session.destroy())\n"
  },
  {
    "path": "test/requirements.txt",
    "content": "websockets==15.0\naiortc==1.10.1\n"
  },
  {
    "path": "test/test_aiortc.sh",
    "content": "#!/bin/bash -eu\n\nSCRIPTPATH=\"$( cd \"$(dirname \"$0\")\" ; pwd -P )\"\n\nVENVPATH=\"$SCRIPTPATH/.venv\"\npython3 -m venv $VENVPATH\n$VENVPATH/bin/pip install -r \"$SCRIPTPATH/requirements.txt\"\n\nJANUS_SRC=\"$( dirname $SCRIPTPATH )\"\n\nTEST=${1-\"$SCRIPTPATH/echo.py\"}\nURL=${2-\"ws://localhost:8188/\"}\n\necho \"Starting Janus binary from $JANUS_SRC ...\"\n$JANUS_SRC/src/janus >/dev/null 2>&1 &\nJANUS_PID=$!\n\necho \"Waiting for some seconds before launching the test ...\"\nsleep 5\n\necho \"Launching test $TEST ...\"\n$VENVPATH/bin/python3 $TEST $URL\nRES=$?\n\nkill $JANUS_PID 2>/dev/null\n\nif [ $RES -eq 0 ]; then\n    echo \"TEST SUCCEEDED\"\n    exit 0\nelse\n    echo \"TEST FAILED\"\n    exit 1\nfi\n"
  }
]